001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks;
021
022import java.util.Arrays;
023
024import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
025import com.puppycrawl.tools.checkstyle.PropertyType;
026import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
030import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
031
032/**
033 * <div>
034 * Checks for restricted tokens beneath other tokens.
035 * </div>
036 *
037 * <p>
038 * WARNING: This is a very powerful and flexible check, but, at the same time,
039 * it is low-level and very implementation-dependent because its results depend
040 * on the grammar we use to build abstract syntax trees. Thus, we recommend using
041 * other checks when they provide the desired functionality. Essentially, this
042 * check just works on the level of an abstract syntax tree and knows nothing
043 * about language structures.
044 * </p>
045 *
046 * @since 3.2
047 */
048@FileStatefulCheck
049public class DescendantTokenCheck extends AbstractCheck {
050
051    /**
052     * A key is pointing to the warning message text in "messages.properties"
053     * file.
054     */
055    public static final String MSG_KEY_MIN = "descendant.token.min";
056
057    /**
058     * A key is pointing to the warning message text in "messages.properties"
059     * file.
060     */
061    public static final String MSG_KEY_MAX = "descendant.token.max";
062
063    /**
064     * A key is pointing to the warning message text in "messages.properties"
065     * file.
066     */
067    public static final String MSG_KEY_SUM_MIN = "descendant.token.sum.min";
068
069    /**
070     * A key is pointing to the warning message text in "messages.properties"
071     * file.
072     */
073    public static final String MSG_KEY_SUM_MAX = "descendant.token.sum.max";
074
075    /** Specify the minimum depth for descendant counts. */
076    private int minimumDepth;
077    /** Specify the maximum depth for descendant counts. */
078    private int maximumDepth = Integer.MAX_VALUE;
079    /** Specify a minimum count for descendants. */
080    private int minimumNumber;
081    /** Specify a maximum count for descendants. */
082    private int maximumNumber = Integer.MAX_VALUE;
083    /**
084     * Control whether the number of tokens found should be calculated from
085     * the sum of the individual token counts.
086     */
087    private boolean sumTokenCounts;
088    /** Specify set of tokens with limited occurrences as descendants. */
089    @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
090    private int[] limitedTokens = CommonUtil.EMPTY_INT_ARRAY;
091    /** Define the violation message when the minimum count is not reached. */
092    private String minimumMessage;
093    /** Define the violation message when the maximum count is exceeded. */
094    private String maximumMessage;
095
096    /**
097     * Counts of descendant tokens.
098     * Indexed by (token ID - 1) for performance.
099     */
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}