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