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  
11  /**
12   * Prohibit a trailing dot in the description of {@code @param} and
13   * {@code @return} Javadoc tags.
14   *
15   * <p>For consistency, descriptions of these tags must not end with
16   * a period.
17   *
18   * <p>Valid:
19   *
20   * <pre>
21   * &#47;**
22   *  * &#64;param text A string with contents. Cannot be null
23   *  * &#64;return True when empty, false otherwise
24   *  *&#47;
25   * </pre>
26   *
27   * <p>Invalid:
28   *
29   * <pre>
30   * &#47;**
31   *  * &#64;param text A string with contents. Cannot be null.
32   *  * &#64;return True when empty, false otherwise.
33   *  *&#47;
34   * </pre>
35   *
36   * @since 0.24.1
37   */
38  public final class JavadocTagsDotCheck extends AbstractCheck {
39  
40      /**
41       * Message reported when a tag description ends with a dot.
42       */
43      private static final String MESSAGE =
44          "No dot allowed at the end of a '@param' or '@return' Javadoc tag";
45  
46      @Override
47      public int[] getDefaultTokens() {
48          return new int[] {
49              TokenTypes.METHOD_DEF,
50              TokenTypes.CTOR_DEF,
51          };
52      }
53  
54      @Override
55      public int[] getAcceptableTokens() {
56          return this.getDefaultTokens();
57      }
58  
59      @Override
60      public int[] getRequiredTokens() {
61          return this.getDefaultTokens();
62      }
63  
64      @Override
65      public void visitToken(final DetailAST ast) {
66          final String[] lines = this.getLines();
67          final int cend = JavadocTagsDotCheck.findTrimmedTextUp(
68              lines, ast.getLineNo() - 1, "*/"
69          );
70          final int cstart = JavadocTagsDotCheck.findTrimmedTextUp(
71              lines, cend, "/**"
72          );
73          if (cstart >= 0 && cend > cstart) {
74              this.inspect(lines, cstart, cend);
75          }
76      }
77  
78      /**
79       * Inspect Javadoc comment and report tags that end with a dot.
80       * @param lines All lines of the file
81       * @param cstart Line index (0-based) where the comment opens
82       * @param cend Line index (0-based) where the comment closes
83       */
84      private void inspect(final String[] lines, final int cstart, final int cend) {
85          int tag = -1;
86          for (int pos = cstart + 1; pos <= cend; pos += 1) {
87              final String trimmed = lines[pos].trim();
88              final boolean next = trimmed.startsWith("* @")
89                  || "*/".equals(trimmed);
90              if (next && tag >= 0) {
91                  this.verify(lines, tag, pos - 1);
92                  tag = -1;
93              }
94              if (JavadocTagsDotCheck.isParamOrReturn(trimmed)) {
95                  tag = pos;
96              }
97          }
98      }
99  
100     /**
101      * Verify that the last non-empty line of a tag does not end with a dot.
102      * @param lines All lines of the file
103      * @param from Line index (0-based) of the tag opening
104      * @param until Line index (0-based) of the last line that belongs to the tag
105      */
106     private void verify(final String[] lines, final int from, final int until) {
107         for (int pos = until; pos >= from; pos -= 1) {
108             final String content = JavadocTagsDotCheck.stripMarker(lines[pos]);
109             if (!content.isEmpty()) {
110                 if (content.endsWith(".")) {
111                     this.log(pos + 1, JavadocTagsDotCheck.MESSAGE);
112                 }
113                 break;
114             }
115         }
116     }
117 
118     /**
119      * Check whether the trimmed line starts a {@code @param} or
120      * {@code @return} tag.
121      * @param trimmed Trimmed line content
122      * @return True when the line opens one of the watched tags
123      */
124     private static boolean isParamOrReturn(final String trimmed) {
125         final String tag;
126         if (trimmed.startsWith("* @")) {
127             tag = trimmed.substring("* @".length());
128         } else {
129             tag = "";
130         }
131         return tag.startsWith("param ")
132             || tag.startsWith("return ")
133             || "return".equals(tag);
134     }
135 
136     /**
137      * Strip the leading {@code *} and surrounding whitespace from a line.
138      * @param line A raw line of the file
139      * @return Content without the Javadoc asterisk marker
140      */
141     private static String stripMarker(final String line) {
142         String result = line.trim();
143         if (result.startsWith("*")) {
144             result = result.substring(1).trim();
145         }
146         return result;
147     }
148 
149     /**
150      * Find a line with given text by going up from a position.
151      * @param lines All lines of the file
152      * @param start Position (0-based) to start searching from
153      * @param text Text to find (compared against the trimmed line)
154      * @return Line index (0-based) where the text was found, or -1 otherwise
155      */
156     private static int findTrimmedTextUp(
157         final String[] lines,
158         final int start,
159         final String text
160     ) {
161         int found = -1;
162         for (int pos = start - 1; pos >= 0; pos -= 1) {
163             if (lines[pos].trim().equals(text)) {
164                 found = pos;
165                 break;
166             }
167         }
168         return found;
169     }
170 }