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