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 is redundant. 051 * </p> 052 * 053 * <p> 054 * Two links are considered to reference the same API name if they resolve to 055 * the same canonical name. Simple names are resolved through explicit imports, 056 * types declared in the current file, star imports and the 057 * {@code java.lang} package. Names containing dots are resolved through 058 * imports of their outermost segment; otherwise they are compared as written. 059 * </p> 060 * 061 * @since 14.1.0 062 */ 063@FileStatefulCheck 064public class JavadocLinkFirstOccurrenceCheck extends AbstractJavadocCheck { 065 066 /** 067 * A key is pointing to the warning message text in "messages.properties" file. 068 */ 069 public static final String MSG_KEY = "javadoc.link.first.occurrence"; 070 071 /** 072 * Dot. 073 */ 074 private static final char DOT = '.'; 075 076 /** 077 * Tokens of type declarations. 078 */ 079 private static final Set<Integer> TYPE_DECLARATION_TOKENS = Set.of( 080 TokenTypes.CLASS_DEF, 081 TokenTypes.INTERFACE_DEF, 082 TokenTypes.ENUM_DEF, 083 TokenTypes.RECORD_DEF, 084 TokenTypes.ANNOTATION_DEF 085 ); 086 087 /** 088 * Set of reference keys already seen in the current Javadoc comment. 089 */ 090 private final Set<String> linkedNames = new HashSet<>(); 091 092 /** 093 * Map of imported simple names to fully qualified names. 094 */ 095 private Map<String, String> importedNames; 096 097 /** 098 * Set of star import base packages. 099 */ 100 private Set<String> starImports; 101 102 /** 103 * Set of simple names of types declared in the current file. 104 */ 105 private Set<String> declaredTypeNames; 106 107 /** 108 * Creates a new {@code JavadocLinkFirstOccurrenceCheck} instance. 109 */ 110 public JavadocLinkFirstOccurrenceCheck() { 111 // no code by default 112 } 113 114 @Override 115 public int[] getRequiredTokens() { 116 return new int[] { 117 TokenTypes.BLOCK_COMMENT_BEGIN, 118 TokenTypes.IMPORT, 119 }; 120 } 121 122 @Override 123 public int[] getDefaultJavadocTokens() { 124 return getRequiredJavadocTokens(); 125 } 126 127 @Override 128 public int[] getRequiredJavadocTokens() { 129 return new int[] { 130 JavadocCommentsTokenTypes.LINK_INLINE_TAG, 131 JavadocCommentsTokenTypes.LINKPLAIN_INLINE_TAG, 132 }; 133 } 134 135 @Override 136 public void beginTree(DetailAST rootAST) { 137 super.beginTree(rootAST); 138 importedNames = new HashMap<>(); 139 starImports = new HashSet<>(); 140 declaredTypeNames = new HashSet<>(); 141 if (rootAST != null) { 142 collectDeclaredTypeNames(rootAST); 143 } 144 } 145 146 @Override 147 public void beginJavadocTree(DetailNode rootAst) { 148 linkedNames.clear(); 149 } 150 151 @Override 152 public void visitToken(DetailAST ast) { 153 if (ast.getType() == TokenTypes.IMPORT) { 154 handleImport(ast); 155 } 156 else { 157 super.visitToken(ast); 158 } 159 } 160 161 @Override 162 public void visitJavadocToken(DetailNode ast) { 163 final DetailNode reference = JavadocUtil.findFirstToken(ast, 164 JavadocCommentsTokenTypes.REFERENCE); 165 final String originalReference = getNodeText(reference); 166 final String resolvedKey = resolveReference(originalReference); 167 if (!linkedNames.add(resolvedKey)) { 168 log(ast, MSG_KEY, originalReference); 169 } 170 } 171 172 /** 173 * Processes import statements and records imported names. 174 * 175 * @param ast import node 176 */ 177 private void handleImport(DetailAST ast) { 178 final String importText = FullIdent.createFullIdentBelow(ast).getText(); 179 if (importText.endsWith(".*")) { 180 starImports.add(importText.substring(0, importText.length() - 2)); 181 } 182 else { 183 final int lastDot = importText.lastIndexOf(DOT); 184 final String simple = importText.substring(lastDot + 1); 185 importedNames.put(simple, importText); 186 } 187 } 188 189 /** 190 * Records the simple names of all type declarations. 191 * The whole tree is scanned so that types declared after their 192 * references are also taken into account. 193 * 194 * @param rootAST the root of the tree to scan 195 */ 196 private void collectDeclaredTypeNames(DetailAST rootAST) { 197 final Deque<DetailAST> stack = new ArrayDeque<>(); 198 stack.push(rootAST); 199 while (!stack.isEmpty()) { 200 final DetailAST ast = stack.pop(); 201 if (TYPE_DECLARATION_TOKENS.contains(ast.getType())) { 202 final DetailAST ident = 203 NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT)); 204 declaredTypeNames.add(ident.getText()); 205 } 206 DetailAST child = ast.getFirstChild(); 207 while (child != null) { 208 stack.push(child); 209 child = child.getNextSibling(); 210 } 211 } 212 } 213 214 /** 215 * Resolves a reference text to a canonical key for identity comparison. 216 * The class part of the reference is resolved through imports and 217 * types declared in the current file. 218 * 219 * @param reference the raw reference text 220 * @return the resolved identity key 221 */ 222 private String resolveReference(String reference) { 223 final int hashIndex = reference.indexOf('#'); 224 final String className; 225 final String memberPart; 226 if (hashIndex == -1) { 227 className = reference; 228 memberPart = ""; 229 } 230 else { 231 className = reference.substring(0, hashIndex); 232 memberPart = reference.substring(hashIndex); 233 } 234 final String resolved = resolveClass(className); 235 return resolved + memberPart; 236 } 237 238 /** 239 * Resolves a class name to its canonical name through imports, 240 * types declared in the current file, star imports and the 241 * {@code java.lang} package. Names containing dots have only their 242 * outermost segment resolved; otherwise they are returned unchanged. 243 * 244 * @param name the class name 245 * @return the resolved canonical name 246 */ 247 private String resolveClass(String name) { 248 final int dotIndex = name.indexOf(DOT); 249 final String result; 250 if (dotIndex == -1) { 251 result = resolveSimpleClassName(name); 252 } 253 else { 254 result = resolveOuterSegment(name, dotIndex); 255 } 256 return result; 257 } 258 259 /** 260 * Resolves a simple class name through imports, types declared in the 261 * current file, star imports and the {@code java.lang} package. 262 * 263 * @param name the simple class name 264 * @return the resolved canonical name 265 */ 266 private String resolveSimpleClassName(String name) { 267 final String importCandidate = importedNames.get(name); 268 final String result; 269 if (importCandidate != null) { 270 result = importCandidate; 271 } 272 else if (declaredTypeNames.contains(name)) { 273 result = name; 274 } 275 else if (starImports.isEmpty()) { 276 result = "java.lang." + name; 277 } 278 else { 279 result = starImports.iterator().next() + DOT + name; 280 } 281 return result; 282 } 283 284 /** 285 * Resolves the outermost segment of a dotted class name through imports, 286 * keeping the remainder unchanged. 287 * 288 * @param name the dotted class name 289 * @param dotIndex the index of the first dot in the name 290 * @return the resolved canonical name 291 */ 292 private String resolveOuterSegment(String name, int dotIndex) { 293 final String outer = name.substring(0, dotIndex); 294 final String importCandidate = importedNames.get(outer); 295 final String result; 296 if (importCandidate != null) { 297 final String remainder = name.substring(dotIndex); 298 result = importCandidate + remainder; 299 } 300 else { 301 result = name; 302 } 303 return result; 304 } 305 306 /** 307 * Recursively builds the full text of a node by concatenating 308 * the text of all its leaf descendants. 309 * 310 * @param node the node to get text from 311 * @return the concatenated text, or null if the node is null 312 */ 313 private static String getNodeText(DetailNode node) { 314 final StringBuilder sb = new StringBuilder(256); 315 DetailNode child = node.getFirstChild(); 316 while (child != null) { 317 sb.append(getNodeText(child)); 318 child = child.getNextSibling(); 319 } 320 final String text; 321 if (sb.isEmpty()) { 322 text = node.getText(); 323 } 324 else { 325 text = sb.toString(); 326 } 327 return text; 328 } 329 330}