View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TextBlock;
10  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
11  import java.util.Locale;
12  
13  /**
14   * Check for a stray asterisk at the end of a Javadoc line.
15   *
16   * <p>A paragraph opened with {@code <p>} is sometimes "closed" with a bare
17   * {@code *} appended to the last line of its text, instead of {@code </p>}
18   * or nothing at all. Javadoc renders that asterisk as literal text, right
19   * after the sentence. See
20   * <a href="https://github.com/yegor256/qulice/issues/1783">#1783</a>.</p>
21   *
22   * <p>The following Javadoc will be reported as a violation, since the last
23   * line of the paragraph ends with an asterisk that belongs to no comment
24   * delimiter:</p>
25   * <pre>
26   * &#47;**
27   *  * &lt;p&gt;The sentinel is not a real expression kind, it is
28   *  <span style="color:red" >* the parent for entries pushed at indent zero. *</span>
29   *  *&#47;
30   * </pre>
31   *
32   * <p>And this is how it should be written instead:</p>
33   * <pre>
34   * &#47;**
35   *  * &lt;p&gt;The sentinel is not a real expression kind, it is
36   *  * the parent for entries pushed at indent zero.
37   *  *&#47;
38   * </pre>
39   *
40   * <p>The asterisks that open the comment, prefix its lines, and close it are
41   * not touched. Lines inside a {@code <pre>...</pre>} block or a
42   * {@code {@snippet ...}} block are skipped, since an asterisk may legally
43   * end a line of example code there.</p>
44   *
45   * @since 0.73.4
46   */
47  public final class JavadocStrayAsteriskCheck extends AbstractCheck {
48  
49      /**
50       * Default constructor.
51       */
52      public JavadocStrayAsteriskCheck() {
53          // nothing to initialize
54      }
55  
56      @Override
57      public int[] getDefaultTokens() {
58          return new int[] {
59              TokenTypes.PACKAGE_DEF,
60              TokenTypes.CLASS_DEF,
61              TokenTypes.INTERFACE_DEF,
62              TokenTypes.ANNOTATION_DEF,
63              TokenTypes.ANNOTATION_FIELD_DEF,
64              TokenTypes.ENUM_DEF,
65              TokenTypes.ENUM_CONSTANT_DEF,
66              TokenTypes.VARIABLE_DEF,
67              TokenTypes.CTOR_DEF,
68              TokenTypes.METHOD_DEF,
69          };
70      }
71  
72      @Override
73      public int[] getAcceptableTokens() {
74          return this.getDefaultTokens();
75      }
76  
77      @Override
78      public int[] getRequiredTokens() {
79          return this.getDefaultTokens();
80      }
81  
82      @Override
83      @SuppressWarnings("deprecation")
84      public void visitToken(final DetailAST ast) {
85          final TextBlock doc =
86              this.getFileContents().getJavadocBefore(ast.getLineNo());
87          if (doc != null) {
88              this.check(doc);
89          }
90      }
91  
92      private void check(final TextBlock doc) {
93          final String[] lines = doc.getText();
94          boolean pre = false;
95          int depth = 0;
96          for (int pos = 0; pos < lines.length; pos += 1) {
97              final String body = JavadocStrayAsteriskCheck.body(lines[pos]);
98              final String low = body.toLowerCase(Locale.ENGLISH);
99              if (depth > 0) {
100                 depth += JavadocStrayAsteriskCheck.braces(body);
101             } else if (low.contains("{@snippet")) {
102                 depth = Math.max(0, JavadocStrayAsteriskCheck.braces(body));
103             } else if (pre) {
104                 pre = !low.contains("</pre>");
105             } else if (low.contains("<pre>")) {
106                 pre = !low.contains("</pre>");
107             } else if (body.endsWith("*")) {
108                 this.log(
109                     doc.getStartLineNo() + pos,
110                     "Javadoc line must not end with a stray asterisk"
111                 );
112             }
113         }
114     }
115 
116     private static String body(final String line) {
117         String trimmed = line.trim();
118         if (trimmed.endsWith("*/")) {
119             trimmed = trimmed.substring(0, trimmed.length() - 2).trim();
120         }
121         if (trimmed.startsWith("/**")) {
122             trimmed = trimmed.substring(3).trim();
123         } else if (trimmed.startsWith("*")) {
124             trimmed = trimmed.substring(1).trim();
125         }
126         return trimmed;
127     }
128 
129     private static int braces(final String body) {
130         int delta = 0;
131         for (int pos = 0; pos < body.length(); pos += 1) {
132             final char chr = body.charAt(pos);
133             if (chr == '{') {
134                 delta += 1;
135             } else if (chr == '}') {
136                 delta -= 1;
137             }
138         }
139         return delta;
140     }
141 }