View Javadoc
1   /*
2    * Copyright (c) 2011-2024 Qulice.com
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.HashMap;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.regex.Pattern;
43  
44  /**
45   * Check if the class/interface javadoc contains properly formatted author
46   * and version tags.
47   *
48   * <p>Correct format is the following (of a class javadoc):
49   *
50   * <pre>
51   * &#47;**
52   *  * This is my new class.
53   *  *
54   *  * &#64;author John Doe (john&#64;example.com)
55   *  * &#64;version &#36;Id&#36;
56   *  *&#47;
57   * public final class Foo {
58   *     // ...
59   * </pre>
60   *
61   * <p>"&#36;Id&#36;" will be replaced by a full text automatically
62   * by Subversion as explained in their documentation (see link below).
63   *
64   * @see <a href="http://svnbook.red-bean.com/en/1.4/svn.advanced.props.special.keywords.html">Keywords substitution in Subversion</a>
65   * @since 0.3
66   */
67  public final class JavadocTagsCheck extends AbstractCheck {
68  
69      /**
70       * Map of tag and its pattern.
71       */
72      private final Map<String, Pattern> tags = new HashMap<>();
73  
74      /**
75       * List of prohibited javadoc tags.
76       */
77      private final Collection<String> prohibited =
78          Arrays.asList("author", "version");
79  
80      @Override
81      public int[] getDefaultTokens() {
82          return new int[]{
83              TokenTypes.CLASS_DEF,
84              TokenTypes.INTERFACE_DEF,
85          };
86      }
87  
88      @Override
89      public int[] getAcceptableTokens() {
90          return this.getDefaultTokens();
91      }
92  
93      @Override
94      public int[] getRequiredTokens() {
95          return this.getDefaultTokens();
96      }
97  
98      @Override
99      public void init() {
100         this.tags.put(
101             "since",
102             Pattern.compile(
103                 "^\\d+(\\.\\d+){1,2}(\\.[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
104             )
105         );
106     }
107 
108     @Override
109     public void visitToken(final DetailAST ast) {
110         final String[] lines = this.getLines();
111         final int start = ast.getLineNo();
112         final int cstart = JavadocTagsCheck.findCommentStart(lines, start);
113         final int cend = JavadocTagsCheck.findCommentEnd(lines, start);
114         if (cend > cstart && cstart >= 0) {
115             for (final String tag : this.prohibited) {
116                 this.findProhibited(lines, start, cstart, cend, tag);
117             }
118             for (final String tag : this.tags.keySet()) {
119                 this.matchTagFormat(lines, cstart, cend, tag);
120             }
121         } else {
122             this.log(0, "Problem finding class/interface comment");
123         }
124     }
125 
126     /**
127      * Get the text of the given tag.
128      * @param line Line with the tag.
129      * @return The text of the tag.
130      */
131     private static String getTagText(final String line) {
132         return line.substring(
133             line.indexOf(' ', line.indexOf('@')) + 1
134         );
135     }
136 
137     /**
138      * Find a text in lines, by going up.
139      * @param lines List of lines to check.
140      * @param start Start searching from this line number.
141      * @param text Text to find.
142      * @return Line number with found text, or -1 if it wasn't found.
143      */
144     private static int findTrimmedTextUp(final String[] lines,
145         final int start, final String text) {
146         int found = -1;
147         for (int pos = start - 1; pos >= 0; pos -= 1) {
148             if (lines[pos].trim().equals(text)) {
149                 found = pos;
150                 break;
151             }
152         }
153         return found;
154     }
155 
156     /**
157      * Find javadoc starting comment.
158      * @param lines List of lines to check.
159      * @param start Start searching from this line number.
160      * @return Line number with found starting comment or -1 otherwise.
161      */
162     private static int findCommentStart(final String[] lines, final int start) {
163         return JavadocTagsCheck.findTrimmedTextUp(lines, start, "/**");
164     }
165 
166     /**
167      * Find javadoc ending comment.
168      * @param lines List of lines to check.
169      * @param start Start searching from this line number.
170      * @return Line number with found ending comment, or -1 if it wasn't found.
171      */
172     private static int findCommentEnd(final String[] lines, final int start) {
173         return JavadocTagsCheck.findTrimmedTextUp(lines, start, "*/");
174     }
175 
176     /**
177      * Check if the tag text matches the format from pattern.
178      * @param lines List of all lines.
179      * @param start Line number where AST starts.
180      * @param cstart Line number where comment starts.
181      * @param cend Line number where comment ends.
182      * @param tag Name of the tag.
183      * @checkstyle ParameterNumber (3 lines)
184      */
185     private void findProhibited(final String[] lines, final int start,
186         final int cstart, final int cend, final String tag) {
187         final List<Integer> found =
188             this.findTagLineNum(lines, cstart, cend, tag);
189         if (!found.isEmpty()) {
190             this.log(
191                 start + 1,
192                 "Prohibited ''@{0}'' tag in class/interface comment",
193                 tag
194             );
195         }
196     }
197 
198     /**
199      * Check if the tag text matches the format from pattern.
200      * @param lines List of all lines.
201      * @param start Line number where comment starts.
202      * @param end Line number where comment ends.
203      * @param tag Name of the tag.
204      * @checkstyle ParameterNumber (3 lines)
205      */
206     private void matchTagFormat(final String[] lines, final int start,
207         final int end, final String tag) {
208         final List<Integer> found = this.findTagLineNum(lines, start, end, tag);
209         if (found.isEmpty()) {
210             this.log(
211                 start + 1,
212                 "Missing ''@{0}'' tag in class/interface comment",
213                 tag
214             );
215             return;
216         }
217         for (final Integer item : found) {
218             final String text = JavadocTagsCheck.getTagText(lines[item]);
219             if (!this.tags.get(tag).matcher(text).matches()) {
220                 this.log(
221                     item + 1,
222                     "Tag text ''{0}'' does not match the pattern ''{1}''",
223                     text,
224                     this.tags.get(tag).toString()
225                 );
226             }
227         }
228     }
229 
230     /**
231      * Find given tag in comment lines.
232      * @param lines Lines to search for the tag.
233      * @param start Starting line number.
234      * @param end Ending line number.
235      * @param tag Name of the tag to look for.
236      * @return Line number with found tag or -1 otherwise.
237      * @checkstyle ParameterNumber (3 lines)
238      */
239     private List<Integer> findTagLineNum(final String[] lines, final int start,
240         final int end, final String tag) {
241         final String prefix = String.format(" * @%s ", tag);
242         final List<Integer> found = new ArrayList<>(1);
243         for (int pos = start; pos <= end; pos += 1) {
244             final String line = lines[pos];
245             if (line.contains(String.format("@%s ", tag))) {
246                 if (!line.trim().startsWith(prefix.trim())) {
247                     this.log(
248                         start + pos + 1,
249                         "Line with ''@{0}'' does not start with a ''{1}''",
250                         tag,
251                         prefix
252                     );
253                     break;
254                 }
255                 found.add(pos);
256             }
257         }
258         return found;
259     }
260 }
261