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.</p>
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.</p>
20   *
21   * @since 0.3
22   */
23  public final class JavadocLocationCheck extends AbstractCheck {
24  
25      /**
26       * Default constructor.
27       */
28      public JavadocLocationCheck() {
29          // nothing to initialize
30      }
31  
32      @Override
33      public int[] getDefaultTokens() {
34          return new int[] {
35              TokenTypes.CLASS_DEF,
36              TokenTypes.INTERFACE_DEF,
37              TokenTypes.VARIABLE_DEF,
38              TokenTypes.CTOR_DEF,
39              TokenTypes.METHOD_DEF,
40          };
41      }
42  
43      @Override
44      public int[] getAcceptableTokens() {
45          return this.getDefaultTokens();
46      }
47  
48      @Override
49      public int[] getRequiredTokens() {
50          return this.getDefaultTokens();
51      }
52  
53      @Override
54      public void visitToken(final DetailAST ast) {
55          if (!JavadocLocationCheck.isField(ast)) {
56              return;
57          }
58          final String[] lines = this.getLines();
59          this.checkEmptyLines(ast, lines);
60          this.checkAnnotationAboveJavadoc(ast, lines);
61      }
62  
63      private void checkEmptyLines(final DetailAST ast, final String... lines) {
64          final int current = JavadocLocationCheck.javadocEnd(
65              ast.getLineNo() - 1, lines
66          );
67          if (current > 0) {
68              final int diff = ast.getLineNo() - current;
69              for (int pos = 1; pos < diff; pos += 1) {
70                  this.log(
71                      current + pos,
72                      "Empty line between javadoc and subject"
73                  );
74              }
75          }
76      }
77  
78      private static int javadocEnd(final int from, final String... lines) {
79          int current = from;
80          int result = 0;
81          while (current > 0) {
82              final String line = lines[current - 1].trim();
83              if (line.endsWith("*/")) {
84                  result = current;
85                  break;
86              }
87              if (!line.isEmpty()) {
88                  break;
89              }
90              current -= 1;
91          }
92          return result;
93      }
94  
95      private void checkAnnotationAboveJavadoc(
96          final DetailAST ast, final String... lines
97      ) {
98          final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
99          if (modifiers != null) {
100             final DetailAST after = modifiers.getNextSibling();
101             final int annotation = JavadocLocationCheck.firstAnnotationLine(
102                 modifiers
103             );
104             if (after != null && annotation != Integer.MAX_VALUE
105                 && JavadocLocationCheck.javadocBetween(
106                     annotation, after.getLineNo(), lines
107                 )) {
108                 this.log(annotation, "Annotation must be placed after Javadoc");
109             }
110         }
111     }
112 
113     private static int firstAnnotationLine(final DetailAST modifiers) {
114         int line = Integer.MAX_VALUE;
115         DetailAST child = modifiers.getFirstChild();
116         while (child != null) {
117             if (child.getType() == TokenTypes.ANNOTATION
118                 && child.getLineNo() < line) {
119                 line = child.getLineNo();
120             }
121             child = child.getNextSibling();
122         }
123         return line;
124     }
125 
126     private static boolean javadocBetween(final int start, final int end,
127         final String... lines) {
128         boolean found = false;
129         for (int pos = start + 1; pos < end; pos += 1) {
130             final String line = lines[pos - 1].trim();
131             if (line.startsWith("/**") || line.endsWith("*/")) {
132                 found = true;
133                 break;
134             }
135         }
136         return found;
137     }
138 
139     private static boolean isField(final DetailAST node) {
140         boolean yes = true;
141         if (TokenTypes.VARIABLE_DEF == node.getType()) {
142             yes = TokenTypes.OBJBLOCK == node.getParent().getType();
143         }
144         return yes;
145     }
146 }