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 }