View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   
6   package com.qulice.checkstyle;
7   
8   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
9   import com.puppycrawl.tools.checkstyle.api.DetailAST;
10  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
11  import java.util.Set;
12  
13  /**
14   * Prohibits an implicit constructor in a class that Javadoc documents.
15   *
16   * <p>A class that declares no constructor gets one from the compiler,
17   * with the access of the class itself. Javadoc puts that constructor
18   * into the generated page, finds no comment on it, and says "use of
19   * default constructor, which does not provide a comment". A build that
20   * turns Javadoc warnings into errors, which the {@code maven-javadoc-plugin}
21   * does through {@code failOnWarnings}, breaks over it, and the mistake
22   * surfaces at site time rather than at the moment the class is written.</p>
23   *
24   * <p>The fix is an explicit constructor with a Javadoc block above it,
25   * and this check asks for it before Javadoc does.</p>
26   *
27   * <p>Only the classes Javadoc documents are reported, because only they
28   * carry the warning. A class is documented when it is public or
29   * protected, and so is every class around it, since a nested class of a
30   * package-private one never reaches the page. A member of an interface
31   * or of an annotation counts as public without saying so.</p>
32   *
33   * <p>The other type declarations stay out. An interface and an
34   * annotation have no constructor to document. An enum gets a private
35   * one, which Javadoc leaves out of the page. A record gets a canonical
36   * one, which Javadoc documents from the {@code @param} tags of the
37   * record itself. A local class and a member of an anonymous one are not
38   * documented either, whatever their modifiers say.</p>
39   *
40   * @since 0.73.4
41   */
42  public final class ImplicitConstructorCheck extends AbstractCheck {
43  
44      /**
45       * Types that may hold a class, and whose own visibility therefore
46       * decides whether the class inside them reaches the Javadoc page.
47       */
48      private static final Set<Integer> TYPES = Set.of(
49          TokenTypes.CLASS_DEF,
50          TokenTypes.INTERFACE_DEF,
51          TokenTypes.ENUM_DEF,
52          TokenTypes.RECORD_DEF,
53          TokenTypes.ANNOTATION_DEF
54      );
55  
56      /**
57       * Default constructor.
58       */
59      public ImplicitConstructorCheck() {
60          // nothing to initialize
61      }
62  
63      @Override
64      public int[] getDefaultTokens() {
65          return new int[]{TokenTypes.CLASS_DEF};
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      public void visitToken(final DetailAST ast) {
80          if (ImplicitConstructorCheck.documented(ast)
81              && ast.findFirstToken(TokenTypes.OBJBLOCK)
82                  .findFirstToken(TokenTypes.CTOR_DEF) == null) {
83              this.log(
84                  ast.getLineNo(),
85                  String.format(
86                      "Implicit constructor of \"%s\" gets no Javadoc, declare it explicitly",
87                      ast.findFirstToken(TokenTypes.IDENT).getText()
88                  )
89              );
90          }
91      }
92  
93      private static boolean documented(final DetailAST ast) {
94          boolean docs = true;
95          DetailAST node = ast;
96          while (node != null && node.getType() != TokenTypes.COMPILATION_UNIT) {
97              final int type = node.getType();
98              if (ImplicitConstructorCheck.TYPES.contains(type)) {
99                  if (!ImplicitConstructorCheck.visible(node)) {
100                     docs = false;
101                     break;
102                 }
103             } else if (type != TokenTypes.OBJBLOCK) {
104                 docs = false;
105                 break;
106             }
107             node = node.getParent();
108         }
109         return docs;
110     }
111 
112     private static boolean visible(final DetailAST type) {
113         final DetailAST mods = type.findFirstToken(TokenTypes.MODIFIERS);
114         return mods.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
115             || mods.findFirstToken(TokenTypes.LITERAL_PROTECTED) != null
116             || ImplicitConstructorCheck.implied(type);
117     }
118 
119     private static boolean implied(final DetailAST type) {
120         final DetailAST block = type.getParent();
121         boolean implied = false;
122         if (block != null && block.getType() == TokenTypes.OBJBLOCK) {
123             final int owner = block.getParent().getType();
124             implied = owner == TokenTypes.INTERFACE_DEF
125                 || owner == TokenTypes.ANNOTATION_DEF;
126         }
127         return implied;
128     }
129 }