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