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