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