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.TokenTypes;
10  import java.util.Locale;
11  
12  /**
13   * Checks that Javadoc body text is not over-indented.
14   *
15   * <p>Every line of a Javadoc comment must have exactly one space between the
16   * leading asterisk and the first word of the text. Extra spaces, used to
17   * push a paragraph line to the right, are not allowed:</p>
18   *
19   * <pre>
20   * &#47;**
21   *  * This is a paragraph.
22   *  *     This line is over-indented and will be reported.
23   *  *&#47;
24   * </pre>
25   *
26   * <p>Lines inside a {@code <pre>} block or a {@code @snippet} block are left
27   * alone, since the indentation there is semantically meaningful and has to be
28   * preserved as written. Block tags and their wrapped continuation lines are
29   * also left alone, since their indentation is governed by other checks.</p>
30   *
31   * @since 0.74
32   */
33  public final class JavadocNoIndentCheck extends AbstractCheck {
34  
35      /**
36       * Default constructor.
37       */
38      public JavadocNoIndentCheck() {
39          // nothing to initialize
40      }
41  
42      @Override
43      public int[] getDefaultTokens() {
44          return new int[] {
45              TokenTypes.PACKAGE_DEF,
46              TokenTypes.CLASS_DEF,
47              TokenTypes.INTERFACE_DEF,
48              TokenTypes.ANNOTATION_DEF,
49              TokenTypes.ANNOTATION_FIELD_DEF,
50              TokenTypes.ENUM_DEF,
51              TokenTypes.ENUM_CONSTANT_DEF,
52              TokenTypes.VARIABLE_DEF,
53              TokenTypes.CTOR_DEF,
54              TokenTypes.METHOD_DEF,
55          };
56      }
57  
58      @Override
59      public int[] getAcceptableTokens() {
60          return this.getDefaultTokens();
61      }
62  
63      @Override
64      public int[] getRequiredTokens() {
65          return this.getDefaultTokens();
66      }
67  
68      @Override
69      public void visitToken(final DetailAST ast) {
70          final String[] lines = this.getLines();
71          final int current = ast.getLineNo();
72          final int start =
73              JavadocNoIndentCheck.findCommentStart(lines, current) + 1;
74          if (JavadocNoIndentCheck.isNodeHavingJavadoc(ast, start)
75              && start < lines.length) {
76              this.check(
77                  lines, start,
78                  JavadocNoIndentCheck.findCommentEnd(lines, current) - 1
79              );
80          }
81      }
82  
83      private void check(final String[] lines, final int start, final int end) {
84          boolean pre = false;
85          boolean tagged = false;
86          int depth = 0;
87          for (int pos = start; pos <= end; pos += 1) {
88              final String line = lines[pos];
89              final String body = JavadocNoIndentCheck.afterAsterisk(line);
90              final boolean region = JavadocNoIndentCheck.region(pre, depth, line);
91              if (!region && body.trim().startsWith("@")) {
92                  tagged = true;
93              }
94              if (!region && !tagged
95                  && JavadocNoIndentCheck.overIndented(body)) {
96                  this.log(
97                      pos + 1,
98                      "Extra indentation is not allowed in Javadoc"
99                  );
100             }
101             pre = JavadocNoIndentCheck.nextPre(pre, line);
102             depth = JavadocNoIndentCheck.nextDepth(depth, line);
103         }
104     }
105 
106     private static boolean region(final boolean pre, final int depth,
107         final String line) {
108         return pre || depth > 0 || line.contains("{@snippet");
109     }
110 
111     private static String afterAsterisk(final String line) {
112         final int star = line.indexOf('*');
113         final String result;
114         if (star < 0) {
115             result = "";
116         } else {
117             result = line.substring(star + 1);
118         }
119         return result;
120     }
121 
122     private static boolean overIndented(final String body) {
123         int spaces = 0;
124         while (spaces < body.length() && body.charAt(spaces) == ' ') {
125             spaces += 1;
126         }
127         return spaces > 1 && spaces < body.length();
128     }
129 
130     private static boolean nextPre(final boolean pre, final String line) {
131         final String lower = line.toLowerCase(Locale.ENGLISH);
132         final boolean result;
133         if (pre) {
134             result = !lower.contains("</pre>");
135         } else {
136             result = lower.contains("<pre>") && !lower.contains("</pre>");
137         }
138         return result;
139     }
140 
141     private static int nextDepth(final int depth, final String line) {
142         final int result;
143         if (depth > 0) {
144             result = Math.max(0, depth + JavadocNoIndentCheck.braces(line));
145         } else {
146             final int idx = line.indexOf("{@snippet");
147             if (idx < 0) {
148                 result = 0;
149             } else {
150                 result = Math.max(
151                     0, JavadocNoIndentCheck.braces(line.substring(idx))
152                 );
153             }
154         }
155         return result;
156     }
157 
158     private static int braces(final String text) {
159         int delta = 0;
160         for (int pos = 0; pos < text.length(); pos += 1) {
161             final char chr = text.charAt(pos);
162             if (chr == '{') {
163                 delta += 1;
164             } else if (chr == '}') {
165                 delta -= 1;
166             }
167         }
168         return delta;
169     }
170 
171     private static boolean isNodeHavingJavadoc(final DetailAST node,
172         final int start) {
173         return start > JavadocNoIndentCheck.getLineNoOfPreviousNode(node);
174     }
175 
176     private static int getLineNoOfPreviousNode(final DetailAST node) {
177         int start = 0;
178         final DetailAST previous = node.getPreviousSibling();
179         if (previous != null) {
180             start = previous.getLineNo();
181         }
182         return start;
183     }
184 
185     private static int findCommentStart(final String[] lines, final int start) {
186         return JavadocNoIndentCheck.findTrimmedTextUp(lines, start, "/**");
187     }
188 
189     private static int findCommentEnd(final String[] lines, final int start) {
190         int found = -1;
191         for (int pos = start - 1; pos >= 0; pos -= 1) {
192             final String trimmed = lines[pos].trim();
193             if ("*/".equals(trimmed) || "**/".equals(trimmed)) {
194                 found = pos;
195                 break;
196             }
197         }
198         return found;
199     }
200 
201     private static int findTrimmedTextUp(final String[] lines,
202         final int start, final String text) {
203         int found = -1;
204         for (int pos = start - 1; pos >= 0; pos -= 1) {
205             if (lines[pos].trim().equals(text)) {
206                 found = pos;
207                 break;
208             }
209         }
210         return found;
211     }
212 }