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.Arrays;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.stream.Collectors;
30
31 import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
32 import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage;
33 import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus;
34 import com.puppycrawl.tools.checkstyle.PropertyType;
35 import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
36 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
37 import com.puppycrawl.tools.checkstyle.api.DetailAST;
38 import com.puppycrawl.tools.checkstyle.api.DetailNode;
39 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
40 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
41 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
42 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
43
44
45
46
47
48
49
50
51
52 public abstract class AbstractJavadocCheck extends AbstractCheck {
53
54
55
56
57 public static final String MSG_JAVADOC_PARSE_RULE_ERROR =
58 JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR;
59
60
61
62
63 public static final String MSG_KEY_UNCLOSED_HTML_TAG =
64 JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
65
66
67
68
69
70
71 private static final ThreadLocal<Map<Integer, ParseStatus>> TREE_CACHE =
72 ThreadLocal.withInitial(HashMap::new);
73
74
75
76
77
78
79
80
81 private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
82
83
84 @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
85 private final Set<Integer> javadocTokens = new HashSet<>();
86
87
88
89
90
91
92
93
94
95
96
97 private boolean violateExecutionOnNonTightHtml;
98
99
100
101
102
103
104
105 public abstract int[] getDefaultJavadocTokens();
106
107
108
109
110
111
112
113 public abstract void visitJavadocToken(DetailNode ast);
114
115
116
117
118
119
120
121
122
123
124 public int[] getAcceptableJavadocTokens() {
125 final int[] defaultJavadocTokens = getDefaultJavadocTokens();
126 final int[] copy = new int[defaultJavadocTokens.length];
127 System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length);
128 return copy;
129 }
130
131
132
133
134
135
136
137 public int[] getRequiredJavadocTokens() {
138 return CommonUtil.EMPTY_INT_ARRAY;
139 }
140
141
142
143
144
145
146
147
148
149
150
151
152 public boolean acceptJavadocWithNonTightHtml() {
153 return true;
154 }
155
156
157
158
159
160
161
162
163
164
165 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
166 violateExecutionOnNonTightHtml = shouldReportViolation;
167 }
168
169
170
171
172
173
174 public void setJavadocTokens(String... strRep) {
175 for (String str : strRep) {
176 javadocTokens.add(JavadocUtil.getTokenId(str));
177 }
178 }
179
180 @Override
181 public void init() {
182 validateDefaultJavadocTokens();
183 if (javadocTokens.isEmpty()) {
184 javadocTokens.addAll(
185 Arrays.stream(getDefaultJavadocTokens()).boxed()
186 .toList());
187 }
188 else {
189 final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
190 Arrays.sort(acceptableJavadocTokens);
191 for (Integer javadocTokenId : javadocTokens) {
192 if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
193 final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
194 + "not found in Acceptable javadoc tokens list in check %s",
195 JavadocUtil.getTokenName(javadocTokenId), getClass().getName());
196 throw new IllegalStateException(message);
197 }
198 }
199 }
200 }
201
202
203
204
205
206
207 private void validateDefaultJavadocTokens() {
208 final Set<Integer> defaultTokens = Arrays.stream(getDefaultJavadocTokens())
209 .boxed()
210 .collect(Collectors.toUnmodifiableSet());
211
212 final List<Integer> missingRequiredTokenNames = Arrays.stream(getRequiredJavadocTokens())
213 .boxed()
214 .filter(token -> !defaultTokens.contains(token))
215 .toList();
216
217 if (!missingRequiredTokenNames.isEmpty()) {
218 final String message = String.format(Locale.ROOT,
219 "Javadoc Token \"%s\" from required javadoc "
220 + "tokens was not found in default "
221 + "javadoc tokens list in check %s",
222 missingRequiredTokenNames.stream()
223 .map(String::valueOf)
224 .collect(Collectors.joining(", ")),
225 getClass().getName());
226 throw new IllegalStateException(message);
227 }
228 }
229
230
231
232
233
234
235
236
237
238 public void beginJavadocTree(DetailNode rootAst) {
239
240 }
241
242
243
244
245
246
247
248
249
250 public void finishJavadocTree(DetailNode rootAst) {
251
252 }
253
254
255
256
257
258
259
260 public void leaveJavadocToken(DetailNode ast) {
261
262 }
263
264
265
266
267
268
269 @Override
270 public final int[] getDefaultTokens() {
271 return getRequiredTokens();
272 }
273
274 @Override
275 public final int[] getAcceptableTokens() {
276 return getRequiredTokens();
277 }
278
279 @Override
280 public final int[] getRequiredTokens() {
281 return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
282 }
283
284
285
286
287
288
289 @Override
290 public final boolean isCommentNodesRequired() {
291 return true;
292 }
293
294 @Override
295 public final void beginTree(DetailAST rootAST) {
296 TREE_CACHE.get().clear();
297 }
298
299 @Override
300 public final void finishTree(DetailAST rootAST) {
301
302 }
303
304 @Override
305 public final void visitToken(DetailAST blockCommentNode) {
306 if (JavadocUtil.isJavadocComment(blockCommentNode)) {
307
308 context.get().blockCommentAst = blockCommentNode;
309
310 final int treeCacheKey = blockCommentNode.getLineNo();
311
312 final ParseStatus result = TREE_CACHE.get()
313 .computeIfAbsent(treeCacheKey, lineNumber -> {
314 return context.get().parser.parseJavadocComment(blockCommentNode);
315 });
316
317 if (result.getParseErrorMessage() == null) {
318 if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) {
319 processTree(result.getTree());
320 }
321
322 if (violateExecutionOnNonTightHtml && result.isNonTight()) {
323 log(result.getFirstNonTightHtmlTag().getLineNumber(),
324 MSG_KEY_UNCLOSED_HTML_TAG,
325 result.getFirstNonTightHtmlTag().getText());
326 }
327 }
328 else {
329 final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
330 log(parseErrorMessage.getLineNumber(),
331 parseErrorMessage.getMessageKey(),
332 parseErrorMessage.getMessageArguments());
333 }
334 }
335 }
336
337
338
339
340
341
342 protected DetailAST getBlockCommentAst() {
343 return context.get().blockCommentAst;
344 }
345
346
347
348
349
350
351
352 private void processTree(DetailNode root) {
353 beginJavadocTree(root);
354 walk(root);
355 finishJavadocTree(root);
356 }
357
358
359
360
361
362
363
364 private void walk(DetailNode root) {
365 DetailNode curNode = root;
366 while (curNode != null) {
367 boolean waitsForProcessing = shouldBeProcessed(curNode);
368
369 if (waitsForProcessing) {
370 visitJavadocToken(curNode);
371 }
372 DetailNode toVisit = curNode.getFirstChild();
373 while (curNode != null && toVisit == null) {
374 if (waitsForProcessing) {
375 leaveJavadocToken(curNode);
376 }
377
378 toVisit = curNode.getNextSibling();
379 curNode = curNode.getParent();
380 if (curNode != null) {
381 waitsForProcessing = shouldBeProcessed(curNode);
382 }
383 }
384 curNode = toVisit;
385 }
386 }
387
388
389
390
391
392
393
394 private boolean shouldBeProcessed(DetailNode curNode) {
395 return javadocTokens.contains(curNode.getType());
396 }
397
398 @Override
399 public void destroy() {
400 super.destroy();
401 context.remove();
402 TREE_CACHE.remove();
403 }
404
405
406
407
408 private static final class FileContext {
409
410
411
412
413 private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
414
415
416
417
418
419 private DetailAST blockCommentAst;
420
421 }
422
423 }