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