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.FileContents;
10  import com.puppycrawl.tools.checkstyle.api.TextBlock;
11  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
12  import java.util.HashSet;
13  import java.util.Set;
14  import java.util.regex.Matcher;
15  import java.util.regex.Pattern;
16  
17  /**
18   * Checks that every {@code @throws} (or {@code @exception}) tag in the
19   * javadoc of a method or constructor refers to an exception actually
20   * declared in the {@code throws} clause of that method/constructor.
21   *
22   * <p>A javadoc that advertises a thrown exception the signature does
23   * not declare is misleading. The same applies when the tag names a
24   * different type than what the signature throws, for example when the
25   * javadoc says {@code @throws IOException} but the signature declares
26   * {@code throws Exception}. Both examples below are rejected:
27   *
28   * <pre>
29   * &#47;**
30   *  * &#64;throws Exception If something goes wrong.
31   *  *&#47;
32   * public void foo() {
33   *     // ...
34   * }
35   *
36   * &#47;**
37   *  * &#64;throws IOException If something goes wrong.
38   *  *&#47;
39   * public void foo() throws Exception {
40   *     // ...
41   * }
42   * </pre>
43   *
44   * <p>Types are compared by their simple name, so a javadoc that uses a
45   * fully qualified name (e.g. {@code java.io.IOException}) still
46   * matches a signature that uses the unqualified form, and vice-versa.
47   *
48   * @since 0.24.1
49   */
50  public final class JavadocThrowsCheck extends AbstractCheck {
51  
52      /**
53       * Compiled regexp matching a single {@code @throws}/{@code @exception}
54       * javadoc line; captures tag name (group 1) and type (group 2).
55       */
56      private static final Pattern TAG = Pattern.compile(
57          "^\\s*(?:\\*|/\\*\\*)?\\s*@(throws|exception)\\s+(\\S+)"
58      );
59  
60      @Override
61      public int[] getDefaultTokens() {
62          return new int[] {
63              TokenTypes.METHOD_DEF,
64              TokenTypes.CTOR_DEF,
65          };
66      }
67  
68      @Override
69      public int[] getAcceptableTokens() {
70          return this.getDefaultTokens();
71      }
72  
73      @Override
74      public int[] getRequiredTokens() {
75          return this.getDefaultTokens();
76      }
77  
78      @Override
79      @SuppressWarnings("deprecation")
80      public void visitToken(final DetailAST ast) {
81          final FileContents contents = this.getFileContents();
82          final TextBlock doc = contents.getJavadocBefore(ast.getLineNo());
83          if (doc == null) {
84              return;
85          }
86          final Set<String> declared = JavadocThrowsCheck.declared(ast);
87          final String[] lines = doc.getText();
88          final int first = doc.getStartLineNo();
89          for (int idx = 0; idx < lines.length; idx += 1) {
90              final Matcher matcher = JavadocThrowsCheck.TAG.matcher(lines[idx]);
91              if (!matcher.find()) {
92                  continue;
93              }
94              final String type = matcher.group(2);
95              if (!declared.contains(JavadocThrowsCheck.simple(type))) {
96                  this.log(
97                      first + idx,
98                      "Javadoc ''@{0} {1}'' is not declared in method signature",
99                      matcher.group(1),
100                     type
101                 );
102             }
103         }
104     }
105 
106     /**
107      * Collect simple names of the exceptions declared in the
108      * {@code throws} clause of a method or constructor.
109      * @param ast Method/constructor definition node
110      * @return Simple names of declared checked exceptions
111      */
112     private static Set<String> declared(final DetailAST ast) {
113         final Set<String> names = new HashSet<>(0);
114         final DetailAST clause = ast.findFirstToken(TokenTypes.LITERAL_THROWS);
115         if (clause != null) {
116             DetailAST child = clause.getFirstChild();
117             while (child != null) {
118                 if (child.getType() == TokenTypes.IDENT) {
119                     names.add(child.getText());
120                 } else if (child.getType() == TokenTypes.DOT) {
121                     names.add(JavadocThrowsCheck.rightmost(child));
122                 }
123                 child = child.getNextSibling();
124             }
125         }
126         return names;
127     }
128 
129     /**
130      * Extract the simple name from a possibly qualified type reference
131      * as written in javadoc text.
132      * @param text Full textual type reference
133      * @return Simple name (last dot-separated segment)
134      */
135     private static String simple(final String text) {
136         final int dot = text.lastIndexOf('.');
137         final String result;
138         if (dot < 0) {
139             result = text;
140         } else {
141             result = text.substring(dot + 1);
142         }
143         return result;
144     }
145 
146     /**
147      * Walk down a DOT-chained name and return the rightmost identifier
148      * text (i.e. the simple name of a qualified reference in the AST).
149      * @param dot AST node of type {@code DOT}
150      * @return Rightmost identifier's text
151      */
152     private static String rightmost(final DetailAST dot) {
153         DetailAST right = dot.getLastChild();
154         while (right.getType() == TokenTypes.DOT) {
155             right = right.getLastChild();
156         }
157         return right.getText();
158     }
159 }