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.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   * &#64;see #field
64   * &#64;see #Constructor(Type, Type...)
65   * &#64;see #Constructor(Type id, Type id...)
66   * &#64;see #method(Type, Type,...)
67   * &#64;see #method(Type id, Type, id...)
68   * &#64;see Class
69   * &#64;see Class#field
70   * &#64;see Class#Constructor(Type, Type...)
71   * &#64;see Class#Constructor(Type id, Type id)
72   * &#64;see Class#method(Type, Type,...)
73   * &#64;see Class#method(Type id, Type id,...)
74   * &#64;see package.Class
75   * &#64;see package.Class#field
76   * &#64;see package.Class#Constructor(Type, Type...)
77   * &#64;see package.Class#Constructor(Type id, Type id)
78   * &#64;see package.Class#method(Type, Type,...)
79   * &#64;see package.Class#method(Type id, Type, id)
80   * &#64;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 }