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):</p>
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       * Javadoc tags that are not allowed.
39       */
40      private static final Collection<String> PROHIBITED =
41          Arrays.asList("author", "version");
42  
43      /**
44       * Map of tag and its pattern.
45       */
46      private final List<RequiredJavaDocTag> required;
47  
48      /**
49       * Default constructor.
50       */
51      public JavadocTagsCheck() {
52          this.required = new ArrayList<>(1);
53      }
54  
55      @Override
56      public int[] getDefaultTokens() {
57          return new int[]{
58              TokenTypes.CLASS_DEF,
59              TokenTypes.INTERFACE_DEF,
60          };
61      }
62  
63      @Override
64      public int[] getAcceptableTokens() {
65          return this.getDefaultTokens();
66      }
67  
68      @Override
69      public int[] getRequiredTokens() {
70          return this.getDefaultTokens();
71      }
72  
73      @Override
74      public void init() {
75          this.required.add(
76              new RequiredJavaDocTag(
77                  "since",
78                  Pattern.compile("(?<name>^ +\\* +@since)( +)(?<cont>.*)"),
79                  Pattern.compile(
80                  "^\\d+(\\.\\d+){1,2}(\\.[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
81                  ),
82                  this::log
83              )
84          );
85      }
86  
87      @Override
88      public void visitToken(final DetailAST ast) {
89          final String[] lines = this.getLines();
90          final int start = ast.getLineNo();
91          final int cstart = JavadocTagsCheck.findCommentStart(lines, start);
92          final int cend = JavadocTagsCheck.findCommentEnd(lines, start);
93          if (cend > cstart && cstart >= 0) {
94              for (final String tag : JavadocTagsCheck.PROHIBITED) {
95                  this.findProhibited(lines, start, cstart, cend, tag);
96              }
97              for (final RequiredJavaDocTag tag : this.required) {
98                  tag.matchTagFormat(lines, cstart, cend);
99              }
100         } else {
101             this.log(0, "Problem finding class/interface comment");
102         }
103     }
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     private static int findCommentStart(final String[] lines, final int start) {
121         return JavadocTagsCheck.findTrimmedTextUp(lines, start, "/**");
122     }
123 
124     private static int findCommentEnd(final String[] lines, final int start) {
125         return JavadocTagsCheck.findTrimmedTextUp(lines, start, "*/");
126     }
127 
128     private void findProhibited(
129         final String[] lines,
130         final int start,
131         final int cstart,
132         final int cend,
133         final String tag
134     ) {
135         final List<Integer> found =
136             this.findTagLineNum(lines, cstart, cend, tag);
137         if (!found.isEmpty()) {
138             this.log(
139                 start + 1,
140                 "Prohibited ''@{0}'' tag in class/interface comment",
141                 tag
142             );
143         }
144     }
145 
146     private List<Integer> findTagLineNum(
147         final String[] lines,
148         final int start,
149         final int end,
150         final String tag
151     ) {
152         final String prefix = String.format(" * @%s ", tag);
153         final List<Integer> found = new ArrayList<>(1);
154         for (int pos = start; pos <= end; pos += 1) {
155             final String line = lines[pos];
156             if (line.contains(String.format("@%s ", tag))) {
157                 if (!line.trim().startsWith(prefix.trim())) {
158                     this.log(
159                         start + pos + 1,
160                         "Line with ''@{0}'' does not start with a ''{1}''",
161                         tag,
162                         prefix
163                     );
164                     break;
165                 }
166                 found.add(pos);
167             }
168         }
169         return found;
170     }
171 }