1
2
3
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
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 public final class FullyQualifiedTypeCheck extends AbstractCheck {
50
51
52
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
64
65
66 private final Set<String> taken;
67
68
69
70
71 private final Map<String, String> imports;
72
73
74
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 }