blob: 53dc033f5f0e9768fc43820345708793f90634d5 [file] [log] [blame]
/*
* 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());
}
}
}