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.ArrayList;
23 import java.util.LinkedHashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.regex.Pattern;
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.JavadocCommentsTokenTypes;
33 import com.puppycrawl.tools.checkstyle.api.Scope;
34 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
36 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
37 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
39 import com.puppycrawl.tools.checkstyle.utils.NullUtil;
40 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
41 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70 @FileStatefulCheck
71 public class JavadocTypeCheck extends AbstractJavadocCheck {
72
73
74
75
76
77 public static final String MSG_UNKNOWN_TAG = "javadoc.unknownTag";
78
79
80
81
82
83 public static final String MSG_TAG_FORMAT = "type.tagFormat";
84
85
86
87
88
89 public static final String MSG_MISSING_TAG = "type.missingTag";
90
91
92
93
94
95 public static final String MSG_MISSING_TAG_WITH_QUOTES =
96 "type.missingTagWithQuotes";
97
98
99
100
101
102 public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
103
104
105
106
107
108 public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
109
110
111 private static final String OPEN_ANGLE_BRACKET = "<";
112
113
114 private static final String CLOSE_ANGLE_BRACKET = ">";
115
116
117 private static final String AUTHOR_TAG_NAME = "@author";
118
119
120 private static final String VERSION_TAG_NAME = "@version";
121
122
123 private final Map<DetailNode, String> javadocTags = new LinkedHashMap<>();
124
125
126 private Scope scope = Scope.PRIVATE;
127
128 private Scope excludeScope;
129
130 private Pattern authorFormat;
131
132 private Pattern versionFormat;
133
134
135
136
137 private boolean allowMissingParamTags;
138
139 private boolean allowUnknownTags;
140
141
142
143
144
145 private Set<String> allowedAnnotations = Set.of("Generated");
146
147
148 private DetailAST currentAst;
149
150
151 private boolean authorTagIsPresent;
152
153
154 private boolean versionTagIsPresent;
155
156
157
158
159 public JavadocTypeCheck() {
160
161 }
162
163
164
165
166
167
168
169 public void setScope(Scope scope) {
170 this.scope = scope;
171 }
172
173
174
175
176
177
178
179 public void setExcludeScope(Scope excludeScope) {
180 this.excludeScope = excludeScope;
181 }
182
183
184
185
186
187
188
189 public void setAuthorFormat(Pattern pattern) {
190 authorFormat = pattern;
191 }
192
193
194
195
196
197
198
199 public void setVersionFormat(Pattern pattern) {
200 versionFormat = pattern;
201 }
202
203
204
205
206
207
208
209
210 public void setAllowMissingParamTags(boolean flag) {
211 allowMissingParamTags = flag;
212 }
213
214
215
216
217
218
219
220 public void setAllowUnknownTags(boolean flag) {
221 allowUnknownTags = flag;
222 }
223
224
225
226
227
228
229
230
231 public void setAllowedAnnotations(String... userAnnotations) {
232 allowedAnnotations = Set.of(userAnnotations);
233 }
234
235
236
237
238
239
240
241
242
243
244
245 @Override
246 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
247 super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
248 }
249
250 @Override
251 public void beginJavadocTree(DetailNode rootAst) {
252 javadocTags.clear();
253 authorTagIsPresent = false;
254 versionTagIsPresent = false;
255 }
256
257 @Override
258 public void finishJavadocTree(DetailNode rootAst) {
259 if (authorFormat != null && !authorTagIsPresent
260 && ScopeUtil.isOuterMostType(currentAst)) {
261 log(currentAst, MSG_MISSING_TAG, AUTHOR_TAG_NAME);
262 }
263 if (versionFormat != null && !versionTagIsPresent
264 && ScopeUtil.isOuterMostType(currentAst)) {
265 log(currentAst, MSG_MISSING_TAG, VERSION_TAG_NAME);
266 }
267 checkCollectedParamTags();
268 }
269
270 @Override
271 public int[] getDefaultJavadocTokens() {
272 return getRequiredJavadocTokens();
273 }
274
275 @Override
276 public int[] getRequiredJavadocTokens() {
277 return new int[] {
278 JavadocCommentsTokenTypes.PARAM_BLOCK_TAG,
279 JavadocCommentsTokenTypes.AUTHOR_BLOCK_TAG,
280 JavadocCommentsTokenTypes.VERSION_BLOCK_TAG,
281 JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG,
282 };
283 }
284
285 @Override
286 public void visitJavadocToken(DetailNode ast) {
287 switch (ast.getType()) {
288 case JavadocCommentsTokenTypes.PARAM_BLOCK_TAG -> collectParam(ast);
289 case JavadocCommentsTokenTypes.AUTHOR_BLOCK_TAG -> {
290 authorTagIsPresent = true;
291 checkTagFormat(ast, AUTHOR_TAG_NAME, authorFormat);
292 }
293 case JavadocCommentsTokenTypes.VERSION_BLOCK_TAG -> {
294 versionTagIsPresent = true;
295 checkTagFormat(ast, VERSION_TAG_NAME, versionFormat);
296 }
297 case JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG -> checkUnknownTag(ast);
298 default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
299 }
300 }
301
302 @Override
303 public int[] getDefaultTokens() {
304 return getAcceptableTokens();
305 }
306
307 @Override
308 public int[] getAcceptableTokens() {
309 return new int[] {
310 TokenTypes.INTERFACE_DEF,
311 TokenTypes.CLASS_DEF,
312 TokenTypes.ENUM_DEF,
313 TokenTypes.ANNOTATION_DEF,
314 TokenTypes.RECORD_DEF,
315 };
316 }
317
318 @Override
319 public int[] getRequiredTokens() {
320 return CommonUtil.EMPTY_INT_ARRAY;
321 }
322
323 @Override
324 public void visitToken(DetailAST ast) {
325 if (shouldCheck(ast)) {
326 final DetailAST blockCommentNode = JavadocUtil.getAttachedJavadocComment(ast);
327 if (blockCommentNode != null) {
328 currentAst = ast;
329 super.visitToken(blockCommentNode);
330 }
331 }
332 }
333
334
335
336
337
338
339
340 private boolean shouldCheck(DetailAST ast) {
341 return ScopeUtil.getSurroundingScope(ast)
342 .map(surroundingScope -> {
343 return surroundingScope.isIn(scope)
344 && (excludeScope == null || !surroundingScope.isIn(excludeScope))
345 && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations);
346 })
347 .orElse(Boolean.FALSE);
348 }
349
350
351
352
353
354
355 private void collectParam(DetailNode ast) {
356 final DetailNode parameterName = JavadocUtil.findFirstToken(
357 ast, JavadocCommentsTokenTypes.PARAMETER_NAME);
358 if (parameterName != null) {
359 javadocTags.put(ast, parameterName.getText());
360 }
361 else {
362 log(ast, MSG_UNUSED_TAG_GENERAL);
363 }
364 }
365
366
367
368
369
370
371 private void checkUnknownTag(DetailNode ast) {
372 if (!allowUnknownTags) {
373 final String tagName = JavadocUtil.findFirstToken(
374 ast, JavadocCommentsTokenTypes.TAG_NAME).getText();
375 log(ast, MSG_UNKNOWN_TAG, tagName);
376 }
377 }
378
379
380
381
382
383
384
385
386 private void checkTagFormat(DetailNode ast, String tagName, Pattern format) {
387 if (format != null && ScopeUtil.isOuterMostType(currentAst)) {
388 String description = "";
389 final DetailNode descriptionNode = JavadocUtil.findFirstToken(
390 ast, JavadocCommentsTokenTypes.DESCRIPTION);
391 if (descriptionNode != null) {
392 description = descriptionNode.getFirstChild().getText().trim();
393 }
394 if (!format.matcher(description).find()) {
395 log(currentAst, MSG_TAG_FORMAT, tagName, format.pattern());
396 }
397 }
398 }
399
400
401
402
403 private void checkCollectedParamTags() {
404 final List<String> params = getRecordComponentNames(currentAst);
405 final List<String> typeParamNames = CheckUtil.getTypeParameterNames(currentAst);
406
407 for (Map.Entry<DetailNode, String> tag : javadocTags.entrySet()) {
408 final String paramName = tag.getValue();
409 boolean found = params.remove(paramName);
410 if (paramName.startsWith(OPEN_ANGLE_BRACKET)) {
411 final String typeParamName = paramName.substring(1, paramName.length() - 1);
412 found = typeParamNames.remove(typeParamName);
413 }
414
415 if (!found) {
416 log(tag.getKey(), MSG_UNUSED_TAG, JavadocTagInfo.PARAM.getText(), paramName);
417 }
418 }
419
420 if (!allowMissingParamTags) {
421 params.forEach(paramName -> {
422 log(currentAst, MSG_MISSING_TAG_WITH_QUOTES,
423 JavadocTagInfo.PARAM.getText(), paramName);
424 });
425 typeParamNames.forEach(typeParamName -> {
426 log(currentAst, MSG_MISSING_TAG_WITH_QUOTES,
427 JavadocTagInfo.PARAM.getText(),
428 OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET);
429 });
430 }
431 }
432
433
434
435
436
437
438
439 private static List<String> getRecordComponentNames(DetailAST node) {
440 final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS);
441 final List<String> componentNames = new ArrayList<>();
442
443 if (components != null) {
444 TokenUtil.forEachChild(components,
445 TokenTypes.RECORD_COMPONENT_DEF, component -> {
446 final DetailAST ident =
447 NullUtil.notNull(component.findFirstToken(TokenTypes.IDENT));
448 componentNames.add(ident.getText());
449 });
450 }
451
452 return componentNames;
453 }
454
455 }