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 }