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(), ast.getColumnNumber(), 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(), ast.getColumnNumber(), MSG_SUMMARY_JAVADOC);
189 }
190 }
191 else {
192 log(ast.getLineNumber(), ast.getColumnNumber(), MSG_SUMMARY_FIRST_SENTENCE);
193 }
194 }
195 else {
196 log(ast.getLineNumber(), ast.getColumnNumber(), 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(), inlineSummaryTag.getColumnNumber(),
280 MSG_SUMMARY_JAVADOC_MISSING);
281 }
282 else if (!period.isEmpty()) {
283 final boolean isPeriodNotAtEnd =
284 summaryVisible.lastIndexOf(period) != summaryVisible.length() - 1;
285 if (isPeriodNotAtEnd) {
286 log(inlineSummaryTag.getLineNumber(), inlineSummaryTag.getColumnNumber(),
287 MSG_SUMMARY_MISSING_PERIOD);
288 }
289 else if (containsForbiddenFragment(inlineSummary)) {
290 log(inlineSummaryTag.getLineNumber(), inlineSummaryTag.getColumnNumber(),
291 MSG_SUMMARY_JAVADOC);
292 }
293 }
294 }
295
296
297
298
299
300
301 private void validateInlineReturnTag(DetailNode inlineReturnTag) {
302 final DetailNode descriptionNode = JavadocUtil.findFirstToken(
303 inlineReturnTag, JavadocCommentsTokenTypes.DESCRIPTION);
304 final String inlineReturn = getContentOfInlineCustomTag(descriptionNode);
305 final String returnVisible = getVisibleContent(inlineReturn);
306 if (returnVisible.isEmpty()) {
307 log(inlineReturnTag.getLineNumber(), inlineReturnTag.getColumnNumber(),
308 MSG_SUMMARY_JAVADOC_MISSING);
309 }
310 else if (containsForbiddenFragment(inlineReturn)) {
311 log(inlineReturnTag.getLineNumber(), inlineReturnTag.getColumnNumber(),
312 MSG_SUMMARY_JAVADOC);
313 }
314 }
315
316
317
318
319
320
321
322 public static String getContentOfInlineCustomTag(DetailNode descriptionNode) {
323 final StringBuilder customTagContent = new StringBuilder(256);
324 DetailNode curNode = descriptionNode;
325 while (curNode != null) {
326 if (curNode.getFirstChild() == null
327 && curNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK) {
328 customTagContent.append(curNode.getText());
329 }
330
331 DetailNode toVisit = curNode.getFirstChild();
332 while (curNode != descriptionNode && toVisit == null) {
333 toVisit = curNode.getNextSibling();
334 curNode = curNode.getParent();
335 }
336
337 curNode = toVisit;
338 }
339 return customTagContent.toString();
340 }
341
342
343
344
345
346
347
348 private static String getVisibleContent(String summary) {
349 final String visibleSummary = HTML_ELEMENTS.matcher(summary).replaceAll("");
350 return visibleSummary.trim();
351 }
352
353
354
355
356
357
358
359 private boolean containsForbiddenFragment(String firstSentence) {
360 final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
361 .matcher(firstSentence).replaceAll(" ");
362 return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find();
363 }
364
365
366
367
368
369
370
371 private static String trimExcessWhitespaces(String text) {
372 final StringBuilder result = new StringBuilder(256);
373 boolean previousWhitespace = true;
374
375 for (int index = 0; index < text.length(); index++) {
376 final char letter = text.charAt(index);
377 final char print;
378 if (Character.isWhitespace(letter)) {
379 if (previousWhitespace) {
380 continue;
381 }
382
383 previousWhitespace = true;
384 print = ' ';
385 }
386 else {
387 previousWhitespace = false;
388 print = letter;
389 }
390
391 result.append(print);
392 }
393
394 return result.toString();
395 }
396
397
398
399
400
401
402
403 private static boolean startsWithInheritDoc(DetailNode root) {
404 boolean found = false;
405 DetailNode node = root.getFirstChild();
406
407 while (node != null) {
408 if (node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG
409 && node.getFirstChild().getType()
410 == JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG) {
411 found = true;
412 }
413 if ((node.getType() == JavadocCommentsTokenTypes.TEXT
414 || node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT)
415 && !CommonUtil.isBlank(node.getText())) {
416 break;
417 }
418 node = node.getNextSibling();
419 }
420
421 return found;
422 }
423
424
425
426
427
428
429
430 private static String getSummarySentence(DetailNode ast) {
431 final StringBuilder result = new StringBuilder(256);
432 DetailNode node = ast.getFirstChild();
433 while (node != null) {
434 if (node.getType() == JavadocCommentsTokenTypes.TEXT) {
435 result.append(node.getText());
436 }
437 else {
438 final String summary = result.toString();
439 if (CommonUtil.isBlank(summary)
440 && node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
441 final DetailNode htmlContentToken = JavadocUtil.findFirstToken(
442 node, JavadocCommentsTokenTypes.HTML_CONTENT);
443 result.append(getStringInsideHtmlTag(summary, htmlContentToken));
444 }
445 }
446 node = node.getNextSibling();
447 }
448 return result.toString().trim();
449 }
450
451
452
453
454
455
456
457
458 private static String getStringInsideHtmlTag(String result, DetailNode detailNode) {
459 final StringBuilder contents = new StringBuilder(result);
460 if (detailNode != null) {
461 DetailNode tempNode = detailNode.getFirstChild();
462 while (tempNode != null) {
463 if (tempNode.getType() == JavadocCommentsTokenTypes.TEXT) {
464 contents.append(tempNode.getText());
465 }
466 tempNode = tempNode.getNextSibling();
467 }
468 }
469 return contents.toString();
470 }
471
472
473
474
475
476
477
478
479
480
481 private static Optional<String> getFirstSentence(DetailNode ast, String period) {
482 final List<String> sentenceParts = new ArrayList<>();
483 Optional<String> result = Optional.empty();
484 for (String text : (Iterable<String>) streamTextParts(ast)::iterator) {
485 final Optional<String> sentenceEnding = findSentenceEnding(text, period);
486
487 if (sentenceEnding.isPresent()) {
488 sentenceParts.add(sentenceEnding.get());
489 result = Optional.of(String.join("", sentenceParts));
490 break;
491 }
492 sentenceParts.add(text);
493 }
494 return result;
495 }
496
497
498
499
500
501
502
503 private static Stream<String> streamTextParts(DetailNode node) {
504 final Stream<String> result;
505 if (node.getFirstChild() == null) {
506 result = Stream.of(node.getText());
507 }
508 else {
509 final List<Stream<String>> childStreams = new ArrayList<>();
510 DetailNode child = node.getFirstChild();
511 while (child != null) {
512 childStreams.add(streamTextParts(child));
513 child = child.getNextSibling();
514 }
515 result = childStreams.stream().flatMap(Function.identity());
516 }
517 return result;
518 }
519
520
521
522
523
524
525
526
527
528
529 private static Optional<String> findSentenceEnding(String text, String period) {
530 int periodIndex = text.indexOf(period);
531 Optional<String> result = Optional.empty();
532 while (periodIndex >= 0) {
533 final int afterPeriodIndex = periodIndex + period.length();
534
535
536
537 if (!DEFAULT_PERIOD.equals(period)
538 || afterPeriodIndex >= text.length()
539 || Character.isWhitespace(text.charAt(afterPeriodIndex))) {
540 final String resultStr = text.substring(0, periodIndex);
541 result = Optional.of(resultStr);
542 break;
543 }
544 periodIndex = text.indexOf(period, afterPeriodIndex);
545 }
546 return result;
547 }
548 }