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