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 switch (currentAst.getType()) {
287 case JavadocTokenTypes.TEXT:
288 isDefinedFirst = currentAst.getText().isBlank();
289 break;
290 case JavadocTokenTypes.HTML_ELEMENT:
291 isDefinedFirst = !isTextPresentInsideHtmlTag(currentAst);
292 break;
293 default:
294 break;
295 }
296 currentAst = JavadocUtil.getPreviousSibling(currentAst);
297 }
298 return isDefinedFirst;
299 }
300
301
302
303
304
305
306
307
308 public static boolean isTextPresentInsideHtmlTag(DetailNode node) {
309 DetailNode nestedChild = JavadocUtil.getFirstChild(node);
310 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
311 nestedChild = JavadocUtil.getFirstChild(nestedChild);
312 }
313 boolean isTextPresentInsideHtmlTag = false;
314 while (nestedChild != null && !isTextPresentInsideHtmlTag) {
315 switch (nestedChild.getType()) {
316 case JavadocTokenTypes.TEXT:
317 isTextPresentInsideHtmlTag = !nestedChild.getText().isBlank();
318 break;
319 case JavadocTokenTypes.HTML_TAG:
320 case JavadocTokenTypes.HTML_ELEMENT:
321 isTextPresentInsideHtmlTag = isTextPresentInsideHtmlTag(nestedChild);
322 break;
323 default:
324 break;
325 }
326 nestedChild = JavadocUtil.getNextSibling(nestedChild);
327 }
328 return isTextPresentInsideHtmlTag;
329 }
330
331
332
333
334
335
336
337 private static boolean isInlineTagPresent(DetailNode ast) {
338 return getInlineTagNodeForAst(ast) != null;
339 }
340
341
342
343
344
345
346
347 private static DetailNode getInlineTagNodeForAst(DetailNode ast) {
348 DetailNode node = ast;
349 DetailNode result = null;
350
351 if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
352 result = node;
353 }
354 else if (node.getType() == JavadocTokenTypes.HTML_TAG) {
355
356 node = node.getChildren()[1];
357 result = getInlineTagNodeForAst(node);
358 }
359 else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT
360
361 && node.getChildren()[0].getChildren().length > 1) {
362
363 node = node.getChildren()[0].getChildren()[1];
364 result = getInlineTagNodeForAst(node);
365 }
366 return result;
367 }
368
369
370
371
372
373
374
375 private static boolean isSummaryTag(DetailNode javadocInlineTag) {
376 return isInlineTagWithName(javadocInlineTag, SUMMARY_TEXT);
377 }
378
379
380
381
382
383
384
385 private static boolean isInlineReturnTag(DetailNode javadocInlineTag) {
386 return isInlineTagWithName(javadocInlineTag, RETURN_TEXT);
387 }
388
389
390
391
392
393
394
395
396
397 private static boolean isInlineTagWithName(DetailNode javadocInlineTag, String name) {
398 final DetailNode[] child = javadocInlineTag.getChildren();
399
400
401
402
403 return name.equals(child[1].getText());
404 }
405
406
407
408
409
410
411 private void validateSummaryTag(DetailNode inlineSummaryTag) {
412 final String inlineSummary = getContentOfInlineCustomTag(inlineSummaryTag);
413 final String summaryVisible = getVisibleContent(inlineSummary);
414 if (summaryVisible.isEmpty()) {
415 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
416 }
417 else if (!period.isEmpty()) {
418 final boolean isPeriodNotAtEnd =
419 summaryVisible.lastIndexOf(period) != summaryVisible.length() - 1;
420 if (isPeriodNotAtEnd) {
421 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_MISSING_PERIOD);
422 }
423 else if (containsForbiddenFragment(inlineSummary)) {
424 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
425 }
426 }
427 }
428
429
430
431
432
433
434 private void validateInlineReturnTag(DetailNode inlineReturnTag) {
435 final String inlineReturn = getContentOfInlineCustomTag(inlineReturnTag);
436 final String returnVisible = getVisibleContent(inlineReturn);
437 if (returnVisible.isEmpty()) {
438 log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
439 }
440 else if (containsForbiddenFragment(inlineReturn)) {
441 log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
442 }
443 }
444
445
446
447
448
449
450
451 public static String getContentOfInlineCustomTag(DetailNode inlineTag) {
452 final DetailNode[] childrenOfInlineTag = inlineTag.getChildren();
453 final StringBuilder customTagContent = new StringBuilder(256);
454 final int indexOfContentOfSummaryTag = 3;
455 if (childrenOfInlineTag.length != indexOfContentOfSummaryTag) {
456 DetailNode currentNode = childrenOfInlineTag[indexOfContentOfSummaryTag];
457 while (currentNode.getType() != JavadocTokenTypes.JAVADOC_INLINE_TAG_END) {
458 extractInlineTagContent(currentNode, customTagContent);
459 currentNode = JavadocUtil.getNextSibling(currentNode);
460 }
461 }
462 return customTagContent.toString();
463 }
464
465
466
467
468
469
470
471 private static void extractInlineTagContent(DetailNode node,
472 StringBuilder customTagContent) {
473 final DetailNode[] children = node.getChildren();
474 if (children.length == 0) {
475 customTagContent.append(node.getText());
476 }
477 else {
478 for (DetailNode child : children) {
479 if (child.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
480 extractInlineTagContent(child, customTagContent);
481 }
482 }
483 }
484 }
485
486
487
488
489
490
491
492 private static String getVisibleContent(String summary) {
493 final String visibleSummary = HTML_ELEMENTS.matcher(summary).replaceAll("");
494 return visibleSummary.trim();
495 }
496
497
498
499
500
501
502
503 private boolean containsForbiddenFragment(String firstSentence) {
504 final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
505 .matcher(firstSentence).replaceAll(" ");
506 return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find();
507 }
508
509
510
511
512
513
514
515 private static String trimExcessWhitespaces(String text) {
516 final StringBuilder result = new StringBuilder(256);
517 boolean previousWhitespace = true;
518
519 for (char letter : text.toCharArray()) {
520 final char print;
521 if (Character.isWhitespace(letter)) {
522 if (previousWhitespace) {
523 continue;
524 }
525
526 previousWhitespace = true;
527 print = ' ';
528 }
529 else {
530 previousWhitespace = false;
531 print = letter;
532 }
533
534 result.append(print);
535 }
536
537 return result.toString();
538 }
539
540
541
542
543
544
545
546 private static boolean startsWithInheritDoc(DetailNode root) {
547 boolean found = false;
548
549 for (DetailNode child : root.getChildren()) {
550 if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG
551 && child.getChildren()[1].getType() == JavadocTokenTypes.INHERIT_DOC_LITERAL) {
552 found = true;
553 }
554 if ((child.getType() == JavadocTokenTypes.TEXT
555 || child.getType() == JavadocTokenTypes.HTML_ELEMENT)
556 && !CommonUtil.isBlank(child.getText())) {
557 break;
558 }
559 }
560
561 return found;
562 }
563
564
565
566
567
568
569
570 private static String getSummarySentence(DetailNode ast) {
571 final StringBuilder result = new StringBuilder(256);
572 for (DetailNode child : ast.getChildren()) {
573 if (child.getType() != JavadocTokenTypes.EOF
574 && ALLOWED_TYPES.get(child.getType())) {
575 result.append(child.getText());
576 }
577 else {
578 final String summary = result.toString();
579 if (child.getType() == JavadocTokenTypes.HTML_ELEMENT
580 && CommonUtil.isBlank(summary)) {
581 result.append(getStringInsideTag(summary,
582 child.getChildren()[0].getChildren()[0]));
583 }
584 }
585 }
586 return result.toString().trim();
587 }
588
589
590
591
592
593
594
595
596 private static String getStringInsideTag(String result, DetailNode detailNode) {
597 final StringBuilder contents = new StringBuilder(result);
598 DetailNode tempNode = detailNode;
599 while (tempNode != null) {
600 if (tempNode.getType() == JavadocTokenTypes.TEXT) {
601 contents.append(tempNode.getText());
602 }
603 tempNode = JavadocUtil.getNextSibling(tempNode);
604 }
605 return contents.toString();
606 }
607
608
609
610
611
612
613
614
615
616
617 private static Optional<String> getFirstSentence(DetailNode ast, String period) {
618 final List<String> sentenceParts = new ArrayList<>();
619 Optional<String> result = Optional.empty();
620 for (String text : (Iterable<String>) streamTextParts(ast)::iterator) {
621 final Optional<String> sentenceEnding = findSentenceEnding(text, period);
622
623 if (sentenceEnding.isPresent()) {
624 sentenceParts.add(sentenceEnding.get());
625 result = Optional.of(String.join("", sentenceParts));
626 break;
627 }
628 else {
629 sentenceParts.add(text);
630 }
631 }
632 return result;
633 }
634
635
636
637
638
639
640
641 private static Stream<String> streamTextParts(DetailNode node) {
642 final Stream<String> stream;
643 if (node.getChildren().length == 0) {
644 stream = Stream.of(node.getText());
645 }
646 else {
647 stream = Stream.of(node.getChildren())
648 .flatMap(SummaryJavadocCheck::streamTextParts);
649 }
650 return stream;
651 }
652
653
654
655
656
657
658
659
660
661
662 private static Optional<String> findSentenceEnding(String text, String period) {
663 int periodIndex = text.indexOf(period);
664 Optional<String> result = Optional.empty();
665 while (periodIndex >= 0) {
666 final int afterPeriodIndex = periodIndex + period.length();
667
668
669
670 if (!DEFAULT_PERIOD.equals(period)
671 || afterPeriodIndex >= text.length()
672 || Character.isWhitespace(text.charAt(afterPeriodIndex))) {
673 final String resultStr = text.substring(0, periodIndex);
674 result = Optional.of(resultStr);
675 break;
676 }
677 else {
678 periodIndex = text.indexOf(period, afterPeriodIndex);
679 }
680 }
681 return result;
682 }
683 }