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