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.Set;
23
24 import javax.annotation.Nullable;
25
26 import com.puppycrawl.tools.checkstyle.StatelessCheck;
27 import com.puppycrawl.tools.checkstyle.api.DetailNode;
28 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
29 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
31
32
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 @StatelessCheck
67 public class JavadocParagraphCheck extends AbstractJavadocCheck {
68
69
70
71
72
73 public static final String MSG_TAG_AFTER = "javadoc.paragraph.tag.after";
74
75
76
77
78
79 public static final String MSG_LINE_BEFORE = "javadoc.paragraph.line.before";
80
81
82
83
84
85 public static final String MSG_REDUNDANT_PARAGRAPH = "javadoc.paragraph.redundant.paragraph";
86
87
88
89
90
91 public static final String MSG_MISPLACED_TAG = "javadoc.paragraph.misplaced.tag";
92
93
94
95
96
97 public static final String MSG_PRECEDED_BLOCK_TAG = "javadoc.paragraph.preceded.block.tag";
98
99
100
101
102 private static final String PARAGRAPH_TAG = "p";
103
104
105
106
107 private static final Set<String> BLOCK_TAGS =
108 Set.of("address", "blockquote", "div", "dl",
109 "h1", "h2", "h3", "h4", "h5", "h6", "hr",
110 "ol", PARAGRAPH_TAG, "pre", "table", "ul");
111
112
113
114
115 private boolean allowNewlineParagraph = true;
116
117
118
119
120 public JavadocParagraphCheck() {
121
122 }
123
124
125
126
127
128
129
130
131 public void setAllowNewlineParagraph(boolean value) {
132 allowNewlineParagraph = value;
133 }
134
135 @Override
136 public int[] getDefaultJavadocTokens() {
137 return new int[] {
138 JavadocCommentsTokenTypes.NEWLINE,
139 JavadocCommentsTokenTypes.HTML_ELEMENT,
140 };
141 }
142
143 @Override
144 public int[] getRequiredJavadocTokens() {
145 return getAcceptableJavadocTokens();
146 }
147
148 @Override
149 public void visitJavadocToken(DetailNode ast) {
150 if (ast.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(ast)) {
151 checkEmptyLine(ast);
152 }
153 else if (JavadocUtil.isTag(ast, PARAGRAPH_TAG)) {
154 checkParagraphTag(ast);
155 }
156 }
157
158
159
160
161
162
163 private void checkEmptyLine(DetailNode newline) {
164 final DetailNode nearestToken = getNearestNode(newline);
165 if (nearestToken != null && nearestToken.getType() == JavadocCommentsTokenTypes.TEXT
166 && !CommonUtil.isBlank(nearestToken.getText())) {
167 log(newline, MSG_TAG_AFTER);
168 }
169 }
170
171
172
173
174
175
176 private void checkParagraphTag(DetailNode tag) {
177 if (!isNestedParagraph(tag) && !isInsideBlockTag(tag)) {
178 final DetailNode newLine = getNearestEmptyLine(tag);
179 if (isFirstParagraph(tag)) {
180 log(tag, MSG_REDUNDANT_PARAGRAPH);
181 }
182 else if (newLine == null || tag.getLineNumber() - newLine.getLineNumber() != 1) {
183 log(tag, MSG_LINE_BEFORE);
184 }
185
186 final String blockTagName = findFollowedBlockTagName(tag);
187 if (blockTagName != null) {
188 log(tag, MSG_PRECEDED_BLOCK_TAG, blockTagName);
189 }
190
191 if (!allowNewlineParagraph && isImmediatelyFollowedByNewLine(tag)) {
192 log(tag, MSG_MISPLACED_TAG);
193 }
194 if (isImmediatelyFollowedByText(tag)) {
195 log(tag, MSG_MISPLACED_TAG);
196 }
197 }
198 }
199
200
201
202
203
204
205
206 private static boolean isNestedParagraph(DetailNode tag) {
207 boolean nested = false;
208 DetailNode parent = tag.getParent();
209
210 while (parent != null) {
211 if (parent.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
212 nested = true;
213 break;
214 }
215 parent = parent.getParent();
216 }
217
218 return nested;
219 }
220
221
222
223
224
225
226
227 private static boolean isInsideBlockTag(DetailNode tag) {
228 boolean result = false;
229 DetailNode parent = tag;
230
231 while (parent != null) {
232 if (parent.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
233 result = true;
234 break;
235 }
236 parent = parent.getParent();
237 }
238
239 return result;
240 }
241
242
243
244
245
246
247
248 @Nullable
249 private static String findFollowedBlockTagName(DetailNode tag) {
250 final DetailNode htmlElement = findFirstHtmlElementAfter(tag);
251 String blockTagName = null;
252
253 if (htmlElement != null) {
254 blockTagName = getHtmlElementName(htmlElement);
255 }
256
257 return blockTagName;
258 }
259
260
261
262
263
264
265
266 @Nullable
267 private static DetailNode findFirstHtmlElementAfter(DetailNode tag) {
268 DetailNode htmlElement = getNextSibling(tag);
269
270 while (htmlElement != null
271 && htmlElement.getType() != JavadocCommentsTokenTypes.HTML_ELEMENT) {
272 if (htmlElement.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
273 htmlElement = htmlElement.getFirstChild();
274 }
275 else if (htmlElement.getType() == JavadocCommentsTokenTypes.TEXT
276 && !CommonUtil.isBlank(htmlElement.getText())) {
277 htmlElement = null;
278 break;
279 }
280 else {
281 htmlElement = htmlElement.getNextSibling();
282 }
283 }
284 if (htmlElement != null
285 && JavadocUtil.findFirstToken(htmlElement,
286 JavadocCommentsTokenTypes.HTML_TAG_END) == null) {
287 htmlElement = null;
288 }
289
290 return htmlElement;
291 }
292
293
294
295
296
297
298
299 @Nullable
300 private static String getHtmlElementName(DetailNode htmlElement) {
301 final DetailNode htmlTagStart = htmlElement.getFirstChild();
302 final DetailNode htmlTagName =
303 JavadocUtil.findFirstToken(htmlTagStart, JavadocCommentsTokenTypes.TAG_NAME);
304 String blockTagName = null;
305 if (BLOCK_TAGS.contains(htmlTagName.getText())) {
306 blockTagName = htmlTagName.getText();
307 }
308
309 return blockTagName;
310 }
311
312
313
314
315
316
317
318 private static DetailNode getNearestNode(DetailNode node) {
319 DetailNode currentNode = node;
320 while (currentNode != null
321 && (currentNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
322 || currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE)) {
323 currentNode = currentNode.getNextSibling();
324 }
325 if (currentNode != null
326 && currentNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
327 currentNode = currentNode.getFirstChild();
328 }
329 return currentNode;
330 }
331
332
333
334
335
336
337
338 private static boolean isEmptyLine(DetailNode newLine) {
339 boolean result = false;
340 DetailNode previousSibling = newLine.getPreviousSibling();
341 if (previousSibling != null && (previousSibling.getParent().getType()
342 == JavadocCommentsTokenTypes.JAVADOC_CONTENT
343 || insideNonTightHtml(previousSibling))) {
344 if (previousSibling.getType() == JavadocCommentsTokenTypes.TEXT
345 && CommonUtil.isBlank(previousSibling.getText())) {
346 previousSibling = previousSibling.getPreviousSibling();
347 }
348 result = previousSibling != null
349 && previousSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK;
350 }
351 return result;
352 }
353
354
355
356
357
358
359
360 private static boolean insideNonTightHtml(DetailNode previousSibling) {
361 final DetailNode parent = previousSibling.getParent();
362 DetailNode htmlElement = parent;
363 if (parent.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
364 htmlElement = parent.getParent();
365 }
366 return htmlElement.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT
367 && JavadocUtil.findFirstToken(htmlElement,
368 JavadocCommentsTokenTypes.HTML_TAG_END) == null;
369 }
370
371
372
373
374
375
376
377 private static boolean isFirstParagraph(DetailNode paragraphTag) {
378 boolean result = true;
379 DetailNode previousNode = paragraphTag.getPreviousSibling();
380 while (previousNode != null) {
381 if (previousNode.getType() == JavadocCommentsTokenTypes.TEXT
382 && !CommonUtil.isBlank(previousNode.getText())
383 || previousNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK
384 && previousNode.getType() != JavadocCommentsTokenTypes.NEWLINE
385 && previousNode.getType() != JavadocCommentsTokenTypes.TEXT) {
386 result = false;
387 break;
388 }
389 previousNode = previousNode.getPreviousSibling();
390 }
391 return result;
392 }
393
394
395
396
397
398
399
400 private static DetailNode getNearestEmptyLine(DetailNode node) {
401 DetailNode newLine = node;
402 while (newLine != null) {
403 final DetailNode previousSibling = newLine.getPreviousSibling();
404 if (newLine.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(newLine)) {
405 break;
406 }
407 newLine = previousSibling;
408 }
409 return newLine;
410 }
411
412
413
414
415
416
417
418 private static boolean isImmediatelyFollowedByText(DetailNode tag) {
419 final DetailNode nextSibling = getNextSibling(tag);
420
421 return nextSibling == null || nextSibling.getText().startsWith(" ");
422 }
423
424
425
426
427
428
429
430 private static boolean isImmediatelyFollowedByNewLine(DetailNode tag) {
431 final DetailNode sibling = getNextSibling(tag);
432 return sibling != null && sibling.getType() == JavadocCommentsTokenTypes.NEWLINE;
433 }
434
435
436
437
438
439
440
441
442 private static DetailNode getNextSibling(DetailNode tag) {
443 DetailNode nextSibling;
444 final DetailNode paragraphStartTagToken = tag.getFirstChild();
445 final DetailNode nextNode = paragraphStartTagToken.getNextSibling();
446
447 if (nextNode == null) {
448 nextSibling = tag.getNextSibling();
449 }
450 else if (nextNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
451 nextSibling = nextNode.getFirstChild();
452 }
453 else {
454 nextSibling = nextNode;
455 }
456
457 if (nextSibling != null
458 && nextSibling.getType() == JavadocCommentsTokenTypes.HTML_COMMENT) {
459 nextSibling = nextSibling.getNextSibling();
460 }
461 return nextSibling;
462 }
463
464 }