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