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  
11  /**
12   * Checks that there is no empty line between a javadoc and it's subject,
13   * and that no annotation is placed above the javadoc.
14   *
15   * <p>You can't have empty lines between javadoc block and
16   * a class/method/variable. They should stay together, always.
17   *
18   * <p>Annotations must be placed after the javadoc, not before it,
19   * so that the javadoc stays next to the subject it describes.
20   *
21   * @since 0.3
22   */
23  public final class JavadocLocationCheck extends AbstractCheck {
24  
25      @Override
26      public int[] getDefaultTokens() {
27          return new int[] {
28              TokenTypes.CLASS_DEF,
29              TokenTypes.INTERFACE_DEF,
30              TokenTypes.VARIABLE_DEF,
31              TokenTypes.CTOR_DEF,
32              TokenTypes.METHOD_DEF,
33          };
34      }
35  
36      @Override
37      public int[] getAcceptableTokens() {
38          return this.getDefaultTokens();
39      }
40  
41      @Override
42      public int[] getRequiredTokens() {
43          return this.getDefaultTokens();
44      }
45  
46      @Override
47      public void visitToken(final DetailAST ast) {
48          if (!JavadocLocationCheck.isField(ast)) {
49              return;
50          }
51          final String[] lines = this.getLines();
52          this.checkEmptyLines(ast, lines);
53          this.checkAnnotationAboveJavadoc(ast, lines);
54      }
55  
56      /**
57       * Check that there are no empty lines between the javadoc and the subject.
58       * @param ast The AST node of the subject
59       * @param lines The file lines
60       */
61      private void checkEmptyLines(final DetailAST ast, final String... lines) {
62          final int current = JavadocLocationCheck.javadocEnd(
63              ast.getLineNo() - 1, lines
64          );
65          if (current > 0) {
66              final int diff = ast.getLineNo() - current;
67              for (int pos = 1; pos < diff; pos += 1) {
68                  this.log(
69                      current + pos,
70                      "Empty line between javadoc and subject"
71                  );
72              }
73          }
74      }
75  
76      /**
77       * Walks upward from the given line and returns the line number of
78       * the closing javadoc marker if only blank lines separate it from
79       * the subject, otherwise zero.
80       * @param from Line just above the subject (one-based)
81       * @param lines The file lines
82       * @return Line number of the javadoc end, or zero if none
83       */
84      private static int javadocEnd(final int from, final String... lines) {
85          int current = from;
86          int result = 0;
87          while (current > 0) {
88              final String line = lines[current - 1].trim();
89              if (line.endsWith("*/")) {
90                  result = current;
91                  break;
92              }
93              if (!line.isEmpty()) {
94                  break;
95              }
96              current -= 1;
97          }
98          return result;
99      }
100 
101     /**
102      * Check that no annotation is placed above the javadoc of the subject.
103      * @param ast The AST node of the subject
104      * @param lines The file lines
105      */
106     private void checkAnnotationAboveJavadoc(
107         final DetailAST ast, final String... lines
108     ) {
109         final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
110         if (modifiers != null) {
111             final DetailAST after = modifiers.getNextSibling();
112             final int annotation = JavadocLocationCheck.firstAnnotationLine(
113                 modifiers
114             );
115             if (after != null && annotation != Integer.MAX_VALUE
116                 && JavadocLocationCheck.javadocBetween(
117                     annotation, after.getLineNo(), lines
118                 )) {
119                 this.log(annotation, "Annotation must be placed after Javadoc");
120             }
121         }
122     }
123 
124     /**
125      * Returns the line number of the first annotation under the given
126      * modifiers node, or {@link Integer#MAX_VALUE} when there is none.
127      * @param modifiers The MODIFIERS token
128      * @return Line number of the first annotation
129      */
130     private static int firstAnnotationLine(final DetailAST modifiers) {
131         int line = Integer.MAX_VALUE;
132         DetailAST child = modifiers.getFirstChild();
133         while (child != null) {
134             if (child.getType() == TokenTypes.ANNOTATION
135                 && child.getLineNo() < line) {
136                 line = child.getLineNo();
137             }
138             child = child.getNextSibling();
139         }
140         return line;
141     }
142 
143     /**
144      * Tells whether any javadoc opening or closing marker appears
145      * between the given lines (exclusive of the start, exclusive of the
146      * end).
147      * @param start Lower bound line, exclusive
148      * @param end Upper bound line, exclusive
149      * @param lines The file lines
150      * @return True when a javadoc marker is found in the range
151      */
152     private static boolean javadocBetween(final int start, final int end,
153         final String... lines) {
154         boolean found = false;
155         for (int pos = start + 1; pos < end; pos += 1) {
156             final String line = lines[pos - 1].trim();
157             if (line.startsWith("/**") || line.endsWith("*/")) {
158                 found = true;
159                 break;
160             }
161         }
162         return found;
163     }
164 
165     /**
166      * Returns {@code TRUE} if a specified node is something that should have
167      * a Javadoc, which includes classes, interface, class methods, and
168      * class variables.
169      * @param node Node to check
170      * @return Is it a Javadoc-required entity?
171      */
172     private static boolean isField(final DetailAST node) {
173         boolean yes = true;
174         if (TokenTypes.VARIABLE_DEF == node.getType()) {
175             yes = TokenTypes.OBJBLOCK == node.getParent().getType();
176         }
177         return yes;
178     }
179 }