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