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 }