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:</p>
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.</p>
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      /**
61       * Default constructor.
62       */
63      public JavadocThrowsCheck() {
64          // nothing to initialize
65      }
66  
67      @Override
68      public int[] getDefaultTokens() {
69          return new int[] {
70              TokenTypes.METHOD_DEF,
71              TokenTypes.CTOR_DEF,
72          };
73      }
74  
75      @Override
76      public int[] getAcceptableTokens() {
77          return this.getDefaultTokens();
78      }
79  
80      @Override
81      public int[] getRequiredTokens() {
82          return this.getDefaultTokens();
83      }
84  
85      @Override
86      @SuppressWarnings("deprecation")
87      public void visitToken(final DetailAST ast) {
88          final FileContents contents = this.getFileContents();
89          final TextBlock doc = contents.getJavadocBefore(ast.getLineNo());
90          if (doc == null) {
91              return;
92          }
93          final Set<String> declared = JavadocThrowsCheck.declared(ast);
94          final String[] lines = doc.getText();
95          final int first = doc.getStartLineNo();
96          for (int idx = 0; idx < lines.length; idx += 1) {
97              final Matcher matcher = JavadocThrowsCheck.TAG.matcher(lines[idx]);
98              if (!matcher.find()) {
99                  continue;
100             }
101             final String type = matcher.group(2);
102             if (!declared.contains(JavadocThrowsCheck.simple(type))) {
103                 this.log(
104                     first + idx,
105                     "Javadoc ''@{0} {1}'' is not declared in method signature",
106                     matcher.group(1),
107                     type
108                 );
109             }
110         }
111     }
112 
113     private static Set<String> declared(final DetailAST ast) {
114         final Set<String> names = new HashSet<>(0);
115         final DetailAST clause = ast.findFirstToken(TokenTypes.LITERAL_THROWS);
116         if (clause != null) {
117             DetailAST child = clause.getFirstChild();
118             while (child != null) {
119                 if (child.getType() == TokenTypes.IDENT) {
120                     names.add(child.getText());
121                 } else if (child.getType() == TokenTypes.DOT) {
122                     names.add(JavadocThrowsCheck.rightmost(child));
123                 }
124                 child = child.getNextSibling();
125             }
126         }
127         return names;
128     }
129 
130     private static String simple(final String text) {
131         final int dot = text.lastIndexOf('.');
132         final String result;
133         if (dot < 0) {
134             result = text;
135         } else {
136             result = text.substring(dot + 1);
137         }
138         return result;
139     }
140 
141     private static String rightmost(final DetailAST dot) {
142         DetailAST right = dot.getLastChild();
143         while (right.getType() == TokenTypes.DOT) {
144             right = right.getLastChild();
145         }
146         return right.getText();
147     }
148 }