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