View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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     @Override
103     public int[] getAcceptableTokens() {
104         return TokenUtil.getAllTokenIds();
105     }
106 
107     @Override
108     public int[] getDefaultTokens() {
109         return getRequiredTokens();
110     }
111 
112     @Override
113     public int[] getRequiredTokens() {
114         return CommonUtil.EMPTY_INT_ARRAY;
115     }
116 
117     @Override
118     public void visitToken(DetailAST ast) {
119         // reset counts
120         Arrays.fill(counts, 0);
121         countTokens(ast, 0);
122 
123         if (sumTokenCounts) {
124             logAsTotal(ast);
125         }
126         else {
127             logAsSeparated(ast);
128         }
129     }
130 
131     /**
132      * Log violations for each Token.
133      *
134      * @param ast token
135      */
136     private void logAsSeparated(DetailAST ast) {
137         // name of this token
138         final String name = TokenUtil.getTokenName(ast.getType());
139 
140         for (int element : limitedTokens) {
141             final int tokenCount = counts[element - 1];
142             if (tokenCount < minimumNumber) {
143                 final String descendantName = TokenUtil.getTokenName(element);
144 
145                 if (minimumMessage == null) {
146                     minimumMessage = MSG_KEY_MIN;
147                 }
148                 log(ast,
149                         minimumMessage,
150                         String.valueOf(tokenCount),
151                         String.valueOf(minimumNumber),
152                         name,
153                         descendantName);
154             }
155             if (tokenCount > maximumNumber) {
156                 final String descendantName = TokenUtil.getTokenName(element);
157 
158                 if (maximumMessage == null) {
159                     maximumMessage = MSG_KEY_MAX;
160                 }
161                 log(ast,
162                         maximumMessage,
163                         String.valueOf(tokenCount),
164                         String.valueOf(maximumNumber),
165                         name,
166                         descendantName);
167             }
168         }
169     }
170 
171     /**
172      * Log validation as one violation.
173      *
174      * @param ast current token
175      */
176     private void logAsTotal(DetailAST ast) {
177         // name of this token
178         final String name = TokenUtil.getTokenName(ast.getType());
179 
180         int total = 0;
181         for (int element : limitedTokens) {
182             total += counts[element - 1];
183         }
184         if (total < minimumNumber) {
185             if (minimumMessage == null) {
186                 minimumMessage = MSG_KEY_SUM_MIN;
187             }
188             log(ast,
189                     minimumMessage,
190                     String.valueOf(total),
191                     String.valueOf(minimumNumber), name);
192         }
193         if (total > maximumNumber) {
194             if (maximumMessage == null) {
195                 maximumMessage = MSG_KEY_SUM_MAX;
196             }
197             log(ast,
198                     maximumMessage,
199                     String.valueOf(total),
200                     String.valueOf(maximumNumber), name);
201         }
202     }
203 
204     /**
205      * Counts the number of occurrences of descendant tokens.
206      *
207      * @param ast the root token for descendants.
208      * @param depth the maximum depth of the counted descendants.
209      */
210     private void countTokens(DetailAST ast, int depth) {
211         if (depth <= maximumDepth) {
212             // update count
213             if (depth >= minimumDepth) {
214                 final int type = ast.getType();
215                 if (type <= counts.length) {
216                     counts[type - 1]++;
217                 }
218             }
219             DetailAST child = ast.getFirstChild();
220             final int nextDepth = depth + 1;
221             while (child != null) {
222                 countTokens(child, nextDepth);
223                 child = child.getNextSibling();
224             }
225         }
226     }
227 
228     /**
229      * Setter to specify set of tokens with limited occurrences as descendants.
230      *
231      * @param limitedTokensParam tokens to ignore.
232      * @since 3.2
233      */
234     public void setLimitedTokens(String... limitedTokensParam) {
235         limitedTokens = new int[limitedTokensParam.length];
236 
237         int maxToken = 0;
238         for (int i = 0; i < limitedTokensParam.length; i++) {
239             limitedTokens[i] = TokenUtil.getTokenId(limitedTokensParam[i]);
240             if (limitedTokens[i] >= maxToken + 1) {
241                 maxToken = limitedTokens[i];
242             }
243         }
244         counts = new int[maxToken];
245     }
246 
247     /**
248      * Setter to specify the minimum depth for descendant counts.
249      *
250      * @param minimumDepth the minimum depth for descendant counts.
251      * @since 3.2
252      */
253     public void setMinimumDepth(int minimumDepth) {
254         this.minimumDepth = minimumDepth;
255     }
256 
257     /**
258      * Setter to specify the maximum depth for descendant counts.
259      *
260      * @param maximumDepth the maximum depth for descendant counts.
261      * @since 3.2
262      */
263     public void setMaximumDepth(int maximumDepth) {
264         this.maximumDepth = maximumDepth;
265     }
266 
267     /**
268      * Setter to specify a minimum count for descendants.
269      *
270      * @param minimumNumber the minimum count for descendants.
271      * @since 3.2
272      */
273     public void setMinimumNumber(int minimumNumber) {
274         this.minimumNumber = minimumNumber;
275     }
276 
277     /**
278      * Setter to specify a maximum count for descendants.
279      *
280      * @param maximumNumber the maximum count for descendants.
281      * @since 3.2
282      */
283     public void setMaximumNumber(int maximumNumber) {
284         this.maximumNumber = maximumNumber;
285     }
286 
287     /**
288      * Setter to define the violation message when the minimum count is not reached.
289      *
290      * @param message the violation message for minimum count not reached.
291      *     Used as a {@code MessageFormat} pattern with arguments
292      *     <ul>
293      *     <li>{0} - token count</li>
294      *     <li>{1} - minimum number</li>
295      *     <li>{2} - name of token</li>
296      *     <li>{3} - name of limited token</li>
297      *     </ul>
298      * @since 3.2
299      */
300     public void setMinimumMessage(String message) {
301         minimumMessage = message;
302     }
303 
304     /**
305      * Setter to define the violation message when the maximum count is exceeded.
306      *
307      * @param message the violation message for maximum count exceeded.
308      *     Used as a {@code MessageFormat} pattern with arguments
309      *     <ul>
310      *     <li>{0} - token count</li>
311      *     <li>{1} - maximum number</li>
312      *     <li>{2} - name of token</li>
313      *     <li>{3} - name of limited token</li>
314      *     </ul>
315      * @since 3.2
316      */
317 
318     public void setMaximumMessage(String message) {
319         maximumMessage = message;
320     }
321 
322     /**
323      * Setter to control whether the number of tokens found should be calculated
324      * from the sum of the individual token counts.
325      *
326      * @param sum whether to use the sum.
327      * @since 5.0
328      */
329     public void setSumTokenCounts(boolean sum) {
330         sumTokenCounts = sum;
331     }
332 
333 }