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 (/** and */) 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 }