1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.Arrays;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Locale;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
30  import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage;
31  import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus;
32  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
33  import com.puppycrawl.tools.checkstyle.api.DetailAST;
34  import com.puppycrawl.tools.checkstyle.api.DetailNode;
35  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
36  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
39  
40  /**
41   * Base class for Checks that process Javadoc comments.
42   * @noinspection NoopMethodInAbstractClass
43   */
44  public abstract class AbstractJavadocCheck extends AbstractCheck {
45  
46      /**
47       * Message key of error message. Missed close HTML tag breaks structure
48       * of parse tree, so parser stops parsing and generates such error
49       * message. This case is special because parser prints error like
50       * {@code "no viable alternative at input 'b \n *\n'"} and it is not
51       * clear that error is about missed close HTML tag.
52       */
53      public static final String MSG_JAVADOC_MISSED_HTML_CLOSE =
54              JavadocDetailNodeParser.MSG_JAVADOC_MISSED_HTML_CLOSE;
55  
56      /**
57       * Message key of error message.
58       */
59      public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
60              JavadocDetailNodeParser.MSG_JAVADOC_WRONG_SINGLETON_TAG;
61  
62      /**
63       * Parse error while rule recognition.
64       */
65      public static final String MSG_JAVADOC_PARSE_RULE_ERROR =
66              JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR;
67  
68      /**
69       * Key is "line:column". Value is {@link DetailNode} tree. Map is stored in {@link ThreadLocal}
70       * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
71       */
72      private static final ThreadLocal<Map<String, ParseStatus>> TREE_CACHE =
73              ThreadLocal.withInitial(HashMap::new);
74  
75      /**
76       * The file context.
77       * @noinspection ThreadLocalNotStaticFinal
78       */
79      private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
80  
81      /** The javadoc tokens the check is interested in. */
82      private final Set<Integer> javadocTokens = new HashSet<>();
83  
84      /**
85       * This property determines if a check should log a violation upon encountering javadoc with
86       * non-tight html. The default return value for this method is set to false since checks
87       * generally tend to be fine with non tight html. It can be set through config file if a check
88       * is to log violation upon encountering non-tight HTML in javadoc.
89       *
90       * @see ParseStatus#isNonTight()
91       * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
92       *     Tight HTML rules</a>
93       */
94      private boolean violateExecutionOnNonTightHtml;
95  
96      /**
97       * Returns the default javadoc token types a check is interested in.
98       * @return the default javadoc token types
99       * @see JavadocTokenTypes
100      */
101     public abstract int[] getDefaultJavadocTokens();
102 
103     /**
104      * Called to process a Javadoc token.
105      * @param ast
106      *        the token to process
107      */
108     public abstract void visitJavadocToken(DetailNode ast);
109 
110     /**
111      * The configurable javadoc token set.
112      * Used to protect Checks against malicious users who specify an
113      * unacceptable javadoc token set in the configuration file.
114      * The default implementation returns the check's default javadoc tokens.
115      * @return the javadoc token set this check is designed for.
116      * @see JavadocTokenTypes
117      */
118     public int[] getAcceptableJavadocTokens() {
119         final int[] defaultJavadocTokens = getDefaultJavadocTokens();
120         final int[] copy = new int[defaultJavadocTokens.length];
121         System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length);
122         return copy;
123     }
124 
125     /**
126      * The javadoc tokens that this check must be registered for.
127      * @return the javadoc token set this must be registered for.
128      * @see JavadocTokenTypes
129      */
130     public int[] getRequiredJavadocTokens() {
131         return CommonUtil.EMPTY_INT_ARRAY;
132     }
133 
134     /**
135      * This method determines if a check should process javadoc containing non-tight html tags.
136      * This method must be overridden in checks extending {@code AbstractJavadocCheck} which
137      * are not supposed to process javadoc containing non-tight html tags.
138      *
139      * @return true if the check should or can process javadoc containing non-tight html tags;
140      *     false otherwise
141      * @see ParseStatus#isNonTight()
142      * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
143      *     Tight HTML rules</a>
144      */
145     public boolean acceptJavadocWithNonTightHtml() {
146         return true;
147     }
148 
149     /**
150      * Setter for {@link #violateExecutionOnNonTightHtml}.
151      * @param shouldReportViolation value to which the field shall be set to
152      * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
153      *     Tight HTML rules</a>
154      */
155     public final void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
156         violateExecutionOnNonTightHtml = shouldReportViolation;
157     }
158 
159     /**
160      * Adds a set of tokens the check is interested in.
161      * @param strRep the string representation of the tokens interested in
162      */
163     public final void setJavadocTokens(String... strRep) {
164         javadocTokens.clear();
165         for (String str : strRep) {
166             javadocTokens.add(JavadocUtil.getTokenId(str));
167         }
168     }
169 
170     @Override
171     public void init() {
172         validateDefaultJavadocTokens();
173         if (javadocTokens.isEmpty()) {
174             for (int id : getDefaultJavadocTokens()) {
175                 javadocTokens.add(id);
176             }
177         }
178         else {
179             final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
180             Arrays.sort(acceptableJavadocTokens);
181             for (Integer javadocTokenId : javadocTokens) {
182                 if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
183                     final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
184                             + "not found in Acceptable javadoc tokens list in check %s",
185                             JavadocUtil.getTokenName(javadocTokenId), getClass().getName());
186                     throw new IllegalStateException(message);
187                 }
188             }
189         }
190     }
191 
192     /**
193      * Validates that check's required javadoc tokens are subset of default javadoc tokens.
194      * @throws IllegalStateException when validation of default javadoc tokens fails
195      */
196     private void validateDefaultJavadocTokens() {
197         if (getRequiredJavadocTokens().length != 0) {
198             final int[] defaultJavadocTokens = getDefaultJavadocTokens();
199             Arrays.sort(defaultJavadocTokens);
200             for (final int javadocToken : getRequiredJavadocTokens()) {
201                 if (Arrays.binarySearch(defaultJavadocTokens, javadocToken) < 0) {
202                     final String message = String.format(Locale.ROOT,
203                             "Javadoc Token \"%s\" from required javadoc "
204                                 + "tokens was not found in default "
205                                 + "javadoc tokens list in check %s",
206                             javadocToken, getClass().getName());
207                     throw new IllegalStateException(message);
208                 }
209             }
210         }
211     }
212 
213     /**
214      * Called before the starting to process a tree.
215      * @param rootAst
216      *        the root of the tree
217      * @noinspection WeakerAccess
218      */
219     public void beginJavadocTree(DetailNode rootAst) {
220         // No code by default, should be overridden only by demand at subclasses
221     }
222 
223     /**
224      * Called after finished processing a tree.
225      * @param rootAst
226      *        the root of the tree
227      * @noinspection WeakerAccess
228      */
229     public void finishJavadocTree(DetailNode rootAst) {
230         // No code by default, should be overridden only by demand at subclasses
231     }
232 
233     /**
234      * Called after all the child nodes have been process.
235      * @param ast
236      *        the token leaving
237      */
238     public void leaveJavadocToken(DetailNode ast) {
239         // No code by default, should be overridden only by demand at subclasses
240     }
241 
242     /**
243      * Defined final to not allow JavadocChecks to change default tokens.
244      * @return default tokens
245      */
246     @Override
247     public final int[] getDefaultTokens() {
248         return getRequiredTokens();
249     }
250 
251     @Override
252     public final int[] getAcceptableTokens() {
253         return getRequiredTokens();
254     }
255 
256     @Override
257     public final int[] getRequiredTokens() {
258         return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
259     }
260 
261     /**
262      * Defined final because all JavadocChecks require comment nodes.
263      * @return true
264      */
265     @Override
266     public final boolean isCommentNodesRequired() {
267         return true;
268     }
269 
270     @Override
271     public final void beginTree(DetailAST rootAST) {
272         TREE_CACHE.get().clear();
273     }
274 
275     @Override
276     public final void finishTree(DetailAST rootAST) {
277         // No code by default
278     }
279 
280     @Override
281     public final void visitToken(DetailAST blockCommentNode) {
282         if (JavadocUtil.isJavadocComment(blockCommentNode)) {
283             // store as field, to share with child Checks
284             context.get().blockCommentAst = blockCommentNode;
285 
286             final String treeCacheKey = blockCommentNode.getLineNo() + ":"
287                     + blockCommentNode.getColumnNo();
288 
289             final ParseStatus result;
290 
291             if (TREE_CACHE.get().containsKey(treeCacheKey)) {
292                 result = TREE_CACHE.get().get(treeCacheKey);
293             }
294             else {
295                 result = context.get().parser
296                         .parseJavadocAsDetailNode(blockCommentNode);
297                 TREE_CACHE.get().put(treeCacheKey, result);
298             }
299 
300             if (result.getParseErrorMessage() == null) {
301                 if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) {
302                     processTree(result.getTree());
303                 }
304 
305                 if (violateExecutionOnNonTightHtml && result.isNonTight()) {
306                     log(result.getFirstNonTightHtmlTag().getLine(),
307                             JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG,
308                             result.getFirstNonTightHtmlTag().getText());
309                 }
310             }
311             else {
312                 final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
313                 log(parseErrorMessage.getLineNumber(),
314                         parseErrorMessage.getMessageKey(),
315                         parseErrorMessage.getMessageArguments());
316             }
317         }
318     }
319 
320     /**
321      * Getter for block comment in Java language syntax tree.
322      * @return A block comment in the syntax tree.
323      */
324     protected DetailAST getBlockCommentAst() {
325         return context.get().blockCommentAst;
326     }
327 
328     /**
329      * Processes JavadocAST tree notifying Check.
330      * @param root
331      *        root of JavadocAST tree.
332      */
333     private void processTree(DetailNode root) {
334         beginJavadocTree(root);
335         walk(root);
336         finishJavadocTree(root);
337     }
338 
339     /**
340      * Processes a node calling Check at interested nodes.
341      * @param root
342      *        the root of tree for process
343      */
344     private void walk(DetailNode root) {
345         DetailNode curNode = root;
346         while (curNode != null) {
347             boolean waitsForProcessing = shouldBeProcessed(curNode);
348 
349             if (waitsForProcessing) {
350                 visitJavadocToken(curNode);
351             }
352             DetailNode toVisit = JavadocUtil.getFirstChild(curNode);
353             while (curNode != null && toVisit == null) {
354                 if (waitsForProcessing) {
355                     leaveJavadocToken(curNode);
356                 }
357 
358                 toVisit = JavadocUtil.getNextSibling(curNode);
359                 if (toVisit == null) {
360                     curNode = curNode.getParent();
361                     if (curNode != null) {
362                         waitsForProcessing = shouldBeProcessed(curNode);
363                     }
364                 }
365             }
366             curNode = toVisit;
367         }
368     }
369 
370     /**
371      * Checks whether the current node should be processed by the check.
372      * @param curNode current node.
373      * @return true if the current node should be processed by the check.
374      */
375     private boolean shouldBeProcessed(DetailNode curNode) {
376         return javadocTokens.contains(curNode.getType());
377     }
378 
379     /**
380      * The file context holder.
381      */
382     private static class FileContext {
383 
384         /**
385          * Parses content of Javadoc comment as DetailNode tree.
386          */
387         private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
388 
389         /**
390          * DetailAST node of considered Javadoc comment that is just a block comment
391          * in Java language syntax tree.
392          */
393         private DetailAST blockCommentAst;
394 
395     }
396 
397 }