View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  import java.util.ArrayList;
11  import java.util.HashMap;
12  import java.util.HashSet;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Set;
16  
17  /**
18   * Prohibits a fully qualified type name where an import would do.
19   *
20   * <p>A package name in the middle of a statement carries nothing the
21   * import block does not already hold, while it lets two files of the
22   * same package spell the very same type in two different ways. A body
23   * that says {@code final java.util.List<String> spans = new
24   * java.util.ArrayList<>(cap)} should import both types and say
25   * {@code final List<String> spans = new ArrayList<>(cap)} instead.</p>
26   *
27   * <p>It is wrong for the reason the {@code java.lang.} prefix is wrong,
28   * which {@link UnnecessaryJavaLangCheck} reports on its own and this
29   * check therefore leaves alone.</p>
30   *
31   * <p>The genuine disambiguation cases stay untouched. A file that
32   * imports {@code java.awt.List} has no way to spell {@code
33   * java.util.List} short, and neither has a class that extends a type of
34   * its own simple name, the way {@code MethodNameCheck} extends the
35   * Checkstyle class of that very name. In both of them the simple name
36   * is taken by something else in the file and the qualified name is the
37   * only name left. A name that two imports of the file bring in is taken
38   * by both of them and stays alone as well.</p>
39   *
40   * <p>A name counts as a type name when it starts with a capital letter,
41   * has a small one after it, and stands behind at least two package
42   * names, so that {@code Foo.BAR} and {@code System.out.println} stay
43   * out of the way. Only plain names count, which is why the chain of
44   * {@code java.util.List.class} ends at its third name and the one of
45   * {@code this.foo} is empty.</p>
46   *
47   * @since 0.73.4
48   */
49  public final class FullyQualifiedTypeCheck extends AbstractCheck {
50  
51      /**
52       * Types a file may declare, taking their simple names.
53       */
54      private static final Set<Integer> DECLARATIONS = Set.of(
55          TokenTypes.CLASS_DEF,
56          TokenTypes.INTERFACE_DEF,
57          TokenTypes.ENUM_DEF,
58          TokenTypes.RECORD_DEF,
59          TokenTypes.ANNOTATION_DEF
60      );
61  
62      /**
63       * Simple names the file binds to something of its own, either by an
64       * import or by a type declaration of that name.
65       */
66      private final Set<String> taken;
67  
68      /**
69       * Fully qualified names the file imports, by their simple names.
70       */
71      private final Map<String, String> imports;
72  
73      /**
74       * Default constructor.
75       */
76      public FullyQualifiedTypeCheck() {
77          this.taken = new HashSet<>(0);
78          this.imports = new HashMap<>(0);
79      }
80  
81      @Override
82      public int[] getDefaultTokens() {
83          return new int[]{TokenTypes.DOT};
84      }
85  
86      @Override
87      public int[] getAcceptableTokens() {
88          return this.getDefaultTokens();
89      }
90  
91      @Override
92      public int[] getRequiredTokens() {
93          return this.getDefaultTokens();
94      }
95  
96      @Override
97      public void beginTree(final DetailAST root) {
98          this.taken.clear();
99          this.imports.clear();
100         this.scan(root);
101     }
102 
103     @Override
104     public void visitToken(final DetailAST ast) {
105         if (ast.getParent().getType() != TokenTypes.DOT
106             && !FullyQualifiedTypeCheck.declaring(ast)) {
107             this.inspect(FullyQualifiedTypeCheck.chain(ast), ast.getLineNo());
108         }
109     }
110 
111     private void inspect(final List<String> names, final int line) {
112         final int pos = FullyQualifiedTypeCheck.classy(names);
113         if (pos > 1) {
114             final String simple = names.get(pos);
115             final String pkg = String.join(".", names.subList(0, pos));
116             final String full = String.format("%s.%s", pkg, simple);
117             if (!"java.lang".equals(pkg) && this.free(simple, full)) {
118                 this.log(
119                     line,
120                     String.format(
121                         "Fully qualified \"%s\" is redundant, import it and use \"%s\"",
122                         full, simple
123                     )
124                 );
125             }
126         }
127     }
128 
129     private boolean free(final String simple, final String full) {
130         return !this.taken.contains(simple)
131             || full.equals(this.imports.get(simple));
132     }
133 
134     private void scan(final DetailAST node) {
135         final int type = node.getType();
136         if (type == TokenTypes.IMPORT || type == TokenTypes.STATIC_IMPORT) {
137             this.remember(
138                 FullyQualifiedTypeCheck.chain(node.getFirstChild())
139             );
140         } else if (FullyQualifiedTypeCheck.DECLARATIONS.contains(type)) {
141             this.taken.add(node.findFirstToken(TokenTypes.IDENT).getText());
142         }
143         DetailAST child = node.getFirstChild();
144         while (child != null) {
145             this.scan(child);
146             child = child.getNextSibling();
147         }
148     }
149 
150     private void remember(final List<String> names) {
151         final int pos = FullyQualifiedTypeCheck.classy(names);
152         if (pos >= 0) {
153             final String simple = names.get(pos);
154             final String full = String.join(".", names.subList(0, pos + 1));
155             if (this.taken.add(simple)) {
156                 this.imports.put(simple, full);
157             } else if (!full.equals(this.imports.get(simple))) {
158                 this.imports.remove(simple);
159             }
160         }
161     }
162 
163     private static boolean declaring(final DetailAST node) {
164         boolean found = false;
165         DetailAST parent = node.getParent();
166         while (parent != null) {
167             final int type = parent.getType();
168             if (type == TokenTypes.PACKAGE_DEF
169                 || type == TokenTypes.IMPORT
170                 || type == TokenTypes.STATIC_IMPORT) {
171                 found = true;
172                 break;
173             }
174             parent = parent.getParent();
175         }
176         return found;
177     }
178 
179     private static List<String> chain(final DetailAST node) {
180         final List<String> names = new ArrayList<>(0);
181         DetailAST dot = node;
182         while (dot.getType() == TokenTypes.DOT) {
183             final DetailAST right = dot.getFirstChild().getNextSibling();
184             if (right != null && right.getType() == TokenTypes.IDENT) {
185                 names.add(0, right.getText());
186             } else {
187                 names.clear();
188             }
189             dot = dot.getFirstChild();
190         }
191         if (dot.getType() == TokenTypes.IDENT) {
192             names.add(0, dot.getText());
193         } else {
194             names.clear();
195         }
196         return names;
197     }
198 
199     private static int classy(final List<String> names) {
200         int found = -1;
201         for (int pos = 0; pos < names.size(); ++pos) {
202             if (FullyQualifiedTypeCheck.classy(names.get(pos))) {
203                 found = pos;
204                 break;
205             }
206         }
207         return found;
208     }
209 
210     private static boolean classy(final String name) {
211         return Character.isUpperCase(name.charAt(0))
212             && name.chars().anyMatch(Character::isLowerCase);
213     }
214 }