| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.lucene.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.AnnotationMirror; |
| 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. |
| * This should be removed when packaging is fixed to no longer be split across JARs. |
| * 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. |
| */ |
| 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 super.run(docEnv); |
| } |
| |
| /** |
| * 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), |
| * that they aren't a license header masquerading as a javadoc comment. |
| */ |
| 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; |
| } |
| 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"); |
| } else if (normalized.startsWith("licensed to the apache software foundation") || |
| normalized.startsWith("copyright 2004 the apache software foundation")) { |
| error(element, "comment is really a license"); |
| } |
| } |
| if (level >= PARAMETER) { |
| checkParameters(element, tree); |
| } |
| } |
| |
| 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://issues.apache.org/jira/browse/LUCENE-9747 |
| // https://bugs.openjdk.java.net/browse/JDK-8224082 |
| reporter.print(Diagnostic.Kind.ERROR, fullMessage.toString()); |
| } else { |
| reporter.print(Diagnostic.Kind.ERROR, element, fullMessage.toString()); |
| } |
| } |
| } |