Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: com/puppycrawl/tools/checkstyle/checks/javadoc/JavadocStyleCheck.java


1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2003  Oliver Burn
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  package com.puppycrawl.tools.checkstyle.checks.javadoc;
20  
21  import com.puppycrawl.tools.checkstyle.api.Check;
22  import com.puppycrawl.tools.checkstyle.api.DetailAST;
23  import com.puppycrawl.tools.checkstyle.api.FileContents;
24  import com.puppycrawl.tools.checkstyle.api.Scope;
25  import com.puppycrawl.tools.checkstyle.api.ScopeUtils;
26  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
27  
28  import java.util.NoSuchElementException;
29  import java.util.Stack;
30  import org.apache.regexp.RE;
31  import org.apache.regexp.RESyntaxException;
32  
33  /**
34   * <p>Custom Checkstyle Check to validate Javadoc.
35   * The following checks are performed:
36   * <ul>
37   * <li>Ensures the first sentence ends with proper punctuation (That is
38   * a period, question mark, or exclaimation mark).  Javadoc  automatically
39   * places the first sentence in the method summary table and index.  With out
40   * proper punctuation the Javadoc may be malformed.
41   * <li>Check text for incomplete html tags.  Verifies that HTML tags have
42   * corresponding end tags and issues an UNCLOSED_HTML error if not.
43   * An EXTRA_HTML error is issued if an end tag is found without a previous
44   * open tag.
45   * </ul>
46   * <p>These checks were patterned after the checks made by the doclet
47   * <code>com.sun.tools.doclets.doccheck.DocCheck</code>
48   *
49   * @author Chris Stillwell
50   * @version 1.1
51   */
52  public class JavadocStyleCheck
53      extends Check
54  {
55      /** Message property key for the Unclosed HTML message. */
56      private static final String UNCLOSED_HTML = "javadoc.unclosedhtml";
57  
58      /** Message property key for the Extra HTML message. */
59      private static final String EXTRA_HTML = "javadoc.extrahtml";
60  
61      /** HTML tags that do not require a close tag. */
62      private static final String[] SINGLE_TAG =
63      {"p", "br", "li", "dt", "dd", "td", "hr", "img", "tr", "th", "td"};
64  
65      /** The scope to check. */
66      private Scope mScope = Scope.PRIVATE;
67  
68      /** Regular expression for matching the end of a sentence. */
69      private RE mEndOfSentenceRE;
70  
71      /**
72       * Indicates if the first sentence should be checked for proper end of
73       * sentence punctuation.
74       */
75      private boolean mCheckFirstSentence = true;
76  
77      /**
78       * Indicates if the HTML within the comment should be checked.
79       */
80      private boolean mCheckHtml = true;
81  
82      /**
83       * The default tokens this Check is used for.
84       * @see com.puppycrawl.tools.checkstyle.api.Check#getDefaultTokens()
85       */
86      public int[] getDefaultTokens()
87      {
88          return new int[] {
89              TokenTypes.INTERFACE_DEF,
90              TokenTypes.CLASS_DEF,
91              TokenTypes.METHOD_DEF,
92              TokenTypes.CTOR_DEF,
93              TokenTypes.VARIABLE_DEF,
94          };
95      }
96  
97      /**
98       * Called to process a token.
99       * @see com.puppycrawl.tools.checkstyle.api.Check
100      */
101     public void visitToken(DetailAST aAST)
102     {
103         if (!ScopeUtils.inCodeBlock(aAST)) {
104             final DetailAST mods =
105                 aAST.findFirstToken(TokenTypes.MODIFIERS);
106             final Scope declaredScope = ScopeUtils.getScopeFromMods(mods);
107             final Scope variableScope =
108                 ScopeUtils.inInterfaceBlock(aAST)
109                     ? Scope.PUBLIC
110                     : declaredScope;
111 
112             if (variableScope.isIn(mScope)) {
113                 final Scope surroundingScope =
114                     ScopeUtils.getSurroundingScope(aAST);
115 
116                 if ((surroundingScope == null)
117                     || surroundingScope.isIn(mScope))
118                 {
119                     final FileContents contents = getFileContents();
120                     final String[] cmt =
121                         contents.getJavadocBefore(aAST.getLineNo());
122 
123                     checkComment(aAST, cmt);
124                 }
125             }
126         }
127     }
128 
129     /**
130      * Performs the various checks agains the Javadoc comment.
131      *
132      * @param aAST (Abstract Syntax Tree) the token to process.
133      * @param aComment the source lines that make up the Javadoc comment.
134      *
135      * @see #checkFirstSentence(DetailAST, String[])
136      */
137     private void checkComment(DetailAST aAST, String[] aComment)
138     {
139         if (aComment == null) {
140             return;
141         }
142 
143         if (mCheckFirstSentence) {
144             checkFirstSentence(aAST, aComment);
145         }
146 
147         if (mCheckHtml) {
148             checkHtml(aAST, aComment);
149         }
150     }
151 
152     /**
153      * Checks that the first sentence ends with proper puctuation.  This method
154      * uses a regular expression that checks for the presence of a period,
155      * question mark, or exclaimation mark followed either by whitespace, an
156      * HTML element, or the end of string. This method ignores {@inheritDoc}
157      * comments.
158      *
159      * @param aAST (Abstract Syntax Tree) the token to process.
160      * @param aComment the source lines that make up the Javadoc comment.
161      */
162     private void checkFirstSentence(DetailAST aAST, String[] aComment)
163     {
164         final String commentText = getCommentText(aComment);
165 
166         if ((commentText.length() != 0)
167             && !getEndOfSentenceRE().match(commentText)
168             && !"{@inheritDoc}".equals(commentText))
169         {
170             log(aAST.getLineNo() - aComment.length, "javadoc.noperiod");
171         }
172     }
173 
174     /**
175      * Returns the comment text from the Javadoc.
176      * @param aComments the lines of Javadoc.
177      * @return a comment text String.
178      */
179     private String getCommentText(String[] aComments)
180     {
181         final StringBuffer buffer = new StringBuffer();
182         boolean foundTag = false;
183 
184         for (int i = 0; i < aComments.length; i++) {
185             String line = aComments[i];
186             final int textStart = findTextStart(line);
187 
188             if (textStart != -1) {
189                 // Look for Javadoc tag that's neither a @link nor a
190                 // @inheritDoc since they can appear
191                 // within the comment text.
192                 final int ndx = line.indexOf('@');
193                 if ((ndx != -1)
194                     && !line.regionMatches(ndx + 1, "link", 0, "link".length())
195                     && !line.regionMatches(
196                         ndx + 1,
197                         "inheritDoc",
198                         0,
199                         "inheritDoc".length()))
200                 {
201                     foundTag = true;
202                     line = line.substring(0, ndx);
203                 }
204 
205                 buffer.append(line.substring(textStart));
206                 trimTail(buffer);
207                 buffer.append('\n');
208 
209                 if (foundTag) {
210                     break;
211                 }
212             }
213         }
214 
215         return buffer.toString().trim();
216     }
217 
218     /**
219      * Finds the index of the first non-whitespace character ignoring the
220      * Javadoc comment start and end strings (&#47** and *&#47) as well as any
221      * leading asterisk.
222      * @param aLine the Javadoc comment line of text to scan.
223      * @return the int index relative to 0 for the start of text
224      *         or -1 if not found.
225      */
226     private int findTextStart(String aLine)
227     {
228         int textStart = -1;
229         for (int i = 0; i < aLine.length(); i++) {
230             if (!Character.isWhitespace(aLine.charAt(i))) {
231                 if (aLine.regionMatches(i, "/**", 0, 3)) {
232                     i += 2;
233                 }
234                 else if (aLine.regionMatches(i, "*/", 0, 2)) {
235                     i++;
236                 }
237                 else if (aLine.charAt(i) != '*') {
238                     textStart = i;
239                     break;
240                 }
241             }
242         }
243         return textStart;
244     }
245 
246     /**
247      * Trims any trailing whitespace or the end of Javadoc comment string.
248      * @param aBuffer the StringBuffer to trim.
249      */
250     private void trimTail(StringBuffer aBuffer)
251     {
252         for (int i = aBuffer.length() - 1; i >= 0; i--) {
253             if (Character.isWhitespace(aBuffer.charAt(i))) {
254                 aBuffer.deleteCharAt(i);
255             }
256             else if ((i > 0)
257                      && (aBuffer.charAt(i - 1) == '*')
258                      && (aBuffer.charAt(i) == '/'))
259             {
260                 aBuffer.deleteCharAt(i);
261                 aBuffer.deleteCharAt(i - 1);
262                 i--;
263             }
264             else {
265                 break;
266             }
267         }
268     }
269 
270     /**
271      * Checks the comment for HTML tags that do not have a corresponding close
272      * tag or a close tage that has no previous open tag.  This code was
273      * primarily copied from the DocCheck checkHtml method.
274      *
275      * @param aAST (Abstract Syntax Tree) the token to process.
276      * @param aComment the source lines that make up the Javadoc comment.
277      */
278     private void checkHtml(DetailAST aAST, String[] aComment)
279     {
280         final int lineno = aAST.getLineNo() - aComment.length;
281         final Stack htmlStack = new Stack();
282 
283         for (int i = 0; i < aComment.length; i++) {
284             TagParser parser = null;
285             try {
286                 // Can throw NoSuchElementException when tokenizing encounters
287                 // "<" at end of aComment[i].
288                 parser = new TagParser(aComment[i], lineno + i);
289             }
290             catch (NoSuchElementException e) {
291                 log(
292                     lineno + i,
293                     "javadoc.incompleteTag",
294                     new Object[] {aComment[i]});
295                 return;
296             }
297             while (parser.hasNextTag()) {
298                 final HtmlTag tag = parser.nextTag();
299 
300                 if (!tag.isCloseTag()) {
301                     htmlStack.push(tag);
302                 }
303                 else {
304                     // We have found a close tag.
305                     if (isExtraHtml(tag.getId(), htmlStack)) {
306                         // No corresponding open tag was found on the stack.
307                         log(tag.getLineno(),
308                             tag.getPosition(),
309                             EXTRA_HTML,
310                             tag);
311                     }
312                     else {
313                         // See if there are any unclosed tags that were opened
314                         // after this one.
315                         checkUnclosedTags(htmlStack, tag.getId());
316                     }
317                 }
318             }
319         }
320 
321         // Identify any tags left on the stack.
322         String lastFound = ""; // Skip multiples, like <b>...<b>
323         for (int i = 0; i < htmlStack.size(); i++) {
324             final HtmlTag htag = (HtmlTag) htmlStack.elementAt(i);
325             if (!isSingleTag(htag) && !htag.getId().equals(lastFound)) {
326                 log(htag.getLineno(), htag.getPosition(), UNCLOSED_HTML, htag);
327                 lastFound = htag.getId();
328             }
329         }
330     }
331 
332     /**
333      * Checks to see if there are any unclosed tags on the stack.  The token
334      * represents a html tag that has been closed and has a corresponding open
335      * tag on the stack.  Any tags, except single tags, that were opened
336      * (pushed on the stack) after the token are missing a close.
337      *
338      * @param aHtmlStack the stack of opened HTML tags.
339      * @param aToken the current HTML tag name that has been closed.
340      */
341     private void checkUnclosedTags(Stack aHtmlStack, String aToken)
342     {
343         final Stack unclosedTags = new Stack();
344         HtmlTag lastOpenTag = (HtmlTag) aHtmlStack.pop();
345         while (!aToken.equalsIgnoreCase(lastOpenTag.getId())) {
346             // Find unclosed elements. Put them on a stack so the
347             // output order won't be back-to-front.
348             if (isSingleTag(lastOpenTag)) {
349                 lastOpenTag = (HtmlTag) aHtmlStack.pop();
350             }
351             else {
352                 unclosedTags.push(lastOpenTag);
353                 lastOpenTag = (HtmlTag) aHtmlStack.pop();
354             }
355         }
356 
357         // Output the unterminated tags, if any
358         String lastFound = ""; // Skip multiples, like <b>..<b>
359         for (int i = 0; i < unclosedTags.size(); i++) {
360             lastOpenTag = (HtmlTag) unclosedTags.get(i);
361             if (lastOpenTag.getId().equals(lastFound)) {
362                 continue;
363             }
364             lastFound = lastOpenTag.getId();
365             log(lastOpenTag.getLineno(),
366                 lastOpenTag.getPosition(),
367                 UNCLOSED_HTML,
368                 lastOpenTag);
369         }
370     }
371 
372     /**
373      * Determines if the HtmlTag is one which does not require a close tag.
374      *
375      * @param aTag the HtmlTag to check.
376      * @return <code>true</code> if the HtmlTag is a single tag.
377      */
378     private boolean isSingleTag(HtmlTag aTag)
379     {
380         boolean isSingleTag = false;
381         for (int i = 0; i < SINGLE_TAG.length; i++) {
382             // If its a singleton tag (<p>, <br>, etc.), ignore it
383             // Can't simply not put them on the stack, since singletons
384             // like <dt> and <dd> (unhappily) may either be terminated
385             // or not terminated. Both options are legal.
386             if (aTag.getId().equalsIgnoreCase(SINGLE_TAG[i])) {
387                 isSingleTag = true;
388             }
389         }
390         return isSingleTag;
391     }
392 
393     /**
394      * Determines if the given token is an extra HTML tag. This indicates that
395      * a close tag was found that does not have a corresponding open tag.
396      *
397      * @param aToken an HTML tag id for which a close was found.
398      * @param aHtmlStack a Stack of previous open HTML tags.
399      * @return <code>false</code> if a previous open tag was found
400      *         for the token.
401      */
402     private boolean isExtraHtml(String aToken, Stack aHtmlStack)
403     {
404         boolean isExtra = true;
405         for (int i = 0; i < aHtmlStack.size(); i++) {
406             // Loop, looking for tags that are closed.
407             // The loop is needed in case there are unclosed
408             // tags on the stack. In that case, the stack would
409             // not be empty, but this tag would still be extra.
410             HtmlTag td = (HtmlTag) aHtmlStack.elementAt(i);
411             if (aToken.equalsIgnoreCase(td.getId())) {
412                 isExtra = false;
413                 break;
414             }
415         }
416 
417         return isExtra;
418     }
419 
420     /**
421      * Sets the scope to check.
422      * @param aFrom string to get the scope from
423      */
424     public void setScope(String aFrom)
425     {
426         mScope = Scope.getInstance(aFrom);
427     }
428 
429     /**
430      * Returns a regular expression for matching the end of a sentence.
431      *
432      * @return a regular expression for matching the end of a sentence.
433      */
434     private RE getEndOfSentenceRE()
435     {
436         if (mEndOfSentenceRE == null) {
437             try {
438                 mEndOfSentenceRE = new RE("([.?!][ \t\n\r\f<])|([.?!]$)");
439             }
440             catch (RESyntaxException e) {
441                 // This should never occur.
442                 e.printStackTrace();
443             }
444         }
445         return mEndOfSentenceRE;
446     }
447 
448     /**
449      * Sets the flag that determines if the first sentence is checked for
450      * proper end of sentence punctuation.
451      * @param aFlag <code>true</code> if the first sentence is to be checked
452      */
453     public void setCheckFirstSentence(boolean aFlag)
454     {
455         mCheckFirstSentence = aFlag;
456     }
457 
458     /**
459      * Sets the flag that determines if HTML checking is to be performed.
460      * @param aFlag <code>true</code> if HTML checking is to be performed.
461      */
462     public void setCheckHtml(boolean aFlag)
463     {
464         mCheckHtml = aFlag;
465     }
466 
467 }