View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.DetailNode;
32  import com.puppycrawl.tools.checkstyle.api.FullIdent;
33  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
34  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
36  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
37  
38  /**
39   * <div>
40   * Checks that in Javadoc comments, each API name is linked with
41   * {@code {@link}} or {@code {@linkplain}} only on its first occurrence.
42   * Subsequent links to the same API name in the same comment are flagged.
43   * </div>
44   *
45   * <p>
46   * Rationale: From the
47   * <a href="https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html">
48   * Documentation Comments style guide</a>, links call attention to
49   * themselves by their color and underline in HTML, and by their length
50   * in source code doc comments. Linking the same name multiple times
51   * is redundant.
52   * </p>
53   *
54   * <p>
55   * Two links are considered to reference the same API name if they resolve to
56   * the same canonical name. Simple names are resolved through explicit imports,
57   * types declared in the current file, star imports and the
58   * {@code java.lang} package. Names containing dots are resolved through
59   * imports of their outermost segment; otherwise they are compared as written.
60   * </p>
61   *
62   * @since 14.1.0
63   */
64  @FileStatefulCheck
65  public class JavadocLinkFirstOccurrenceCheck extends AbstractJavadocCheck {
66  
67      /**
68       * A key is pointing to the warning message text in "messages.properties"
69       * file.
70       */
71      public static final String MSG_KEY = "javadoc.link.first.occurrence";
72  
73      /**
74       * Dot.
75       */
76      private static final char DOT = '.';
77  
78      /**
79       * Tokens of type declarations.
80       */
81      private static final Set<Integer> TYPE_DECLARATION_TOKENS = Set.of(
82              TokenTypes.CLASS_DEF,
83              TokenTypes.INTERFACE_DEF,
84              TokenTypes.ENUM_DEF,
85              TokenTypes.RECORD_DEF,
86              TokenTypes.ANNOTATION_DEF
87      );
88  
89      /**
90       * Set of reference keys already seen in the current Javadoc comment.
91       */
92      private final Set<String> linkedNames = new HashSet<>();
93  
94      /**
95       * Map of imported simple names to fully qualified names.
96       */
97      private Map<String, String> importedNames;
98  
99      /**
100      * Set of star import base packages.
101      */
102     private Set<String> starImports;
103 
104     /**
105      * Set of simple names of types declared in the current file.
106      */
107     private Set<String> declaredTypeNames;
108 
109     /**
110      * Creates a new {@code JavadocLinkFirstOccurrenceCheck} instance.
111      */
112     public JavadocLinkFirstOccurrenceCheck() {
113         // no code by default
114     }
115 
116     @Override
117     public int[] getRequiredTokens() {
118         return new int[] {
119             TokenTypes.BLOCK_COMMENT_BEGIN,
120             TokenTypes.IMPORT,
121         };
122     }
123 
124     @Override
125     public int[] getDefaultJavadocTokens() {
126         return getRequiredJavadocTokens();
127     }
128 
129     @Override
130     public int[] getRequiredJavadocTokens() {
131         return new int[] {
132             JavadocCommentsTokenTypes.LINK_INLINE_TAG,
133             JavadocCommentsTokenTypes.LINKPLAIN_INLINE_TAG,
134         };
135     }
136 
137     @Override
138     public void beginTree(DetailAST rootAST) {
139         super.beginTree(rootAST);
140         importedNames = new HashMap<>();
141         starImports = new HashSet<>();
142         declaredTypeNames = new HashSet<>();
143         if (rootAST != null) {
144             collectDeclaredTypeNames(rootAST);
145         }
146     }
147 
148     @Override
149     public void beginJavadocTree(DetailNode rootAst) {
150         linkedNames.clear();
151     }
152 
153     @Override
154     public void visitToken(DetailAST ast) {
155         if (ast.getType() == TokenTypes.IMPORT) {
156             handleImport(ast);
157         }
158         else {
159             super.visitToken(ast);
160         }
161     }
162 
163     @Override
164     public void visitJavadocToken(DetailNode ast) {
165         final DetailNode reference = JavadocUtil.findFirstToken(ast,
166                 JavadocCommentsTokenTypes.REFERENCE);
167         final String originalReference = getNodeText(reference);
168         final String resolvedKey = resolveReference(originalReference);
169         if (!linkedNames.add(resolvedKey)) {
170             log(ast, MSG_KEY, originalReference);
171         }
172     }
173 
174     /**
175      * Processes import statements and records imported names.
176      *
177      * @param ast import node
178      */
179     private void handleImport(DetailAST ast) {
180         final String importText = FullIdent.createFullIdentBelow(ast).getText();
181         if (importText.endsWith(".*")) {
182             starImports.add(importText.substring(0, importText.length() - 2));
183         }
184         else {
185             final int lastDot = importText.lastIndexOf(DOT);
186             final String simple = importText.substring(lastDot + 1);
187             importedNames.put(simple, importText);
188         }
189     }
190 
191     /**
192      * Records the simple names of all type declarations.
193      * The whole tree is scanned so that types declared after their
194      * references are also taken into account.
195      *
196      * @param rootAST the root of the tree to scan
197      */
198     private void collectDeclaredTypeNames(DetailAST rootAST) {
199         final Deque<DetailAST> stack = new ArrayDeque<>();
200         stack.push(rootAST);
201         while (!stack.isEmpty()) {
202             final DetailAST ast = stack.pop();
203             if (TYPE_DECLARATION_TOKENS.contains(ast.getType())) {
204                 final DetailAST ident =
205                         NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT));
206                 declaredTypeNames.add(ident.getText());
207             }
208             DetailAST child = ast.getFirstChild();
209             while (child != null) {
210                 stack.push(child);
211                 child = child.getNextSibling();
212             }
213         }
214     }
215 
216     /**
217      * Resolves a reference text to a canonical key for identity comparison.
218      * The class part of the reference is resolved through imports and
219      * types declared in the current file.
220      *
221      * @param reference the raw reference text
222      * @return the resolved identity key
223      */
224     private String resolveReference(String reference) {
225         final int hashIndex = reference.indexOf('#');
226         final String className;
227         final String memberPart;
228         if (hashIndex == -1) {
229             className = reference;
230             memberPart = "";
231         }
232         else {
233             className = reference.substring(0, hashIndex);
234             memberPart = reference.substring(hashIndex);
235         }
236         final String resolved = resolveClass(className);
237         return resolved + memberPart;
238     }
239 
240     /**
241      * Resolves a class name to its canonical name through imports,
242      * types declared in the current file, star imports and the
243      * {@code java.lang} package. Names containing dots have only their
244      * outermost segment resolved; otherwise they are returned unchanged.
245      *
246      * @param name the class name
247      * @return the resolved canonical name
248      */
249     private String resolveClass(String name) {
250         final int dotIndex = name.indexOf(DOT);
251         final String result;
252         if (dotIndex == -1) {
253             result = resolveSimpleClassName(name);
254         }
255         else {
256             result = resolveOuterSegment(name, dotIndex);
257         }
258         return result;
259     }
260 
261     /**
262      * Resolves a simple class name through imports, types declared in the
263      * current file, star imports and the {@code java.lang} package.
264      *
265      * @param name the simple class name
266      * @return the resolved canonical name
267      */
268     private String resolveSimpleClassName(String name) {
269         final String importCandidate = importedNames.get(name);
270         final String result;
271         if (importCandidate != null) {
272             result = importCandidate;
273         }
274         else if (declaredTypeNames.contains(name)) {
275             result = name;
276         }
277         else if (starImports.isEmpty()) {
278             result = "java.lang." + name;
279         }
280         else {
281             result = starImports.iterator().next() + DOT + name;
282         }
283         return result;
284     }
285 
286     /**
287      * Resolves the outermost segment of a dotted class name through imports,
288      * keeping the remainder unchanged.
289      *
290      * @param name the dotted class name
291      * @param dotIndex the index of the first dot in the name
292      * @return the resolved canonical name
293      */
294     private String resolveOuterSegment(String name, int dotIndex) {
295         final String outer = name.substring(0, dotIndex);
296         final String importCandidate = importedNames.get(outer);
297         final String result;
298         if (importCandidate != null) {
299             final String remainder = name.substring(dotIndex);
300             result = importCandidate + remainder;
301         }
302         else {
303             result = name;
304         }
305         return result;
306     }
307 
308     /**
309      * Recursively builds the full text of a node by concatenating
310      * the text of all its leaf descendants.
311      *
312      * @param node the node to get text from
313      * @return the concatenated text, or null if the node is null
314      */
315     private static String getNodeText(DetailNode node) {
316         final StringBuilder sb = new StringBuilder(256);
317         DetailNode child = node.getFirstChild();
318         while (child != null) {
319             sb.append(getNodeText(child));
320             child = child.getNextSibling();
321         }
322         final String text;
323         if (sb.isEmpty()) {
324             text = node.getText();
325         }
326         else {
327             text = sb.toString();
328         }
329         return text;
330     }
331 
332 }