1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.checks.javadoc;
21
22 import java.util.ArrayDeque;
23 import java.util.Deque;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Set;
28
29 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30 import com.puppycrawl.tools.checkstyle.api.DetailAST;
31 import com.puppycrawl.tools.checkstyle.api.DetailNode;
32 import com.puppycrawl.tools.checkstyle.api.FullIdent;
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.NullUtil;
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 @FileStatefulCheck
65 public class JavadocLinkFirstOccurrenceCheck extends AbstractJavadocCheck {
66
67
68
69
70
71 public static final String MSG_KEY = "javadoc.link.first.occurrence";
72
73
74
75
76 private static final char DOT = '.';
77
78
79
80
81 private static final Set<Integer> TYPE_DECLARATION_TOKENS = Set.of(
82 TokenTypes.CLASS_DEF,
83 TokenTypes.INTERFACE_DEF,
84 TokenTypes.ENUM_DEF,
85 TokenTypes.RECORD_DEF,
86 TokenTypes.ANNOTATION_DEF
87 );
88
89
90
91
92 private final Set<String> linkedNames = new HashSet<>();
93
94
95
96
97 private Map<String, String> importedNames;
98
99
100
101
102 private Set<String> starImports;
103
104
105
106
107 private Set<String> declaredTypeNames;
108
109
110
111
112 public JavadocLinkFirstOccurrenceCheck() {
113
114 }
115
116 @Override
117 public int[] getRequiredTokens() {
118 return new int[] {
119 TokenTypes.BLOCK_COMMENT_BEGIN,
120 TokenTypes.IMPORT,
121 };
122 }
123
124 @Override
125 public int[] getDefaultJavadocTokens() {
126 return getRequiredJavadocTokens();
127 }
128
129 @Override
130 public int[] getRequiredJavadocTokens() {
131 return new int[] {
132 JavadocCommentsTokenTypes.LINK_INLINE_TAG,
133 JavadocCommentsTokenTypes.LINKPLAIN_INLINE_TAG,
134 };
135 }
136
137 @Override
138 public void beginTree(DetailAST rootAST) {
139 super.beginTree(rootAST);
140 importedNames = new HashMap<>();
141 starImports = new HashSet<>();
142 declaredTypeNames = new HashSet<>();
143 if (rootAST != null) {
144 collectDeclaredTypeNames(rootAST);
145 }
146 }
147
148 @Override
149 public void beginJavadocTree(DetailNode rootAst) {
150 linkedNames.clear();
151 }
152
153 @Override
154 public void visitToken(DetailAST ast) {
155 if (ast.getType() == TokenTypes.IMPORT) {
156 handleImport(ast);
157 }
158 else {
159 super.visitToken(ast);
160 }
161 }
162
163 @Override
164 public void visitJavadocToken(DetailNode ast) {
165 final DetailNode reference = JavadocUtil.findFirstToken(ast,
166 JavadocCommentsTokenTypes.REFERENCE);
167 final String originalReference = getNodeText(reference);
168 final String resolvedKey = resolveReference(originalReference);
169 if (!linkedNames.add(resolvedKey)) {
170 log(ast, MSG_KEY, originalReference);
171 }
172 }
173
174
175
176
177
178
179 private void handleImport(DetailAST ast) {
180 final String importText = FullIdent.createFullIdentBelow(ast).getText();
181 if (importText.endsWith(".*")) {
182 starImports.add(importText.substring(0, importText.length() - 2));
183 }
184 else {
185 final int lastDot = importText.lastIndexOf(DOT);
186 final String simple = importText.substring(lastDot + 1);
187 importedNames.put(simple, importText);
188 }
189 }
190
191
192
193
194
195
196
197
198 private void collectDeclaredTypeNames(DetailAST rootAST) {
199 final Deque<DetailAST> stack = new ArrayDeque<>();
200 stack.push(rootAST);
201 while (!stack.isEmpty()) {
202 final DetailAST ast = stack.pop();
203 if (TYPE_DECLARATION_TOKENS.contains(ast.getType())) {
204 final DetailAST ident =
205 NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT));
206 declaredTypeNames.add(ident.getText());
207 }
208 DetailAST child = ast.getFirstChild();
209 while (child != null) {
210 stack.push(child);
211 child = child.getNextSibling();
212 }
213 }
214 }
215
216
217
218
219
220
221
222
223
224 private String resolveReference(String reference) {
225 final int hashIndex = reference.indexOf('#');
226 final String className;
227 final String memberPart;
228 if (hashIndex == -1) {
229 className = reference;
230 memberPart = "";
231 }
232 else {
233 className = reference.substring(0, hashIndex);
234 memberPart = reference.substring(hashIndex);
235 }
236 final String resolved = resolveClass(className);
237 return resolved + memberPart;
238 }
239
240
241
242
243
244
245
246
247
248
249 private String resolveClass(String name) {
250 final int dotIndex = name.indexOf(DOT);
251 final String result;
252 if (dotIndex == -1) {
253 result = resolveSimpleClassName(name);
254 }
255 else {
256 result = resolveOuterSegment(name, dotIndex);
257 }
258 return result;
259 }
260
261
262
263
264
265
266
267
268 private String resolveSimpleClassName(String name) {
269 final String importCandidate = importedNames.get(name);
270 final String result;
271 if (importCandidate != null) {
272 result = importCandidate;
273 }
274 else if (declaredTypeNames.contains(name)) {
275 result = name;
276 }
277 else if (starImports.isEmpty()) {
278 result = "java.lang." + name;
279 }
280 else {
281 result = starImports.iterator().next() + DOT + name;
282 }
283 return result;
284 }
285
286
287
288
289
290
291
292
293
294 private String resolveOuterSegment(String name, int dotIndex) {
295 final String outer = name.substring(0, dotIndex);
296 final String importCandidate = importedNames.get(outer);
297 final String result;
298 if (importCandidate != null) {
299 final String remainder = name.substring(dotIndex);
300 result = importCandidate + remainder;
301 }
302 else {
303 result = name;
304 }
305 return result;
306 }
307
308
309
310
311
312
313
314
315 private static String getNodeText(DetailNode node) {
316 final StringBuilder sb = new StringBuilder(256);
317 DetailNode child = node.getFirstChild();
318 while (child != null) {
319 sb.append(getNodeText(child));
320 child = child.getNextSibling();
321 }
322 final String text;
323 if (sb.isEmpty()) {
324 text = node.getText();
325 }
326 else {
327 text = sb.toString();
328 }
329 return text;
330 }
331
332 }