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.Collection;
24 import java.util.List;
25 import java.util.Set;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import com.puppycrawl.tools.checkstyle.StatelessCheck;
30 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31 import com.puppycrawl.tools.checkstyle.api.DetailAST;
32 import com.puppycrawl.tools.checkstyle.api.FileContents;
33 import com.puppycrawl.tools.checkstyle.api.Scope;
34 import com.puppycrawl.tools.checkstyle.api.TextBlock;
35 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
37 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
38 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
39 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153 @StatelessCheck
154 public class JavadocTypeCheck
155 extends AbstractCheck {
156
157
158
159
160
161 public static final String MSG_UNKNOWN_TAG = "javadoc.unknownTag";
162
163
164
165
166
167 public static final String MSG_TAG_FORMAT = "type.tagFormat";
168
169
170
171
172
173 public static final String MSG_MISSING_TAG = "type.missingTag";
174
175
176
177
178
179 public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
180
181
182
183
184
185 public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
186
187
188 private static final String OPEN_ANGLE_BRACKET = "<";
189
190
191 private static final String CLOSE_ANGLE_BRACKET = ">";
192
193
194 private static final String SPACE = " ";
195
196
197 private static final Pattern TYPE_NAME_IN_JAVADOC_TAG =
198 Pattern.compile("^<([^>]+)");
199
200
201 private static final Pattern TYPE_NAME_IN_JAVADOC_TAG_SPLITTER =
202 Pattern.compile("\\s+");
203
204
205 private Scope scope = Scope.PRIVATE;
206
207 private Scope excludeScope;
208
209 private Pattern authorFormat;
210
211 private Pattern versionFormat;
212
213
214
215
216 private boolean allowMissingParamTags;
217
218 private boolean allowUnknownTags;
219
220
221
222
223
224 private Set<String> allowedAnnotations = Set.of("Generated");
225
226
227
228
229
230
231
232 public void setScope(Scope scope) {
233 this.scope = scope;
234 }
235
236
237
238
239
240
241
242 public void setExcludeScope(Scope excludeScope) {
243 this.excludeScope = excludeScope;
244 }
245
246
247
248
249
250
251
252 public void setAuthorFormat(Pattern pattern) {
253 authorFormat = pattern;
254 }
255
256
257
258
259
260
261
262 public void setVersionFormat(Pattern pattern) {
263 versionFormat = pattern;
264 }
265
266
267
268
269
270
271
272
273 public void setAllowMissingParamTags(boolean flag) {
274 allowMissingParamTags = flag;
275 }
276
277
278
279
280
281
282
283 public void setAllowUnknownTags(boolean flag) {
284 allowUnknownTags = flag;
285 }
286
287
288
289
290
291
292
293
294 public void setAllowedAnnotations(String... userAnnotations) {
295 allowedAnnotations = Set.of(userAnnotations);
296 }
297
298 @Override
299 public int[] getDefaultTokens() {
300 return getAcceptableTokens();
301 }
302
303 @Override
304 public int[] getAcceptableTokens() {
305 return new int[] {
306 TokenTypes.INTERFACE_DEF,
307 TokenTypes.CLASS_DEF,
308 TokenTypes.ENUM_DEF,
309 TokenTypes.ANNOTATION_DEF,
310 TokenTypes.RECORD_DEF,
311 };
312 }
313
314 @Override
315 public int[] getRequiredTokens() {
316 return CommonUtil.EMPTY_INT_ARRAY;
317 }
318
319
320 @SuppressWarnings("deprecation")
321 @Override
322 public void visitToken(DetailAST ast) {
323 if (shouldCheck(ast)) {
324 final FileContents contents = getFileContents();
325 final int lineNo = ast.getLineNo();
326 final TextBlock textBlock = contents.getJavadocBefore(lineNo);
327 if (textBlock != null) {
328 final List<JavadocTag> tags = getJavadocTags(textBlock);
329 if (ScopeUtil.isOuterMostType(ast)) {
330
331 checkTag(ast, tags, JavadocTagInfo.AUTHOR.getName(),
332 authorFormat);
333 checkTag(ast, tags, JavadocTagInfo.VERSION.getName(),
334 versionFormat);
335 }
336
337 final List<String> typeParamNames =
338 CheckUtil.getTypeParameterNames(ast);
339 final List<String> recordComponentNames =
340 getRecordComponentNames(ast);
341
342 if (!allowMissingParamTags) {
343
344 typeParamNames.forEach(typeParamName -> {
345 checkTypeParamTag(ast, tags, typeParamName);
346 });
347
348 recordComponentNames.forEach(componentName -> {
349 checkComponentParamTag(ast, tags, componentName);
350 });
351 }
352
353 checkUnusedParamTags(tags, typeParamNames, recordComponentNames);
354 }
355 }
356 }
357
358
359
360
361
362
363
364 private boolean shouldCheck(DetailAST ast) {
365 final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
366
367 return surroundingScope.isIn(scope)
368 && (excludeScope == null || !surroundingScope.isIn(excludeScope))
369 && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations);
370 }
371
372
373
374
375
376
377
378 private List<JavadocTag> getJavadocTags(TextBlock textBlock) {
379 final JavadocTags tags = JavadocUtil.getJavadocTags(textBlock,
380 JavadocUtil.JavadocTagType.BLOCK);
381 if (!allowUnknownTags) {
382 tags.getInvalidTags().forEach(tag -> {
383 log(tag.getLine(), tag.getCol(), MSG_UNKNOWN_TAG, tag.getName());
384 });
385 }
386 return tags.getValidTags();
387 }
388
389
390
391
392
393
394
395
396
397 private void checkTag(DetailAST ast, Iterable<JavadocTag> tags, String tagName,
398 Pattern formatPattern) {
399 if (formatPattern != null) {
400 boolean hasTag = false;
401 final String tagPrefix = "@";
402
403 for (final JavadocTag tag :tags) {
404 if (tag.getTagName().equals(tagName)) {
405 hasTag = true;
406 if (!formatPattern.matcher(tag.getFirstArg()).find()) {
407 log(ast, MSG_TAG_FORMAT, tagPrefix + tagName, formatPattern.pattern());
408 }
409 }
410 }
411 if (!hasTag) {
412 log(ast, MSG_MISSING_TAG, tagPrefix + tagName);
413 }
414 }
415 }
416
417
418
419
420
421
422
423
424
425 private void checkComponentParamTag(DetailAST ast,
426 Collection<JavadocTag> tags,
427 String recordComponentName) {
428
429 final boolean found = tags
430 .stream()
431 .filter(JavadocTag::isParamTag)
432 .anyMatch(tag -> tag.getFirstArg().indexOf(recordComponentName) == 0);
433
434 if (!found) {
435 log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
436 + SPACE + recordComponentName);
437 }
438 }
439
440
441
442
443
444
445
446
447
448 private void checkTypeParamTag(DetailAST ast,
449 Collection<JavadocTag> tags, String typeParamName) {
450 final String typeParamNameWithBrackets =
451 OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET;
452
453 final boolean found = tags
454 .stream()
455 .filter(JavadocTag::isParamTag)
456 .anyMatch(tag -> tag.getFirstArg().indexOf(typeParamNameWithBrackets) == 0);
457
458 if (!found) {
459 log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
460 + SPACE + typeParamNameWithBrackets);
461 }
462 }
463
464
465
466
467
468
469
470
471 private void checkUnusedParamTags(
472 List<JavadocTag> tags,
473 List<String> typeParamNames,
474 List<String> recordComponentNames) {
475
476 for (final JavadocTag tag: tags) {
477 if (tag.isParamTag()) {
478 final String paramName = extractParamNameFromTag(tag);
479 final boolean found = typeParamNames.contains(paramName)
480 || recordComponentNames.contains(paramName);
481
482 if (!found) {
483 if (paramName.isEmpty()) {
484 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG_GENERAL);
485 }
486 else {
487 final String actualParamName =
488 TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0];
489 log(tag.getLineNo(), tag.getColumnNo(),
490 MSG_UNUSED_TAG,
491 JavadocTagInfo.PARAM.getText(), actualParamName);
492 }
493 }
494 }
495 }
496
497 }
498
499
500
501
502
503
504
505 private static String extractParamNameFromTag(JavadocTag tag) {
506 final String typeParamName;
507 final Matcher matchInAngleBrackets =
508 TYPE_NAME_IN_JAVADOC_TAG.matcher(tag.getFirstArg());
509 if (matchInAngleBrackets.find()) {
510 typeParamName = matchInAngleBrackets.group(1).trim();
511 }
512 else {
513 typeParamName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0];
514 }
515 return typeParamName;
516 }
517
518
519
520
521
522
523
524 private static List<String> getRecordComponentNames(DetailAST node) {
525 final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS);
526 final List<String> componentList = new ArrayList<>();
527
528 if (components != null) {
529 TokenUtil.forEachChild(components,
530 TokenTypes.RECORD_COMPONENT_DEF, component -> {
531 final DetailAST ident = component.findFirstToken(TokenTypes.IDENT);
532 componentList.add(ident.getText());
533 });
534 }
535
536 return componentList;
537 }
538 }