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