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/JavadocMethodCheck.java


1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2002  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;
20  
21  import com.puppycrawl.tools.checkstyle.api.DetailAST;
22  import com.puppycrawl.tools.checkstyle.api.FileContents;
23  import com.puppycrawl.tools.checkstyle.api.FullIdent;
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  import com.puppycrawl.tools.checkstyle.api.Utils;
28  
29  import java.util.ArrayList;
30  import java.util.HashSet;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.ListIterator;
34  import java.util.Set;
35  
36  import org.apache.regexp.RE;
37  
38  /**
39   * <p>
40   * Checks the Javadoc of a method or constructor.
41   * By default, does not check for unused throws.
42   * To allow documented <code>java.lang.RuntimeException</code>s
43   * that are not declared, set property allowUndeclaredRTE to true.
44   * The scope to verify is specified using the {@link Scope} class and
45   * defaults to {@link Scope#PRIVATE}. To verify another scope,
46   * set property scope to one of the {@link Scope} constants.
47   * </p>
48   * <p>
49   * An example of how to configure the check is:
50   * </p>
51   * <pre>
52   * &lt;module name="JavadocMethod"/&gt;
53   * </pre>
54   * <p> An example of how to configure the check to check to allow
55   * documentation of undeclared RuntimeExceptions
56   * and for the {@link Scope#PUBLIC} scope is:
57   *</p>
58   * <pre>
59   * &lt;module name="JavadocMethod"&gt;
60   *    &lt;property name="scope" value="public"/&gt;
61   *    &lt;property name="allowUndeclaredRTE" value="true"/&gt;
62   * &lt;/module&gt;
63   * </pre>
64   *
65   * @author <a href="mailto:checkstyle@puppycrawl.com">Oliver Burn</a>
66   * @author Rick Giles
67   * @version 1.0
68   */
69  public class JavadocMethodCheck
70      extends AbstractImportCheck
71  {
72  
73         // {{{ Data declarations
74      /** the pattern to match Javadoc tags that take an argument **/
75      private static final String MATCH_JAVADOC_ARG_PAT =
76          "@(throws|exception|param)\\s+(\\S+)\\s+\\S";
77      /** compiled regexp to match Javadoc tags that take an argument **/
78      private static final RE MATCH_JAVADOC_ARG =
79          Utils.createRE(MATCH_JAVADOC_ARG_PAT);
80  
81     /**
82      * the pattern to match the first line of a multi-line Javadoc
83      * tag that takes an argument.
84      **/
85      private static final String MATCH_JAVADOC_ARG_MULTILINE_START_PAT =
86          "@(throws|exception|param)\\s+(\\S+)\\s*$";
87      /** compiled regexp to match first part of multilineJavadoc tags **/
88      private static final RE MATCH_JAVADOC_ARG_MULTILINE_START =
89          Utils.createRE(MATCH_JAVADOC_ARG_MULTILINE_START_PAT);
90  
91      /** the pattern that looks for a continuation of the comment **/
92      private static final String MATCH_JAVADOC_MULTILINE_CONT_PAT =
93          "(\\*/|@|[^\\s\\*])";
94      /** compiled regexp to look for a continuation of the comment **/
95      private static final RE MATCH_JAVADOC_MULTILINE_CONT =
96          Utils.createRE(MATCH_JAVADOC_MULTILINE_CONT_PAT);
97      /** Multiline finished at end of comment **/
98      private static final String END_JAVADOC = "*/";
99      /** Multiline finished at next Javadoc **/
100     private static final String NEXT_TAG = "@";
101 
102     /** the pattern to match Javadoc tags with no argument **/
103     private static final String MATCH_JAVADOC_NOARG_PAT =
104         "@(return|see)\\s+\\S";
105     /** compiled regexp to match Javadoc tags with no argument **/
106     private static final RE MATCH_JAVADOC_NOARG =
107         Utils.createRE(MATCH_JAVADOC_NOARG_PAT);
108    /**
109     * the pattern to match the first line of a multi-line Javadoc
110     * tag that takes no argument.
111     **/
112     private static final String MATCH_JAVADOC_NOARG_MULTILINE_START_PAT =
113         "@(return|see)\\s*$";
114     /** compiled regexp to match first part of multilineJavadoc tags **/
115     private static final RE MATCH_JAVADOC_NOARG_MULTILINE_START =
116         Utils.createRE(MATCH_JAVADOC_NOARG_MULTILINE_START_PAT);
117 
118     /** the pattern to match Javadoc tags with no argument and {} **/
119     private static final String MATCH_JAVADOC_NOARG_CURLY_PAT =
120         "\\{\\s*@(inheritDoc)\\s*\\}";
121     /** compiled regexp to match Javadoc tags with no argument and {} **/
122     private static final RE MATCH_JAVADOC_NOARG_CURLY =
123         Utils.createRE(MATCH_JAVADOC_NOARG_CURLY_PAT);
124 
125     /** full identifier for package of the method **/
126     private FullIdent mPackageFullIdent = null;
127 
128     /** imports details **/
129     private Set mImports = new HashSet();
130 
131     /** the visibility scope where Javadoc comments are checked **/
132     private Scope mScope = Scope.PRIVATE;
133 
134     /**
135      * controls whether to allow documented exceptions that
136      * are not declared if they are a subclass of
137      * java.lang.RuntimeException.
138      **/
139     private boolean mAllowUndeclaredRTE = false;
140 
141     /**
142      * Set the scope.
143      * @param aFrom a <code>String</code> value
144      */
145     public void setScope(String aFrom)
146     {
147         mScope = Scope.getInstance(aFrom);
148     }
149 
150     /**
151      * controls whether to allow documented exceptions that
152      * are not declared if they are a subclass of
153      * java.lang.RuntimeException.
154      * @param aFlag a <code>Boolean</code> value
155      */
156     public void setAllowUndeclaredRTE(boolean aFlag)
157     {
158         mAllowUndeclaredRTE = aFlag;
159     }
160 
161     /** @see com.puppycrawl.tools.checkstyle.api.Check */
162     public int[] getDefaultTokens()
163     {
164         return new int[] {
165             TokenTypes.PACKAGE_DEF,
166             TokenTypes.IMPORT,
167             TokenTypes.METHOD_DEF,
168             TokenTypes.CTOR_DEF};
169     }
170     
171     /** @see com.puppycrawl.tools.checkstyle.api.Check */
172     public int[] getAcceptableTokens()
173     {
174         return new int[] {
175             TokenTypes.METHOD_DEF,
176             TokenTypes.CTOR_DEF};
177     }
178     
179     /** @see com.puppycrawl.tools.checkstyle.api.Check */
180     public int[] getRequiredTokens()
181     {
182         return new int[] {
183             TokenTypes.PACKAGE_DEF,
184             TokenTypes.IMPORT};
185     }
186 
187     /** @see com.puppycrawl.tools.checkstyle.api.Check */
188     public void beginTree()
189     {
190         mPackageFullIdent = FullIdent.createFullIdent(null);
191         mImports.clear();
192     }
193 
194     /** @see com.puppycrawl.tools.checkstyle.api.Check */
195     public void visitToken(DetailAST aAST)
196     {
197         if (aAST.getType() == TokenTypes.PACKAGE_DEF) {
198             if (mAllowUndeclaredRTE) {
199                 processPackage(aAST);
200             }
201         }
202         else if (aAST.getType() == TokenTypes.IMPORT) {
203             processImport(aAST);
204         }
205         else {
206             //TokenTypes.METHOD_DEF or TokenTypes.CTOR_DEF
207             processMethod(aAST);
208         }
209     }
210 
211 
212 
213     /**
214      * Collects the details of a package.
215      * @param aAST node containing the package details
216      */
217     private void processPackage(DetailAST aAST)
218     {
219         final DetailAST nameAST = (DetailAST) aAST.getFirstChild();
220         mPackageFullIdent = FullIdent.createFullIdent(nameAST);
221     }
222 
223     /**
224      * Collects the details of imports.
225      * @param aAST node containing the import details
226      */
227     private void processImport(DetailAST aAST)
228     {
229         final FullIdent name = getImportText(aAST);
230         if (name != null) {
231             mImports.add(name.getText());
232         }
233     }
234 
235     /**
236      * Checks Javadoc comments for a method or constructor.
237      * @param aAST the tree node for the method or constructor.
238      */
239     private void processMethod(DetailAST aAST)
240     {
241         final DetailAST mods = aAST.findFirstToken(TokenTypes.MODIFIERS);
242         final Scope declaredScope = ScopeUtils.getScopeFromMods(mods);
243         final Scope targetScope =
244             ScopeUtils.inInterfaceBlock(aAST)
245                 ? Scope.PUBLIC
246                 : declaredScope;
247 
248         if (targetScope.isIn(mScope)) {
249             final Scope surroundingScope =
250                 ScopeUtils.getSurroundingScope(aAST);
251 
252             if (surroundingScope.isIn(mScope)) {
253                 final FileContents contents = getFileContents();
254                 final String[] cmt =
255                     contents.getJavadocBefore(aAST.getLineNo());
256 
257                 if (cmt == null) {
258                     log(aAST.getLineNo(),
259                         aAST.getColumnNo(),
260                         "javadoc.missing");
261                 }
262                 else {
263                     checkComment(aAST, cmt);
264                 }
265             }
266         }
267     }
268 
269     /**
270      * Checks the Javadoc for a method.
271      * @param aAST the token for the method
272      * @param aComment the Javadoc comment
273      */
274     private void checkComment(DetailAST aAST, String[] aComment)
275     {
276         final List tags = getMethodTags(aComment, aAST.getLineNo() - 1);
277         // Check for only one @see tag
278         if ((tags.size() != 1)
279             || !((JavadocTag) tags.get(0)).isSeeOrInheritDocTag())
280         {
281             checkParamTags(tags, getParameters(aAST));
282             checkThrowsTags(tags, getThrows(aAST));
283             if (isFunction(aAST)) {
284                 checkReturnTag(tags, aAST.getLineNo());
285             }
286 
287             // Dump out all unused tags
288             final Iterator it = tags.iterator();
289             while (it.hasNext()) {
290                 final JavadocTag jt = (JavadocTag) it.next();
291                 if (!jt.isSeeOrInheritDocTag()) {
292                     log(jt.getLineNo(), "javadoc.unusedTagGeneral");
293                 }
294             }
295         }
296     }
297     /**
298      * Returns the tags in a javadoc comment. Only finds throws, exception,
299      * param, return and see tags.
300      * @return the tags found
301      * @param aLines the Javadoc comment
302      * @param aLastLineNo the line number of the last line in the Javadoc
303      *                    comment
304      **/
305     private List getMethodTags(String[] aLines, int aLastLineNo)
306     {
307         final List tags = new ArrayList();
308         int currentLine = aLastLineNo - aLines.length;
309         for (int i = 0; i < aLines.length; i++) {
310             currentLine++;
311             if (MATCH_JAVADOC_ARG.match(aLines[i])) {
312                 tags.add(new JavadocTag(currentLine,
313                                         MATCH_JAVADOC_ARG.getParen(1),
314                                         MATCH_JAVADOC_ARG.getParen(2)));
315             }
316             else if (MATCH_JAVADOC_NOARG.match(aLines[i])) {
317                 tags.add(new JavadocTag(currentLine,
318                                         MATCH_JAVADOC_NOARG.getParen(1)));
319             }
320             else if (MATCH_JAVADOC_NOARG_CURLY.match(aLines[i])) {
321                 tags.add(new JavadocTag(currentLine,
322                                         MATCH_JAVADOC_NOARG_CURLY.getParen(1)));
323             }
324             else if (MATCH_JAVADOC_ARG_MULTILINE_START.match(aLines[i])) {
325                 final String p1 = MATCH_JAVADOC_ARG_MULTILINE_START.getParen(1);
326                 final String p2 = MATCH_JAVADOC_ARG_MULTILINE_START.getParen(2);
327 
328                 // Look for the rest of the comment if all we saw was
329                 // the tag and the name. Stop when we see '*/' (end of
330                 // Javadoc, '@' (start of next tag), or anything that's
331                 // not whitespace or '*' characters.
332                 int remIndex = i + 1;
333                 while (remIndex < aLines.length) {
334                     if (MATCH_JAVADOC_MULTILINE_CONT.match(aLines[remIndex])) {
335                         remIndex = aLines.length;
336                         String lFin = MATCH_JAVADOC_MULTILINE_CONT.getParen(1);
337                         if (!lFin.equals(NEXT_TAG)
338                             && !lFin.equals(END_JAVADOC))
339                         {
340                             tags.add(new JavadocTag(currentLine, p1, p2));
341                         }
342                     }
343                     remIndex++;
344                 }
345             }
346             else if (MATCH_JAVADOC_NOARG_MULTILINE_START.match(aLines[i])) {
347                 final String p1 =
348                     MATCH_JAVADOC_NOARG_MULTILINE_START.getParen(1);
349 
350                 // Look for the rest of the comment if all we saw was
351                 // the tag and the name. Stop when we see '*/' (end of
352                 // Javadoc, '@' (start of next tag), or anything that's
353                 // not whitespace or '*' characters.
354                 int remIndex = i + 1;
355                 while (remIndex < aLines.length) {
356                     if (MATCH_JAVADOC_MULTILINE_CONT.match(aLines[remIndex])) {
357                         remIndex = aLines.length;
358                         String lFin = MATCH_JAVADOC_MULTILINE_CONT.getParen(1);
359                         if (!lFin.equals(NEXT_TAG)
360                             && !lFin.equals(END_JAVADOC))
361                         {
362                             tags.add(new JavadocTag(currentLine, p1));
363                         }
364                     }
365                     remIndex++;
366                 }
367             }
368         }
369         return tags;
370     }
371 
372     /**
373      * Computes the parameter nodes for a method.
374      * @param aAST the method node.
375      * @return the list of parameter nodes for aAST.
376      **/
377     private List getParameters(DetailAST aAST)
378     {
379         final DetailAST params = aAST.findFirstToken(TokenTypes.PARAMETERS);
380         final List retVal = new ArrayList();
381 
382         DetailAST child = (DetailAST) params.getFirstChild();
383         while (child != null) {
384             if (child.getType() == TokenTypes.PARAMETER_DEF) {
385                 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
386                 retVal.add(ident);
387             }
388             child = (DetailAST) child.getNextSibling();
389         }
390         return retVal;
391     }
392 
393      /**
394      * Computes the exception nodes for a method.
395      * @param aAST the method node.
396      * @return the list of exception nodes for aAST.
397      **/
398     private List getThrows(DetailAST aAST)
399     {
400         final List retVal = new ArrayList();
401         final DetailAST throwsAST =
402             aAST.findFirstToken(TokenTypes.LITERAL_THROWS);
403         if (throwsAST != null) {
404             DetailAST child = (DetailAST) throwsAST.getFirstChild();
405             while (child != null) {
406                 if ((child.getType() == TokenTypes.IDENT)
407                     || (child.getType() == TokenTypes.DOT))
408                 {
409                     final FullIdent fi = FullIdent.createFullIdent(child);
410                     retVal.add(fi);
411                 }
412                 child = (DetailAST) child.getNextSibling();
413             }
414         }
415         return retVal;
416     }
417 
418 
419     /**
420      * Checks a set of tags for matching parameters.
421      * @param aTags the tags to check
422      * @param aParams the list of parameters to check
423      **/
424     private void checkParamTags(List aTags, List aParams)
425     {
426         // Loop over the tags, checking to see they exist in the params.
427         final ListIterator tagIt = aTags.listIterator();
428         while (tagIt.hasNext()) {
429             final JavadocTag tag = (JavadocTag) tagIt.next();
430 
431             if (!tag.isParamTag()) {
432                 continue;
433             }
434 
435             tagIt.remove();
436 
437             // Loop looking for matching param
438             boolean found = false;
439             final Iterator paramIt = aParams.iterator();
440             while (paramIt.hasNext()) {
441                 final DetailAST param = (DetailAST) paramIt.next();
442                 if (param.getText().equals(tag.getArg1())) {
443                     found = true;
444                     paramIt.remove();
445                     break;
446                 }
447             }
448 
449             // Handle extra JavadocTag
450             if (!found) {
451                 log(tag.getLineNo(), "javadoc.unusedTag",
452                               "@param", tag.getArg1());
453             }
454         }
455 
456         // Now dump out all parameters without tags
457         final Iterator paramIt = aParams.iterator();
458         while (paramIt.hasNext()) {
459             final DetailAST param = (DetailAST) paramIt.next();
460             log(param.getLineNo(), param.getColumnNo(),
461                 "javadoc.expectedTag", "@param", param.getText());
462         }
463     }
464 
465     /**
466      * Return if two Strings represent the same type, inspecting the
467      * import statements if necessary
468      *
469      * @param aDeclared type declared in throws clause
470      * @param aDocumented type declared in javadoc throws tag
471      * @return true iff type names represent the same type
472      */
473     private boolean isSameType(String aDeclared, String aDocumented)
474     {
475         return aDeclared.equals(aDocumented)
476                 || isShortName(aDeclared, aDocumented)
477                 || isShortName(aDocumented, aDeclared);
478     }
479 
480     /**
481      * Calculate if one type name is a shortname for another type name.
482      * @param aShortName a shorthand, such as <code>IOException</code>
483      * @param aFullName a full name, such as <code>java.io.IOException</code>
484      * @return true iff aShortName represents the same type as aFullName
485      */
486     private boolean isShortName(String aShortName, String aFullName)
487     {
488         if (aShortName.length() >= aFullName.length()) {
489             return false;
490         }
491 
492         final String base = basename(aFullName);
493         if (aShortName.length() >= aFullName.length()
494                 || !base.equals(aShortName))
495         {
496             return false;
497         }
498 
499         // check fully qualified import
500         if (mImports.contains(aFullName)) {
501             return true;
502         }
503 
504         // check .* import
505         final int endIndex = aFullName.length() - base.length() - 1;
506         final String packageName = aFullName.substring(0, endIndex);
507         final String starImport = packageName + ".*";
508         if (mImports.contains(starImport)) {
509             return true;
510         }
511 
512         // check fully qualified class from same package
513         return packageName.equals(mPackageFullIdent.getText());
514     }
515 
516     /**
517      * Checks whether a method is a function.
518      * @param aAST the method node.
519      * @return whether the method is a function.
520      **/
521     private boolean isFunction(DetailAST aAST)
522     {
523         boolean retVal = false;
524         if (aAST.getType() == TokenTypes.METHOD_DEF) {
525             final DetailAST typeAST = aAST.findFirstToken(TokenTypes.TYPE);
526             if ((typeAST != null)
527                 && (typeAST.findFirstToken(TokenTypes.LITERAL_VOID) == null))
528             {
529                 retVal = true;
530             }
531         }
532         return retVal;
533     }
534 
535     /**
536      * Checks for only one return tag. All return tags will be removed from the
537      * supplied list.
538      * @param aTags the tags to check
539      * @param aLineNo the line number of the expected tag
540      **/
541     private void checkReturnTag(List aTags, int aLineNo)
542     {
543         // Loop over tags finding return tags. After the first one, report an
544         // error.
545         boolean found = false;
546         final ListIterator it = aTags.listIterator();
547         while (it.hasNext()) {
548             final JavadocTag jt = (JavadocTag) it.next();
549             if (jt.isReturnTag()) {
550                 if (found) {
551                     log(jt.getLineNo(), "javadoc.return.duplicate");
552                 }
553                 found = true;
554                 it.remove();
555             }
556         }
557 
558         // Handle there being no @return tags
559         if (!found) {
560             log(aLineNo, "javadoc.return.expected");
561         }
562     }
563 
564 
565     /**
566      * Checks a set of tags for matching throws.
567      * @param aTags the tags to check
568      * @param aThrows the throws to check
569      **/
570     private void checkThrowsTags(List aTags, List aThrows)
571     {
572         // Loop over the tags, checking to see they exist in the throws.
573         final Set foundThrows = new HashSet();
574         final ListIterator tagIt = aTags.listIterator();
575         while (tagIt.hasNext()) {
576             final JavadocTag tag = (JavadocTag) tagIt.next();
577 
578             if (!tag.isThrowsTag()) {
579                 continue;
580             }
581 
582             tagIt.remove();
583 
584             // Loop looking for matching throw
585             final String documentedEx = tag.getArg1();
586             boolean found = foundThrows.contains(documentedEx);
587             final ListIterator throwIt = aThrows.listIterator();
588             while (!found && throwIt.hasNext()) {
589                 final FullIdent fi = (FullIdent) throwIt.next();
590                 final String declaredEx = fi.getText();
591                 if (isSameType(declaredEx, documentedEx)) {
592                     found = true;
593                     throwIt.remove();
594                     foundThrows.add(documentedEx);
595                 }
596             }
597 
598             // Handle extra JavadocTag.
599             if (!found) {
600                 boolean reqd = true;
601                 if (mAllowUndeclaredRTE) {
602                     final ClassResolver cr =
603                         new ClassResolver(
604                             getClassLoader(),
605                             mPackageFullIdent.getText(),
606                             mImports);
607                     try {
608                         final Class clazz = cr.resolve(tag.getArg1());
609                         reqd =
610                             !RuntimeException.class.isAssignableFrom(clazz)
611                                 && !Error.class.isAssignableFrom(clazz);
612                     }
613                     catch (ClassNotFoundException e) {
614                         log(tag.getLineNo(), "javadoc.classInfo",
615                                       "@throws", tag.getArg1());
616                     }
617                 }
618 
619                 if (reqd) {
620                     log(tag.getLineNo(), "javadoc.unusedTag",
621                                   "@throws", tag.getArg1());
622                 }
623             }
624         }
625 
626         // Now dump out all throws without tags
627         final ListIterator throwIt = aThrows.listIterator();
628         while (throwIt.hasNext()) {
629             final FullIdent fi = (FullIdent) throwIt.next();
630             log(fi.getLineNo(), fi.getColumnNo(),
631                 "javadoc.expectedTag", "@throws", fi.getText());
632         }
633     }
634 
635     // TODO: clean up duplicate code in UnusedImports and IllegalInstantiation
636     /**
637      * @return the class name from a fully qualified name
638      * @param aType the fully qualified name
639      */
640     private String basename(String aType)
641     {
642         final int i = aType.lastIndexOf(".");
643         return (i == -1) ? aType : aType.substring(i + 1);
644     }
645 
646 }