001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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.HashMap;
026import java.util.HashSet;
027import java.util.Locale;
028import java.util.Map;
029import java.util.Set;
030import java.util.SortedSet;
031import java.util.TreeSet;
032
033import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
034import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
035import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
036import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
037import com.puppycrawl.tools.checkstyle.api.Configuration;
038import com.puppycrawl.tools.checkstyle.api.Context;
039import com.puppycrawl.tools.checkstyle.api.DetailAST;
040import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
041import com.puppycrawl.tools.checkstyle.api.FileContents;
042import com.puppycrawl.tools.checkstyle.api.FileText;
043import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
044import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
045
046/**
047 * Responsible for walking an abstract syntax tree and notifying interested
048 * checks at each each node.
049 *
050 */
051@FileStatefulCheck
052public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder {
053
054    /** Default distance between tab stops. */
055    private static final int DEFAULT_TAB_WIDTH = 8;
056
057    /** Maps from token name to ordinary checks. */
058    private final Map<String, Set<AbstractCheck>> tokenToOrdinaryChecks =
059        new HashMap<>();
060
061    /** Maps from token name to comment checks. */
062    private final Map<String, Set<AbstractCheck>> tokenToCommentChecks =
063            new HashMap<>();
064
065    /** Registered ordinary checks, that don't use comment nodes. */
066    private final Set<AbstractCheck> ordinaryChecks = new HashSet<>();
067
068    /** Registered comment checks. */
069    private final Set<AbstractCheck> commentChecks = new HashSet<>();
070
071    /** The ast filters. */
072    private final Set<TreeWalkerFilter> filters = new HashSet<>();
073
074    /** The sorted set of messages. */
075    private final SortedSet<LocalizedMessage> messages = new TreeSet<>();
076
077    /** The distance between tab stops. */
078    private int tabWidth = DEFAULT_TAB_WIDTH;
079
080    /** Class loader to resolve classes with. **/
081    private ClassLoader classLoader;
082
083    /** Context of child components. */
084    private Context childContext;
085
086    /** A factory for creating submodules (i.e. the Checks) */
087    private ModuleFactory moduleFactory;
088
089    /**
090     * Creates a new {@code TreeWalker} instance.
091     */
092    public TreeWalker() {
093        setFileExtensions("java");
094    }
095
096    /**
097     * Sets tab width.
098     * @param tabWidth the distance between tab stops
099     */
100    public void setTabWidth(int tabWidth) {
101        this.tabWidth = tabWidth;
102    }
103
104    /**
105     * Sets cache file.
106     * @deprecated Use {@link Checker#setCacheFile} instead. It does not do anything now. We just
107     *             keep the setter for transition period to the same option in Checker. The
108     *             method will be completely removed in Checkstyle 8.0. See
109     *             <a href="https://github.com/checkstyle/checkstyle/issues/2883">issue#2883</a>
110     * @param fileName the cache file
111     */
112    @Deprecated
113    public void setCacheFile(String fileName) {
114        // Deprecated
115    }
116
117    /**
118     * Sets classLoader to load class.
119     * @param classLoader class loader to resolve classes with.
120     */
121    public void setClassLoader(ClassLoader classLoader) {
122        this.classLoader = classLoader;
123    }
124
125    /**
126     * Sets the module factory for creating child modules (Checks).
127     * @param moduleFactory the factory
128     */
129    public void setModuleFactory(ModuleFactory moduleFactory) {
130        this.moduleFactory = moduleFactory;
131    }
132
133    @Override
134    public void finishLocalSetup() {
135        final DefaultContext checkContext = new DefaultContext();
136        checkContext.add("classLoader", classLoader);
137        checkContext.add("severity", getSeverity());
138        checkContext.add("tabWidth", String.valueOf(tabWidth));
139
140        childContext = checkContext;
141    }
142
143    /**
144     * {@inheritDoc} Creates child module.
145     * @noinspection ChainOfInstanceofChecks
146     */
147    @Override
148    public void setupChild(Configuration childConf)
149            throws CheckstyleException {
150        final String name = childConf.getName();
151        final Object module;
152
153        try {
154            module = moduleFactory.createModule(name);
155            if (module instanceof AutomaticBean) {
156                final AutomaticBean bean = (AutomaticBean) module;
157                bean.contextualize(childContext);
158                bean.configure(childConf);
159            }
160        }
161        catch (final CheckstyleException ex) {
162            throw new CheckstyleException("cannot initialize module " + name
163                    + " - " + ex.getMessage(), ex);
164        }
165        if (module instanceof AbstractCheck) {
166            final AbstractCheck check = (AbstractCheck) module;
167            check.init();
168            registerCheck(check);
169        }
170        else if (module instanceof TreeWalkerFilter) {
171            final TreeWalkerFilter filter = (TreeWalkerFilter) module;
172            filters.add(filter);
173        }
174        else {
175            throw new CheckstyleException(
176                "TreeWalker is not allowed as a parent of " + name
177                        + " Please review 'Parent Module' section for this Check in web"
178                        + " documentation if Check is standard.");
179        }
180    }
181
182    @Override
183    protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
184        // check if already checked and passed the file
185        if (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty()) {
186            final FileContents contents = new FileContents(fileText);
187            final DetailAST rootAST = JavaParser.parse(contents);
188            if (!ordinaryChecks.isEmpty()) {
189                walk(rootAST, contents, AstState.ORDINARY);
190            }
191            if (!commentChecks.isEmpty()) {
192                final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST);
193                walk(astWithComments, contents, AstState.WITH_COMMENTS);
194            }
195            if (filters.isEmpty()) {
196                addMessages(messages);
197            }
198            else {
199                final SortedSet<LocalizedMessage> filteredMessages =
200                    getFilteredMessages(file.getAbsolutePath(), contents, rootAST);
201                addMessages(filteredMessages);
202            }
203            messages.clear();
204        }
205    }
206
207    /**
208     * Returns filtered set of {@link LocalizedMessage}.
209     * @param fileName path to the file
210     * @param fileContents the contents of the file
211     * @param rootAST root AST element {@link DetailAST} of the file
212     * @return filtered set of messages
213     */
214    private SortedSet<LocalizedMessage> getFilteredMessages(
215            String fileName, FileContents fileContents, DetailAST rootAST) {
216        final SortedSet<LocalizedMessage> result = new TreeSet<>(messages);
217        for (LocalizedMessage element : messages) {
218            final TreeWalkerAuditEvent event =
219                    new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
220            for (TreeWalkerFilter filter : filters) {
221                if (!filter.accept(event)) {
222                    result.remove(element);
223                    break;
224                }
225            }
226        }
227        return result;
228    }
229
230    /**
231     * Register a check for a given configuration.
232     * @param check the check to register
233     * @throws CheckstyleException if an error occurs
234     */
235    private void registerCheck(AbstractCheck check)
236            throws CheckstyleException {
237        validateDefaultTokens(check);
238        final int[] tokens;
239        final Set<String> checkTokens = check.getTokenNames();
240        if (checkTokens.isEmpty()) {
241            tokens = check.getDefaultTokens();
242        }
243        else {
244            tokens = check.getRequiredTokens();
245
246            //register configured tokens
247            final int[] acceptableTokens = check.getAcceptableTokens();
248            Arrays.sort(acceptableTokens);
249            for (String token : checkTokens) {
250                final int tokenId = TokenUtil.getTokenId(token);
251                if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
252                    registerCheck(token, check);
253                }
254                else {
255                    final String message = String.format(Locale.ROOT, "Token \"%s\" was "
256                            + "not found in Acceptable tokens list in check %s",
257                            token, check.getClass().getName());
258                    throw new CheckstyleException(message);
259                }
260            }
261        }
262        for (int element : tokens) {
263            registerCheck(element, check);
264        }
265        if (check.isCommentNodesRequired()) {
266            commentChecks.add(check);
267        }
268        else {
269            ordinaryChecks.add(check);
270        }
271    }
272
273    /**
274     * Register a check for a specified token id.
275     * @param tokenId the id of the token
276     * @param check the check to register
277     * @throws CheckstyleException if Check is misconfigured
278     */
279    private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException {
280        registerCheck(TokenUtil.getTokenName(tokenId), check);
281    }
282
283    /**
284     * Register a check for a specified token name.
285     * @param token the name of the token
286     * @param check the check to register
287     * @throws CheckstyleException if Check is misconfigured
288     */
289    private void registerCheck(String token, AbstractCheck check) throws CheckstyleException {
290        if (check.isCommentNodesRequired()) {
291            tokenToCommentChecks.computeIfAbsent(token, empty -> new HashSet<>()).add(check);
292        }
293        else if (TokenUtil.isCommentType(token)) {
294            final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type "
295                    + "token ('%s') and should override 'isCommentNodesRequired()' "
296                    + "method to return 'true'", check.getClass().getName(), token);
297            throw new CheckstyleException(message);
298        }
299        else {
300            tokenToOrdinaryChecks.computeIfAbsent(token, empty -> new HashSet<>()).add(check);
301        }
302    }
303
304    /**
305     * Validates that check's required tokens are subset of default tokens.
306     * @param check to validate
307     * @throws CheckstyleException when validation of default tokens fails
308     */
309    private static void validateDefaultTokens(AbstractCheck check) throws CheckstyleException {
310        final int[] defaultTokens = check.getDefaultTokens();
311        Arrays.sort(defaultTokens);
312        for (final int token : check.getRequiredTokens()) {
313            if (Arrays.binarySearch(defaultTokens, token) < 0) {
314                final String message = String.format(Locale.ROOT, "Token \"%s\" from required "
315                        + "tokens was not found in default tokens list in check %s",
316                        token, check.getClass().getName());
317                throw new CheckstyleException(message);
318            }
319        }
320    }
321
322    /**
323     * Initiates the walk of an AST.
324     * @param ast the root AST
325     * @param contents the contents of the file the AST was generated from.
326     * @param astState state of AST.
327     */
328    private void walk(DetailAST ast, FileContents contents,
329            AstState astState) {
330        notifyBegin(ast, contents, astState);
331        processIter(ast, astState);
332        notifyEnd(ast, astState);
333    }
334
335    /**
336     * Notify checks that we are about to begin walking a tree.
337     * @param rootAST the root of the tree.
338     * @param contents the contents of the file the AST was generated from.
339     * @param astState state of AST.
340     */
341    private void notifyBegin(DetailAST rootAST, FileContents contents,
342            AstState astState) {
343        final Set<AbstractCheck> checks;
344
345        if (astState == AstState.WITH_COMMENTS) {
346            checks = commentChecks;
347        }
348        else {
349            checks = ordinaryChecks;
350        }
351
352        for (AbstractCheck check : checks) {
353            check.setFileContents(contents);
354            check.clearMessages();
355            check.beginTree(rootAST);
356        }
357    }
358
359    /**
360     * Notify checks that we have finished walking a tree.
361     * @param rootAST the root of the tree.
362     * @param astState state of AST.
363     */
364    private void notifyEnd(DetailAST rootAST, AstState astState) {
365        final Set<AbstractCheck> checks;
366
367        if (astState == AstState.WITH_COMMENTS) {
368            checks = commentChecks;
369        }
370        else {
371            checks = ordinaryChecks;
372        }
373
374        for (AbstractCheck check : checks) {
375            check.finishTree(rootAST);
376            messages.addAll(check.getMessages());
377        }
378    }
379
380    /**
381     * Notify checks that visiting a node.
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     * @param ast
398     *        the node to notify for
399     * @param astState state of AST.
400     */
401    private void notifyLeave(DetailAST ast, AstState astState) {
402        final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
403
404        if (visitors != null) {
405            for (AbstractCheck check : visitors) {
406                check.leaveToken(ast);
407            }
408        }
409    }
410
411    /**
412     * Method returns list of checks.
413     *
414     * @param ast
415     *            the node to notify for
416     * @param astState
417     *            state of AST.
418     * @return list of visitors
419     */
420    private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) {
421        final Collection<AbstractCheck> visitors;
422        final String tokenType = TokenUtil.getTokenName(ast.getType());
423
424        if (astState == AstState.WITH_COMMENTS) {
425            visitors = tokenToCommentChecks.get(tokenType);
426        }
427        else {
428            visitors = tokenToOrdinaryChecks.get(tokenType);
429        }
430        return visitors;
431    }
432
433    @Override
434    public void destroy() {
435        ordinaryChecks.forEach(AbstractCheck::destroy);
436        commentChecks.forEach(AbstractCheck::destroy);
437        super.destroy();
438    }
439
440    @Override
441    public Set<String> getExternalResourceLocations() {
442        final Set<String> ordinaryChecksResources =
443                getExternalResourceLocationsOfChecks(ordinaryChecks);
444        final Set<String> commentChecksResources =
445                getExternalResourceLocationsOfChecks(commentChecks);
446        final Set<String> filtersResources =
447                getExternalResourceLocationsOfFilters();
448        final int resultListSize = commentChecksResources.size()
449                + ordinaryChecksResources.size()
450                + filtersResources.size();
451        final Set<String> resourceLocations = new HashSet<>(resultListSize);
452        resourceLocations.addAll(ordinaryChecksResources);
453        resourceLocations.addAll(commentChecksResources);
454        resourceLocations.addAll(filtersResources);
455        return resourceLocations;
456    }
457
458    /**
459     * Returns a set of external configuration resource locations which are used by the filters set.
460     * @return a set of external configuration resource locations which are used by the filters set.
461     */
462    private Set<String> getExternalResourceLocationsOfFilters() {
463        final Set<String> externalConfigurationResources = new HashSet<>();
464        filters.stream().filter(filter -> filter instanceof ExternalResourceHolder)
465                .forEach(filter -> {
466                    final Set<String> checkExternalResources =
467                        ((ExternalResourceHolder) filter).getExternalResourceLocations();
468                    externalConfigurationResources.addAll(checkExternalResources);
469                });
470        return externalConfigurationResources;
471    }
472
473    /**
474     * Returns a set of external configuration resource locations which are used by the checks set.
475     * @param checks a set of checks.
476     * @return a set of external configuration resource locations which are used by the checks set.
477     */
478    private static Set<String> getExternalResourceLocationsOfChecks(Set<AbstractCheck> checks) {
479        final Set<String> externalConfigurationResources = new HashSet<>();
480        checks.stream().filter(check -> check instanceof ExternalResourceHolder).forEach(check -> {
481            final Set<String> checkExternalResources =
482                ((ExternalResourceHolder) check).getExternalResourceLocations();
483            externalConfigurationResources.addAll(checkExternalResources);
484        });
485        return externalConfigurationResources;
486    }
487
488    /**
489     * Processes a node calling interested checks at each node.
490     * Uses iterative algorithm.
491     * @param root the root of tree for process
492     * @param astState state of AST.
493     */
494    private void processIter(DetailAST root, AstState astState) {
495        DetailAST curNode = root;
496        while (curNode != null) {
497            notifyVisit(curNode, astState);
498            DetailAST toVisit = curNode.getFirstChild();
499            while (curNode != null && toVisit == null) {
500                notifyLeave(curNode, astState);
501                toVisit = curNode.getNextSibling();
502                curNode = curNode.getParent();
503            }
504            curNode = toVisit;
505        }
506    }
507
508    /**
509     * State of AST.
510     * Indicates whether tree contains certain nodes.
511     */
512    private enum AstState {
513
514        /**
515         * Ordinary tree.
516         */
517        ORDINARY,
518
519        /**
520         * AST contains comment nodes.
521         */
522        WITH_COMMENTS,
523
524    }
525
526}