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.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.CommonUtil;
045import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
046
047/**
048 * Responsible for walking an abstract syntax tree and notifying interested
049 * checks at each each node.
050 *
051 */
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 = moduleFactory.createModule(name);
152        if (module instanceof AutomaticBean) {
153            final AutomaticBean bean = (AutomaticBean) module;
154            bean.contextualize(childContext);
155            bean.configure(childConf);
156        }
157        if (module instanceof AbstractCheck) {
158            final AbstractCheck check = (AbstractCheck) module;
159            check.init();
160            registerCheck(check);
161        }
162        else if (module instanceof TreeWalkerFilter) {
163            final TreeWalkerFilter filter = (TreeWalkerFilter) module;
164            filters.add(filter);
165        }
166        else {
167            throw new CheckstyleException(
168                "TreeWalker is not allowed as a parent of " + name
169                        + " Please review 'Parent Module' section for this Check in web"
170                        + " documentation if Check is standard.");
171        }
172    }
173
174    @Override
175    protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
176        // check if already checked and passed the file
177        if (CommonUtil.matchesFileExtension(file, getFileExtensions())
178                && (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty())) {
179            final FileContents contents = new FileContents(fileText);
180            final DetailAST rootAST = JavaParser.parse(contents);
181            if (!ordinaryChecks.isEmpty()) {
182                walk(rootAST, contents, AstState.ORDINARY);
183            }
184            if (!commentChecks.isEmpty()) {
185                final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST);
186                walk(astWithComments, contents, AstState.WITH_COMMENTS);
187            }
188            if (filters.isEmpty()) {
189                addMessages(messages);
190            }
191            else {
192                final SortedSet<LocalizedMessage> filteredMessages =
193                    getFilteredMessages(file.getPath(), contents, rootAST);
194                addMessages(filteredMessages);
195            }
196            messages.clear();
197        }
198    }
199
200    /**
201     * Returns filtered set of {@link LocalizedMessage}.
202     * @param fileName path to the file
203     * @param fileContents the contents of the file
204     * @param rootAST root AST element {@link DetailAST} of the file
205     * @return filtered set of messages
206     */
207    private SortedSet<LocalizedMessage> getFilteredMessages(
208            String fileName, FileContents fileContents, DetailAST rootAST) {
209        final SortedSet<LocalizedMessage> result = new TreeSet<>(messages);
210        for (LocalizedMessage element : messages) {
211            final TreeWalkerAuditEvent event =
212                    new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
213            for (TreeWalkerFilter filter : filters) {
214                if (!filter.accept(event)) {
215                    result.remove(element);
216                    break;
217                }
218            }
219        }
220        return result;
221    }
222
223    /**
224     * Register a check for a given configuration.
225     * @param check the check to register
226     * @throws CheckstyleException if an error occurs
227     */
228    private void registerCheck(AbstractCheck check)
229            throws CheckstyleException {
230        validateDefaultTokens(check);
231        final int[] tokens;
232        final Set<String> checkTokens = check.getTokenNames();
233        if (checkTokens.isEmpty()) {
234            tokens = check.getDefaultTokens();
235        }
236        else {
237            tokens = check.getRequiredTokens();
238
239            //register configured tokens
240            final int[] acceptableTokens = check.getAcceptableTokens();
241            Arrays.sort(acceptableTokens);
242            for (String token : checkTokens) {
243                final int tokenId = TokenUtil.getTokenId(token);
244                if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
245                    registerCheck(token, check);
246                }
247                else {
248                    final String message = String.format(Locale.ROOT, "Token \"%s\" was "
249                            + "not found in Acceptable tokens list in check %s",
250                            token, check.getClass().getName());
251                    throw new CheckstyleException(message);
252                }
253            }
254        }
255        for (int element : tokens) {
256            registerCheck(element, check);
257        }
258        if (check.isCommentNodesRequired()) {
259            commentChecks.add(check);
260        }
261        else {
262            ordinaryChecks.add(check);
263        }
264    }
265
266    /**
267     * Register a check for a specified token id.
268     * @param tokenId the id of the token
269     * @param check the check to register
270     * @throws CheckstyleException if Check is misconfigured
271     */
272    private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException {
273        registerCheck(TokenUtil.getTokenName(tokenId), check);
274    }
275
276    /**
277     * Register a check for a specified token name.
278     * @param token the name of the token
279     * @param check the check to register
280     * @throws CheckstyleException if Check is misconfigured
281     */
282    private void registerCheck(String token, AbstractCheck check) throws CheckstyleException {
283        if (check.isCommentNodesRequired()) {
284            tokenToCommentChecks.computeIfAbsent(token, empty -> new HashSet<>()).add(check);
285        }
286        else if (TokenUtil.isCommentType(token)) {
287            final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type "
288                    + "token ('%s') and should override 'isCommentNodesRequired()' "
289                    + "method to return 'true'", check.getClass().getName(), token);
290            throw new CheckstyleException(message);
291        }
292        else {
293            tokenToOrdinaryChecks.computeIfAbsent(token, empty -> new HashSet<>()).add(check);
294        }
295    }
296
297    /**
298     * Validates that check's required tokens are subset of default tokens.
299     * @param check to validate
300     * @throws CheckstyleException when validation of default tokens fails
301     */
302    private static void validateDefaultTokens(AbstractCheck check) throws CheckstyleException {
303        if (check.getRequiredTokens().length != 0) {
304            final int[] defaultTokens = check.getDefaultTokens();
305            Arrays.sort(defaultTokens);
306            for (final int token : check.getRequiredTokens()) {
307                if (Arrays.binarySearch(defaultTokens, token) < 0) {
308                    final String message = String.format(Locale.ROOT, "Token \"%s\" from required "
309                            + "tokens was not found in default tokens list in check %s",
310                            token, check.getClass().getName());
311                    throw new CheckstyleException(message);
312                }
313            }
314        }
315    }
316
317    /**
318     * Initiates the walk of an AST.
319     * @param ast the root AST
320     * @param contents the contents of the file the AST was generated from.
321     * @param astState state of AST.
322     */
323    private void walk(DetailAST ast, FileContents contents,
324            AstState astState) {
325        notifyBegin(ast, contents, astState);
326
327        // empty files are not flagged by javac, will yield ast == null
328        if (ast != null) {
329            processIter(ast, astState);
330        }
331        notifyEnd(ast, astState);
332    }
333
334    /**
335     * Notify checks that we are about to begin walking a tree.
336     * @param rootAST the root of the tree.
337     * @param contents the contents of the file the AST was generated from.
338     * @param astState state of AST.
339     */
340    private void notifyBegin(DetailAST rootAST, FileContents contents,
341            AstState astState) {
342        final Set<AbstractCheck> checks;
343
344        if (astState == AstState.WITH_COMMENTS) {
345            checks = commentChecks;
346        }
347        else {
348            checks = ordinaryChecks;
349        }
350
351        for (AbstractCheck check : checks) {
352            check.setFileContents(contents);
353            check.clearMessages();
354            check.beginTree(rootAST);
355        }
356    }
357
358    /**
359     * Notify checks that we have finished walking a tree.
360     * @param rootAST the root of the tree.
361     * @param astState state of AST.
362     */
363    private void notifyEnd(DetailAST rootAST, AstState astState) {
364        final Set<AbstractCheck> checks;
365
366        if (astState == AstState.WITH_COMMENTS) {
367            checks = commentChecks;
368        }
369        else {
370            checks = ordinaryChecks;
371        }
372
373        for (AbstractCheck check : checks) {
374            check.finishTree(rootAST);
375            messages.addAll(check.getMessages());
376        }
377    }
378
379    /**
380     * Notify checks that visiting a node.
381     * @param ast the node to notify for.
382     * @param astState state of AST.
383     */
384    private void notifyVisit(DetailAST ast, AstState astState) {
385        final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
386
387        if (visitors != null) {
388            for (AbstractCheck check : visitors) {
389                check.visitToken(ast);
390            }
391        }
392    }
393
394    /**
395     * Notify checks that leaving a node.
396     * @param ast
397     *        the node to notify for
398     * @param astState state of AST.
399     */
400    private void notifyLeave(DetailAST ast, AstState astState) {
401        final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
402
403        if (visitors != null) {
404            for (AbstractCheck check : visitors) {
405                check.leaveToken(ast);
406            }
407        }
408    }
409
410    /**
411     * Method returns list of checks.
412     *
413     * @param ast
414     *            the node to notify for
415     * @param astState
416     *            state of AST.
417     * @return list of visitors
418     */
419    private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) {
420        Collection<AbstractCheck> visitors = null;
421        final String tokenType = TokenUtil.getTokenName(ast.getType());
422
423        if (astState == AstState.WITH_COMMENTS) {
424            if (tokenToCommentChecks.containsKey(tokenType)) {
425                visitors = tokenToCommentChecks.get(tokenType);
426            }
427        }
428        else {
429            if (tokenToOrdinaryChecks.containsKey(tokenType)) {
430                visitors = tokenToOrdinaryChecks.get(tokenType);
431            }
432        }
433        return visitors;
434    }
435
436    @Override
437    public void destroy() {
438        ordinaryChecks.forEach(AbstractCheck::destroy);
439        commentChecks.forEach(AbstractCheck::destroy);
440        super.destroy();
441    }
442
443    @Override
444    public Set<String> getExternalResourceLocations() {
445        final Set<String> ordinaryChecksResources =
446                getExternalResourceLocationsOfChecks(ordinaryChecks);
447        final Set<String> commentChecksResources =
448                getExternalResourceLocationsOfChecks(commentChecks);
449        final Set<String> filtersResources =
450                getExternalResourceLocationsOfFilters();
451        final int resultListSize = commentChecksResources.size()
452                + ordinaryChecksResources.size()
453                + filtersResources.size();
454        final Set<String> resourceLocations = new HashSet<>(resultListSize);
455        resourceLocations.addAll(ordinaryChecksResources);
456        resourceLocations.addAll(commentChecksResources);
457        resourceLocations.addAll(filtersResources);
458        return resourceLocations;
459    }
460
461    /**
462     * Returns a set of external configuration resource locations which are used by the filters set.
463     * @return a set of external configuration resource locations which are used by the filters set.
464     */
465    private Set<String> getExternalResourceLocationsOfFilters() {
466        final Set<String> externalConfigurationResources = new HashSet<>();
467        filters.stream().filter(filter -> filter instanceof ExternalResourceHolder)
468                .forEach(filter -> {
469                    final Set<String> checkExternalResources =
470                        ((ExternalResourceHolder) filter).getExternalResourceLocations();
471                    externalConfigurationResources.addAll(checkExternalResources);
472                });
473        return externalConfigurationResources;
474    }
475
476    /**
477     * Returns a set of external configuration resource locations which are used by the checks set.
478     * @param checks a set of checks.
479     * @return a set of external configuration resource locations which are used by the checks set.
480     */
481    private static Set<String> getExternalResourceLocationsOfChecks(Set<AbstractCheck> checks) {
482        final Set<String> externalConfigurationResources = new HashSet<>();
483        checks.stream().filter(check -> check instanceof ExternalResourceHolder).forEach(check -> {
484            final Set<String> checkExternalResources =
485                ((ExternalResourceHolder) check).getExternalResourceLocations();
486            externalConfigurationResources.addAll(checkExternalResources);
487        });
488        return externalConfigurationResources;
489    }
490
491    /**
492     * Processes a node calling interested checks at each node.
493     * Uses iterative algorithm.
494     * @param root the root of tree for process
495     * @param astState state of AST.
496     */
497    private void processIter(DetailAST root, AstState astState) {
498        DetailAST curNode = root;
499        while (curNode != null) {
500            notifyVisit(curNode, astState);
501            DetailAST toVisit = curNode.getFirstChild();
502            while (curNode != null && toVisit == null) {
503                notifyLeave(curNode, astState);
504                toVisit = curNode.getNextSibling();
505                if (toVisit == null) {
506                    curNode = curNode.getParent();
507                }
508            }
509            curNode = toVisit;
510        }
511    }
512
513    /**
514     * State of AST.
515     * Indicates whether tree contains certain nodes.
516     */
517    private enum AstState {
518
519        /**
520         * Ordinary tree.
521         */
522        ORDINARY,
523
524        /**
525         * AST contains comment nodes.
526         */
527        WITH_COMMENTS
528
529    }
530
531}