001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.javadoc; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.Map; 027import java.util.Set; 028 029import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.DetailNode; 032import com.puppycrawl.tools.checkstyle.api.FullIdent; 033import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 034import com.puppycrawl.tools.checkstyle.api.TokenTypes; 035import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 036import com.puppycrawl.tools.checkstyle.utils.NullUtil; 037 038/** 039 * <div> 040 * Checks that in Javadoc comments, each API name is linked with 041 * {@code {@link}} or {@code {@linkplain}} only on its first occurrence. 042 * Subsequent links to the same API name in the same comment are flagged. 043 * </div> 044 * 045 * <p> 046 * Rationale: From the 047 * <a href="https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html"> 048 * Documentation Comments style guide</a>, links call attention to 049 * themselves by their color and underline in HTML, and by their length 050 * in source code doc comments. Linking the same name multiple times 051 * is redundant. 052 * </p> 053 * 054 * <p> 055 * Two links are considered to reference the same API name if they resolve to 056 * the same canonical name. Simple names are resolved through explicit imports, 057 * types declared in the current file, star imports and the 058 * {@code java.lang} package. Names containing dots are resolved through 059 * imports of their outermost segment; otherwise they are compared as written. 060 * </p> 061 * 062 * @since 14.1.0 063 */ 064@FileStatefulCheck 065public class JavadocLinkFirstOccurrenceCheck extends AbstractJavadocCheck { 066 067 /** 068 * A key is pointing to the warning message text in "messages.properties" 069 * file. 070 */ 071 public static final String MSG_KEY = "javadoc.link.first.occurrence"; 072 073 /** 074 * Dot. 075 */ 076 private static final char DOT = '.'; 077 078 /** 079 * Tokens of type declarations. 080 */ 081 private static final Set<Integer> TYPE_DECLARATION_TOKENS = Set.of( 082 TokenTypes.CLASS_DEF, 083 TokenTypes.INTERFACE_DEF, 084 TokenTypes.ENUM_DEF, 085 TokenTypes.RECORD_DEF, 086 TokenTypes.ANNOTATION_DEF 087 ); 088 089 /** 090 * Set of reference keys already seen in the current Javadoc comment. 091 */ 092 private final Set<String> linkedNames = new HashSet<>(); 093 094 /** 095 * Map of imported simple names to fully qualified names. 096 */ 097 private Map<String, String> importedNames; 098 099 /** 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}