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