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.List;
24 import java.util.Optional;
25 import java.util.function.Function;
26 import java.util.regex.Pattern;
27 import java.util.stream.Stream;
28
29 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30 import com.puppycrawl.tools.checkstyle.api.DetailNode;
31 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
32 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
33 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 @FileStatefulCheck
54 public class SummaryJavadocCheck extends AbstractJavadocCheck {
55
56
57
58
59
60 public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence";
61
62
63
64
65
66 public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc";
67
68
69
70
71
72 public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing";
73
74
75
76
77 public static final String MSG_SUMMARY_MISSING_PERIOD = "summary.javaDoc.missing.period";
78
79
80
81
82 private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN =
83 Pattern.compile("\n[ \\t]*(\\*)|^[ \\t]*(\\*)");
84
85
86
87
88 private static final Pattern HTML_ELEMENTS =
89 Pattern.compile("<[^>]*>");
90
91
92 private static final String DEFAULT_PERIOD = ".";
93
94
95
96
97 private Pattern forbiddenSummaryFragments = CommonUtil.createPattern("^$");
98
99
100
101
102
103
104
105 private String period = DEFAULT_PERIOD;
106
107
108
109
110 private boolean shouldValidateUntaggedSummary = true;
111
112
113
114
115 public SummaryJavadocCheck() {
116
117 }
118
119
120
121
122
123
124
125 public void setForbiddenSummaryFragments(Pattern pattern) {
126 forbiddenSummaryFragments = pattern;
127 }
128
129
130
131
132
133
134
135
136
137
138
139 public void setPeriod(String period) {
140 this.period = period;
141 }
142
143 @Override
144 public int[] getDefaultJavadocTokens() {
145 return new int[] {
146 JavadocCommentsTokenTypes.JAVADOC_CONTENT,
147 JavadocCommentsTokenTypes.SUMMARY_INLINE_TAG,
148 JavadocCommentsTokenTypes.RETURN_INLINE_TAG,
149 };
150 }
151
152 @Override
153 public int[] getRequiredJavadocTokens() {
154 return getAcceptableJavadocTokens();
155 }
156
157 @Override
158 public void visitJavadocToken(DetailNode ast) {
159 if (isSummaryTag(ast) && isDefinedFirst(ast.getParent())) {
160 shouldValidateUntaggedSummary = false;
161 validateSummaryTag(ast);
162 }
163 else if (isInlineReturnTag(ast)) {
164 shouldValidateUntaggedSummary = false;
165 validateInlineReturnTag(ast);
166 }
167 }
168
169 @Override
170 public void leaveJavadocToken(DetailNode ast) {
171 if (ast.getType() == JavadocCommentsTokenTypes.JAVADOC_CONTENT) {
172 if (shouldValidateUntaggedSummary && !startsWithInheritDoc(ast)) {
173 validateUntaggedSummary(ast);
174 }
175 shouldValidateUntaggedSummary = true;
176 }
177 }
178
179
180
181
182
183
184 private void validateUntaggedSummary(DetailNode ast) {
185 final String summaryDoc = getSummarySentence(ast);
186 if (summaryDoc.isEmpty()) {
187 log(ast, MSG_SUMMARY_JAVADOC_MISSING);
188 }
189 else if (!period.isEmpty()) {
190 if (summaryDoc.contains(period)) {
191 final Optional<String> firstSentence = getFirstSentence(ast, period);
192
193 if (firstSentence.isPresent()) {
194 if (containsForbiddenFragment(firstSentence.get())) {
195 log(ast, MSG_SUMMARY_JAVADOC);
196 }
197 }
198 else {
199 log(ast, MSG_SUMMARY_FIRST_SENTENCE);
200 }
201 }
202 else {
203 log(ast, MSG_SUMMARY_FIRST_SENTENCE);
204 }
205 }
206 }
207
208
209
210
211
212
213
214 private static boolean isDefinedFirst(DetailNode inlineTagNode) {
215 boolean isDefinedFirst = true;
216 DetailNode currentAst = inlineTagNode.getPreviousSibling();
217 while (currentAst != null && isDefinedFirst) {
218 switch (currentAst.getType()) {
219 case JavadocCommentsTokenTypes.TEXT ->
220 isDefinedFirst = currentAst.getText().isBlank();
221 case JavadocCommentsTokenTypes.HTML_ELEMENT ->
222 isDefinedFirst = isHtmlTagWithoutText(currentAst);
223 case JavadocCommentsTokenTypes.LEADING_ASTERISK,
224 JavadocCommentsTokenTypes.LEADING_ASTERISKS,
225 JavadocCommentsTokenTypes.NEWLINE -> {
226
227 }
228 default -> isDefinedFirst = false;
229 }
230 currentAst = currentAst.getPreviousSibling();
231 }
232 return isDefinedFirst;
233 }
234
235
236
237
238
239
240
241 public static boolean isHtmlTagWithoutText(DetailNode node) {
242 boolean isEmpty = true;
243 final DetailNode htmlContentToken =
244 JavadocUtil.findFirstToken(node, JavadocCommentsTokenTypes.HTML_CONTENT);
245
246 if (htmlContentToken != null) {
247 final DetailNode child = htmlContentToken.getFirstChild();
248 isEmpty = child.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT
249 && isHtmlTagWithoutText(child);
250 }
251 return isEmpty;
252 }
253
254
255
256
257
258
259
260
261 private static boolean isSummaryTag(DetailNode javadocInlineTag) {
262 return javadocInlineTag.getType() == JavadocCommentsTokenTypes.SUMMARY_INLINE_TAG;
263 }
264
265
266
267
268
269
270
271
272 private static boolean isInlineReturnTag(DetailNode javadocInlineTag) {
273 return javadocInlineTag.getType() == JavadocCommentsTokenTypes.RETURN_INLINE_TAG;
274 }
275
276
277
278
279
280
281 private void validateSummaryTag(DetailNode inlineSummaryTag) {
282 final DetailNode descriptionNode = JavadocUtil.findFirstToken(
283 inlineSummaryTag, JavadocCommentsTokenTypes.DESCRIPTION);
284 final String inlineSummary = getContentOfInlineCustomTag(descriptionNode);
285 final String summaryVisible = getVisibleContent(inlineSummary);
286 if (summaryVisible.isEmpty()) {
287 log(inlineSummaryTag, MSG_SUMMARY_JAVADOC_MISSING);
288 }
289 else if (!period.isEmpty()) {
290 final boolean isPeriodNotAtEnd =
291 summaryVisible.lastIndexOf(period) != summaryVisible.length() - 1;
292 if (isPeriodNotAtEnd) {
293 log(inlineSummaryTag, MSG_SUMMARY_MISSING_PERIOD);
294 }
295 else if (containsForbiddenFragment(inlineSummary)) {
296 log(inlineSummaryTag, MSG_SUMMARY_JAVADOC);
297 }
298 }
299 }
300
301
302
303
304
305
306 private void validateInlineReturnTag(DetailNode inlineReturnTag) {
307 final DetailNode descriptionNode = JavadocUtil.findFirstToken(
308 inlineReturnTag, JavadocCommentsTokenTypes.DESCRIPTION);
309 final String inlineReturn = getContentOfInlineCustomTag(descriptionNode);
310 final String returnVisible = getVisibleContent(inlineReturn);
311 if (returnVisible.isEmpty()) {
312 log(inlineReturnTag, MSG_SUMMARY_JAVADOC_MISSING);
313 }
314 else if (containsForbiddenFragment(prependJavadocToolWord(inlineReturn))) {
315 log(inlineReturnTag, MSG_SUMMARY_JAVADOC);
316 }
317 }
318
319
320
321
322
323
324
325 public static String getContentOfInlineCustomTag(DetailNode descriptionNode) {
326 final StringBuilder customTagContent = new StringBuilder(256);
327 DetailNode curNode = descriptionNode;
328 while (curNode != null) {
329 if (curNode.getFirstChild() == null
330 && !isLeadingAsterisk(curNode)) {
331 customTagContent.append(curNode.getText());
332 }
333
334 DetailNode toVisit = curNode.getFirstChild();
335 while (curNode != descriptionNode && toVisit == null) {
336 toVisit = curNode.getNextSibling();
337 curNode = curNode.getParent();
338 }
339
340 curNode = toVisit;
341 }
342 return customTagContent.toString();
343 }
344
345
346
347
348
349
350
351 private static boolean isLeadingAsterisk(DetailNode node) {
352 return node.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
353 || node.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISKS;
354 }
355
356
357
358
359
360
361
362 private static String getVisibleContent(String summary) {
363 final String visibleSummary = HTML_ELEMENTS.matcher(summary).replaceAll("");
364 return visibleSummary.trim();
365 }
366
367
368
369
370
371
372
373 private boolean containsForbiddenFragment(String firstSentence) {
374 final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
375 .matcher(firstSentence).replaceAll(" ");
376 return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find();
377 }
378
379
380
381
382
383
384
385
386 private static String prependJavadocToolWord(String inlineReturn) {
387 return "Returns " + inlineReturn;
388 }
389
390
391
392
393
394
395
396 private static String trimExcessWhitespaces(String text) {
397 final StringBuilder result = new StringBuilder(256);
398 boolean previousWhitespace = true;
399
400 for (int index = 0; index < text.length(); index++) {
401 final char letter = text.charAt(index);
402 final char print;
403 if (Character.isWhitespace(letter)) {
404 if (previousWhitespace) {
405 continue;
406 }
407
408 previousWhitespace = true;
409 print = ' ';
410 }
411 else {
412 previousWhitespace = false;
413 print = letter;
414 }
415
416 result.append(print);
417 }
418
419 return result.toString();
420 }
421
422
423
424
425
426
427
428 private static boolean startsWithInheritDoc(DetailNode root) {
429 boolean found = false;
430 DetailNode node = root.getFirstChild();
431
432 while (node != null) {
433 if (node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG
434 && node.getFirstChild().getType()
435 == JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG) {
436 found = true;
437 }
438 if ((node.getType() == JavadocCommentsTokenTypes.TEXT
439 || node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT)
440 && !CommonUtil.isBlank(node.getText())) {
441 break;
442 }
443 node = node.getNextSibling();
444 }
445
446 return found;
447 }
448
449
450
451
452
453
454
455 private static String getSummarySentence(DetailNode ast) {
456 final StringBuilder result = new StringBuilder(256);
457 DetailNode node = ast.getFirstChild();
458 while (node != null) {
459 if (node.getType() == JavadocCommentsTokenTypes.TEXT) {
460 result.append(node.getText());
461 }
462 else {
463 final String summary = result.toString();
464 if (CommonUtil.isBlank(summary)
465 && node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
466 final DetailNode htmlContentToken = JavadocUtil.findFirstToken(
467 node, JavadocCommentsTokenTypes.HTML_CONTENT);
468 result.append(getStringInsideHtmlTag(summary, htmlContentToken));
469 }
470 }
471 node = node.getNextSibling();
472 }
473 return result.toString().trim();
474 }
475
476
477
478
479
480
481
482
483 private static String getStringInsideHtmlTag(String result, DetailNode detailNode) {
484 final StringBuilder contents = new StringBuilder(result);
485 if (detailNode != null) {
486 DetailNode tempNode = detailNode.getFirstChild();
487 while (tempNode != null) {
488 if (tempNode.getType() == JavadocCommentsTokenTypes.TEXT) {
489 contents.append(tempNode.getText());
490 }
491 else {
492 final DetailNode htmlContentToken = JavadocUtil.findFirstToken(
493 tempNode, JavadocCommentsTokenTypes.HTML_CONTENT);
494 contents.append(getStringInsideHtmlTag("", htmlContentToken));
495 }
496 tempNode = tempNode.getNextSibling();
497 }
498 }
499 return contents.toString();
500 }
501
502
503
504
505
506
507
508
509
510
511 private static Optional<String> getFirstSentence(DetailNode ast, String period) {
512 final List<String> sentenceParts = new ArrayList<>();
513 Optional<String> result = Optional.empty();
514 for (String text : (Iterable<String>) streamTextParts(ast)::iterator) {
515 final Optional<String> sentenceEnding = findSentenceEnding(text, period);
516
517 if (sentenceEnding.isPresent()) {
518 sentenceParts.add(sentenceEnding.get());
519 result = Optional.of(String.join("", sentenceParts));
520 break;
521 }
522 sentenceParts.add(text);
523 }
524 return result;
525 }
526
527
528
529
530
531
532
533 private static Stream<String> streamTextParts(DetailNode node) {
534 final Stream<String> result;
535 if (node.getFirstChild() == null) {
536 result = Stream.of(node.getText());
537 }
538 else {
539 final List<Stream<String>> childStreams = new ArrayList<>();
540 DetailNode child = node.getFirstChild();
541 while (child != null) {
542 childStreams.add(streamTextParts(child));
543 child = child.getNextSibling();
544 }
545 result = childStreams.stream().flatMap(Function.identity());
546 }
547 return result;
548 }
549
550
551
552
553
554
555
556
557
558
559 private static Optional<String> findSentenceEnding(String text, String period) {
560 int periodIndex = text.indexOf(period);
561 Optional<String> result = Optional.empty();
562 while (periodIndex >= 0) {
563 final int afterPeriodIndex = periodIndex + period.length();
564
565
566
567 if (!DEFAULT_PERIOD.equals(period)
568 || afterPeriodIndex >= text.length()
569 || Character.isWhitespace(text.charAt(afterPeriodIndex))) {
570 final String resultStr = text.substring(0, periodIndex);
571 result = Optional.of(resultStr);
572 break;
573 }
574 periodIndex = text.indexOf(period, afterPeriodIndex);
575 }
576 return result;
577 }
578
579 }