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.ArrayList;
23 import java.util.Arrays;
24 import java.util.BitSet;
25 import java.util.List;
26 import java.util.Optional;
27 import java.util.regex.Pattern;
28 import java.util.stream.Stream;
29
30 import javax.annotation.Nullable;
31
32 import com.puppycrawl.tools.checkstyle.StatelessCheck;
33 import com.puppycrawl.tools.checkstyle.api.DetailNode;
34 import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
35 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
36 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
37 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
38
39
40
41
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
97
98
99
100
101
102
103
104
105
106 @StatelessCheck
107 public class SummaryJavadocCheck extends AbstractJavadocCheck {
108
109
110
111
112
113 public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence";
114
115
116
117
118
119 public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc";
120
121
122
123
124
125 public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing";
126
127
128
129
130 public static final String MSG_SUMMARY_MISSING_PERIOD = "summary.javaDoc.missing.period";
131
132
133
134
135 private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN =
136 Pattern.compile("\n +(\\*)|^ +(\\*)");
137
138
139
140
141 private static final Pattern HTML_ELEMENTS =
142 Pattern.compile("<[^>]*>");
143
144
145 private static final String DEFAULT_PERIOD = ".";
146
147
148 private static final String SUMMARY_TEXT = "@summary";
149
150
151 private static final String RETURN_TEXT = "@return";
152
153
154 private static final BitSet ALLOWED_TYPES = TokenUtil.asBitSet(
155 JavadocTokenTypes.WS,
156 JavadocTokenTypes.DESCRIPTION,
157 JavadocTokenTypes.TEXT);
158
159
160
161
162 private Pattern forbiddenSummaryFragments = CommonUtil.createPattern("^$");
163
164
165
166
167
168
169
170 private String period = DEFAULT_PERIOD;
171
172
173
174
175
176
177
178 public void setForbiddenSummaryFragments(Pattern pattern) {
179 forbiddenSummaryFragments = pattern;
180 }
181
182
183
184
185
186
187
188
189
190
191
192 public void setPeriod(String period) {
193 this.period = period;
194 }
195
196 @Override
197 public int[] getDefaultJavadocTokens() {
198 return new int[] {
199 JavadocTokenTypes.JAVADOC,
200 };
201 }
202
203 @Override
204 public int[] getRequiredJavadocTokens() {
205 return getAcceptableJavadocTokens();
206 }
207
208 @Override
209 public void visitJavadocToken(DetailNode ast) {
210 final Optional<DetailNode> inlineTag = getInlineTagNode(ast);
211 final DetailNode inlineTagNode = inlineTag.orElse(null);
212 if (inlineTag.isPresent()
213 && isSummaryTag(inlineTagNode)
214 && isDefinedFirst(inlineTagNode)) {
215 validateSummaryTag(inlineTagNode);
216 }
217 else if (inlineTag.isPresent() && isInlineReturnTag(inlineTagNode)) {
218 validateInlineReturnTag(inlineTagNode);
219 }
220 else if (!startsWithInheritDoc(ast)) {
221 validateUntaggedSummary(ast);
222 }
223 }
224
225
226
227
228
229
230 private void validateUntaggedSummary(DetailNode ast) {
231 final String summaryDoc = getSummarySentence(ast);
232 if (summaryDoc.isEmpty()) {
233 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
234 }
235 else if (!period.isEmpty()) {
236 if (summaryDoc.contains(period)) {
237 final String firstSentence = getFirstSentenceOrNull(ast, period);
238 if (firstSentence == null) {
239 log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE);
240 }
241 else if (containsForbiddenFragment(firstSentence)) {
242 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC);
243 }
244 }
245 else {
246 log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE);
247 }
248 }
249 }
250
251
252
253
254
255
256
257 private static Optional<DetailNode> getInlineTagNode(DetailNode javadoc) {
258 return Arrays.stream(javadoc.getChildren())
259 .filter(SummaryJavadocCheck::isInlineTagPresent)
260 .findFirst()
261 .map(SummaryJavadocCheck::getInlineTagNodeForAst);
262 }
263
264
265
266
267
268
269
270 private static boolean isDefinedFirst(DetailNode inlineSummaryTag) {
271 boolean isDefinedFirst = true;
272 DetailNode currentAst = inlineSummaryTag;
273 while (currentAst != null && isDefinedFirst) {
274 switch (currentAst.getType()) {
275 case JavadocTokenTypes.TEXT:
276 isDefinedFirst = currentAst.getText().isBlank();
277 break;
278 case JavadocTokenTypes.HTML_ELEMENT:
279 isDefinedFirst = !isTextPresentInsideHtmlTag(currentAst);
280 break;
281 default:
282 break;
283 }
284 currentAst = JavadocUtil.getPreviousSibling(currentAst);
285 }
286 return isDefinedFirst;
287 }
288
289
290
291
292
293
294
295
296 public static boolean isTextPresentInsideHtmlTag(DetailNode node) {
297 DetailNode nestedChild = JavadocUtil.getFirstChild(node);
298 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
299 nestedChild = JavadocUtil.getFirstChild(nestedChild);
300 }
301 boolean isTextPresentInsideHtmlTag = false;
302 while (nestedChild != null && !isTextPresentInsideHtmlTag) {
303 switch (nestedChild.getType()) {
304 case JavadocTokenTypes.TEXT:
305 isTextPresentInsideHtmlTag = !nestedChild.getText().isBlank();
306 break;
307 case JavadocTokenTypes.HTML_TAG:
308 case JavadocTokenTypes.HTML_ELEMENT:
309 isTextPresentInsideHtmlTag = isTextPresentInsideHtmlTag(nestedChild);
310 break;
311 default:
312 break;
313 }
314 nestedChild = JavadocUtil.getNextSibling(nestedChild);
315 }
316 return isTextPresentInsideHtmlTag;
317 }
318
319
320
321
322
323
324
325 private static boolean isInlineTagPresent(DetailNode ast) {
326 return getInlineTagNodeForAst(ast) != null;
327 }
328
329
330
331
332
333
334
335 private static DetailNode getInlineTagNodeForAst(DetailNode ast) {
336 DetailNode node = ast;
337 DetailNode result = null;
338
339 if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
340 result = node;
341 }
342 else if (node.getType() == JavadocTokenTypes.HTML_TAG) {
343
344 node = node.getChildren()[1];
345 result = getInlineTagNodeForAst(node);
346 }
347 else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT
348
349 && node.getChildren()[0].getChildren().length > 1) {
350
351 node = node.getChildren()[0].getChildren()[1];
352 result = getInlineTagNodeForAst(node);
353 }
354 return result;
355 }
356
357
358
359
360
361
362
363 private static boolean isSummaryTag(DetailNode javadocInlineTag) {
364 return isInlineTagWithName(javadocInlineTag, SUMMARY_TEXT);
365 }
366
367
368
369
370
371
372
373 private static boolean isInlineReturnTag(DetailNode javadocInlineTag) {
374 return isInlineTagWithName(javadocInlineTag, RETURN_TEXT);
375 }
376
377
378
379
380
381
382
383
384
385 private static boolean isInlineTagWithName(DetailNode javadocInlineTag, String name) {
386 final DetailNode[] child = javadocInlineTag.getChildren();
387
388
389
390
391 return name.equals(child[1].getText());
392 }
393
394
395
396
397
398
399 private void validateSummaryTag(DetailNode inlineSummaryTag) {
400 final String inlineSummary = getContentOfInlineCustomTag(inlineSummaryTag);
401 final String summaryVisible = getVisibleContent(inlineSummary);
402 if (summaryVisible.isEmpty()) {
403 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
404 }
405 else if (!period.isEmpty()) {
406 final boolean isPeriodNotAtEnd =
407 summaryVisible.lastIndexOf(period) != summaryVisible.length() - 1;
408 if (isPeriodNotAtEnd) {
409 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_MISSING_PERIOD);
410 }
411 else if (containsForbiddenFragment(inlineSummary)) {
412 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
413 }
414 }
415 }
416
417
418
419
420
421
422 private void validateInlineReturnTag(DetailNode inlineReturnTag) {
423 final String inlineReturn = getContentOfInlineCustomTag(inlineReturnTag);
424 final String returnVisible = getVisibleContent(inlineReturn);
425 if (returnVisible.isEmpty()) {
426 log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
427 }
428 else if (containsForbiddenFragment(inlineReturn)) {
429 log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
430 }
431 }
432
433
434
435
436
437
438
439 public static String getContentOfInlineCustomTag(DetailNode inlineTag) {
440 final DetailNode[] childrenOfInlineTag = inlineTag.getChildren();
441 final StringBuilder customTagContent = new StringBuilder(256);
442 final int indexOfContentOfSummaryTag = 3;
443 if (childrenOfInlineTag.length != indexOfContentOfSummaryTag) {
444 DetailNode currentNode = childrenOfInlineTag[indexOfContentOfSummaryTag];
445 while (currentNode.getType() != JavadocTokenTypes.JAVADOC_INLINE_TAG_END) {
446 extractInlineTagContent(currentNode, customTagContent);
447 currentNode = JavadocUtil.getNextSibling(currentNode);
448 }
449 }
450 return customTagContent.toString();
451 }
452
453
454
455
456
457
458
459 private static void extractInlineTagContent(DetailNode node,
460 StringBuilder customTagContent) {
461 final DetailNode[] children = node.getChildren();
462 if (children.length == 0) {
463 customTagContent.append(node.getText());
464 }
465 else {
466 for (DetailNode child : children) {
467 if (child.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
468 extractInlineTagContent(child, customTagContent);
469 }
470 }
471 }
472 }
473
474
475
476
477
478
479
480 private static String getVisibleContent(String summary) {
481 final String visibleSummary = HTML_ELEMENTS.matcher(summary).replaceAll("");
482 return visibleSummary.trim();
483 }
484
485
486
487
488
489
490
491 private boolean containsForbiddenFragment(String firstSentence) {
492 final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
493 .matcher(firstSentence).replaceAll(" ");
494 return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find();
495 }
496
497
498
499
500
501
502
503 private static String trimExcessWhitespaces(String text) {
504 final StringBuilder result = new StringBuilder(256);
505 boolean previousWhitespace = true;
506
507 for (char letter : text.toCharArray()) {
508 final char print;
509 if (Character.isWhitespace(letter)) {
510 if (previousWhitespace) {
511 continue;
512 }
513
514 previousWhitespace = true;
515 print = ' ';
516 }
517 else {
518 previousWhitespace = false;
519 print = letter;
520 }
521
522 result.append(print);
523 }
524
525 return result.toString();
526 }
527
528
529
530
531
532
533
534 private static boolean startsWithInheritDoc(DetailNode root) {
535 boolean found = false;
536
537 for (DetailNode child : root.getChildren()) {
538 if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG
539 && child.getChildren()[1].getType() == JavadocTokenTypes.INHERIT_DOC_LITERAL) {
540 found = true;
541 }
542 if ((child.getType() == JavadocTokenTypes.TEXT
543 || child.getType() == JavadocTokenTypes.HTML_ELEMENT)
544 && !CommonUtil.isBlank(child.getText())) {
545 break;
546 }
547 }
548
549 return found;
550 }
551
552
553
554
555
556
557
558 private static String getSummarySentence(DetailNode ast) {
559 final StringBuilder result = new StringBuilder(256);
560 for (DetailNode child : ast.getChildren()) {
561 if (child.getType() != JavadocTokenTypes.EOF
562 && ALLOWED_TYPES.get(child.getType())) {
563 result.append(child.getText());
564 }
565 else {
566 final String summary = result.toString();
567 if (child.getType() == JavadocTokenTypes.HTML_ELEMENT
568 && CommonUtil.isBlank(summary)) {
569 result.append(getStringInsideTag(summary,
570 child.getChildren()[0].getChildren()[0]));
571 }
572 }
573 }
574 return result.toString().trim();
575 }
576
577
578
579
580
581
582
583
584 private static String getStringInsideTag(String result, DetailNode detailNode) {
585 final StringBuilder contents = new StringBuilder(result);
586 DetailNode tempNode = detailNode;
587 while (tempNode != null) {
588 if (tempNode.getType() == JavadocTokenTypes.TEXT) {
589 contents.append(tempNode.getText());
590 }
591 tempNode = JavadocUtil.getNextSibling(tempNode);
592 }
593 return contents.toString();
594 }
595
596
597
598
599
600
601
602
603 @Nullable
604 private static String getFirstSentenceOrNull(DetailNode ast, String period) {
605 final List<String> sentenceParts = new ArrayList<>();
606 String sentence = null;
607 for (String text : (Iterable<String>) streamTextParts(ast)::iterator) {
608 final String sentenceEnding = findSentenceEndingOrNull(text, period);
609 if (sentenceEnding != null) {
610 sentenceParts.add(sentenceEnding);
611 sentence = String.join("", sentenceParts);
612 break;
613 }
614 else {
615 sentenceParts.add(text);
616 }
617 }
618 return sentence;
619 }
620
621
622
623
624
625
626
627 private static Stream<String> streamTextParts(DetailNode node) {
628 final Stream<String> stream;
629 if (node.getChildren().length == 0) {
630 stream = Stream.of(node.getText());
631 }
632 else {
633 stream = Stream.of(node.getChildren())
634 .flatMap(SummaryJavadocCheck::streamTextParts);
635 }
636 return stream;
637 }
638
639
640
641
642
643
644
645
646
647
648 @Nullable
649 private static String findSentenceEndingOrNull(String text, String period) {
650 int periodIndex = text.indexOf(period);
651 String sentenceEnding = null;
652 while (periodIndex >= 0) {
653 final int afterPeriodIndex = periodIndex + period.length();
654
655
656
657 if (!DEFAULT_PERIOD.equals(period)
658 || afterPeriodIndex >= text.length()
659 || Character.isWhitespace(text.charAt(afterPeriodIndex))) {
660 sentenceEnding = text.substring(0, periodIndex);
661 break;
662 }
663 else {
664 periodIndex = text.indexOf(period, afterPeriodIndex);
665 }
666 }
667 return sentenceEnding;
668 }
669 }