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