View Javadoc
1   /*
2    * Copyright (c) 2011-2025 Yegor Bugayenko
3    *
4    * All rights reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions
8    * are met: 1) Redistributions of source code must retain the above
9    * copyright notice, this list of conditions and the following
10   * disclaimer. 2) Redistributions in binary form must reproduce the above
11   * copyright notice, this list of conditions and the following
12   * disclaimer in the documentation and/or other materials provided
13   * with the distribution. 3) Neither the name of the Qulice.com nor
14   * the names of its contributors may be used to endorse or promote
15   * products derived from this software without specific prior written
16   * permission.
17   *
18   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
20   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
21   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
22   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29   * OF THE POSSIBILITY OF SUCH DAMAGE.
30   */
31  package com.qulice.checkstyle;
32  
33  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
34  import com.puppycrawl.tools.checkstyle.api.DetailAST;
35  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collection;
39  import java.util.List;
40  import java.util.regex.Pattern;
41  
42  /**
43   * Check if the class/interface javadoc contains properly formatted author
44   * and version tags.
45   *
46   * <p>Correct format is the following (of a class javadoc):
47   *
48   * <pre>
49   * &#47;**
50   *  * This is my new class.
51   *  *
52   *  * &#64;author John Doe (john&#64;example.com)
53   *  * &#64;version &#36;Id&#36;
54   *  *&#47;
55   * public final class Foo {
56   *     // ...
57   * </pre>
58   *
59   * <p>"&#36;Id&#36;" will be replaced by a full text automatically
60   * by Subversion as explained in their documentation (see link below).
61   *
62   * @see <a href="http://svnbook.red-bean.com/en/1.4/svn.advanced.props.special.keywords.html">Keywords substitution in Subversion</a>
63   * @since 0.3
64   */
65  public final class JavadocTagsCheck extends AbstractCheck {
66  
67      /**
68       * Map of tag and its pattern.
69       */
70      private final List<RequiredJavaDocTag> required = new ArrayList<>(1);
71  
72      /**
73       * List of prohibited javadoc tags.
74       */
75      private final Collection<String> prohibited =
76          Arrays.asList("author", "version");
77  
78      @Override
79      public int[] getDefaultTokens() {
80          return new int[]{
81              TokenTypes.CLASS_DEF,
82              TokenTypes.INTERFACE_DEF,
83          };
84      }
85  
86      @Override
87      public int[] getAcceptableTokens() {
88          return this.getDefaultTokens();
89      }
90  
91      @Override
92      public int[] getRequiredTokens() {
93          return this.getDefaultTokens();
94      }
95  
96      @Override
97      public void init() {
98          this.required.add(
99              new RequiredJavaDocTag(
100                 "since",
101                 Pattern.compile(
102                 "^\\d+(\\.\\d+){1,2}(\\.[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
103                 ),
104                 this::log
105             )
106         );
107     }
108 
109     @Override
110     public void visitToken(final DetailAST ast) {
111         final String[] lines = this.getLines();
112         final int start = ast.getLineNo();
113         final int cstart = JavadocTagsCheck.findCommentStart(lines, start);
114         final int cend = JavadocTagsCheck.findCommentEnd(lines, start);
115         if (cend > cstart && cstart >= 0) {
116             for (final String tag : this.prohibited) {
117                 this.findProhibited(lines, start, cstart, cend, tag);
118             }
119             for (final RequiredJavaDocTag tag : this.required) {
120                 tag.matchTagFormat(lines, cstart, cend);
121             }
122         } else {
123             this.log(0, "Problem finding class/interface comment");
124         }
125     }
126 
127     /**
128      * Find a text in lines, by going up.
129      * @param lines List of lines to check.
130      * @param start Start searching from this line number.
131      * @param text Text to find.
132      * @return Line number with found text, or -1 if it wasn't found.
133      */
134     private static int findTrimmedTextUp(
135         final String[] lines,
136         final int start,
137         final String text
138     ) {
139         int found = -1;
140         for (int pos = start - 1; pos >= 0; pos -= 1) {
141             if (lines[pos].trim().equals(text)) {
142                 found = pos;
143                 break;
144             }
145         }
146         return found;
147     }
148 
149     /**
150      * Find javadoc starting comment.
151      * @param lines List of lines to check.
152      * @param start Start searching from this line number.
153      * @return Line number with found starting comment or -1 otherwise.
154      */
155     private static int findCommentStart(final String[] lines, final int start) {
156         return JavadocTagsCheck.findTrimmedTextUp(lines, start, "/**");
157     }
158 
159     /**
160      * Find javadoc ending comment.
161      * @param lines List of lines to check.
162      * @param start Start searching from this line number.
163      * @return Line number with found ending comment, or -1 if it wasn't found.
164      */
165     private static int findCommentEnd(final String[] lines, final int start) {
166         return JavadocTagsCheck.findTrimmedTextUp(lines, start, "*/");
167     }
168 
169     /**
170      * Check if the tag text matches the format from pattern.
171      * @param lines List of all lines.
172      * @param start Line number where AST starts.
173      * @param cstart Line number where comment starts.
174      * @param cend Line number where comment ends.
175      * @param tag Name of the tag.
176      * @checkstyle ParameterNumber (3 lines)
177      */
178     private void findProhibited(
179         final String[] lines,
180         final int start,
181         final int cstart,
182         final int cend,
183         final String tag
184     ) {
185         final List<Integer> found =
186             this.findTagLineNum(lines, cstart, cend, tag);
187         if (!found.isEmpty()) {
188             this.log(
189                 start + 1,
190                 "Prohibited ''@{0}'' tag in class/interface comment",
191                 tag
192             );
193         }
194     }
195 
196     /**
197      * Find given tag in comment lines.
198      * @param lines Lines to search for the tag.
199      * @param start Starting line number.
200      * @param end Ending line number.
201      * @param tag Name of the tag to look for.
202      * @return Line number with found tag or -1 otherwise.
203      * @checkstyle ParameterNumber (3 lines)
204      */
205     private List<Integer> findTagLineNum(
206         final String[] lines,
207         final int start,
208         final int end,
209         final String tag
210     ) {
211         final String prefix = String.format(" * @%s ", tag);
212         final List<Integer> found = new ArrayList<>(1);
213         for (int pos = start; pos <= end; pos += 1) {
214             final String line = lines[pos];
215             if (line.contains(String.format("@%s ", tag))) {
216                 if (!line.trim().startsWith(prefix.trim())) {
217                     this.log(
218                         start + pos + 1,
219                         "Line with ''@{0}'' does not start with a ''{1}''",
220                         tag,
221                         prefix
222                     );
223                     break;
224                 }
225                 found.add(pos);
226             }
227         }
228         return found;
229     }
230 }