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.ArrayList;
23 import java.util.Arrays;
24 import java.util.BitSet;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29
30 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
31 import com.puppycrawl.tools.checkstyle.api.DetailAST;
32 import com.puppycrawl.tools.checkstyle.api.DetailNode;
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.TokenUtil;
37
38 /**
39 * <div>
40 * Checks that multiple {@code @see} tags are ordered in a predictable way,
41 * roughly following the order in which their arguments are searched for by javadoc,
42 * from nearest to farthest access, from least-qualified to fully-qualified.
43 * </div>
44 *
45 * <p>The order of {@code @see} tags should be:</p>
46 * <ol>
47 * <li>local members first</li>
48 * <li>simple class references after local members</li>
49 * <li>simple class member references after simple class references</li>
50 * <li>qualified class references after simple class member references</li>
51 * <li>qualified class member references after qualified class references</li>
52 * <li>package references last</li>
53 * </ol>
54 *
55 * <p>Inside each member group, fields come first, then constructors, then
56 * methods. Overloaded constructors and methods with the same name must be
57 * grouped together and ordered by the number of parameters, with the fewest
58 * parameters first.</p>
59 *
60 * <p>For example, this is the order recommended by the OpenJDK documentation
61 * comments style guide:</p>
62 * <div class="wrapper"><pre>
63 * @see #field
64 * @see #Constructor(Type, Type...)
65 * @see #Constructor(Type id, Type id...)
66 * @see #method(Type, Type,...)
67 * @see #method(Type id, Type, id...)
68 * @see Class
69 * @see Class#field
70 * @see Class#Constructor(Type, Type...)
71 * @see Class#Constructor(Type id, Type id)
72 * @see Class#method(Type, Type,...)
73 * @see Class#method(Type id, Type id,...)
74 * @see package.Class
75 * @see package.Class#field
76 * @see package.Class#Constructor(Type, Type...)
77 * @see package.Class#Constructor(Type id, Type id)
78 * @see package.Class#method(Type, Type,...)
79 * @see package.Class#method(Type id, Type, id)
80 * @see package
81 * </pre></div>
82 *
83 * <p>References that are not in a recognizable structured form (for example
84 * {@code @see "Effective Java"}, or an HTML anchor) are ignored for ordering
85 * purposes, so the check only reports violations when it is confident about
86 * the correct order. References using the {@code Type##fragment} syntax to
87 * link to a named fragment within a page (rather than to a member) are
88 * intentionally ignored as well, since such fragments are not javadoc type
89 * or member references and cannot be meaningfully compared to one.</p>
90 *
91 * @since 14.2.0
92 */
93 @FileStatefulCheck
94 public class JavadocSeeTagOrderCheck extends AbstractJavadocCheck {
95
96 /**
97 * A key is pointing to the warning message text in "messages.properties" file.
98 */
99 public static final String MSG_KEY = "javadoc.seeTagOrder";
100
101 /** Dot. */
102 private static final char DOT = '.';
103
104 /** Hash sign used to separate a type name from its member. */
105 private static final char HASH = '#';
106
107 /** Number of {@link Kind} values, used to combine category and kind into one key. */
108 private static final int KIND_COUNT = 3;
109
110 /** Token types that represent a type declaration. */
111 private static final BitSet TYPE_DEFINITION_TOKENS = TokenUtil.asBitSet(
112 TokenTypes.CLASS_DEF, TokenTypes.INTERFACE_DEF, TokenTypes.ENUM_DEF,
113 TokenTypes.RECORD_DEF, TokenTypes.ANNOTATION_DEF);
114
115 /** The most distant structural position seen so far in the current Javadoc tree. */
116 private SeeReference maxReference;
117
118 /** The reference that immediately precedes the current one. */
119 private SeeReference previousReference;
120
121 /** The most recent reference for each member name within the current Javadoc tree. */
122 private Map<String, SeeReference> lastByName;
123
124 /** Simple name of the type that most closely encloses the current Javadoc comment. */
125 private String enclosingTypeName;
126
127 /**
128 * Creates a new {@code JavadocSeeTagOrderCheck} instance.
129 */
130 public JavadocSeeTagOrderCheck() {
131 // no code by default
132 }
133
134 @Override
135 public int[] getDefaultJavadocTokens() {
136 return getRequiredJavadocTokens();
137 }
138
139 @Override
140 public int[] getRequiredJavadocTokens() {
141 return new int[] {
142 JavadocCommentsTokenTypes.SEE_BLOCK_TAG,
143 };
144 }
145
146 @Override
147 public void beginJavadocTree(DetailNode rootAst) {
148 maxReference = null;
149 lastByName = new HashMap<>();
150 enclosingTypeName = findEnclosingTypeName(getBlockCommentAst());
151 }
152
153 /**
154 * Finds the simple name of the type declaration that most closely encloses the
155 * given block comment, so that local {@code @see #Name()} references can be
156 * recognized as referring to a constructor of that type.
157 *
158 * @param commentBlock the block comment to start searching from
159 * @return the enclosing type's simple name, or an empty string if none is found
160 */
161 private static String findEnclosingTypeName(DetailAST commentBlock) {
162 DetailAST current = commentBlock;
163 while (current != null && !isTypeDefinition(current)) {
164 current = current.getParent();
165 }
166 String result = "";
167 if (current != null) {
168 result = current.findFirstToken(TokenTypes.IDENT).getText();
169 }
170 return result;
171 }
172
173 /**
174 * Checks whether the given node is a type declaration.
175 *
176 * @param ast the node to check
177 * @return {@code true} if the node is a class, interface, enum, record, or
178 * annotation declaration
179 */
180 private static boolean isTypeDefinition(DetailAST ast) {
181 return TYPE_DEFINITION_TOKENS.get(ast.getType());
182 }
183
184 @Override
185 public void visitJavadocToken(DetailNode ast) {
186 final SeeReference current = SeeReference.from(ast, enclosingTypeName);
187 if (current != null) {
188 if (maxReference == null) {
189 maxReference = current;
190 }
191 else if (isStructuralViolation(current, maxReference)) {
192 log(ast, MSG_KEY, current.text(), maxReference.text());
193 }
194 else if (isTelescopingViolation(current)) {
195 log(ast, MSG_KEY, current.text(), lastByName.get(lastByKey(current)).text());
196 }
197 else if (isGroupingViolation(current)) {
198 log(ast, MSG_KEY, current.text(), previousReference.text());
199 }
200 if (current.structurallyAfterThan(maxReference)) {
201 maxReference = current;
202 }
203 previousReference = current;
204 lastByName.put(lastByKey(current), current);
205 }
206 }
207
208 /**
209 * Checks whether the current reference breaks the structural order
210 * (category and field-before-constructor-before-method).
211 *
212 * @param current the current reference
213 * @param maximum the greatest structural reference seen so far
214 * @return {@code true} if the structural order is violated
215 */
216 private static boolean isStructuralViolation(SeeReference current, SeeReference maximum) {
217 return current.structuralKey() < maximum.structuralKey();
218 }
219
220 /**
221 * Checks whether the current reference breaks the telescoping order of an
222 * overloaded constructor or method with the same name.
223 *
224 * @param current the current reference
225 * @return {@code true} if the telescoping order is violated
226 */
227 private boolean isTelescopingViolation(SeeReference current) {
228 final SeeReference sameName = lastByName.get(lastByKey(current));
229 return sameName != null
230 && current.parameterCount() < sameName.parameterCount();
231 }
232
233 /**
234 * Checks whether the current reference breaks the grouping of overloaded
235 * members with the same name.
236 *
237 * @param current the current reference
238 * @return {@code true} if the grouping is violated
239 */
240 private boolean isGroupingViolation(SeeReference current) {
241 return lastByName.containsKey(lastByKey(current))
242 && previousReference.kind() == current.kind()
243 && !previousReference.name().equals(current.name());
244 }
245
246 /**
247 * Returns the map key that scopes a member name to its owning type and category
248 * group, so that grouping and telescoping checks only apply to overloads of the
249 * same member on the same type.
250 *
251 * @param reference the reference
252 * @return the map key
253 */
254 private static String lastByKey(SeeReference reference) {
255 return reference.category() + ":" + reference.qualifier() + "#" + reference.name();
256 }
257
258 /**
259 * Category of a {@code @see} reference, ordered from the closest to the most
260 * distant access.
261 */
262 private enum Category {
263 /** Local member such as {@code #field} or {@code #method()}. */
264 LOCAL(0),
265 /** Simple type reference such as {@code OtherClass}. */
266 SIMPLE_TYPE(1),
267 /** Simple type member such as {@code OtherClass#field}. */
268 SIMPLE_MEMBER(2),
269 /** Qualified type reference such as {@code java.util.List}. */
270 QUALIFIED_TYPE(3),
271 /** Qualified type member such as {@code java.util.List#size()}. */
272 QUALIFIED_MEMBER(4),
273 /** Package reference such as {@code java.util}. */
274 PACKAGE(5);
275
276 /** Explicit structural order, independent of the enum's declaration order. */
277 private final int structuralOrder;
278
279 /**
280 * Creates a new {@code Category} instance.
281 *
282 * @param order the explicit structural order
283 */
284 Category(int order) {
285 structuralOrder = order;
286 }
287
288 /**
289 * Returns the explicit structural order.
290 *
291 * @return the structural order
292 */
293 /* package */ int order() {
294 return structuralOrder;
295 }
296
297 }
298
299 /**
300 * Kind of a member {@code @see} reference, ordered from the closest to the most
301 * distant access. Type and package references are not members and are always
302 * classified as {@link #METHOD}, since that kind never needs to be compared
303 * against another kind within their own category.
304 */
305 private enum Kind {
306 /** A field reference such as {@code #field}. */
307 FIELD(0),
308 /** A constructor reference such as {@code #Example()}. */
309 CONSTRUCTOR(1),
310 /** A method or type/package reference such as {@code #getName()}. */
311 METHOD(2);
312
313 /** Explicit structural order, independent of the enum's declaration order. */
314 private final int structuralOrder;
315
316 /**
317 * Creates a new {@code Kind} instance.
318 *
319 * @param order the explicit structural order
320 */
321 Kind(int order) {
322 structuralOrder = order;
323 }
324
325 /**
326 * Returns the explicit structural order.
327 *
328 * @return the structural order
329 */
330 /* package */ int order() {
331 return structuralOrder;
332 }
333
334 }
335
336 /**
337 * Represents a parsed {@code @see} reference together with the ordering
338 * information needed to validate the order.
339 *
340 * @param category category that defines the primary structural ordering
341 * @param kind whether the reference is a field, constructor, or method
342 * @param qualifier owning type name, or an empty string for local and
343 * non-member references
344 * @param name simple member or type name used for grouping
345 * @param parameterCount number of parameters, used to order overloaded methods
346 * @param text full reference text used for messages
347 */
348 private record SeeReference(
349 Category category,
350 Kind kind,
351 String qualifier,
352 String name,
353 int parameterCount,
354 String text) {
355
356 /**
357 * Parses a {@code @see} block tag into a {@code SeeReference}, or returns
358 * {@code null} if the reference is not in a form that can be confidently ordered.
359 *
360 * @param seeBlock the {@code @see} block tag
361 * @param enclosingTypeName simple name of the type that most closely encloses
362 * the Javadoc comment, used to recognize local constructor references
363 * @return the parsed reference, or {@code null} if it cannot be classified
364 */
365 private static SeeReference from(DetailNode seeBlock, String enclosingTypeName) {
366 final DetailNode reference = JavadocUtil.findFirstToken(
367 seeBlock, JavadocCommentsTokenTypes.REFERENCE);
368 SeeReference result = null;
369 if (reference != null && !isFragmentReference(reference)) {
370 final DetailNode firstChild = reference.getFirstChild();
371 final int firstType = firstChild.getType();
372
373 if (firstType == JavadocCommentsTokenTypes.HASH) {
374 final DetailNode member = JavadocUtil.findFirstToken(
375 reference, JavadocCommentsTokenTypes.MEMBER_REFERENCE);
376 result = parseMember(member, "", Category.LOCAL, enclosingTypeName);
377 }
378 else {
379 result = parseIdentifierReference(reference, firstChild);
380 }
381 }
382 return result;
383 }
384
385 /**
386 * Checks whether the given reference uses the {@code Type##fragment} syntax to
387 * link to a named fragment within a page, recognizable by two consecutive hash
388 * signs among its children. Such references are not javadoc type or member
389 * references and cannot be meaningfully compared to one.
390 *
391 * @param reference the reference node
392 * @return {@code true} if the reference contains two consecutive hash signs
393 */
394 private static boolean isFragmentReference(DetailNode reference) {
395 boolean sawHash = false;
396 boolean result = false;
397 DetailNode child = reference.getFirstChild();
398 while (child != null) {
399 if (child.getType() == JavadocCommentsTokenTypes.HASH) {
400 if (sawHash) {
401 result = true;
402 break;
403 }
404 sawHash = true;
405 }
406 child = child.getNextSibling();
407 }
408 return result;
409 }
410
411 /**
412 * Parses an identifier-based reference (simple or qualified type, member,
413 * or package reference).
414 *
415 * @param reference the reference node
416 * @param identifierNode the identifier node
417 * @return the parsed reference, or {@code null} if it cannot be classified
418 */
419 private static SeeReference parseIdentifierReference(DetailNode reference,
420 DetailNode identifierNode) {
421 final String typeName = identifierNode.getText();
422 final DetailNode member = JavadocUtil.findFirstToken(
423 reference, JavadocCommentsTokenTypes.MEMBER_REFERENCE);
424 SeeReference result = null;
425 if (member != null) {
426 final Category category;
427 if (typeName.indexOf(DOT) == -1) {
428 category = Category.SIMPLE_MEMBER;
429 }
430 else {
431 category = Category.QUALIFIED_MEMBER;
432 }
433 result = parseMember(member, typeName, category, lastIdentifier(typeName));
434 }
435 else if (isTypeReference(typeName)) {
436 final Category category;
437 if (typeName.indexOf(DOT) == -1) {
438 category = Category.SIMPLE_TYPE;
439 }
440 else {
441 category = Category.QUALIFIED_TYPE;
442 }
443 result = new SeeReference(category, Kind.METHOD, "",
444 lastIdentifier(typeName), 0, typeName);
445 }
446 else if (typeName.indexOf(DOT) != -1 && isPackageReference(typeName)) {
447 result = new SeeReference(Category.PACKAGE, Kind.METHOD, "",
448 typeName, 0, typeName);
449 }
450 return result;
451 }
452
453 /**
454 * Parses a member reference (local member or class member).
455 *
456 * @param member the member reference node
457 * @param qualifier the owning type name, or an empty string for local members
458 * @param category category of the reference
459 * @param ownerSimpleName simple name of the owning type, compared against the
460 * member name to recognize constructor references
461 * @return the parsed member reference
462 */
463 private static SeeReference parseMember(DetailNode member, String qualifier,
464 Category category, String ownerSimpleName) {
465 final DetailNode identifier = JavadocUtil.findFirstToken(
466 member, JavadocCommentsTokenTypes.IDENTIFIER);
467 final boolean callable = JavadocUtil.findFirstToken(
468 member, JavadocCommentsTokenTypes.LPAREN) != null;
469 final String memberName = identifier.getText();
470 final List<String> parameterTypes = parameterTypes(member);
471 final StringBuilder text = new StringBuilder(qualifier)
472 .append(HASH);
473 if (callable) {
474 text.append(memberName).append('(');
475 for (int ind = 0; ind < parameterTypes.size(); ind++) {
476 if (ind > 0) {
477 text.append(", ");
478 }
479 text.append(parameterTypes.get(ind));
480 }
481 text.append(')');
482 }
483 else {
484 text.append(memberName);
485 }
486 final Kind kind;
487 if (callable) {
488 if (memberName.equals(ownerSimpleName)) {
489 kind = Kind.CONSTRUCTOR;
490 }
491 else {
492 kind = Kind.METHOD;
493 }
494 }
495 else {
496 kind = Kind.FIELD;
497 }
498 return new SeeReference(category, kind, qualifier, memberName,
499 parameterTypes.size(), text.toString());
500 }
501
502 /**
503 * Returns the parameter type texts of a member reference.
504 *
505 * @param member the member reference node
506 * @return the list of parameter type texts
507 */
508 private static List<String> parameterTypes(DetailNode member) {
509 final DetailNode parameterList = JavadocUtil.findFirstToken(
510 member, JavadocCommentsTokenTypes.PARAMETER_TYPE_LIST);
511 final List<String> parameterTypes = new ArrayList<>();
512 if (parameterList != null) {
513 DetailNode node = parameterList.getFirstChild();
514 while (node != null) {
515 if (node.getType() == JavadocCommentsTokenTypes.PARAMETER_TYPE) {
516 parameterTypes.add(node.getText());
517 }
518 node = node.getNextSibling();
519 }
520 }
521 return parameterTypes;
522 }
523
524 /**
525 * Checks whether the given text looks like a type reference (last segment
526 * starts with an uppercase letter).
527 *
528 * @param referenceText the reference text to check
529 * @return {@code true} if the text is a type reference
530 */
531 private static boolean isTypeReference(String referenceText) {
532 final String lastSegment = lastIdentifier(referenceText);
533 return Character.isUpperCase(lastSegment.charAt(0));
534 }
535
536 /**
537 * Checks whether the given text looks like a package reference (all lowercase
538 * segments and contains at least one dot).
539 *
540 * @param referenceText the reference text to check
541 * @return {@code true} if the text is a package reference
542 */
543 private static boolean isPackageReference(String referenceText) {
544 return Arrays.stream(referenceText.split("\\" + DOT, -1))
545 .allMatch(segment -> segment.toLowerCase(Locale.ROOT).equals(segment));
546 }
547
548 /**
549 * Returns the last identifier segment of a dotted name.
550 *
551 * @param name the dotted name
552 * @return the last segment
553 */
554 private static String lastIdentifier(String name) {
555 return name.substring(name.lastIndexOf(DOT) + 1);
556 }
557
558 /**
559 * Returns the structural ordering key combining the category and, for member
560 * references, whether the reference is a field, constructor, or method.
561 *
562 * @return the structural ordering key
563 */
564 /* package */ int structuralKey() {
565 return category.order() * KIND_COUNT + kind.order();
566 }
567
568 /**
569 * Checks whether this reference is structurally after the given reference.
570 *
571 * @param other the reference to compare to
572 * @return {@code true} if this reference is structurally after the other
573 */
574 /* package */ boolean structurallyAfterThan(SeeReference other) {
575 return structuralKey() >= other.structuralKey();
576 }
577
578 }
579
580 }