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 * <module name="JavadocMethod"/>
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 * <module name="JavadocMethod">
60 * <property name="scope" value="public"/>
61 * <property name="allowUndeclaredRTE" value="true"/>
62 * </module>
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 }