1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.checks.javadoc;
21
22 import java.util.ArrayDeque;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.Deque;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Set;
29 import java.util.TreeSet;
30 import java.util.regex.Pattern;
31 import java.util.stream.Collectors;
32
33 import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
34 import com.puppycrawl.tools.checkstyle.StatelessCheck;
35 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
36 import com.puppycrawl.tools.checkstyle.api.DetailAST;
37 import com.puppycrawl.tools.checkstyle.api.FileContents;
38 import com.puppycrawl.tools.checkstyle.api.Scope;
39 import com.puppycrawl.tools.checkstyle.api.TextBlock;
40 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
41 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
42 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
43 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
44
45
46
47
48
49 @StatelessCheck
50 public class JavadocStyleCheck
51 extends AbstractCheck {
52
53
54 public static final String MSG_JAVADOC_MISSING = "javadoc.missing";
55
56
57 public static final String MSG_EMPTY = "javadoc.empty";
58
59
60 public static final String MSG_NO_PERIOD = "javadoc.noPeriod";
61
62
63 public static final String MSG_INCOMPLETE_TAG = "javadoc.incompleteTag";
64
65
66 public static final String MSG_UNCLOSED_HTML = JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
67
68
69 public static final String MSG_EXTRA_HTML = "javadoc.extraHtml";
70
71
72 private static final Set<String> SINGLE_TAGS = Collections.unmodifiableSortedSet(
73 Arrays.stream(new String[] {"br", "li", "dt", "dd", "hr", "img", "p", "td", "tr", "th", })
74 .collect(Collectors.toCollection(TreeSet::new)));
75
76
77
78
79
80 private static final Set<String> ALLOWED_TAGS = Collections.unmodifiableSortedSet(
81 Arrays.stream(new String[] {
82 "a", "abbr", "acronym", "address", "area", "b", "bdo", "big",
83 "blockquote", "br", "caption", "cite", "code", "colgroup", "dd",
84 "del", "div", "dfn", "dl", "dt", "em", "fieldset", "font", "h1",
85 "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd",
86 "li", "ol", "p", "pre", "q", "samp", "small", "span", "strong",
87 "style", "sub", "sup", "table", "tbody", "td", "tfoot", "th",
88 "thead", "tr", "tt", "u", "ul", "var", })
89 .collect(Collectors.toCollection(TreeSet::new)));
90
91
92 private Scope scope = Scope.PRIVATE;
93
94
95 private Scope excludeScope;
96
97
98 private Pattern endOfSentenceFormat = Pattern.compile("([.?!][ \t\n\r\f<])|([.?!]$)");
99
100
101
102
103
104 private boolean checkFirstSentence = true;
105
106
107
108
109 private boolean checkHtml = true;
110
111
112
113
114 private boolean checkEmptyJavadoc;
115
116 @Override
117 public int[] getDefaultTokens() {
118 return getAcceptableTokens();
119 }
120
121 @Override
122 public int[] getAcceptableTokens() {
123 return new int[] {
124 TokenTypes.ANNOTATION_DEF,
125 TokenTypes.ANNOTATION_FIELD_DEF,
126 TokenTypes.CLASS_DEF,
127 TokenTypes.CTOR_DEF,
128 TokenTypes.ENUM_CONSTANT_DEF,
129 TokenTypes.ENUM_DEF,
130 TokenTypes.INTERFACE_DEF,
131 TokenTypes.METHOD_DEF,
132 TokenTypes.PACKAGE_DEF,
133 TokenTypes.VARIABLE_DEF,
134 };
135 }
136
137 @Override
138 public int[] getRequiredTokens() {
139 return CommonUtil.EMPTY_INT_ARRAY;
140 }
141
142 @Override
143 public void visitToken(DetailAST ast) {
144 if (shouldCheck(ast)) {
145 final FileContents contents = getFileContents();
146
147
148
149 final TextBlock textBlock =
150 contents.getJavadocBefore(ast.getFirstChild().getLineNo());
151
152 checkComment(ast, textBlock);
153 }
154 }
155
156
157
158
159
160
161 private boolean shouldCheck(final DetailAST ast) {
162 boolean check = false;
163
164 if (ast.getType() == TokenTypes.PACKAGE_DEF) {
165 check = getFileContents().inPackageInfo();
166 }
167 else if (!ScopeUtil.isInCodeBlock(ast)) {
168 final Scope customScope;
169
170 if (ScopeUtil.isInInterfaceOrAnnotationBlock(ast)
171 || ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
172 customScope = Scope.PUBLIC;
173 }
174 else {
175 customScope = ScopeUtil.getScopeFromMods(ast.findFirstToken(TokenTypes.MODIFIERS));
176 }
177 final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
178
179 check = customScope.isIn(scope)
180 && (surroundingScope == null || surroundingScope.isIn(scope))
181 && (excludeScope == null
182 || !customScope.isIn(excludeScope)
183 || surroundingScope != null
184 && !surroundingScope.isIn(excludeScope));
185 }
186 return check;
187 }
188
189
190
191
192
193
194
195
196
197
198 private void checkComment(final DetailAST ast, final TextBlock comment) {
199 if (comment == null) {
200
201
202
203
204 if (getFileContents().inPackageInfo()) {
205 log(ast.getLineNo(), MSG_JAVADOC_MISSING);
206 }
207 }
208 else {
209 if (checkFirstSentence) {
210 checkFirstSentenceEnding(ast, comment);
211 }
212
213 if (checkHtml) {
214 checkHtmlTags(ast, comment);
215 }
216
217 if (checkEmptyJavadoc) {
218 checkJavadocIsNotEmpty(comment);
219 }
220 }
221 }
222
223
224
225
226
227
228
229
230
231
232
233 private void checkFirstSentenceEnding(final DetailAST ast, TextBlock comment) {
234 final String commentText = getCommentText(comment.getText());
235
236 if (!commentText.isEmpty()
237 && !endOfSentenceFormat.matcher(commentText).find()
238 && !(commentText.startsWith("{@inheritDoc}")
239 && JavadocTagInfo.INHERIT_DOC.isValidOn(ast))) {
240 log(comment.getStartLineNo(), MSG_NO_PERIOD);
241 }
242 }
243
244
245
246
247
248
249 private void checkJavadocIsNotEmpty(TextBlock comment) {
250 final String commentText = getCommentText(comment.getText());
251
252 if (commentText.isEmpty()) {
253 log(comment.getStartLineNo(), MSG_EMPTY);
254 }
255 }
256
257
258
259
260
261
262 private static String getCommentText(String... comments) {
263 final StringBuilder builder = new StringBuilder(1024);
264 for (final String line : comments) {
265 final int textStart = findTextStart(line);
266
267 if (textStart != -1) {
268 if (line.charAt(textStart) == '@') {
269
270 break;
271 }
272 builder.append(line.substring(textStart));
273 trimTail(builder);
274 builder.append('\n');
275 }
276 }
277
278 return builder.toString().trim();
279 }
280
281
282
283
284
285
286
287
288
289 private static int findTextStart(String line) {
290 int textStart = -1;
291 int index = 0;
292 while (index < line.length()) {
293 if (!Character.isWhitespace(line.charAt(index))) {
294 if (line.regionMatches(index, "/**", 0, "/**".length())) {
295 index += 2;
296 }
297 else if (line.regionMatches(index, "*/", 0, 2)) {
298 index++;
299 }
300 else if (line.charAt(index) != '*') {
301 textStart = index;
302 break;
303 }
304 }
305 index++;
306 }
307 return textStart;
308 }
309
310
311
312
313
314 private static void trimTail(StringBuilder builder) {
315 int index = builder.length() - 1;
316 while (true) {
317 if (Character.isWhitespace(builder.charAt(index))) {
318 builder.deleteCharAt(index);
319 }
320 else if (index > 0 && builder.charAt(index) == '/'
321 && builder.charAt(index - 1) == '*') {
322 builder.deleteCharAt(index);
323 builder.deleteCharAt(index - 1);
324 index--;
325 while (builder.charAt(index - 1) == '*') {
326 builder.deleteCharAt(index - 1);
327 index--;
328 }
329 }
330 else {
331 break;
332 }
333 index--;
334 }
335 }
336
337
338
339
340
341
342
343
344
345
346
347
348 private void checkHtmlTags(final DetailAST ast, final TextBlock comment) {
349 final int lineNo = comment.getStartLineNo();
350 final Deque<HtmlTag> htmlStack = new ArrayDeque<>();
351 final String[] text = comment.getText();
352
353 final TagParser parser = new TagParser(text, lineNo);
354
355 while (parser.hasNextTag()) {
356 final HtmlTag tag = parser.nextTag();
357
358 if (tag.isIncompleteTag()) {
359 log(tag.getLineNo(), MSG_INCOMPLETE_TAG,
360 text[tag.getLineNo() - lineNo]);
361 return;
362 }
363 if (tag.isClosedTag()) {
364
365 continue;
366 }
367 if (tag.isCloseTag()) {
368
369 if (isExtraHtml(tag.getId(), htmlStack)) {
370
371 log(tag.getLineNo(),
372 tag.getPosition(),
373 MSG_EXTRA_HTML,
374 tag.getText());
375 }
376 else {
377
378
379 checkUnclosedTags(htmlStack, tag.getId());
380 }
381 }
382 else {
383
384 if (isAllowedTag(tag)) {
385 htmlStack.push(tag);
386 }
387 }
388 }
389
390
391
392 String lastFound = "";
393 final List<String> typeParameters = CheckUtil.getTypeParameterNames(ast);
394 for (final HtmlTag htmlTag : htmlStack) {
395 if (!isSingleTag(htmlTag)
396 && !htmlTag.getId().equals(lastFound)
397 && !typeParameters.contains(htmlTag.getId())) {
398 log(htmlTag.getLineNo(), htmlTag.getPosition(),
399 MSG_UNCLOSED_HTML, htmlTag.getText());
400 lastFound = htmlTag.getId();
401 }
402 }
403 }
404
405
406
407
408
409
410
411
412
413
414 private void checkUnclosedTags(Deque<HtmlTag> htmlStack, String token) {
415 final Deque<HtmlTag> unclosedTags = new ArrayDeque<>();
416 HtmlTag lastOpenTag = htmlStack.pop();
417 while (!token.equalsIgnoreCase(lastOpenTag.getId())) {
418
419
420 if (isSingleTag(lastOpenTag)) {
421 lastOpenTag = htmlStack.pop();
422 }
423 else {
424 unclosedTags.push(lastOpenTag);
425 lastOpenTag = htmlStack.pop();
426 }
427 }
428
429
430
431 String lastFound = "";
432 for (final HtmlTag htag : unclosedTags) {
433 lastOpenTag = htag;
434 if (lastOpenTag.getId().equals(lastFound)) {
435 continue;
436 }
437 lastFound = lastOpenTag.getId();
438 log(lastOpenTag.getLineNo(),
439 lastOpenTag.getPosition(),
440 MSG_UNCLOSED_HTML,
441 lastOpenTag.getText());
442 }
443 }
444
445
446
447
448
449
450
451 private static boolean isSingleTag(HtmlTag tag) {
452
453
454
455
456 return SINGLE_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
457 }
458
459
460
461
462
463
464
465 private static boolean isAllowedTag(HtmlTag tag) {
466 return ALLOWED_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
467 }
468
469
470
471
472
473
474
475
476
477
478 private static boolean isExtraHtml(String token, Deque<HtmlTag> htmlStack) {
479 boolean isExtra = true;
480 for (final HtmlTag tag : htmlStack) {
481
482
483
484
485 if (token.equalsIgnoreCase(tag.getId())) {
486 isExtra = false;
487 break;
488 }
489 }
490
491 return isExtra;
492 }
493
494
495
496
497
498 public void setScope(Scope scope) {
499 this.scope = scope;
500 }
501
502
503
504
505
506 public void setExcludeScope(Scope excludeScope) {
507 this.excludeScope = excludeScope;
508 }
509
510
511
512
513
514 public void setEndOfSentenceFormat(Pattern pattern) {
515 endOfSentenceFormat = pattern;
516 }
517
518
519
520
521
522
523 public void setCheckFirstSentence(boolean flag) {
524 checkFirstSentence = flag;
525 }
526
527
528
529
530
531 public void setCheckHtml(boolean flag) {
532 checkHtml = flag;
533 }
534
535
536
537
538
539 public void setCheckEmptyJavadoc(boolean flag) {
540 checkEmptyJavadoc = flag;
541 }
542
543 }