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
116
117
118 public void setForbiddenSummaryFragments(Pattern pattern) {
119 forbiddenSummaryFragments = pattern;
120 }
121
122
123
124
125
126
127
128
129
130
131
132 public void setPeriod(String period) {
133 this.period = period;
134 }
135
136 @Override
137 public int[] getDefaultJavadocTokens() {
138 return new int[] {
139 JavadocCommentsTokenTypes.JAVADOC_CONTENT,
140 JavadocCommentsTokenTypes.SUMMARY_INLINE_TAG,
141 JavadocCommentsTokenTypes.RETURN_INLINE_TAG,
142 };
143 }
144
145 @Override
146 public int[] getRequiredJavadocTokens() {
147 return getAcceptableJavadocTokens();
148 }
149
150 @Override
151 public void visitJavadocToken(DetailNode ast) {
152 if (isSummaryTag(ast) && isDefinedFirst(ast.getParent())) {
153 shouldValidateUntaggedSummary = false;
154 validateSummaryTag(ast);
155 }
156 else if (isInlineReturnTag(ast)) {
157 shouldValidateUntaggedSummary = false;
158 validateInlineReturnTag(ast);
159 }
160 }
161
162 @Override
163 public void leaveJavadocToken(DetailNode ast) {
164 if (ast.getType() == JavadocCommentsTokenTypes.JAVADOC_CONTENT) {
165 if (shouldValidateUntaggedSummary && !startsWithInheritDoc(ast)) {
166 validateUntaggedSummary(ast);
167 }
168 shouldValidateUntaggedSummary = true;
169 }
170 }
171
172
173
174
175
176
177 private void validateUntaggedSummary(DetailNode ast) {
178 final String summaryDoc = getSummarySentence(ast);
179 if (summaryDoc.isEmpty()) {
180 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
181 }
182 else if (!period.isEmpty()) {
183 if (summaryDoc.contains(period)) {
184 final Optional<String> firstSentence = getFirstSentence(ast, period);
185
186 if (firstSentence.isPresent()) {
187 if (containsForbiddenFragment(firstSentence.get())) {
188 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC);
189 }
190 }
191 else {
192 log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE);
193 }
194 }
195 else {
196 log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE);
197 }
198 }
199 }
200
201
202
203
204
205
206
207 private static boolean isDefinedFirst(DetailNode inlineTagNode) {
208 boolean isDefinedFirst = true;
209 DetailNode currentAst = inlineTagNode.getPreviousSibling();
210 while (currentAst != null && isDefinedFirst) {
211 switch (currentAst.getType()) {
212 case JavadocCommentsTokenTypes.TEXT ->
213 isDefinedFirst = currentAst.getText().isBlank();
214 case JavadocCommentsTokenTypes.HTML_ELEMENT ->
215 isDefinedFirst = isHtmlTagWithoutText(currentAst);
216 case JavadocCommentsTokenTypes.LEADING_ASTERISK,
217 JavadocCommentsTokenTypes.NEWLINE -> {
218
219 }
220 default -> isDefinedFirst = false;
221 }
222 currentAst = currentAst.getPreviousSibling();
223 }
224 return isDefinedFirst;
225 }
226
227
228
229
230
231
232
233 public static boolean isHtmlTagWithoutText(DetailNode node) {
234 boolean isEmpty = true;
235 final DetailNode htmlContentToken =
236 JavadocUtil.findFirstToken(node, JavadocCommentsTokenTypes.HTML_CONTENT);
237
238 if (htmlContentToken != null) {
239 final DetailNode child = htmlContentToken.getFirstChild();
240 isEmpty = child.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT
241 && isHtmlTagWithoutText(child);
242 }
243 return isEmpty;
244 }
245
246
247
248
249
250
251
252
253 private static boolean isSummaryTag(DetailNode javadocInlineTag) {
254 return javadocInlineTag.getType() == JavadocCommentsTokenTypes.SUMMARY_INLINE_TAG;
255 }
256
257
258
259
260
261
262
263
264 private static boolean isInlineReturnTag(DetailNode javadocInlineTag) {
265 return javadocInlineTag.getType() == JavadocCommentsTokenTypes.RETURN_INLINE_TAG;
266 }
267
268
269
270
271
272
273 private void validateSummaryTag(DetailNode inlineSummaryTag) {
274 final DetailNode descriptionNode = JavadocUtil.findFirstToken(
275 inlineSummaryTag, JavadocCommentsTokenTypes.DESCRIPTION);
276 final String inlineSummary = getContentOfInlineCustomTag(descriptionNode);
277 final String summaryVisible = getVisibleContent(inlineSummary);
278 if (summaryVisible.isEmpty()) {
279 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
280 }
281 else if (!period.isEmpty()) {
282 final boolean isPeriodNotAtEnd =
283 summaryVisible.lastIndexOf(period) != summaryVisible.length() - 1;
284 if (isPeriodNotAtEnd) {
285 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_MISSING_PERIOD);
286 }
287 else if (containsForbiddenFragment(inlineSummary)) {
288 log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
289 }
290 }
291 }
292
293
294
295
296
297
298 private void validateInlineReturnTag(DetailNode inlineReturnTag) {
299 final DetailNode descriptionNode = JavadocUtil.findFirstToken(
300 inlineReturnTag, JavadocCommentsTokenTypes.DESCRIPTION);
301 final String inlineReturn = getContentOfInlineCustomTag(descriptionNode);
302 final String returnVisible = getVisibleContent(inlineReturn);
303 if (returnVisible.isEmpty()) {
304 log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
305 }
306 else if (containsForbiddenFragment(inlineReturn)) {
307 log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
308 }
309 }
310
311
312
313
314
315
316
317 public static String getContentOfInlineCustomTag(DetailNode descriptionNode) {
318 final StringBuilder customTagContent = new StringBuilder(256);
319 DetailNode curNode = descriptionNode;
320 while (curNode != null) {
321 if (curNode.getFirstChild() == null
322 && curNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK) {
323 customTagContent.append(curNode.getText());
324 }
325
326 DetailNode toVisit = curNode.getFirstChild();
327 while (curNode != descriptionNode && toVisit == null) {
328 toVisit = curNode.getNextSibling();
329 curNode = curNode.getParent();
330 }
331
332 curNode = toVisit;
333 }
334 return customTagContent.toString();
335 }
336
337
338
339
340
341
342
343 private static String getVisibleContent(String summary) {
344 final String visibleSummary = HTML_ELEMENTS.matcher(summary).replaceAll("");
345 return visibleSummary.trim();
346 }
347
348
349
350
351
352
353
354 private boolean containsForbiddenFragment(String firstSentence) {
355 final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
356 .matcher(firstSentence).replaceAll(" ");
357 return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find();
358 }
359
360
361
362
363
364
365
366 private static String trimExcessWhitespaces(String text) {
367 final StringBuilder result = new StringBuilder(256);
368 boolean previousWhitespace = true;
369
370 for (int index = 0; index < text.length(); index++) {
371 final char letter = text.charAt(index);
372 final char print;
373 if (Character.isWhitespace(letter)) {
374 if (previousWhitespace) {
375 continue;
376 }
377
378 previousWhitespace = true;
379 print = ' ';
380 }
381 else {
382 previousWhitespace = false;
383 print = letter;
384 }
385
386 result.append(print);
387 }
388
389 return result.toString();
390 }
391
392
393
394
395
396
397
398 private static boolean startsWithInheritDoc(DetailNode root) {
399 boolean found = false;
400 DetailNode node = root.getFirstChild();
401
402 while (node != null) {
403 if (node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG
404 && node.getFirstChild().getType()
405 == JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG) {
406 found = true;
407 }
408 if ((node.getType() == JavadocCommentsTokenTypes.TEXT
409 || node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT)
410 && !CommonUtil.isBlank(node.getText())) {
411 break;
412 }
413 node = node.getNextSibling();
414 }
415
416 return found;
417 }
418
419
420
421
422
423
424
425 private static String getSummarySentence(DetailNode ast) {
426 final StringBuilder result = new StringBuilder(256);
427 DetailNode node = ast.getFirstChild();
428 while (node != null) {
429 if (node.getType() == JavadocCommentsTokenTypes.TEXT) {
430 result.append(node.getText());
431 }
432 else {
433 final String summary = result.toString();
434 if (CommonUtil.isBlank(summary)
435 && node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
436 final DetailNode htmlContentToken = JavadocUtil.findFirstToken(
437 node, JavadocCommentsTokenTypes.HTML_CONTENT);
438 result.append(getStringInsideHtmlTag(summary, htmlContentToken));
439 }
440 }
441 node = node.getNextSibling();
442 }
443 return result.toString().trim();
444 }
445
446
447
448
449
450
451
452
453 private static String getStringInsideHtmlTag(String result, DetailNode detailNode) {
454 final StringBuilder contents = new StringBuilder(result);
455 if (detailNode != null) {
456 DetailNode tempNode = detailNode.getFirstChild();
457 while (tempNode != null) {
458 if (tempNode.getType() == JavadocCommentsTokenTypes.TEXT) {
459 contents.append(tempNode.getText());
460 }
461 tempNode = tempNode.getNextSibling();
462 }
463 }
464 return contents.toString();
465 }
466
467
468
469
470
471
472
473
474
475
476 private static Optional<String> getFirstSentence(DetailNode ast, String period) {
477 final List<String> sentenceParts = new ArrayList<>();
478 Optional<String> result = Optional.empty();
479 for (String text : (Iterable<String>) streamTextParts(ast)::iterator) {
480 final Optional<String> sentenceEnding = findSentenceEnding(text, period);
481
482 if (sentenceEnding.isPresent()) {
483 sentenceParts.add(sentenceEnding.get());
484 result = Optional.of(String.join("", sentenceParts));
485 break;
486 }
487 sentenceParts.add(text);
488 }
489 return result;
490 }
491
492
493
494
495
496
497
498 private static Stream<String> streamTextParts(DetailNode node) {
499 final Stream<String> result;
500 if (node.getFirstChild() == null) {
501 result = Stream.of(node.getText());
502 }
503 else {
504 final List<Stream<String>> childStreams = new ArrayList<>();
505 DetailNode child = node.getFirstChild();
506 while (child != null) {
507 childStreams.add(streamTextParts(child));
508 child = child.getNextSibling();
509 }
510 result = childStreams.stream().flatMap(Function.identity());
511 }
512 return result;
513 }
514
515
516
517
518
519
520
521
522
523
524 private static Optional<String> findSentenceEnding(String text, String period) {
525 int periodIndex = text.indexOf(period);
526 Optional<String> result = Optional.empty();
527 while (periodIndex >= 0) {
528 final int afterPeriodIndex = periodIndex + period.length();
529
530
531
532 if (!DEFAULT_PERIOD.equals(period)
533 || afterPeriodIndex >= text.length()
534 || Character.isWhitespace(text.charAt(afterPeriodIndex))) {
535 final String resultStr = text.substring(0, periodIndex);
536 result = Optional.of(resultStr);
537 break;
538 }
539 periodIndex = text.indexOf(period, afterPeriodIndex);
540 }
541 return result;
542 }
543 }