forked from opensearch-project/OpenSearch
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMissingDoclet.java
460 lines (417 loc) · 17.5 KB
/
MissingDoclet.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
package org.opensearch.missingdoclet;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import com.sun.source.doctree.DocCommentTree;
import com.sun.source.doctree.ParamTree;
import com.sun.source.util.DocTrees;
import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.doclet.Reporter;
import jdk.javadoc.doclet.StandardDoclet;
/**
* Checks for missing javadocs, where missing also means "only whitespace" or "license header".
* Has option --missing-level (package, class, method, parameter) so that we can improve over time.
* Has option --missing-ignore to ignore individual elements (such as split packages).
* It isn't recursive, just ignores exactly the elements you tell it.
* Has option --missing-method to apply "method" level to selected packages (fix one at a time).
* Matches package names exactly: so you'll need to list subpackages separately.
* <p>
* Note: This by default ignores javadoc validation on overridden methods.
*/
// Original version of this class is ported from MissingDoclet code in Lucene,
// which is under the Apache Software Foundation under Apache 2.0 license
// See - https://github.com/apache/lucene-solr/tree/master/dev-tools/missing-doclet
public class MissingDoclet extends StandardDoclet {
// checks that modules and packages have documentation
private static final int PACKAGE = 0;
// checks that classes, interfaces, enums, and annotation types have documentation
private static final int CLASS = 1;
// checks that methods, constructors, fields, and enumerated constants have documentation
private static final int METHOD = 2;
// checks that @param tags are present for any method/constructor parameters
private static final int PARAMETER = 3;
int level = PARAMETER;
Reporter reporter;
DocletEnvironment docEnv;
DocTrees docTrees;
Elements elementUtils;
Set<String> ignored = Collections.emptySet();
Set<String> methodPackages = Collections.emptySet();
@Override
public Set<Doclet.Option> getSupportedOptions() {
Set<Doclet.Option> options = new HashSet<>();
options.addAll(super.getSupportedOptions());
options.add(new Doclet.Option() {
@Override
public int getArgumentCount() {
return 1;
}
@Override
public String getDescription() {
return "level to enforce for missing javadocs: [package, class, method, parameter]";
}
@Override
public Kind getKind() {
return Option.Kind.STANDARD;
}
@Override
public List<String> getNames() {
return Collections.singletonList("--missing-level");
}
@Override
public String getParameters() {
return "level";
}
@Override
public boolean process(String option, List<String> arguments) {
switch (arguments.get(0)) {
case "package":
level = PACKAGE;
return true;
case "class":
level = CLASS;
return true;
case "method":
level = METHOD;
return true;
case "parameter":
level = PARAMETER;
return true;
default:
return false;
}
}
});
options.add(new Doclet.Option() {
@Override
public int getArgumentCount() {
return 1;
}
@Override
public String getDescription() {
return "comma separated list of element names to ignore (e.g. as a workaround for split packages)";
}
@Override
public Kind getKind() {
return Option.Kind.STANDARD;
}
@Override
public List<String> getNames() {
return Collections.singletonList("--missing-ignore");
}
@Override
public String getParameters() {
return "ignoredNames";
}
@Override
public boolean process(String option, List<String> arguments) {
ignored = new HashSet<>(Arrays.asList(arguments.get(0).split(",")));
return true;
}
});
options.add(new Doclet.Option() {
@Override
public int getArgumentCount() {
return 1;
}
@Override
public String getDescription() {
return "comma separated list of packages to check at 'method' level";
}
@Override
public Kind getKind() {
return Option.Kind.STANDARD;
}
@Override
public List<String> getNames() {
return Collections.singletonList("--missing-method");
}
@Override
public String getParameters() {
return "packages";
}
@Override
public boolean process(String option, List<String> arguments) {
methodPackages = new HashSet<>(Arrays.asList(arguments.get(0).split(",")));
return true;
}
});
return options;
}
@Override
public void init(Locale locale, Reporter reporter) {
this.reporter = reporter;
super.init(locale, reporter);
}
@Override
public boolean run(DocletEnvironment docEnv) {
this.docEnv = docEnv;
this.docTrees = docEnv.getDocTrees();
this.elementUtils = docEnv.getElementUtils();
for (var element : docEnv.getIncludedElements()) {
check(element);
}
return true;
}
/**
* Returns effective check level for this element
*/
private int level(Element element) {
String pkg = elementUtils.getPackageOf(element).getQualifiedName().toString();
if (methodPackages.contains(pkg)) {
return METHOD;
} else {
return level;
}
}
/**
* Check an individual element.
* This checks packages and types from the doctrees.
* It will recursively check methods/fields from encountered types when the level is "method"
*/
private void check(Element element) {
switch(element.getKind()) {
case MODULE:
// don't check the unnamed module, it won't have javadocs
if (!((ModuleElement)element).isUnnamed()) {
checkComment(element);
}
break;
case PACKAGE:
checkComment(element);
break;
// class-like elements, check them, then recursively check their children (fields and methods)
case CLASS:
case INTERFACE:
case ENUM:
case ANNOTATION_TYPE:
if (level(element) >= CLASS) {
checkComment(element);
for (var subElement : element.getEnclosedElements()) {
// don't recurse into enclosed types, otherwise we'll double-check since they are already in the included docTree
if (subElement.getKind() == ElementKind.METHOD ||
subElement.getKind() == ElementKind.CONSTRUCTOR ||
subElement.getKind() == ElementKind.FIELD ||
subElement.getKind() == ElementKind.ENUM_CONSTANT) {
check(subElement);
}
}
}
break;
// method-like elements, check them if we are configured to do so
case METHOD:
case CONSTRUCTOR:
case FIELD:
case ENUM_CONSTANT:
if (level(element) >= METHOD && !isSyntheticEnumMethod(element)) {
checkComment(element);
}
break;
default:
error(element, "I don't know how to analyze " + element.getKind() + " yet.");
}
}
/**
* Return true if the method is synthetic enum method (values/valueOf).
* According to the doctree documentation, the "included" set never includes synthetic elements.
* UweSays: It should not happen but it happens!
*/
private boolean isSyntheticEnumMethod(Element element) {
String simpleName = element.getSimpleName().toString();
if (simpleName.equals("values") || simpleName.equals("valueOf")) {
if (element.getEnclosingElement().getKind() == ElementKind.ENUM) {
return true;
}
}
return false;
}
/**
* Checks that an element doesn't have missing javadocs.
* In addition to truly "missing", check that comments aren't solely whitespace (generated by some IDEs)
*/
private void checkComment(Element element) {
// sanity check that the element is really "included", because we do some recursion into types
if (!docEnv.isIncluded(element)) {
return;
}
// check that this element isn't on our ignore list. This is only used as a workaround for "split packages".
// ignoring a package isn't recursive (on purpose), we still check all the classes, etc. inside it.
// we just need to cope with the fact package-info.java isn't there because it is split across multiple jars.
if (ignored.contains(element.toString())) {
return;
}
// Ignore classes annotated with @Generated and all enclosed elements in them.
if (isGenerated(element)) {
return;
}
Element enclosing = element.getEnclosingElement();
if (enclosing != null && isGenerated(enclosing)) {
return;
}
// If a package contains only generated classes, ignore the package as well.
if (element.getKind() == ElementKind.PACKAGE) {
List<? extends Element> enclosedElements = element.getEnclosedElements();
Optional<?> elm = enclosedElements.stream().findFirst().filter(e -> ((e.getKind() != ElementKind.CLASS) || !isGenerated(e)));
if (elm.isEmpty()) {
return;
}
}
var tree = docTrees.getDocCommentTree(element);
if (tree == null || tree.getFirstSentence().isEmpty()) {
// Check for methods that override other stuff and perhaps inherit their Javadocs.
if (hasInheritedJavadocs(element)) {
return;
} else {
error(element, "javadocs are missing");
}
} else {
var normalized = tree.getFirstSentence().get(0).toString()
.replace('\u00A0', ' ')
.trim()
.toLowerCase(Locale.ROOT);
if (normalized.isEmpty()) {
error(element, "blank javadoc comment");
}
}
if (level >= PARAMETER) {
checkParameters(element, tree);
}
}
// Ignore classes annotated with @Generated and all enclosed elements in them.
private boolean isGenerated(Element element) {
return element
.getAnnotationMirrors()
.stream()
.anyMatch(m -> m
.getAnnotationType()
.toString() /* ClassSymbol.toString() returns class name */
.equalsIgnoreCase("javax.annotation.Generated"));
}
private boolean hasInheritedJavadocs(Element element) {
boolean hasOverrides = element.getAnnotationMirrors().stream()
.anyMatch(ann -> ann.getAnnotationType().toString().equals(Override.class.getName()));
if (hasOverrides) {
// If an element has explicit @Overrides annotation, assume it does
// have inherited javadocs somewhere.
reporter.print(Diagnostic.Kind.NOTE, element, "javadoc empty but @Override declared, skipping.");
return true;
}
// Check for methods up the types tree.
if (element instanceof ExecutableElement) {
ExecutableElement thisMethod = (ExecutableElement) element;
Iterable<Element> superTypes =
() -> superTypeForInheritDoc(thisMethod.getEnclosingElement()).iterator();
for (Element sup : superTypes) {
for (ExecutableElement supMethod : ElementFilter.methodsIn(sup.getEnclosedElements())) {
TypeElement clazz = (TypeElement) thisMethod.getEnclosingElement();
if (elementUtils.overrides(thisMethod, supMethod, clazz)) {
// We could check supMethod for non-empty javadoc here. Don't know if this makes
// sense though as all methods will be verified in the end so it'd fail on the
// top of the hierarchy (if empty) anyway.
reporter.print(Diagnostic.Kind.NOTE, element, "javadoc empty but method overrides another, skipping.");
return true;
}
}
}
}
return false;
}
/* Find types from which methods in type may inherit javadoc, in the proper order.*/
private Stream<Element> superTypeForInheritDoc(Element type) {
TypeElement clazz = (TypeElement) type;
List<Element> interfaces = clazz.getInterfaces()
.stream()
.filter(tm -> tm.getKind() == TypeKind.DECLARED)
.map(tm -> ((DeclaredType) tm).asElement())
.collect(Collectors.toList());
Stream<Element> result = interfaces.stream();
result = Stream.concat(result, interfaces.stream().flatMap(this::superTypeForInheritDoc));
if (clazz.getSuperclass().getKind() == TypeKind.DECLARED) {
Element superClass = ((DeclaredType) clazz.getSuperclass()).asElement();
result = Stream.concat(result, Stream.of(superClass));
result = Stream.concat(result, superTypeForInheritDoc(superClass));
}
return result;
}
/** Checks there is a corresponding "param" tag for each method parameter */
private void checkParameters(Element element, DocCommentTree tree) {
if (element instanceof ExecutableElement) {
// record each @param that we see
Set<String> seenParameters = new HashSet<>();
if (tree != null) {
for (var tag : tree.getBlockTags()) {
if (tag instanceof ParamTree) {
var name = ((ParamTree)tag).getName().getName().toString();
seenParameters.add(name);
}
}
}
// now compare the method's formal parameter list against it
for (var param : ((ExecutableElement)element).getParameters()) {
var name = param.getSimpleName().toString();
if (!seenParameters.contains(name)) {
error(element, "missing javadoc @param for parameter '" + name + "'");
}
}
}
}
/** logs a new error for the particular element */
private void error(Element element, String message) {
var fullMessage = new StringBuilder();
switch (element.getKind()) {
case MODULE:
case PACKAGE:
// for modules/packages, we don't have filename + line number, fully qualify
fullMessage.append(element.toString());
break;
case METHOD:
case CONSTRUCTOR:
case FIELD:
case ENUM_CONSTANT:
// for method-like elements, include the enclosing type to make it easier
fullMessage.append(element.getEnclosingElement().getSimpleName());
fullMessage.append(".");
fullMessage.append(element.getSimpleName());
break;
default:
// for anything else, use a simple name
fullMessage.append(element.getSimpleName());
break;
}
fullMessage.append(" (");
fullMessage.append(element.getKind().toString().toLowerCase(Locale.ROOT));
fullMessage.append("): ");
fullMessage.append(message);
if (Runtime.version().feature() == 11 && element.getKind() == ElementKind.PACKAGE) {
// Avoid JDK 11 bug:
// https://bugs.openjdk.java.net/browse/JDK-8224082
reporter.print(Diagnostic.Kind.ERROR, fullMessage.toString());
} else {
reporter.print(Diagnostic.Kind.ERROR, element, fullMessage.toString());
}
}
}