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.ArrayList;
11  import java.util.Arrays;
12  import java.util.Collection;
13  import java.util.List;
14  import java.util.regex.Pattern;
15  
16  /**
17   * Check that the class/interface javadoc does not contain prohibited
18   * {@code author} or {@code version} tags and has a properly formatted
19   * {@code since} tag.
20   *
21   * <p>Correct format is the following (of a class javadoc):
22   *
23   * <pre>
24   * &#47;**
25   *  * This is my new class.
26   *  *
27   *  * &#64;since 0.3
28   *  *&#47;
29   * public final class Foo {
30   *     // ...
31   * </pre>
32   *
33   * @since 0.3
34   */
35  public final class JavadocTagsCheck extends AbstractCheck {
36  
37      /**
38       * Map of tag and its pattern.
39       */
40      private final List<RequiredJavaDocTag> required = new ArrayList<>(1);
41  
42      /**
43       * List of prohibited javadoc tags.
44       */
45      private final Collection<String> prohibited =
46          Arrays.asList("author", "version");
47  
48      @Override
49      public int[] getDefaultTokens() {
50          return new int[]{
51              TokenTypes.CLASS_DEF,
52              TokenTypes.INTERFACE_DEF,
53          };
54      }
55  
56      @Override
57      public int[] getAcceptableTokens() {
58          return this.getDefaultTokens();
59      }
60  
61      @Override
62      public int[] getRequiredTokens() {
63          return this.getDefaultTokens();
64      }
65  
66      @Override
67      public void init() {
68          this.required.add(
69              new RequiredJavaDocTag(
70                  "since",
71                  Pattern.compile("(?<name>^ +\\* +@since)( +)(?<cont>.*)"),
72                  Pattern.compile(
73                  "^\\d+(\\.\\d+){1,2}(\\.[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
74                  ),
75                  this::log
76              )
77          );
78      }
79  
80      @Override
81      public void visitToken(final DetailAST ast) {
82          final String[] lines = this.getLines();
83          final int start = ast.getLineNo();
84          final int cstart = JavadocTagsCheck.findCommentStart(lines, start);
85          final int cend = JavadocTagsCheck.findCommentEnd(lines, start);
86          if (cend > cstart && cstart >= 0) {
87              for (final String tag : this.prohibited) {
88                  this.findProhibited(lines, start, cstart, cend, tag);
89              }
90              for (final RequiredJavaDocTag tag : this.required) {
91                  tag.matchTagFormat(lines, cstart, cend);
92              }
93          } else {
94              this.log(0, "Problem finding class/interface comment");
95          }
96      }
97  
98      /**
99       * Find a text in lines, by going up.
100      * @param lines List of lines to check
101      * @param start Start searching from this line number
102      * @param text Text to find
103      * @return Line number with found text, or -1 if it wasn't found
104      */
105     private static int findTrimmedTextUp(
106         final String[] lines,
107         final int start,
108         final String text
109     ) {
110         int found = -1;
111         for (int pos = start - 1; pos >= 0; pos -= 1) {
112             if (lines[pos].trim().equals(text)) {
113                 found = pos;
114                 break;
115             }
116         }
117         return found;
118     }
119 
120     /**
121      * Find javadoc starting comment.
122      * @param lines List of lines to check
123      * @param start Start searching from this line number
124      * @return Line number with found starting comment or -1 otherwise
125      */
126     private static int findCommentStart(final String[] lines, final int start) {
127         return JavadocTagsCheck.findTrimmedTextUp(lines, start, "/**");
128     }
129 
130     /**
131      * Find javadoc ending comment.
132      * @param lines List of lines to check
133      * @param start Start searching from this line number
134      * @return Line number with found ending comment, or -1 if it wasn't found
135      */
136     private static int findCommentEnd(final String[] lines, final int start) {
137         return JavadocTagsCheck.findTrimmedTextUp(lines, start, "*/");
138     }
139 
140     /**
141      * Check if the tag text matches the format from pattern.
142      * @param lines List of all lines
143      * @param start Line number where AST starts
144      * @param cstart Line number where comment starts
145      * @param cend Line number where comment ends
146      * @param tag Name of the tag
147      * @checkstyle ParameterNumber (3 lines)
148      */
149     private void findProhibited(
150         final String[] lines,
151         final int start,
152         final int cstart,
153         final int cend,
154         final String tag
155     ) {
156         final List<Integer> found =
157             this.findTagLineNum(lines, cstart, cend, tag);
158         if (!found.isEmpty()) {
159             this.log(
160                 start + 1,
161                 "Prohibited ''@{0}'' tag in class/interface comment",
162                 tag
163             );
164         }
165     }
166 
167     /**
168      * Find given tag in comment lines.
169      * @param lines Lines to search for the tag
170      * @param start Starting line number
171      * @param end Ending line number
172      * @param tag Name of the tag to look for
173      * @return Line number with found tag or -1 otherwise
174      * @checkstyle ParameterNumber (3 lines)
175      */
176     private List<Integer> findTagLineNum(
177         final String[] lines,
178         final int start,
179         final int end,
180         final String tag
181     ) {
182         final String prefix = String.format(" * @%s ", tag);
183         final List<Integer> found = new ArrayList<>(1);
184         for (int pos = start; pos <= end; pos += 1) {
185             final String line = lines[pos];
186             if (line.contains(String.format("@%s ", tag))) {
187                 if (!line.trim().startsWith(prefix.trim())) {
188                     this.log(
189                         start + pos + 1,
190                         "Line with ''@{0}'' does not start with a ''{1}''",
191                         tag,
192                         prefix
193                     );
194                     break;
195                 }
196                 found.add(pos);
197             }
198         }
199         return found;
200     }
201 }