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