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;
021
022import java.io.File;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Comparator;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.Locale;
029import java.util.Map;
030import java.util.Set;
031import java.util.SortedSet;
032import java.util.TreeSet;
033import java.util.stream.Collectors;
034import java.util.stream.Stream;
035
036import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
037import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
038import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
039import com.puppycrawl.tools.checkstyle.api.Configuration;
040import com.puppycrawl.tools.checkstyle.api.Context;
041import com.puppycrawl.tools.checkstyle.api.DetailAST;
042import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
043import com.puppycrawl.tools.checkstyle.api.FileContents;
044import com.puppycrawl.tools.checkstyle.api.FileText;
045import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
046import com.puppycrawl.tools.checkstyle.api.Violation;
047import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
048
049/**
050 * Responsible for walking an abstract syntax tree and notifying interested
051 * checks at each node.
052 *
053 */
054@FileStatefulCheck
055public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder {
056
057    /** Message to use when an exception occurs and should be printed as a violation. */
058    public static final String PARSE_EXCEPTION_MSG = "parse.exception";
059
060    /** Maps from token name to ordinary checks. */
061    private final Map<Integer, Set<AbstractCheck>> tokenToOrdinaryChecks =
062        new HashMap<>();
063
064    /** Maps from token name to comment checks. */
065    private final Map<Integer, Set<AbstractCheck>> tokenToCommentChecks =
066            new HashMap<>();
067
068    /** Registered ordinary checks, that don't use comment nodes. */
069    private final Set<AbstractCheck> ordinaryChecks = createNewCheckSortedSet();
070
071    /** Registered comment checks. */
072    private final Set<AbstractCheck> commentChecks = createNewCheckSortedSet();
073
074    /** The ast filters. */
075    private final Set<TreeWalkerFilter> filters = new HashSet<>();
076
077    /** The sorted set of violations. */
078    private final SortedSet<Violation> violations = new TreeSet<>();
079
080    /** Context of child components. */
081    private Context childContext;
082
083    /** A factory for creating submodules (i.e. the Checks) */
084    private ModuleFactory moduleFactory;
085
086    /** Control whether to skip files with Java parsing exceptions. */
087    private boolean skipFileOnJavaParseException;
088
089    /** Specify severity Level to log Java parsing exceptions when they are skipped. */
090    private SeverityLevel javaParseExceptionSeverity = SeverityLevel.ERROR;
091
092    /**
093     * Creates a new {@code TreeWalker} instance.
094     */
095    public TreeWalker() {
096        setFileExtensions("java");
097    }
098
099    /**
100     * Sets the module factory for creating child modules (Checks).
101     *
102     * @param moduleFactory the factory
103     */
104    public void setModuleFactory(ModuleFactory moduleFactory) {
105        this.moduleFactory = moduleFactory;
106    }
107
108    /**
109     * Setter to control whether to skip files with Java parsing exceptions.
110     *
111     *  @param skipFileOnJavaParseException whether to skip files with Java parsing errors.
112     *  @since 10.18.0
113     */
114    public void setSkipFileOnJavaParseException(boolean skipFileOnJavaParseException) {
115        this.skipFileOnJavaParseException = skipFileOnJavaParseException;
116    }
117
118    /**
119     * Setter to specify the severity level
120     * to log Java parsing exceptions when they are skipped.
121     *
122     *  @param javaParseExceptionSeverity severity level to log parsing exceptions
123     *      when they are skipped.
124     *  @since 10.18.0
125     */
126    public void setJavaParseExceptionSeverity(SeverityLevel javaParseExceptionSeverity) {
127        this.javaParseExceptionSeverity = javaParseExceptionSeverity;
128    }
129
130    @Override
131    public void finishLocalSetup() {
132        final DefaultContext checkContext = new DefaultContext();
133        checkContext.add("severity", getSeverity());
134        checkContext.add("tabWidth", String.valueOf(getTabWidth()));
135        childContext = checkContext;
136    }
137
138    /**
139     * {@inheritDoc} Creates child module.
140     *
141     * @noinspection ChainOfInstanceofChecks
142     * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently
143     */
144    @Override
145    public void setupChild(Configuration childConf)
146            throws CheckstyleException {
147        final String name = childConf.getName();
148        final Object module;
149
150        try {
151            module = moduleFactory.createModule(name);
152            if (module instanceof AbstractAutomaticBean bean) {
153                bean.contextualize(childContext);
154                bean.configure(childConf);
155            }
156        }
157        catch (final CheckstyleException exc) {
158            throw new CheckstyleException("cannot initialize module " + name
159                    + " - " + exc.getMessage(), exc);
160        }
161        if (module instanceof AbstractCheck check) {
162            check.init();
163            registerCheck(check);
164        }
165        else if (module instanceof TreeWalkerFilter filter) {
166            filters.add(filter);
167        }
168        else {
169            throw new CheckstyleException(
170                "TreeWalker is not allowed as a parent of " + name
171                        + " Please review 'Parent Module' section for this Check in web"
172                        + " documentation if Check is standard.");
173        }
174    }
175
176    /**
177     * {@inheritDoc} Processes the file.
178     *
179     * @noinspection ProhibitedExceptionThrown
180     * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey
181     *     skipFileOnJavaParseException field
182     */
183    @Override
184    protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
185        // check if already checked and passed the file
186        if (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty()) {
187            final FileContents contents = getFileContents();
188            DetailAST rootAST = null;
189            // whether skip the procedure after parsing Java files.
190            boolean skip = false;
191            try {
192                rootAST = JavaParser.parse(contents);
193            }
194            // -@cs[IllegalCatch] There is no other way to obey skipFileOnJavaParseException field
195            catch (Exception exc) {
196                if (!skipFileOnJavaParseException) {
197                    throw exc;
198                }
199                skip = true;
200                violations.add(new Violation(1, Definitions.CHECKSTYLE_BUNDLE, PARSE_EXCEPTION_MSG,
201                            new Object[] {exc.getMessage()}, javaParseExceptionSeverity, null,
202                            getClass(), null));
203                addViolations(violations);
204            }
205
206            if (!skip) {
207                if (!ordinaryChecks.isEmpty()) {
208                    walk(rootAST, contents, AstState.ORDINARY);
209                }
210                if (!commentChecks.isEmpty()) {
211                    final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST);
212                    walk(astWithComments, contents, AstState.WITH_COMMENTS);
213                }
214                if (filters.isEmpty()) {
215                    addViolations(violations);
216                }
217                else {
218                    final SortedSet<Violation> filteredViolations =
219                            getFilteredViolations(file.getAbsolutePath(), contents, rootAST);
220                    addViolations(filteredViolations);
221                }
222            }
223            violations.clear();
224        }
225    }
226
227    /**
228     * Returns filtered set of {@link Violation}.
229     *
230     * @param fileName path to the file
231     * @param fileContents the contents of the file
232     * @param rootAST root AST element {@link DetailAST} of the file
233     * @return filtered set of violations
234     */
235    private SortedSet<Violation> getFilteredViolations(
236            String fileName, FileContents fileContents, DetailAST rootAST) {
237        final SortedSet<Violation> result = new TreeSet<>(violations);
238        for (Violation element : violations) {
239            final TreeWalkerAuditEvent event =
240                    new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
241            for (TreeWalkerFilter filter : filters) {
242                if (!filter.accept(event)) {
243                    result.remove(element);
244                    break;
245                }
246            }
247        }
248        return result;
249    }
250
251    /**
252     * Register a check for a given configuration.
253     *
254     * @param check the check to register
255     * @throws CheckstyleException if an error occurs
256     */
257    private void registerCheck(AbstractCheck check) throws CheckstyleException {
258        final int[] tokens;
259        final Set<String> checkTokens = check.getTokenNames();
260        if (checkTokens.isEmpty()) {
261            tokens = check.getDefaultTokens();
262        }
263        else {
264            tokens = check.getRequiredTokens();
265
266            // register configured tokens
267            final int[] acceptableTokens = check.getAcceptableTokens();
268            Arrays.sort(acceptableTokens);
269            for (String token : checkTokens) {
270                final int tokenId = TokenUtil.getTokenId(token);
271                if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
272                    registerCheck(tokenId, check);
273                }
274                else {
275                    final String message = String.format(Locale.ROOT, "Token \"%s\" was "
276                            + "not found in Acceptable tokens list in check %s",
277                            token, check.getClass().getName());
278                    throw new CheckstyleException(message);
279                }
280            }
281        }
282        for (int element : tokens) {
283            registerCheck(element, check);
284        }
285        if (check.isCommentNodesRequired()) {
286            commentChecks.add(check);
287        }
288        else {
289            ordinaryChecks.add(check);
290        }
291    }
292
293    /**
294     * Register a check for a specified token id.
295     *
296     * @param tokenId the id of the token
297     * @param check the check to register
298     * @throws CheckstyleException if Check is misconfigured
299     */
300    private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException {
301        if (check.isCommentNodesRequired()) {
302            tokenToCommentChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
303                    .add(check);
304        }
305        else if (TokenUtil.isCommentType(tokenId)) {
306            final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type "
307                    + "token ('%s') and should override 'isCommentNodesRequired()' "
308                    + "method to return 'true'", check.getClass().getName(),
309                    TokenUtil.getTokenName(tokenId));
310            throw new CheckstyleException(message);
311        }
312        else {
313            tokenToOrdinaryChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
314                    .add(check);
315        }
316    }
317
318    /**
319     * Initiates the walk of an AST.
320     *
321     * @param ast the root AST
322     * @param contents the contents of the file the AST was generated from.
323     * @param astState state of AST.
324     */
325    private void walk(DetailAST ast, FileContents contents,
326            AstState astState) {
327        notifyBegin(ast, contents, astState);
328        processIter(ast, astState);
329        notifyEnd(ast, astState);
330    }
331
332    /**
333     * Notify checks that we are about to begin walking a tree.
334     *
335     * @param rootAST the root of the tree.
336     * @param contents the contents of the file the AST was generated from.
337     * @param astState state of AST.
338     */
339    private void notifyBegin(DetailAST rootAST, FileContents contents,
340            AstState astState) {
341        final Set<AbstractCheck> checks;
342
343        if (astState == AstState.WITH_COMMENTS) {
344            checks = commentChecks;
345        }
346        else {
347            checks = ordinaryChecks;
348        }
349
350        for (AbstractCheck check : checks) {
351            check.setFileContents(contents);
352            check.clearViolations();
353            check.beginTree(rootAST);
354        }
355    }
356
357    /**
358     * Notify checks that we have finished walking a tree.
359     *
360     * @param rootAST the root of the tree.
361     * @param astState state of AST.
362     */
363    private void notifyEnd(DetailAST rootAST, AstState astState) {
364        final Set<AbstractCheck> checks;
365
366        if (astState == AstState.WITH_COMMENTS) {
367            checks = commentChecks;
368        }
369        else {
370            checks = ordinaryChecks;
371        }
372
373        for (AbstractCheck check : checks) {
374            check.finishTree(rootAST);
375            violations.addAll(check.getViolations());
376        }
377    }
378
379    /**
380     * Notify checks that visiting a node.
381     *
382     * @param ast the node to notify for.
383     * @param astState state of AST.
384     */
385    private void notifyVisit(DetailAST ast, AstState astState) {
386        final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
387
388        if (visitors != null) {
389            for (AbstractCheck check : visitors) {
390                check.visitToken(ast);
391            }
392        }
393    }
394
395    /**
396     * Notify checks that leaving a node.
397     *
398     * @param ast
399     *        the node to notify for
400     * @param astState state of AST.
401     */
402    private void notifyLeave(DetailAST ast, AstState astState) {
403        final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
404
405        if (visitors != null) {
406            for (AbstractCheck check : visitors) {
407                check.leaveToken(ast);
408            }
409        }
410    }
411
412    /**
413     * Method returns list of checks.
414     *
415     * @param ast
416     *            the node to notify for
417     * @param astState
418     *            state of AST.
419     * @return list of visitors
420     */
421    private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) {
422        final Collection<AbstractCheck> visitors;
423        final int tokenId = ast.getType();
424
425        if (astState == AstState.WITH_COMMENTS) {
426            visitors = tokenToCommentChecks.get(tokenId);
427        }
428        else {
429            visitors = tokenToOrdinaryChecks.get(tokenId);
430        }
431        return visitors;
432    }
433
434    @Override
435    public void destroy() {
436        ordinaryChecks.forEach(AbstractCheck::destroy);
437        commentChecks.forEach(AbstractCheck::destroy);
438        super.destroy();
439    }
440
441    @Override
442    public Set<String> getExternalResourceLocations() {
443        return Stream.concat(filters.stream(),
444                Stream.concat(ordinaryChecks.stream(), commentChecks.stream()))
445            .filter(ExternalResourceHolder.class::isInstance)
446            .flatMap(resource -> {
447                return ((ExternalResourceHolder) resource)
448                        .getExternalResourceLocations().stream();
449            })
450            .collect(Collectors.toUnmodifiableSet());
451    }
452
453    /**
454     * Processes a node calling interested checks at each node.
455     * Uses iterative algorithm.
456     *
457     * @param root the root of tree for process
458     * @param astState state of AST.
459     */
460    private void processIter(DetailAST root, AstState astState) {
461        DetailAST curNode = root;
462        while (curNode != null) {
463            notifyVisit(curNode, astState);
464            DetailAST toVisit = curNode.getFirstChild();
465            while (curNode != null && toVisit == null) {
466                notifyLeave(curNode, astState);
467                toVisit = curNode.getNextSibling();
468                curNode = curNode.getParent();
469            }
470            curNode = toVisit;
471        }
472    }
473
474    /**
475     * Creates a new {@link SortedSet} with a deterministic order based on the
476     * Check's name before the default ordering.
477     *
478     * @return The new {@link SortedSet}.
479     */
480    private static SortedSet<AbstractCheck> createNewCheckSortedSet() {
481        return new TreeSet<>(
482                Comparator.<AbstractCheck, String>comparing(check -> check.getClass().getName())
483                        .thenComparing(AbstractCheck::getId,
484                                Comparator.nullsLast(Comparator.naturalOrder()))
485                        .thenComparingInt(AbstractCheck::hashCode));
486    }
487
488    /**
489     * State of AST.
490     * Indicates whether tree contains certain nodes.
491     */
492    private enum AstState {
493
494        /**
495         * Ordinary tree.
496         */
497        ORDINARY,
498
499        /**
500         * AST contains comment nodes.
501         */
502        WITH_COMMENTS,
503
504    }
505
506}