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