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