tools/javac/ApiCollector.java (219 lines of code) (raw):
/*
* Copyright 2000-2024 JetBrains s.r.o.
*
* Licensed 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.
*/
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.ElementScanner8;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static javax.lang.model.element.Modifier.*;
/**
* Collects API info from parsed elements and raw source code.
*/
public class ApiCollector {
private final ProcessingEnvironment processingEnv;
private Round round;
// Maps source files to hashes of qualified names of associated root elements, used to calculate total API hash.
private final Map<JavaFileObject, Integer> compilationUnitHashMap = new HashMap<>(); // Hash map of hashes!
private final Set<Element> encounteredSupertypes = new HashSet<>(), nonAnnotatedUnusedApiTypes = new HashSet<>();
private final Api.Module api = new Api.Module();
public ApiCollector(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
}
public List<String> getJava8CompilationUnitPaths() {
Path moduleInfo = Path.of("module-info.java");
return compilationUnitHashMap.keySet().stream().map(f -> {
Path p = Path.of(f.toUri()).toAbsolutePath();
return p.getFileName().equals(moduleInfo) ? null : p.toString();
}).filter(Objects::nonNull).collect(Collectors.toList());
}
public Api.Module collect(Round round) {
this.round = round;
// Validate usage of annotations
Set<? extends Element> serviceAnnotated = round.annotations.service == null ? Set.of() :
round.env.getElementsAnnotatedWith(round.annotations.service);
Set<? extends Element> providedAnnotated = round.annotations.provided == null ? Set.of() :
round.env.getElementsAnnotatedWith(round.annotations.provided);
Set<? extends Element> providesAnnotated = round.annotations.provides == null ? Set.of() :
round.env.getElementsAnnotatedWith(round.annotations.provides);
serviceAnnotated.forEach(e -> {
if (!providedAnnotated.contains(e)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Service also requires @Provided", e);
}
if (providesAnnotated.contains(e)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Service cannot be used with @Provides", e);
}
});
providedAnnotated.forEach(e -> {
if (e.getModifiers().contains(FINAL) || e.getModifiers().contains(SEALED)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Final/sealed type marked with @Provided", e);
}
});
Stream.concat(providedAnnotated.stream(), providesAnnotated.stream()).forEach(e -> {
if (e.getKind() != ElementKind.INTERFACE && e.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Non-class/interface marked with @Provided/@Provides", e);
}
});
// Collect API info from roots.
for (Element element : round.env.getRootElements()) {
JavaFileObject sourceFile = processingEnv.getElementUtils().getFileObjectOf(element);
if (sourceFile != null && sourceFile.getKind() == JavaFileObject.Kind.SOURCE) {
String name = element instanceof QualifiedNameable ?
((QualifiedNameable) element).getQualifiedName().toString() : element.toString();
// API hash should not depend on traversal order, so use XOR.
compilationUnitHashMap.merge(sourceFile, name.hashCode(), (a, b) -> a ^ b);
}
element.accept(scanner, api);
}
if (!round.env.processingOver()) return null;
// Check that non-annotated inheritable API types are actually used.
nonAnnotatedUnusedApiTypes.removeAll(encounteredSupertypes);
for (Element e : nonAnnotatedUnusedApiTypes) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"API types must ether be final, or annotated with @Service/@Provided/@Provides", e);
}
// Calculate hash from all source files on the last round.
try {
for (Map.Entry<JavaFileObject, Integer> e : compilationUnitHashMap.entrySet()) {
// Calculate hash the same way String does it, but converting all line breaks to LF.
int hash = 0;
CharSequence content = e.getKey().getCharContent(true);
for (int i = 0; i < content.length(); i++) {
char c = content.charAt(i);
if (c == '\r') {
c = '\n';
if (i < content.length() - 1 && content.charAt(i + 1) == '\n') i++;
}
hash = 31 * hash + c;
}
hash = 31 * e.getValue() + hash;
api.hash ^= hash; // Ignore order of source files.
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return api;
}
private final ElementScanner8<Void, Api> scanner = new ElementScanner8<>() {
@Override
public Void visitUnknown(Element e, Api api) { return null; }
@Override
public Void visitType(TypeElement e, Api api) {
if (api == null || isNonApi(e)) {
collectAllSupertypes(e.asType(), null);
return super.visitType(e, null);
}
Api.Type type = new Api.Type(api, processingEnv.getElementUtils().getBinaryName(e).toString());
type.modifiers.addAll(getModifiers(e));
type.kind = e.getKind();
collectAllSupertypes(e.asType(), type.supertypes);
type.typeParameters = getTypeParameters(e);
type.deprecation = getDeprecationStatus(e);
type.usage = getUsage(e);
if (type.usage == Api.Usage.NONE &&
!type.modifiers.contains(FINAL) &&
(e.getKind() == ElementKind.CLASS || e.getKind() == ElementKind.INTERFACE)) {
nonAnnotatedUnusedApiTypes.add(e);
}
super.visitType(e, type);
api.types.put(type, type);
return null;
}
@Override
public Void visitVariable(VariableElement e, Api api) {
if (api == null || isNonApi(e)) return null;
Api.Type parent = (Api.Type) api;
Api.Field field = new Api.Field(parent, e.getSimpleName().toString());
field.modifiers.addAll(getModifiers(e));
field.type = e.asType().toString();
field.constantValue = (Serializable) e.getConstantValue();
field.deprecation = getDeprecationStatus(e);
if (field.modifiers.contains(STATIC) && !field.modifiers.contains(FINAL)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Static API fields must be final", e);
}
parent.fields.put(field, field);
return null;
}
@Override
public Void visitExecutable(ExecutableElement e, Api api) {
if (api == null || isNonApi(e)) return null;
Api.Type parent = (Api.Type) api;
Api.Method method = new Api.Method(parent, e.getSimpleName().toString(),
e.getParameters().stream()
.map(VariableElement::asType)
.map(TypeMirror::toString)
.toArray(String[]::new));
method.modifiers.addAll(getModifiers(e));
method.returnType = e.getReturnType().toString();
method.thrownTypes = collectTypesToSet(e.getThrownTypes());
method.typeParameters = getTypeParameters(e);
method.deprecation = getDeprecationStatus(e);
method.extension = round.getExtensionName(e);
if (method.extension != null && (!parent.usage.inheritableByBackend ||
method.modifiers.contains(STATIC) || method.modifiers.contains(FINAL))) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Only methods intended to be inherited by the backend can be marked with @Extension", e);
}
parent.methods.put(method, method);
return null;
}
};
private static boolean isNonApi(Element e) {
Set<Modifier> mods = e.getModifiers();
return !mods.contains(PUBLIC) && !mods.contains(PROTECTED);
}
private static Api.TypeParameter[] getTypeParameters(Parameterizable e) {
return e.getTypeParameters().stream()
.map(t -> new Api.TypeParameter(t.getSimpleName().toString(), collectTypesToSet(t.getBounds())))
.toArray(Api.TypeParameter[]::new);
}
private static HashSet<String> collectTypesToSet(Collection<? extends TypeMirror> types) {
return types.stream().map(TypeMirror::toString).collect(Collectors.toCollection(HashSet::new));
}
private void collectAllSupertypes(TypeMirror t, Set<String> result) {
for (TypeMirror s : processingEnv.getTypeUtils().directSupertypes(t)) {
if (s.getKind() == TypeKind.DECLARED) {
encounteredSupertypes.add(((DeclaredType) s).asElement());
}
if (result != null) {
String name = s.toString();
if (!name.equals("java.lang.Object") && result.add(name)) {
collectAllSupertypes(s, result);
}
}
}
}
private static final Set<Modifier> ALLOWED_MODIFIERS = EnumSet.of(PUBLIC, PROTECTED, ABSTRACT, DEFAULT, STATIC, FINAL);
private Set<Modifier> getModifiers(Element e) {
Set<Modifier> set = e.getModifiers();
if (!ALLOWED_MODIFIERS.containsAll(set)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Only some modifiers are allowed in public API: " + ALLOWED_MODIFIERS, e);
}
return set;
}
private Api.Deprecation getDeprecationStatus(Element e) {
Deprecated a = e.getAnnotation(Deprecated.class);
return a == null ? Api.Deprecation.NONE : a.forRemoval() ? Api.Deprecation.FOR_REMOVAL : Api.Deprecation.DEPRECATED;
}
private Api.Usage getUsage(TypeElement e) {
AnnotationMirror service = null, provided = null, provides = null;
for (AnnotationMirror am : e.getAnnotationMirrors()) {
DeclaredType t = am.getAnnotationType();
if (round.annotations.service != null && t.equals(round.annotations.service.asType())) {
service = am;
} else if (round.annotations.provided != null && t.equals(round.annotations.provided.asType())) {
provided = am;
} else if (round.annotations.provides != null && t.equals(round.annotations.provides.asType())) {
provides = am;
}
}
if (service != null) {
return Api.Usage.SERVICE;
} else if (provided != null) {
if (provides != null) return Api.Usage.TWO_WAY;
else return Api.Usage.PROVIDED;
}
if (provides != null) return Api.Usage.PROVIDES;
return Api.Usage.NONE;
}
}