diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java index 42f0e363f7b..45d998f2bc6 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java @@ -17,15 +17,10 @@ package io.helidon.codegen.apt; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.Elements; @@ -35,34 +30,11 @@ /** * Factory for annotations. */ +@SuppressWarnings("removal") final class AptAnnotationFactory { private AptAnnotationFactory() { } - /** - * Creates a set of annotations using annotation processor. - * - * @param annoMirrors the annotation type mirrors - * @param elements annotation processing element utils - * @return the annotation value set - */ - public static Set createAnnotations(List annoMirrors, Elements elements) { - return annoMirrors.stream() - .map(it -> createAnnotation(it, elements)) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - /** - * Creates a set of annotations based using annotation processor. - * - * @param type the enclosing/owing type element - * @param elements annotation processing element utils - * @return the annotation value set - */ - public static Set createAnnotations(Element type, Elements elements) { - return createAnnotations(type.getAnnotationMirrors(), elements); - } - /** * Creates an instance from an annotation mirror during annotation processing. * diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java index 4cd2555d5a7..b8c769a45fd 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContext.java @@ -16,16 +16,22 @@ package io.helidon.codegen.apt; +import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; import javax.annotation.processing.ProcessingEnvironment; import io.helidon.codegen.CodegenContext; import io.helidon.codegen.Option; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; /** * Annotation processing code generation context. + * @deprecated this API will be package local in the future, use through Helidon codegen only */ +@Deprecated(forRemoval = true, since = "4.1.0") public interface AptContext extends CodegenContext { /** * Create context from the processing environment, and a set of additional supported options. @@ -44,4 +50,14 @@ static AptContext create(ProcessingEnvironment env, Set> options) { * @return environment */ ProcessingEnvironment aptEnv(); + + /** + * Get a cached instance of the type info, and if not cached, cache the provided one. + * Only type infos known not to be modified during this build are cached. + * + * @param typeName type name + * @param typeInfoSupplier supplier of value if it is not yet cached + * @return type info for that name, in case the type info cannot be created, an empty optional + */ + Optional cache(TypeName typeName, Supplier> typeInfoSupplier); } diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java index 8ce02a3d4d8..b5247ed926c 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptContextImpl.java @@ -19,9 +19,12 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -40,11 +43,14 @@ import io.helidon.common.types.TypeName; import io.helidon.common.types.TypedElementInfo; +@SuppressWarnings("removal") class AptContextImpl extends CodegenContextBase implements AptContext { private static final Pattern SCOPE_PATTERN = Pattern.compile("(\\w+).*classes"); private final ProcessingEnvironment env; private final ModuleInfo moduleInfo; + private final Map> safeTypeCache = new HashMap<>(); + private final Map> typeCache = new HashMap<>(); AptContextImpl(ProcessingEnvironment env, CodegenOptions options, @@ -59,7 +65,7 @@ class AptContextImpl extends CodegenContextBase implements AptContext { this.moduleInfo = moduleInfo; } - static AptContext create(ProcessingEnvironment env, Set> supportedOptions) { + static AptContextImpl create(ProcessingEnvironment env, Set> supportedOptions) { CodegenOptions options = AptOptions.create(env); CodegenScope scope = guessScope(env, options); @@ -81,11 +87,16 @@ public ProcessingEnvironment aptEnv() { @Override public Optional typeInfo(TypeName typeName) { + if (typeCache.containsKey(typeName)) { + return typeCache.get(typeName); + } + // cached by the factory return AptTypeInfoFactory.create(this, typeName); } @Override public Optional typeInfo(TypeName typeName, Predicate elementPredicate) { + // cannot be cached return AptTypeInfoFactory.create(this, typeName, elementPredicate); } @@ -94,6 +105,39 @@ public Optional module() { return Optional.ofNullable(moduleInfo); } + @Override + public Optional cache(TypeName typeName, Supplier> typeInfoSupplier) { + if (typeName.generic() || !typeName.typeArguments().isEmpty() || !typeName.typeParameters().isEmpty()) { + // generic types cannot be cached + return typeInfoSupplier.get(); + } + + if (typeName.packageName().startsWith("java.") + || typeName.packageName().startsWith("javax.") + || typeName.packageName().startsWith("sun.") + || typeName.packageName().startsWith("com.sun")) { + Optional typeInfo = safeTypeCache.get(typeName); + if (typeInfo != null) { + return typeInfo; + } + typeInfo = typeInfoSupplier.get(); + safeTypeCache.put(typeName, typeInfo); + return typeInfo; + } + + Optional typeInfo = typeCache.get(typeName); + if (typeInfo != null) { + return typeInfo; + } + typeInfo = typeInfoSupplier.get(); + typeCache.put(typeName, typeInfo); + return typeInfo; + } + + void resetCache() { + typeCache.clear(); + } + private static Optional findModule(Filer filer) { // expected is source location try { diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java index 0d95a814394..af10b1337d5 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptProcessor.java @@ -17,12 +17,12 @@ package io.helidon.codegen.apt; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; @@ -35,7 +35,9 @@ import io.helidon.codegen.Codegen; import io.helidon.codegen.CodegenEvent; +import io.helidon.codegen.CodegenException; import io.helidon.codegen.Option; +import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; @@ -45,10 +47,11 @@ /** * Annotation processor that maps APT types to Helidon types, and invokes {@link io.helidon.codegen.Codegen}. */ +@SuppressWarnings("removal") public final class AptProcessor extends AbstractProcessor { private static final TypeName GENERATOR = TypeName.create(AptProcessor.class); - private AptContext ctx; + private AptContextImpl ctx; private Codegen codegen; /** @@ -66,13 +69,8 @@ public SourceVersion getSupportedSourceVersion() { @Override public Set getSupportedAnnotationTypes() { - return Stream.concat(codegen.supportedAnnotations() - .stream() - .map(TypeName::fqName), - codegen.supportedAnnotationPackagePrefixes() - .stream() - .map(it -> it + "*")) - .collect(Collectors.toSet()); + // we need to support all annotations, to be able to use meta-annotations + return Set.of("*"); } @Override @@ -87,12 +85,14 @@ public Set getSupportedOptions() { public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - this.ctx = AptContext.create(processingEnv, Codegen.supportedOptions()); + this.ctx = AptContextImpl.create(processingEnv, Codegen.supportedOptions()); this.codegen = Codegen.create(ctx, GENERATOR); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { + this.ctx.resetCache(); + Thread thread = Thread.currentThread(); ClassLoader previousClassloader = thread.getContextClassLoader(); thread.setContextClassLoader(AptProcessor.class.getClassLoader()); @@ -102,6 +102,19 @@ public boolean process(Set annotations, RoundEnvironment try { doProcess(annotations, roundEnv); return true; + } catch (CodegenException e) { + Object originatingElement = e.originatingElement() + .orElse(null); + if (originatingElement instanceof Element element) { + processingEnv.getMessager().printError(e.getMessage(), element); + } else if (originatingElement instanceof TypeName typeName) { + processingEnv.getMessager().printError(e.getMessage() + ", source: " + typeName.fqName()); + } else { + if (originatingElement != null) { + processingEnv.getMessager().printError(e.getMessage() + ", source: " + originatingElement); + } + } + throw e; } finally { thread.setContextClassLoader(previousClassloader); } @@ -115,32 +128,96 @@ private void doProcess(Set annotations, RoundEnvironment return; } - if (annotations.isEmpty()) { + Set usedAnnotations = usedAnnotations(annotations); + + if (usedAnnotations.isEmpty()) { // no annotations, no types, still call the codegen, maybe it has something to do codegen.process(List.of()); return; } - List allTypes = discoverTypes(annotations, roundEnv); + List allTypes = discoverTypes(usedAnnotations, roundEnv); codegen.process(allTypes); } - private List discoverTypes(Set annotations, RoundEnvironment roundEnv) { + private Set usedAnnotations(Set annotations) { + var exactTypes = codegen.supportedAnnotations() + .stream() + .map(TypeName::fqName) + .collect(Collectors.toSet()); + var prefixes = codegen.supportedAnnotationPackagePrefixes(); + + Set result = new HashSet<>(); + + for (TypeElement annotation : annotations) { + TypeName typeName = TypeName.create(annotation.getQualifiedName().toString()); + + /* + find meta annotations that are supported: + - annotation that annotates the current annotation + */ + Set supportedAnnotations = new HashSet<>(); + if (supportedAnnotation(exactTypes, prefixes, typeName)) { + supportedAnnotations.add(typeName); + } + addSupportedAnnotations(exactTypes, prefixes, supportedAnnotations, typeName); + if (!supportedAnnotations.isEmpty()) { + result.add(new UsedAnnotation(typeName, annotation, supportedAnnotations)); + } + } + + return result; + } + + private boolean supportedAnnotation(Set exactTypes, Set prefixes, TypeName annotationType) { + if (exactTypes.contains(annotationType.fqName())) { + return true; + } + String packagePrefix = annotationType.packageName() + "."; + for (String prefix : prefixes) { + if (packagePrefix.startsWith(prefix)) { + return true; + } + } + return false; + } + + private void addSupportedAnnotations(Set exactTypes, + Set prefixes, + Set supportedAnnotations, + TypeName annotationType) { + Optional foundInfo = AptTypeInfoFactory.create(ctx, annotationType); + if (foundInfo.isPresent()) { + TypeInfo annotationInfo = foundInfo.get(); + List annotations = annotationInfo.annotations(); + for (Annotation annotation : annotations) { + TypeName typeName = annotation.typeName(); + if (supportedAnnotation(exactTypes, prefixes, typeName)) { + if (supportedAnnotations.add(typeName)) { + addSupportedAnnotations(exactTypes, prefixes, supportedAnnotations, typeName); + } + } + } + } + } + + private List discoverTypes(Set annotations, RoundEnvironment roundEnv) { // we must discover all types that should be handled, create TypeInfo and only then check if these should be processed // as we may replace annotations, elements, and whole types. // first collect all types (group by type name, so we do not have duplicity) Map types = new HashMap<>(); - for (TypeElement annotation : annotations) { - Set elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(annotation); + for (UsedAnnotation annotation : annotations) { + TypeElement annotationElement = annotation.annotationElement(); + Set elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(annotationElement); for (Element element : elementsAnnotatedWith) { ElementKind kind = element.getKind(); switch (kind) { - case ENUM, INTERFACE, CLASS, ANNOTATION_TYPE, RECORD -> addType(types, element, element, annotation); + case ENUM, INTERFACE, CLASS, ANNOTATION_TYPE, RECORD -> addType(types, element, element, annotationElement); case ENUM_CONSTANT, CONSTRUCTOR, METHOD, FIELD, STATIC_INIT, INSTANCE_INIT, RECORD_COMPONENT -> - addType(types, element.getEnclosingElement(), element, annotation); - case PARAMETER -> addType(types, element.getEnclosingElement().getEnclosingElement(), element, annotation); + addType(types, element.getEnclosingElement(), element, annotationElement); + case PARAMETER -> addType(types, element.getEnclosingElement().getEnclosingElement(), element, annotationElement); default -> ctx.logger().log(TRACE, "Ignoring annotated element, not supported: " + element + ", kind: " + kind); } } @@ -177,4 +254,16 @@ private void addType(Map types, processedElement); } } + + /** + * Annotation that annotates a processed type and that must be processed. + * + * @param annotationType annotation on processed type + * @param annotationElement element of the annotation + * @param supportedAnnotations annotations that are supported (either the actual annotation, or meta-annotations) + */ + private record UsedAnnotation(TypeName annotationType, + TypeElement annotationElement, + Set supportedAnnotations) { + } } diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java index f752bc8b0c8..19248d1600f 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java @@ -17,9 +17,11 @@ package io.helidon.codegen.apt; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.lang.model.element.Element; @@ -40,8 +42,12 @@ /** * Factory for types. + * + * @deprecated this is intended for internal use. This will be a package private type in the future. */ +@Deprecated(forRemoval = true, since = "4.2.0") public final class AptTypeFactory { + private static final Pattern NESTED_TYPES = Pattern.compile("(? createTypeName(Element type) { List classNames = new ArrayList<>(); String simpleName = type.getSimpleName().toString(); + // if there is a single $, we consider that to be an inner class + String[] split = NESTED_TYPES.split(simpleName); + if (split.length > 1) { + classNames.addAll(Arrays.asList(split) + .subList(0, split.length - 1)); + simpleName = split[split.length - 1]; + } + Element enclosing = type.getEnclosingElement(); while (enclosing != null && ElementKind.PACKAGE != enclosing.getKind()) { if (enclosing.getKind() == ElementKind.CLASS @@ -182,6 +196,7 @@ public static Optional createTypeName(Element type) { } enclosing = enclosing.getEnclosingElement(); } + Collections.reverse(classNames); // try to find the package diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java index d044bc99704..70146921928 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java @@ -64,7 +64,13 @@ /** * Factory to analyze processed types and to provide {@link io.helidon.common.types.TypeInfo} for them. + * + * @deprecated this is an internal API, all usage should be done through {@code helidon-codegen} APIs, + * such as {@link io.helidon.codegen.CodegenContext#typeInfo(io.helidon.common.types.TypeName)}; + * this type will be package local in the future */ +@SuppressWarnings("removal") +@Deprecated(forRemoval = true) public final class AptTypeInfoFactory extends TypeInfoFactoryBase { // we expect that annotations themselves are not code generated, and can be cached @@ -116,7 +122,10 @@ public static Optional create(AptContext ctx, * @return type info for the type element * @throws IllegalArgumentException when the element cannot be resolved into type info (such as if you ask for * a primitive type) + * @deprecated this is an internal API, all usage should be done through {@code helidon-codegen} APIs, + * such as {@link io.helidon.codegen.CodegenContext#typeInfo(io.helidon.common.types.TypeName)} */ + @Deprecated(forRemoval = true) public static Optional create(AptContext ctx, TypeElement typeElement) { @@ -177,7 +186,8 @@ public static Optional createTypedElementInfoFromElement(AptCo if (v instanceof ExecutableElement ee) { typeMirror = Objects.requireNonNull(ee.getReturnType()); - params = ee.getParameters().stream() + params = ee.getParameters() + .stream() .map(it -> createTypedElementInfoFromElement(ctx, processedType, it, elements).orElseThrow(() -> { return new CodegenException("Failed to create element info for parameter: " + it + ", either it uses " + "invalid type, or it was removed by an element mapper. This would" @@ -390,6 +400,19 @@ private static Optional create(AptContext ctx, // Object is not to be analyzed return Optional.empty(); } + + if (elementPredicate == ElementInfoPredicates.ALL_PREDICATE) { + // we can safely cache + return ctx.cache(typeName, () -> createUncached(ctx, typeElement, elementPredicate, typeName)); + } + + return createUncached(ctx, typeElement, elementPredicate, typeName); + } + + private static Optional createUncached(AptContext ctx, + TypeElement typeElement, + Predicate elementPredicate, + TypeName typeName) { TypeName genericTypeName = typeName.genericTypeName(); Set allInterestingTypeNames = new LinkedHashSet<>(); allInterestingTypeNames.add(genericTypeName); @@ -402,8 +425,13 @@ private static Optional create(AptContext ctx, Elements elementUtils = ctx.aptEnv().getElementUtils(); try { + TypeElement foundType = elementUtils.getTypeElement(genericTypeName.resolvedName()); + if (foundType == null) { + // this is probably forward referencing a generated type, ignore + return Optional.empty(); + } List annotations = createAnnotations(ctx, - elementUtils.getTypeElement(genericTypeName.resolvedName()), + foundType, elementUtils); List inheritedAnnotations = createInheritedAnnotations(ctx, genericTypeName, annotations); @@ -417,22 +445,11 @@ private static Optional create(AptContext ctx, typeElement.getEnclosedElements() .stream() .flatMap(it -> createTypedElementInfoFromElement(ctx, genericTypeName, it, elementUtils).stream()) - .forEach(it -> { - if (elementPredicate.test(it)) { - elementsWeCareAbout.add(it); - } else { - otherElements.add(it); - } - annotationsOnTypeOrElements.addAll(it.annotations() - .stream() - .map(Annotation::typeName) - .collect(Collectors.toSet())); - it.parameterArguments() - .forEach(arg -> annotationsOnTypeOrElements.addAll(arg.annotations() - .stream() - .map(Annotation::typeName) - .collect(Collectors.toSet()))); - }); + .forEach(it -> collectEnclosedElements(elementPredicate, + elementsWeCareAbout, + otherElements, + annotationsOnTypeOrElements, + it)); Set modifiers = toModifierNames(typeElement.getModifiers()); TypeInfo.Builder builder = TypeInfo.builder() @@ -530,6 +547,27 @@ private static Optional create(AptContext ctx, } } + private static void collectEnclosedElements(Predicate elementPredicate, + List elementsWeCareAbout, + List otherElements, + Set annotationsOnTypeOrElements, + TypedElementInfo enclosedElement) { + if (elementPredicate.test(enclosedElement)) { + elementsWeCareAbout.add(enclosedElement); + } else { + otherElements.add(enclosedElement); + } + annotationsOnTypeOrElements.addAll(enclosedElement.annotations() + .stream() + .map(Annotation::typeName) + .collect(Collectors.toSet())); + enclosedElement.parameterArguments() + .forEach(arg -> annotationsOnTypeOrElements.addAll(arg.annotations() + .stream() + .map(Annotation::typeName) + .collect(Collectors.toSet()))); + } + private static AccessModifier accessModifier(Set stringModifiers) { for (String stringModifier : stringModifiers) { try { diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java index 58b513935c3..6430c3566e9 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java @@ -84,7 +84,7 @@ static void addCreateElement(ContentBuilder contentBuilder, TypedElementInfo Set modifiers = element.elementModifiers(); for (Modifier modifier : modifiers) { - contentBuilder.addContent(".addModifier(") + contentBuilder.addContent(".addElementModifier(") .addContent(MODIFIER) .addContent(".") .addContent(modifier.name()) diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java index 611ce2b5c02..3f1b3f2ae26 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Field.java @@ -33,12 +33,14 @@ public final class Field extends AnnotatedComponent { private final Content defaultValue; private final boolean isFinal; private final boolean isStatic; + private final boolean isVolatile; private Field(Builder builder) { super(builder); this.defaultValue = builder.defaultValueBuilder.build(); this.isFinal = builder.isFinal; this.isStatic = builder.isStatic; + this.isVolatile = builder.isVolatile; } /** @@ -72,6 +74,9 @@ void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrgani if (isFinal) { writer.write("final "); } + if (isVolatile) { + writer.write("volatile "); + } } type().writeComponent(writer, declaredTokens, imports, classType); writer.write(" "); @@ -136,6 +141,7 @@ public static final class Builder extends AnnotatedComponent.Builder annotatedTypes(List allTypes) { List result = new ArrayList<>(); for (TypeInfo typeInfo : allTypes) { - result.add(new TypeInfoAndAnnotations(typeInfo, annotations(typeInfo))); + result.add(new TypeInfoAndAnnotations(typeInfo, TypeHierarchy.nestedAnnotations(ctx, typeInfo))); } return result; } @@ -271,41 +269,12 @@ private RoundContextImpl createRoundContext(List annotat } return new RoundContextImpl( + ctx, Set.copyOf(extAnnots), Map.copyOf(extAnnotToType), List.copyOf(extTypes.values())); } - private Set annotations(TypeInfo theTypeInfo) { - Set result = new HashSet<>(); - - // on type - theTypeInfo.annotations() - .stream() - .map(Annotation::typeName) - .forEach(result::add); - - // on fields, methods etc. - theTypeInfo.elementInfo() - .stream() - .map(TypedElementInfo::annotations) - .flatMap(List::stream) - .map(Annotation::typeName) - .forEach(result::add); - - // on parameters - theTypeInfo.elementInfo() - .stream() - .map(TypedElementInfo::parameterArguments) - .flatMap(List::stream) - .map(TypedElementInfo::annotations) - .flatMap(List::stream) - .map(Annotation::typeName) - .forEach(result::add); - - return result; - } - private record TypeInfoAndAnnotations(TypeInfo typeInfo, Set annotations) { } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java index 89b240254d0..aadcecaf8c0 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContext.java @@ -44,7 +44,8 @@ public interface RoundContext { Collection types(); /** - * All types annotated with a specific annotation. + * All types annotated with a specific annotation (including types that inherit such annotation from super types or + * through interfaces). * * @param annotationType annotation to check * @return types that contain the annotation diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java index e19ca8a735e..1e765544a71 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/RoundContextImpl.java @@ -33,12 +33,14 @@ class RoundContextImpl implements RoundContext { private final Map newTypes = new HashMap<>(); private final Map> annotationToTypes; private final List types; + private final CodegenContext ctx; private final Collection annotations; - RoundContextImpl(Set annotations, + RoundContextImpl(CodegenContext ctx, + Set annotations, Map> annotationToTypes, List types) { - + this.ctx = ctx; this.annotations = annotations; this.annotationToTypes = annotationToTypes; this.types = types; @@ -83,7 +85,9 @@ public Collection annotatedTypes(TypeName annotationType) { List result = new ArrayList<>(); for (TypeInfo typeInfo : typeInfos) { - if (typeInfo.hasAnnotation(annotationType)) { + if (typeInfo.hasAnnotation(annotationType) || TypeHierarchy.hierarchyAnnotations(ctx, typeInfo) + .stream() + .anyMatch(it -> it.typeName().equals(annotationType))) { result.add(typeInfo); } } diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/TypeHierarchy.java b/codegen/codegen/src/main/java/io/helidon/codegen/TypeHierarchy.java new file mode 100644 index 00000000000..0afbd359520 --- /dev/null +++ b/codegen/codegen/src/main/java/io/helidon/codegen/TypeHierarchy.java @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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. + */ + +package io.helidon.codegen; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.ElementKind; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; + +import static io.helidon.common.types.TypeNames.INHERITED; +import static java.util.function.Predicate.not; + +/** + * Utilities for type hierarchy. + */ +public final class TypeHierarchy { + private TypeHierarchy() { + } + + /** + * Find all annotations on the whole type hierarchy. + * Adds all annotations on the provided type, and all + * {@link java.lang.annotation.Inherited} annotations on supertype(s) and/or interface(s). + * + * @param ctx codegen context + * @param type type info to process + * @return all annotations on the type and in its hierarchy + */ + public static List hierarchyAnnotations(CodegenContext ctx, TypeInfo type) { + Map annotations = new LinkedHashMap<>(); + + // this type + type.annotations().forEach(annot -> annotations.put(annot.typeName(), annot)); + // inherited from supertype + type.superTypeInfo().ifPresent(it -> { + it.inheritedAnnotations() + .forEach(annot -> annotations.putIfAbsent(annot.typeName(), annot)); + }); + // and from interfaces (in order of implementation) + for (TypeInfo typeInfo : type.interfaceTypeInfo()) { + typeInfo.annotations() + .stream() + .filter(annot -> typeInfo.hasMetaAnnotation(annot.typeName(), INHERITED)) + .forEach(annot -> annotations.putIfAbsent(annot.typeName(), annot)); + } + + Set processedTypes = new HashSet<>(annotations.keySet()); + + // now we have a full list of annotations that are explicitly written in sources, now collect meta-annotations + // i.e. all annotations on the annotations we have that have @Inherited placed on them + processMetaAnnotations(ctx, processedTypes, annotations); + + return List.copyOf(annotations.values()); + } + + /** + * Find all annotations on the whole type hierarchy. + * Adds all annotations on the provided element, and all + * {@link java.lang.annotation.Inherited} annotations on the same element from supertype(s), + * and/or interfaces, and/or annotations. + *

+ * Based on element type: + *

    + *
  • Constructor: only uses annotations from the current element
  • + *
  • Constructor parameter: ditto
  • + *
  • Method: uses annotations from the current element, and from the overridden method/interface method
  • + *
  • Method parameter: use + * {@link #hierarchyAnnotations(CodegenContext, + * io.helidon.common.types.TypeInfo, + * io.helidon.common.types.TypedElementInfo, + * io.helidon.common.types.TypedElementInfo, + * int)} instead
  • + *
  • Field: only uses annotations from the current element
  • + *
+ * If the same annotation is on multiple levels (i.e. method, super type method, and interface), it will always be used + * ONLY from the "closest" type - order is: this element, super type element, interface element. + * + * @param ctx codegen context + * @param type type info owning the executable + * @param element executable (method or constructor) element info + * @return all annotations on the type and in its hierarchy + */ + public static List hierarchyAnnotations(CodegenContext ctx, TypeInfo type, TypedElementInfo element) { + if (element.kind() != ElementKind.METHOD) { + return element.annotations(); + } + + // find the same method on supertype/interfaces + List prototypes = new ArrayList<>(); + Set processedTypes = new HashSet<>(); + String packageName = type.typeName().packageName(); + // extends + type.superTypeInfo().ifPresent(it -> collectInheritedMethods( + processedTypes, + prototypes, + it, + element, + packageName)); + // implements + type.interfaceTypeInfo().forEach(it -> collectInheritedMethods( + processedTypes, + prototypes, + it, + element, + packageName)); + + // we have collected all methods in the hierarchy, let's collect their annotations + Map annotations = new LinkedHashMap<>(); + // this type + element.annotations().forEach(annot -> annotations.put(annot.typeName(), annot)); + // inherited from supertype(s) and interface(s) + for (TypedElementInfo prototype : prototypes) { + prototype.annotations().forEach(annot -> annotations.putIfAbsent(annot.typeName(), annot)); + } + + // now we have a full list of annotations that are explicitly written in sources, now collect meta-annotations + // i.e. all annotations on the annotations we have that have @Inherited placed on them + processMetaAnnotations(ctx, processedTypes, annotations); + + return List.copyOf(annotations.values()); + } + + /** + * Annotations of a parameter, taken from the full inheritance hierarchy (super type(s), interface(s). + * + * @param ctx codegen context to obtain {@link io.helidon.common.types.TypeInfo} of types + * @param type type info of the processed type + * @param executable owner of the parameter (constructor or method) + * @param parameter parameter info itself + * @param parameterIndex index of the parameter within the method (as names may be wrong at runtime) + * @return list of annotations on this parameter on this type, super type(s), and interface methods it implements + */ + public static List hierarchyAnnotations(CodegenContext ctx, + TypeInfo type, + TypedElementInfo executable, + TypedElementInfo parameter, + int parameterIndex) { + if (parameter.kind() != ElementKind.PARAMETER) { + throw new CodegenException("This method only supports processing of parameter, yet kind is: " + parameter.kind()); + } + if (!(executable.kind() == ElementKind.CONSTRUCTOR || executable.kind() == ElementKind.METHOD)) { + throw new CodegenException("This method only supports processing of parameters of methods or constructors, yet " + + "executable kind is: " + executable.kind()); + } + if (executable.kind() == ElementKind.CONSTRUCTOR) { + // constructor parameters are not inherited + return parameter.annotations(); + } + + // find the same method on supertype/interfaces + List prototypes = new ArrayList<>(); + Set processedTypes = new HashSet<>(); + String packageName = type.typeName().packageName(); + // extends + type.superTypeInfo().ifPresent(it -> collectInheritedMethods( + processedTypes, + prototypes, + it, + executable, + packageName)); + // implements + type.interfaceTypeInfo().forEach(it -> collectInheritedMethods( + processedTypes, + prototypes, + it, + executable, + packageName)); + + // we have collected all methods in the hierarchy, let's collect their annotations + Map annotations = new LinkedHashMap<>(); + // this type + parameter.annotations().forEach(annot -> annotations.put(annot.typeName(), annot)); + // inherited from supertype(s) and interface(s) + for (TypedElementInfo prototype : prototypes) { + prototype.parameterArguments() + .get(parameterIndex) + .annotations() + .forEach(annot -> annotations.putIfAbsent(annot.typeName(), annot)); + } + + // now we have a full list of annotations that are explicitly written in sources, now collect meta-annotations + // i.e. all annotations on the annotations we have that have @Inherited placed on them + processMetaAnnotations(ctx, processedTypes, annotations); + + return List.copyOf(annotations.values()); + + } + + /** + * Annotations on the {@code typeInfo}, it's methods, and method parameters. + * + * @param ctx context + * @param typeInfo type info to check + * @return a set of all annotation types on any of the elements, including inherited annotations + */ + public static Set nestedAnnotations(CodegenContext ctx, TypeInfo typeInfo) { + Set result = new HashSet<>(); + + // on type + typeInfo.annotations() + .stream() + .map(Annotation::typeName) + .forEach(result::add); + typeInfo.inheritedAnnotations() + .stream() + .map(Annotation::typeName) + .forEach(result::add); + + // on fields, methods etc. + typeInfo.elementInfo() + .stream() + .map(TypedElementInfo::annotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + typeInfo.elementInfo() + .stream() + .map(TypedElementInfo::inheritedAnnotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + // on parameters + typeInfo.elementInfo() + .stream() + .map(TypedElementInfo::parameterArguments) + .flatMap(List::stream) + .map(TypedElementInfo::annotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + typeInfo.elementInfo() + .stream() + .map(TypedElementInfo::parameterArguments) + .flatMap(List::stream) + .map(TypedElementInfo::inheritedAnnotations) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + return result; + /* + Set result = new HashSet<>(); + + // on type + hierarchyAnnotations(ctx, typeInfo) + .stream() + .map(Annotation::typeName) + .forEach(result::add); + + // on fields, methods etc. + typeInfo.elementInfo() + .stream() + .map(it -> hierarchyAnnotations(ctx, typeInfo, it)) + .flatMap(List::stream) + .map(Annotation::typeName) + .forEach(result::add); + + // on parameters + typeInfo.elementInfo() + .stream() + .forEach(it -> { + int index = 0; + for (var param : it.parameterArguments()) { + hierarchyAnnotations(ctx, typeInfo, it, param, index++) + .stream() + .map(Annotation::typeName) + .forEach(result::add); + } + }); + + return result; + */ + } + + private static void processMetaAnnotations(CodegenContext ctx, + Set processedTypes, + Map annotations) { + + List newAnnotations = new ArrayList<>(); + + for (Annotation value : annotations.values()) { + Optional typeInfo = ctx.typeInfo(value.typeName()); + if (typeInfo.isPresent()) { + // we can handle only annotations on classpath, all others are just ignored + TypeInfo annotationInfo = typeInfo.get(); + + annotationInfo + .annotations() + .forEach(metaAnnotation -> { + collectMetaAnnotations(ctx, processedTypes, newAnnotations, metaAnnotation); + }); + } + } + + newAnnotations.forEach(it -> annotations.putIfAbsent(it.typeName(), it)); + } + + private static void collectMetaAnnotations(CodegenContext ctx, + Set processedTypes, + List metaAnnotations, + Annotation annotation) { + if (!processedTypes.add(annotation.typeName())) { + // this annotation was already processed + return; + } + Optional typeInfo = ctx.typeInfo(annotation.typeName()); + if (typeInfo.isEmpty()) { + return; + } + TypeInfo annotationInfo = typeInfo.get(); + if (annotationInfo.hasAnnotation(INHERITED)) { + metaAnnotations.add(annotation); + } + // and check all annotations of this annotation + annotationInfo.annotations() + .forEach(metaAnnotation -> collectMetaAnnotations(ctx, processedTypes, metaAnnotations, metaAnnotation)); + } + + private static void collectInheritedMethods(Set processed, + List collected, + TypeInfo type, + TypedElementInfo method, + String currentPackage) { + if (!processed.add(type.typeName())) { + // already handled this type + return; + } + + inherited( + type, + method, + method.parameterArguments() + .stream() + .map(TypedElementInfo::typeName) + .collect(Collectors.toUnmodifiableList()), + currentPackage) + .ifPresent(collected::add); + + type.superTypeInfo().ifPresent(it -> collectInheritedMethods(processed, collected, it, method, currentPackage)); + for (TypeInfo typeInfo : type.interfaceTypeInfo()) { + collectInheritedMethods(processed, collected, typeInfo, method, currentPackage); + } + } + + /** + * Check if the provided type declares a method that is overridden. + * + * @param type first immediate supertype we will be checking + * @param method method we are investigating + * @param arguments method signature + * @param currentPackage package of the current type declaring the method + * @return overridden method element + */ + private static Optional inherited(TypeInfo type, + TypedElementInfo method, + List arguments, + String currentPackage) { + + String methodName = method.elementName(); + // we look only for exact match (including types) + Optional found = type.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .filter(not(ElementInfoPredicates::isPrivate)) + .filter(ElementInfoPredicates.elementName(methodName)) + .filter(ElementInfoPredicates.hasParams(arguments)) + .findFirst(); + + if (found.isPresent()) { + TypedElementInfo superMethod = found.get(); + + // method has same signature, but is package local and is in a different package + boolean realOverride = superMethod.accessModifier() != AccessModifier.PACKAGE_PRIVATE + || currentPackage.equals(type.typeName().packageName()); + + if (realOverride) { + // this is a valid method that the type overrides + return Optional.of(superMethod); + } + } + + return Optional.empty(); + } + +} diff --git a/common/types/src/main/java/io/helidon/common/types/Annotated.java b/common/types/src/main/java/io/helidon/common/types/Annotated.java index 07f13ac5c29..292368d72de 100644 --- a/common/types/src/main/java/io/helidon/common/types/Annotated.java +++ b/common/types/src/main/java/io/helidon/common/types/Annotated.java @@ -85,11 +85,10 @@ default Optional findAnnotation(TypeName annotationType) { * @see #findAnnotation(TypeName) */ default Annotation annotation(TypeName annotationType) { - return findAnnotation(annotationType).orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " " - + "is not present. Guard " - + "with hasAnnotation(), or " - + "use findAnnotation() " - + "instead")); + return findAnnotation(annotationType) + .orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " is not present. " + + "Guard with hasAnnotation(), " + + "or use findAnnotation() instead")); } /** diff --git a/common/types/src/main/java/io/helidon/common/types/Annotation.java b/common/types/src/main/java/io/helidon/common/types/Annotation.java index 5e829998047..f08439985d6 100644 --- a/common/types/src/main/java/io/helidon/common/types/Annotation.java +++ b/common/types/src/main/java/io/helidon/common/types/Annotation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -163,7 +163,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance diff --git a/common/types/src/main/java/io/helidon/common/types/TypeName.java b/common/types/src/main/java/io/helidon/common/types/TypeName.java index 188ce614f23..daed5cb816a 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeName.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -130,6 +130,9 @@ abstract class BuilderBase typeParameters = new ArrayList<>(); private boolean array = false; private boolean generic = false; + private boolean isEnclosingNamesMutated; + private boolean isTypeArgumentsMutated; + private boolean isTypeParametersMutated; private boolean primitive = false; private boolean wildcard = false; private String className; @@ -142,7 +145,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance @@ -150,12 +153,21 @@ protected BuilderBase() { public BUILDER from(TypeName prototype) { packageName(prototype.packageName()); className(prototype.className()); + if (!isEnclosingNamesMutated) { + enclosingNames.clear(); + } addEnclosingNames(prototype.enclosingNames()); primitive(prototype.primitive()); array(prototype.array()); generic(prototype.generic()); wildcard(prototype.wildcard()); + if (!isTypeArgumentsMutated) { + typeArguments.clear(); + } addTypeArguments(prototype.typeArguments()); + if (!isTypeParametersMutated) { + typeParameters.clear(); + } addTypeParameters(prototype.typeParameters()); return self(); } @@ -169,13 +181,34 @@ public BUILDER from(TypeName prototype) { public BUILDER from(TypeName.BuilderBase builder) { packageName(builder.packageName()); builder.className().ifPresent(this::className); - addEnclosingNames(builder.enclosingNames()); + if (isEnclosingNamesMutated) { + if (builder.isEnclosingNamesMutated) { + addEnclosingNames(builder.enclosingNames); + } + } else { + enclosingNames.clear(); + addEnclosingNames(builder.enclosingNames); + } primitive(builder.primitive()); array(builder.array()); generic(builder.generic()); wildcard(builder.wildcard()); - addTypeArguments(builder.typeArguments()); - addTypeParameters(builder.typeParameters()); + if (isTypeArgumentsMutated) { + if (builder.isTypeArgumentsMutated) { + addTypeArguments(builder.typeArguments); + } + } else { + typeArguments.clear(); + addTypeArguments(builder.typeArguments); + } + if (isTypeParametersMutated) { + if (builder.isTypeParametersMutated) { + addTypeParameters(builder.typeParameters); + } + } else { + typeParameters.clear(); + addTypeParameters(builder.typeParameters); + } return self(); } @@ -227,6 +260,7 @@ public BUILDER className(String className) { */ public BUILDER enclosingNames(List enclosingNames) { Objects.requireNonNull(enclosingNames); + isEnclosingNamesMutated = true; this.enclosingNames.clear(); this.enclosingNames.addAll(enclosingNames); return self(); @@ -243,6 +277,7 @@ public BUILDER enclosingNames(List enclosingNames) { */ public BUILDER addEnclosingNames(List enclosingNames) { Objects.requireNonNull(enclosingNames); + isEnclosingNamesMutated = true; this.enclosingNames.addAll(enclosingNames); return self(); } @@ -259,6 +294,7 @@ public BUILDER addEnclosingNames(List enclosingNames) { public BUILDER addEnclosingName(String enclosingName) { Objects.requireNonNull(enclosingName); this.enclosingNames.add(enclosingName); + isEnclosingNamesMutated = true; return self(); } @@ -319,6 +355,7 @@ public BUILDER wildcard(boolean wildcard) { */ public BUILDER typeArguments(List typeArguments) { Objects.requireNonNull(typeArguments); + isTypeArgumentsMutated = true; this.typeArguments.clear(); this.typeArguments.addAll(typeArguments); return self(); @@ -333,6 +370,7 @@ public BUILDER typeArguments(List typeArguments) { */ public BUILDER addTypeArguments(List typeArguments) { Objects.requireNonNull(typeArguments); + isTypeArgumentsMutated = true; this.typeArguments.addAll(typeArguments); return self(); } @@ -348,6 +386,7 @@ public BUILDER addTypeArguments(List typeArguments) { public BUILDER addTypeArgument(TypeName typeArgument) { Objects.requireNonNull(typeArgument); this.typeArguments.add(typeArgument); + isTypeArgumentsMutated = true; return self(); } @@ -378,6 +417,7 @@ public BUILDER addTypeArgument(Consumer consumer) { */ public BUILDER typeParameters(List typeParameters) { Objects.requireNonNull(typeParameters); + isTypeParametersMutated = true; this.typeParameters.clear(); this.typeParameters.addAll(typeParameters); return self(); @@ -394,6 +434,7 @@ public BUILDER typeParameters(List typeParameters) { */ public BUILDER addTypeParameters(List typeParameters) { Objects.requireNonNull(typeParameters); + isTypeParametersMutated = true; this.typeParameters.addAll(typeParameters); return self(); } @@ -410,6 +451,7 @@ public BUILDER addTypeParameters(List typeParameters) { public BUILDER addTypeParameter(String typeParameter) { Objects.requireNonNull(typeParameter); this.typeParameters.add(typeParameter); + isTypeParametersMutated = true; return self(); } diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java index 9c29b6cf301..482d7bbb3d0 100644 --- a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java @@ -92,6 +92,10 @@ private Stream typesToProcess(Set typeNames) { } private void storeMetadata() { + if (moduleTypes.isEmpty()) { + // only store if anything is available + return; + } List root = new ArrayList<>(); for (var module : moduleTypes.entrySet()) { diff --git a/config/metadata/processor/src/main/java/io/helidon/config/metadata/processor/ConfigMetadataHandler.java b/config/metadata/processor/src/main/java/io/helidon/config/metadata/processor/ConfigMetadataHandler.java index 6cb879f0e9f..746f4154bf1 100644 --- a/config/metadata/processor/src/main/java/io/helidon/config/metadata/processor/ConfigMetadataHandler.java +++ b/config/metadata/processor/src/main/java/io/helidon/config/metadata/processor/ConfigMetadataHandler.java @@ -49,7 +49,7 @@ /* * This class is separated so javac correctly reports possible errors. */ -class ConfigMetadataHandler { +class ConfigMetadataHandler { /* * Configuration metadata file location. */ @@ -177,6 +177,9 @@ private void processClass(Element aClass) { } private void storeMetadata() { + if (moduleTypes.isEmpty()) { + return; + } try (PrintWriter metaWriter = new PrintWriter(filer.createResource(StandardLocation.CLASS_OUTPUT, "", META_FILE)