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 }