diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesBuildItem.java
new file mode 100644
index 0000000000000..e53b22895944c
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesBuildItem.java
@@ -0,0 +1,29 @@
+package io.quarkus.deployment.dev;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.jboss.jandex.DotName;
+
+import io.quarkus.builder.item.MultiBuildItem;
+
+public final class AnnotationDependentClassesBuildItem extends MultiBuildItem {
+
+ private final DotName annotationName;
+
+ private final Map> dependencyToAnnotatedClasses;
+
+ public AnnotationDependentClassesBuildItem(DotName annotationName,
+ Map> dependencyToAnnotatedClasses) {
+ this.annotationName = annotationName;
+ this.dependencyToAnnotatedClasses = dependencyToAnnotatedClasses;
+ }
+
+ public DotName getAnnotationName() {
+ return annotationName;
+ }
+
+ public Map> getDependencyToAnnotatedClasses() {
+ return dependencyToAnnotatedClasses;
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesConfig.java
new file mode 100644
index 0000000000000..6810bd3d2f4ec
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesConfig.java
@@ -0,0 +1,19 @@
+package io.quarkus.deployment.dev;
+
+import java.util.Optional;
+import java.util.Set;
+
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+
+@ConfigRoot(phase = ConfigPhase.BUILD_TIME)
+@ConfigMapping(prefix = "quarkus.dev")
+public interface AnnotationDependentClassesConfig {
+
+ /**
+ * FQDNs of annotations that trigger automatic recompilation of annotated classes when their dependencies change
+ * during dev mode. This is useful for annotation processors that generate code based on these classes (e.g. Mapstruct).
+ */
+ Optional> recompileAnnotations();
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesProcessor.java
new file mode 100644
index 0000000000000..a037a56f89261
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/AnnotationDependentClassesProcessor.java
@@ -0,0 +1,335 @@
+package io.quarkus.deployment.dev;
+
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.ClassType;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.FieldInfo;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.MethodParameterInfo;
+import org.jboss.jandex.ParameterizedType;
+import org.jboss.jandex.Type;
+import org.jboss.jandex.TypeVariable;
+import org.jboss.jandex.WildcardType;
+import org.jboss.logging.Logger;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.Produce;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.deployment.builditem.ServiceStartBuildItem;
+
+/**
+ * Processor which figures out annotation marked recompilation dependencies. This is needed to solve the problem of "how to
+ * recompile classes generated by annotation processor, where the generated class accesses another type in the users code".
+ *
+ * Only classes annotated with an annotation configured in {@link AnnotationDependentClassesConfig} are discovered.
+ *
+ * From these annotated classes, the set of all directly referenced types is collected. This includes all fields, method return
+ * types, and method parameters type of any visibility of the full class hierarchy.
+ *
+ * Then based on the set of directly referenced types, another set of indirectly referenced types is discovered. This includes
+ * all the public, protected, and package private fields, method return types, and method parameters types of the current type
+ * and upwards in the hierarchy. Package private and protected (which is also package private) member are only included, if they
+ * are declared on a class in the same package as the annotated class. This step is addition repeated for each additional public
+ * referenced type, until every direct or public type was visited once.
+ *
+ * Result is a Mapping of the referenced type to the annotated class. Note that no chaining information between the referenced
+ * types is kept. Since for the recompilation to take place we just need to resolve later on from the dependency to the
+ * annotated class. This discovery is repeated for all the configured annotations.
+ *
+ *
+ * A consolidation step takes place, which resolves inner classes to their outer classes. Inner classes and outer classes share
+ * source file (at least in java). For our goal of recompilation we therefore only need to outer class.
+ *
+ * After resolving outer classes, all the mappings of referenced type to annotated class are combined into one mapping, which is
+ * then given to the {@link RuntimeUpdatesProcessor#setRecompilationDependencies(Map)}.
+ */
+public class AnnotationDependentClassesProcessor {
+
+ private static final Logger LOGGER = Logger.getLogger(AnnotationDependentClassesProcessor.class);
+
+ @BuildStep
+ public List discoverAnnotationDependentClasses(AnnotationDependentClassesConfig config,
+ CombinedIndexBuildItem combinedIndexBuildItem) {
+
+ if (config.recompileAnnotations().isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ // Sometimes the annotation itself is not in a jandex index, but that is fine as long as we can find it
+ Set recompileAnnotationNames = resolveConfiguredAnnotationNames(config.recompileAnnotations().get(),
+ combinedIndexBuildItem.getComputingIndex());
+
+ List result = new ArrayList<>();
+ for (DotName recompileAnnotationName : recompileAnnotationNames) {
+ // the classes the annotation is applied on have to be in our index though.
+
+ AnnotationDependentClassesBuildItem annotationDependentClassesBuildItem = determineDependencies(
+ combinedIndexBuildItem.getIndex(), recompileAnnotationName);
+
+ if (annotationDependentClassesBuildItem != null) {
+ result.add(annotationDependentClassesBuildItem);
+ }
+ }
+
+ return result;
+ }
+
+ private Set resolveConfiguredAnnotationNames(Set recompileAnnotations, IndexView index) {
+ // quick check and warn if the annotation type even exists
+ // warning is enough, we just optionally want to determine additional classes to recompile based on the configured recompileAnnotations
+ Set result = new HashSet<>();
+ for (String recompileAnnotation : recompileAnnotations) {
+
+ ClassInfo classByName = index.getClassByName(recompileAnnotation);
+ if (classByName == null) {
+ LOGGER.warnf("""
+ Configured recompile annotation type %s not found.\
+ Won't automatically recompile annotated classes when dependent classes change.""",
+ recompileAnnotation);
+ continue;
+ }
+
+ if (!classByName.isAnnotation()) {
+ LOGGER.warnf("""
+ Configured recompile annotation type %s is not an annotation class.""",
+ recompileAnnotation);
+ continue;
+ }
+
+ result.add(classByName.name());
+ }
+
+ return result;
+ }
+
+ private AnnotationDependentClassesBuildItem determineDependencies(IndexView index, DotName supportedAnnotationName) {
+ Collection annotations = index.getAnnotations(supportedAnnotationName);
+ Map> dependencyToAnnotatedClasses = new HashMap<>();
+ for (AnnotationInstance annotation : annotations) {
+ ClassInfo annotatedClass;
+ try {
+ annotatedClass = annotation.target().asClass();
+ } catch (Exception exception) {
+ LOGGER.warnf(exception, "Annotation %s is not placed on a class. Target %s instead. Skipping.",
+ supportedAnnotationName, annotation.target());
+ continue;
+ }
+
+ Set referencedTypes = collectAllReferencedTypeNames(annotatedClass, index);
+
+ Set dependencies = new HashSet<>();
+ for (DotName referencedType : referencedTypes) {
+ collectVisibleTypeNames(dependencies, referencedType, index, annotatedClass.name());
+ }
+
+ for (DotName dependency : dependencies) {
+ dependencyToAnnotatedClasses.computeIfAbsent(dependency, k -> new HashSet<>()).add(annotatedClass.name());
+ }
+ }
+
+ if (dependencyToAnnotatedClasses.isEmpty()) {
+ return null;
+ }
+
+ return new AnnotationDependentClassesBuildItem(supportedAnnotationName, dependencyToAnnotatedClasses);
+ }
+
+ private Set collectAllReferencedTypeNames(ClassInfo startingClass, IndexView index) {
+ Set visited = new HashSet<>();
+ ArrayDeque stack = new ArrayDeque<>();
+ stack.add(startingClass.name());
+
+ Set referencedTypeNames = new HashSet<>();
+ while (!stack.isEmpty()) {
+ DotName currentClassName = stack.poll();
+ if (!visited.add(currentClassName)) {
+ continue;
+ }
+ if (currentClassName.equals(DotName.OBJECT_NAME)) {
+ continue;
+ }
+
+ ClassInfo classInfo = index.getClassByName(currentClassName);
+ if (classInfo == null) {
+ continue;
+ }
+
+ // search up and down the inheritance chain
+ stack.add(classInfo.superClassType().name());
+ stack.addAll(classInfo.interfaceNames());
+
+ for (ClassInfo knownDirectSubclass : index.getKnownDirectSubclasses(currentClassName)) {
+ stack.add(knownDirectSubclass.name());
+ }
+ for (ClassInfo knownDirectImplementation : index.getKnownDirectImplementations(currentClassName)) {
+ stack.add(knownDirectImplementation.name());
+ }
+
+ // Collect types of any fields in the inheritance chain of the annotated class
+ for (FieldInfo field : classInfo.fields()) {
+ if (!field.isSynthetic()) {
+ extractTypeNames(field.type(), referencedTypeNames);
+ }
+ }
+
+ // Collect types of any methods in the inheritance chain of the annotated class
+ for (MethodInfo method : classInfo.methods()) {
+ if (!method.isSynthetic()) {
+ extractTypeNames(method.returnType(), referencedTypeNames);
+ for (MethodParameterInfo parameter : method.parameters()) {
+ extractTypeNames(parameter.type(), referencedTypeNames);
+ }
+ }
+ }
+ }
+
+ return referencedTypeNames;
+ }
+
+ private void extractTypeNames(Type type, Collection names) {
+ switch (type.kind()) {
+ case CLASS -> names.add(type.name());
+ case PARAMETERIZED_TYPE -> {
+ names.add(type.name());
+ ParameterizedType parameterizedType = type.asParameterizedType();
+ for (Type argument : parameterizedType.arguments()) {
+ // useful for nested generics, e.g. Map>
+ extractTypeNames(argument, names);
+ }
+ }
+ case WILDCARD_TYPE -> {
+ names.add(type.name());
+ WildcardType wildcardType = type.asWildcardType();
+ if (!ClassType.OBJECT_TYPE.equals(wildcardType.extendsBound())) {
+ extractTypeNames(wildcardType.extendsBound(), names);
+ } else if (wildcardType.superBound() != null) {
+ extractTypeNames(wildcardType.superBound(), names);
+ }
+ }
+ case TYPE_VARIABLE -> {
+ names.add(type.name());
+ TypeVariable typeVariable = type.asTypeVariable();
+ for (Type bound : typeVariable.bounds()) {
+ extractTypeNames(bound, names);
+ }
+ }
+ case ARRAY -> extractTypeNames(type.asArrayType().constituent(), names);
+ }
+ }
+
+ private void collectVisibleTypeNames(Set collectedTypes, DotName startingPoint, IndexView index,
+ DotName annotatedClassName) {
+ ArrayDeque stack = new ArrayDeque<>();
+ stack.add(startingPoint);
+
+ while (!stack.isEmpty()) {
+ DotName currentClassName = stack.poll();
+ if (!collectedTypes.add(currentClassName)) {
+ // already know about this property type
+ continue;
+ }
+ if (currentClassName.equals(DotName.OBJECT_NAME)) {
+ continue;
+ }
+
+ ClassInfo classInfo = index.getClassByName(currentClassName);
+ if (classInfo == null) {
+ continue;
+ }
+
+ // only search upwards. The annotated class should only contain references to the public types it can see, i.e. our own and our parents public types
+ stack.add(classInfo.superClassType().name());
+ stack.addAll(classInfo.interfaceNames());
+
+ for (FieldInfo field : classInfo.fields()) {
+ if (isVisibleForAnnotatedClass(field.flags(), classInfo, annotatedClassName) && !field.isSynthetic()) {
+ extractTypeNames(field.type(), stack);
+ }
+ }
+
+ for (MethodInfo method : classInfo.methods()) {
+ if (isVisibleForAnnotatedClass(method.flags(), classInfo, annotatedClassName) && !method.isSynthetic()) {
+ extractTypeNames(method.returnType(), stack);
+ for (MethodParameterInfo parameter : method.parameters()) {
+ extractTypeNames(parameter.type(), stack);
+ }
+ }
+ }
+ }
+ }
+
+ private boolean isVisibleForAnnotatedClass(short flags, ClassInfo declaringClass, DotName annotatedClassName) {
+ if (Modifier.isPublic(flags)) {
+ return true;
+ }
+
+ boolean isProtected = Modifier.isProtected(flags);
+ boolean isPackagePrivate = !isProtected && !Modifier.isPrivate(flags);
+ if (isProtected || isPackagePrivate) {
+ return declaringClass.name().packagePrefix().equals(annotatedClassName.packagePrefix());
+ }
+
+ return false;
+ }
+
+ @BuildStep
+ @Produce(ServiceStartBuildItem.class)
+ public void consolidateRecompilationDependencies(CombinedIndexBuildItem combinedIndexBuildItem,
+ List annotationDependentClassesBuildItems) {
+
+ // Cleanup and combine all the dependencyToAnnotatedClasses maps:
+ // - Resolve inner classes to their top-level class names
+ // - Remove entries where the class is not in the index
+
+ Map> dependencyToAnnotatedClasses = new HashMap<>();
+ for (AnnotationDependentClassesBuildItem buildItem : annotationDependentClassesBuildItems) {
+ for (Map.Entry> entry : buildItem.getDependencyToAnnotatedClasses().entrySet()) {
+
+ DotName dependency = resolveOutermostClassName(entry.getKey(), combinedIndexBuildItem.getIndex());
+ if (dependency == null) {
+ continue;
+ }
+
+ for (DotName annotatedClass : entry.getValue()) {
+ annotatedClass = resolveOutermostClassName(annotatedClass, combinedIndexBuildItem.getIndex());
+ if (annotatedClass == null) {
+ continue;
+ }
+
+ dependencyToAnnotatedClasses.computeIfAbsent(dependency, k -> new HashSet<>()).add(annotatedClass);
+ }
+ }
+ }
+
+ if (RuntimeUpdatesProcessor.INSTANCE != null) {
+ RuntimeUpdatesProcessor.INSTANCE.setRecompilationDependencies(dependencyToAnnotatedClasses);
+ }
+ }
+
+ private DotName resolveOutermostClassName(DotName name, IndexView index) {
+
+ ClassInfo classInfo = index.getClassByName(name);
+ if (classInfo == null) {
+ return null;
+ }
+
+ if (classInfo.nestingType() != ClassInfo.NestingType.TOP_LEVEL) {
+ return resolveOutermostClassName(classInfo.enclosingClassAlways(), index);
+ }
+
+ return name;
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
index c4ba5228fcb18..3e8b0169976b4 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
@@ -2,7 +2,6 @@
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
-import static java.util.stream.Collectors.groupingBy;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
@@ -21,9 +20,11 @@
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -52,6 +53,7 @@
import java.util.stream.Stream;
import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
import org.jboss.jandex.Index;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Indexer;
@@ -104,6 +106,7 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable
private final TimestampSet main = new TimestampSet();
private final TimestampSet test = new TimestampSet();
final Map sourceFileTimestamps = new ConcurrentHashMap<>();
+ private Map> recompilationDependencies = new HashMap<>();
private final List preScanSteps = new CopyOnWriteArrayList<>();
private final List postRestartSteps = new CopyOnWriteArrayList<>();
@@ -709,6 +712,24 @@ ClassScanResult checkForChangedTestClasses(boolean firstScan) {
return ret;
}
+ private void collectRecompilationTargets(DotName changedDependency, Set annotatedClassesToRecompile) {
+ Deque toResolve = new ArrayDeque<>();
+ toResolve.add(changedDependency);
+ while (!toResolve.isEmpty()) {
+ DotName currentDependency = toResolve.poll();
+
+ Set annotatedClasses = recompilationDependencies.get(currentDependency);
+
+ if (annotatedClasses != null) {
+ for (DotName annotatedClass : annotatedClasses) {
+ if (annotatedClassesToRecompile.add(annotatedClass)) {
+ toResolve.add(annotatedClass);
+ }
+ }
+ }
+ }
+ }
+
/**
* A first scan is considered done when we have visited all modules at least once.
* This is useful in two ways.
@@ -720,33 +741,91 @@ ClassScanResult checkForChangedClasses(QuarkusCompiler compiler,
Function cuf, boolean firstScan,
TimestampSet timestampSet, boolean compilingTests) {
ClassScanResult classScanResult = new ClassScanResult();
- boolean ignoreFirstScanChanges = firstScan;
+
+ record RecompilableLocationsBySourcePath(Path sourcePath, Set changedFiles, Set changedDependencies) {
+ }
+ record ChangeDetectionResult(DevModeContext.ModuleInfo moduleInfo,
+ List changedLocations) {
+ }
+ List changeDetectionResults = new ArrayList<>();
+ Set annotatedClassesToRecompile = new HashSet<>();
for (DevModeContext.ModuleInfo module : context.getAllModules()) {
- final List moduleChangedSourceFilePaths = new ArrayList<>();
+ ChangeDetectionResult changeDetectionResult = new ChangeDetectionResult(module, new ArrayList<>());
for (Path sourcePath : cuf.apply(module).getSourcePaths()) {
if (!Files.exists(sourcePath)) {
continue;
}
+
final Set changedSourceFiles;
try (final Stream sourcesStream = Files.walk(sourcePath)) {
changedSourceFiles = sourcesStream
.parallel()
.filter(p -> matchingHandledExtension(p).isPresent()
- && sourceFileWasRecentModified(p, ignoreFirstScanChanges, firstScan))
+ && sourceFileWasRecentModified(p, firstScan, firstScan))
.map(Path::toFile)
//Needing a concurrent Set, not many standard options:
.collect(Collectors.toCollection(ConcurrentSkipListSet::new));
} catch (IOException e) {
throw new RuntimeException(e);
}
+
if (!changedSourceFiles.isEmpty()) {
+ RecompilableLocationsBySourcePath recompilableLocationsBySourcePath = new RecompilableLocationsBySourcePath(
+ sourcePath, changedSourceFiles, new HashSet<>());
+ changeDetectionResult.changedLocations().add(recompilableLocationsBySourcePath);
+
+ for (File changedSourceFile : changedSourceFiles) {
+ String changedDependency = convertFileToClassname(sourcePath, changedSourceFile);
+
+ collectRecompilationTargets(DotName.createSimple(changedDependency), annotatedClassesToRecompile);
+ }
+ }
+ }
+ changeDetectionResults.add(changeDetectionResult);
+ }
+
+ for (DotName annotatedClass : annotatedClassesToRecompile) {
+ String partialRelativePath = annotatedClass.toString('/');
+
+ boolean found = false;
+ for (ChangeDetectionResult changeDetectionResult : changeDetectionResults) {
+ for (RecompilableLocationsBySourcePath recompilableLocationsBySourcePath : changeDetectionResult
+ .changedLocations()) {
+ for (String extension : compiler.allHandledExtensions()) {
+ Path resolved = recompilableLocationsBySourcePath.sourcePath().resolve(partialRelativePath + extension);
+
+ if (Files.exists(resolved)) {
+ recompilableLocationsBySourcePath.changedDependencies().add(resolved.toFile());
+ found = true;
+ break;
+ }
+ }
+
+ if (found) {
+ break;
+ }
+ }
+
+ if (found) {
+ break;
+ }
+ }
+ }
+
+ for (ChangeDetectionResult changeDetectionResult : changeDetectionResults) {
+ final List moduleChangedSourceFilePaths = new ArrayList<>();
+ for (RecompilableLocationsBySourcePath recompilableLocationsBySourcePath : changeDetectionResult
+ .changedLocations()) {
+ Path sourcePath = recompilableLocationsBySourcePath.sourcePath();
+ Set changedSourceFiles = recompilableLocationsBySourcePath.changedFiles();
+ if (!changedSourceFiles.isEmpty() || !recompilableLocationsBySourcePath.changedDependencies().isEmpty()) {
classScanResult.compilationHappened = true;
//so this is pretty yuck, but on a lot of systems a write is actually a truncate + write
//its possible we see the truncated file timestamp, then the write updates the timestamp
//which will then re-trigger continuous testing/live reload
- //the empty fine does not normally cause issues as by the time we actually compile it the write
+ //the empty file does not normally cause issues as by the time we actually compile it the write
//has completed (but the old timestamp is used)
for (File i : changedSourceFiles) {
if (i.length() == 0) {
@@ -761,6 +840,7 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges, firstScan))
}
}
}
+
Map compileTimestamps = new HashMap<>();
//now we record the timestamps as they are before the compile phase
@@ -769,12 +849,18 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges, firstScan))
}
for (;;) {
try {
- final Set changedPaths = changedSourceFiles.stream()
- .map(File::toPath)
- .collect(Collectors.toSet());
+ Map> changedFilesByExtension = new HashMap<>();
+ Set changedPaths = new HashSet<>();
+ Stream.concat(changedSourceFiles.stream(),
+ recompilableLocationsBySourcePath.changedDependencies.stream()).forEach(file -> {
+ changedPaths.add(file.toPath());
+
+ Set files = changedFilesByExtension.computeIfAbsent(this.getFileExtension(file),
+ k -> new HashSet<>());
+ files.add(file);
+ });
moduleChangedSourceFilePaths.addAll(changedPaths);
- compiler.compile(sourcePath.toString(), changedSourceFiles.stream()
- .collect(groupingBy(this::getFileExtension, Collectors.toSet())));
+ compiler.compile(sourcePath.toString(), changedFilesByExtension);
compileProblem = null;
if (compilingTests) {
testCompileProblem = null;
@@ -812,12 +898,10 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges, firstScan))
sourceFileTimestamps.put(entry.getKey().toPath(), entry.getValue());
}
}
-
}
-
- checkForClassFilesChangesInModule(module, moduleChangedSourceFilePaths, ignoreFirstScanChanges, classScanResult,
+ checkForClassFilesChangesInModule(changeDetectionResult.moduleInfo(), moduleChangedSourceFilePaths,
+ firstScan, classScanResult,
cuf, timestampSet);
-
}
return classScanResult;
@@ -922,6 +1006,19 @@ private String getFileExtension(File file) {
return name.substring(lastIndexOf);
}
+ // convert a filename to a class name with package
+ private String convertFileToClassname(Path sourcePath, File file) {
+ String className = sourcePath.relativize(file.toPath())
+ .toString();
+ className = className.replace(File.separatorChar, '.');
+
+ int lastIndexOf = className.lastIndexOf('.');
+ if (lastIndexOf > 0) {
+ className = className.substring(0, lastIndexOf);
+ }
+ return className;
+ }
+
Set checkForFileChange() {
return checkForFileChange(DevModeContext.ModuleInfo::getMain, main);
}
@@ -1155,6 +1252,11 @@ public RuntimeUpdatesProcessor setDisableInstrumentationForIndexPredicate(
return this;
}
+ RuntimeUpdatesProcessor setRecompilationDependencies(Map> dependencyToAnnotatedClasses) {
+ this.recompilationDependencies = dependencyToAnnotatedClasses;
+ return this;
+ }
+
public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedFilePaths,
List, Boolean>> watchedFilePredicates, boolean isTest) {
if (isTest) {
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/AnnotationDependentClassesProcessorTest.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/AnnotationDependentClassesProcessorTest.java
new file mode 100644
index 0000000000000..f1d6e88c5b099
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/AnnotationDependentClassesProcessorTest.java
@@ -0,0 +1,543 @@
+package io.quarkus.deployment.dev;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.Index;
+import org.jboss.jandex.Indexer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.APMarker;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.Address;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.Contact;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.ContactMapper;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.ContactMapperImpl;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.ContactMapperMultipleAnnotation;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.DefaultEmailCreator;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.Email;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.MapperHelper;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.MapperHelperImpl;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.Marker1;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.Marker2;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.ModelBase;
+import io.quarkus.deployment.dev.annotation_dependent_classes.model.PackagePrivateData;
+import io.quarkus.deployment.index.IndexWrapper;
+import io.quarkus.deployment.index.PersistentClassIndex;
+
+class AnnotationDependentClassesProcessorTest {
+ private AnnotationDependentClassesProcessor processor;
+
+ @BeforeEach
+ void setup() {
+ processor = new AnnotationDependentClassesProcessor();
+ RuntimeUpdatesProcessor.INSTANCE = new TestRuntimeUpdatesProcessor();
+ }
+
+ @Test
+ void testNoAnnotations() {
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(Optional::empty, null);
+
+ assertEquals(0, annotationDependentClassesBuildItems.size());
+ }
+
+ @Test
+ void fullTest() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ Index index = buildIndex(Address.class, APMarker.class, Contact.class, ContactMapper.class, ContactMapperImpl.class,
+ DefaultEmailCreator.class, Email.class, MapperHelper.class, MapperHelperImpl.class, ModelBase.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(1, annotationDependentClassesBuildItems.size());
+
+ AnnotationDependentClassesBuildItem annotationDependentClassesBuildItem = annotationDependentClassesBuildItems.get(0);
+ assertEquals(DotName.createSimple(APMarker.class), annotationDependentClassesBuildItem.getAnnotationName());
+
+ Map> dependencies = annotationDependentClassesBuildItem
+ .getDependencyToAnnotatedClasses();
+ assertEquals(14, dependencies.size());
+ assertAffectedClasses(dependencies, ModelBase.class, ContactMapper.class);
+ assertAffectedClasses(dependencies, DefaultEmailCreator.class, ContactMapper.class);
+ assertAffectedClasses(dependencies, Address.class, ContactMapper.class);
+ assertAffectedClasses(dependencies, Email.class, DefaultEmailCreator.class, ContactMapper.class);
+ assertAffectedClasses(dependencies, Contact.class, ContactMapper.class);
+ assertAffectedClasses(dependencies, MapperHelper.class, ContactMapper.class);
+ assertAffectedClasses(dependencies, MapperHelperImpl.class, ContactMapper.class);
+ assertAffectedClasses(dependencies, Address.LocalizationInfo.class, ContactMapper.class);
+ }
+
+ /**
+ * Test that circular dependencies between annotated classes do not cause infinite loops.
+ */
+ @Test
+ void testCyclicAPMarkedClasses() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ Index index = buildIndex(CyclicApMarked1.class, CyclicApMarked2.class, APMarker.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ Map> dependencies = annotationDependentClassesBuildItems.get(0)
+ .getDependencyToAnnotatedClasses();
+ assertEquals(3, dependencies.size());
+ assertAffectedClasses(dependencies, CyclicApMarked1.class, CyclicApMarked2.class);
+ assertAffectedClasses(dependencies, CyclicApMarked2.class, CyclicApMarked1.class);
+ }
+
+ @APMarker
+ class CyclicApMarked1 {
+ private CyclicApMarked2 cyclicApMarked2;
+ }
+
+ @APMarker
+ class CyclicApMarked2 {
+ private CyclicApMarked1 cyclicApMarked1;
+ }
+
+ @Test
+ void testAPMarkedAnnotationRecomputed() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ Index index = buildIndex(Address.class, ContactMapper.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(0, annotationDependentClassesBuildItems.size());
+
+ combinedIndexBuildItem = new CombinedIndexBuildItem(index,
+ new IndexWrapper(index, APMarker.class.getClassLoader(), new PersistentClassIndex()));
+ annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(1, annotationDependentClassesBuildItems.size());
+ }
+
+ @Test
+ void testConfiguredAnnotationIsInRealityAClass() throws IOException {
+
+ class D {
+ }
+ class C extends D {
+ }
+ class B extends C {
+ }
+ @APMarker
+ record A(B b) {
+ }
+
+ Index index = buildIndex(D.class, C.class, B.class, A.class, APMarker.class);
+
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(A.class.getName()));
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(0, annotationDependentClassesBuildItems.size());
+ }
+
+ @Test
+ void testNonClassAnnotated() throws IOException {
+ class A {
+ @APMarker
+ String field;
+
+ @APMarker
+ void method() {
+ }
+ }
+
+ Index index = buildIndex(A.class, APMarker.class);
+
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(0, annotationDependentClassesBuildItems.size());
+ }
+
+ @Test
+ void testInheritanceOfReferencedPublicType() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ class D {
+ }
+ class C extends D {
+ }
+ class B extends C {
+ }
+ @APMarker
+ record A(B b) {
+ }
+
+ Index index = buildIndex(D.class, C.class, A.class, B.class, APMarker.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(1, annotationDependentClassesBuildItems.size());
+ Map> dependencies = annotationDependentClassesBuildItems.get(0)
+ .getDependencyToAnnotatedClasses();
+ assertEquals(5, dependencies.size());
+ assertAffectedClasses(dependencies, B.class, A.class);
+ assertAffectedClasses(dependencies, C.class, A.class);
+ assertAffectedClasses(dependencies, D.class, A.class);
+ }
+
+ @Test
+ void testGenericParameterizedType() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ record D() {
+ }
+ record C() {
+ }
+ record B() {
+ }
+
+ @APMarker
+ record A(List bs, Map> map) {
+ }
+
+ Index index = buildIndex(D.class, C.class, B.class, A.class, APMarker.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(1, annotationDependentClassesBuildItems.size());
+ Map> dependencies = annotationDependentClassesBuildItems.get(0)
+ .getDependencyToAnnotatedClasses();
+ assertEquals(8, dependencies.size());
+ assertAffectedClasses(dependencies, D.class, A.class);
+ assertAffectedClasses(dependencies, C.class, A.class);
+ assertAffectedClasses(dependencies, B.class, A.class);
+ }
+
+ @Test
+ void testWildcardType() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ class D {
+ }
+ class C {
+ }
+ class B {
+ }
+
+ @APMarker
+ record A(List extends C> bs, Map super D, B> map) {
+ }
+
+ Index index = buildIndex(D.class, C.class, B.class, A.class, APMarker.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(1, annotationDependentClassesBuildItems.size());
+ Map> dependencies = annotationDependentClassesBuildItems.get(0)
+ .getDependencyToAnnotatedClasses();
+ assertEquals(7, dependencies.size());
+ assertAffectedClasses(dependencies, D.class, A.class);
+ assertAffectedClasses(dependencies, C.class, A.class);
+ assertAffectedClasses(dependencies, B.class, A.class);
+ }
+
+ @Test
+ void testTypeVariable() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ class B {
+ }
+
+ @APMarker
+ class A {
+ public void helper(T variable) {
+ }
+ }
+
+ Index index = buildIndex(B.class, A.class, APMarker.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(1, annotationDependentClassesBuildItems.size());
+ Map> dependencies = annotationDependentClassesBuildItems.get(0)
+ .getDependencyToAnnotatedClasses();
+ assertEquals(2, dependencies.size());
+ assertAffectedClasses(dependencies, B.class, A.class);
+ }
+
+ @Test
+ void testArrayType() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ class C {
+ }
+ class B {
+ }
+
+ @APMarker
+ class A {
+ public void helper(T[] array, C[] array2) {
+ }
+ }
+
+ Index index = buildIndex(C.class, A.class, B.class, APMarker.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(1, annotationDependentClassesBuildItems.size());
+ Map> dependencies = annotationDependentClassesBuildItems.get(0)
+ .getDependencyToAnnotatedClasses();
+ assertEquals(3, dependencies.size());
+ assertFalse(dependencies.containsKey(DotName.createSimple(B[].class.getName())));
+ assertFalse(dependencies.containsKey(DotName.createSimple(C[].class.getName())));
+ assertAffectedClasses(dependencies, B.class, A.class);
+ assertAffectedClasses(dependencies, C.class, A.class);
+ }
+
+ @Test
+ void testPublicTypeCollectionVisibilityCheck() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+ class F {
+ }
+ class E {
+ }
+ class D {
+ }
+ class C {
+ }
+ class B {
+ private C c;
+ D d;
+ protected E e;
+ public F f;
+ }
+
+ class B2 {
+ private C help1() {
+ return null;
+ }
+
+ D help2() {
+ return null;
+ }
+
+ protected E help3() {
+ return null;
+ }
+
+ public F help4() {
+ return null;
+ }
+ }
+
+ class B3 {
+ private void help1(C c) {
+ }
+
+ void help2(D d) {
+ }
+
+ protected void help3(E e) {
+ }
+
+ public void help4(F f) {
+ }
+ }
+
+ @APMarker
+ class A {
+ B b;
+
+ PackagePrivateData packagePrivateData;
+ }
+
+ @APMarker
+ class A2 {
+ B2 b2;
+ }
+
+ @APMarker
+ class A3 {
+ B3 b3;
+ }
+
+ Index index = buildIndex(A.class, A2.class, A3.class, APMarker.class, B.class, B2.class, B3.class, C.class, D.class,
+ E.class, F.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ Map> dependencies = annotationDependentClassesBuildItems.get(0)
+ .getDependencyToAnnotatedClasses();
+
+ // C is private, should not be included
+ assertFalse(dependencies.containsKey(DotName.createSimple(C.class)));
+ // Address and Contact are in different packages compared to the annoated classes, and should not be included
+ assertFalse(dependencies.containsKey(DotName.createSimple(Address.class)));
+ assertFalse(dependencies.containsKey(DotName.createSimple(Contact.class)));
+ // F is public
+ // E is protected, and can see package private
+ // D is package private
+ assertAffectedClasses(dependencies, F.class, A.class, A2.class, A3.class);
+ assertAffectedClasses(dependencies, E.class, A.class, A2.class, A3.class);
+ assertAffectedClasses(dependencies, D.class, A.class, A2.class, A3.class);
+ }
+
+ @Test
+ void testReferencedTypeInheritanceNotEvaluated() throws IOException {
+ AnnotationDependentClassesConfig config = () -> Optional.of(Set.of(APMarker.class.getName()));
+
+ class D {
+ }
+
+ class B extends D implements TestInheritanceOfReferencedTypesC {
+ }
+
+ @APMarker
+ class A {
+ TestInheritanceOfReferencedTypesC c;
+
+ D d;
+ }
+
+ Index index = buildIndex(TestInheritanceOfReferencedTypesC.class, A.class, B.class, APMarker.class);
+
+ CombinedIndexBuildItem combinedIndexBuildItem = new CombinedIndexBuildItem(index, index);
+ List annotationDependentClassesBuildItems = processor
+ .discoverAnnotationDependentClasses(config, combinedIndexBuildItem);
+
+ assertEquals(1, annotationDependentClassesBuildItems.size());
+ Map> dependencies = annotationDependentClassesBuildItems.get(0)
+ .getDependencyToAnnotatedClasses();
+
+ assertFalse(dependencies.containsKey(DotName.createSimple(B.class.getName())));
+ assertAffectedClasses(dependencies, TestInheritanceOfReferencedTypesC.class, A.class);
+ assertAffectedClasses(dependencies, D.class, A.class);
+ }
+
+ interface TestInheritanceOfReferencedTypesC {
+ }
+
+ @Test
+ void testConsolidateRecompilationDependencies() throws IOException {
+ Index index = buildIndex(Contact.class, ContactMapper.class, MapperHelper.class, Address.class,
+ Address.LocalizationInfo.class, Address.LocalizationInfo.LocalizationInfo2.class,
+ Address.LocalizationInfo.LocalizationInfo2.LocalizationInfo3.class);
+
+ AnnotationDependentClassesBuildItem b1 = new AnnotationDependentClassesBuildItem(DotName.createSimple("APMarker1"),
+ Map.of(DotName.createSimple(Contact.class), Set.of(DotName.createSimple(ContactMapper.class))));
+ AnnotationDependentClassesBuildItem b2 = new AnnotationDependentClassesBuildItem(DotName.createSimple("APMarker2"),
+ Map.of(DotName.createSimple(Address.LocalizationInfo.LocalizationInfo2.LocalizationInfo3.class),
+ Set.of(DotName.createSimple(MapperHelper.class))));
+
+ processor.consolidateRecompilationDependencies(new CombinedIndexBuildItem(index, index), List.of(b1, b2));
+
+ Map> recompilationDependencies = ((TestRuntimeUpdatesProcessor) RuntimeUpdatesProcessor.INSTANCE).recompilationDependencies;
+
+ assertEquals(2, recompilationDependencies.size());
+
+ assertAffectedClasses(recompilationDependencies, Contact.class, ContactMapper.class);
+
+ // Tests that inner class is dropped, and only outer class passed.
+ assertAffectedClasses(recompilationDependencies, Address.class, MapperHelper.class);
+ }
+
+ @Test
+ void testConsolidationOfMultipleAnnotations() throws IOException {
+
+ AnnotationDependentClassesConfig config = () -> Optional.of(
+ Set.of(Marker1.class.getName(), Marker2.class.getName(), APMarker.class.getName()));
+
+ Index index = buildIndex(Address.class, ContactMapper.class, ContactMapperMultipleAnnotation.class, APMarker.class,
+ Marker1.class, Marker2.class);
+
+ List buildItems = processor
+ .discoverAnnotationDependentClasses(config, new CombinedIndexBuildItem(index, index));
+
+ // 3 builditems, 2 are Address -> ContactMapperMultipleAnnotation, the other one is Address -> ContactMapper
+ assertEquals(3, buildItems.size());
+ for (AnnotationDependentClassesBuildItem buildItem : buildItems) {
+ if (buildItem.getAnnotationName().equals(DotName.createSimple(APMarker.class.getName()))) {
+ assertAffectedClasses(buildItem.getDependencyToAnnotatedClasses(), Address.class, ContactMapper.class);
+ } else {
+ assertAffectedClasses(buildItem.getDependencyToAnnotatedClasses(), Address.class,
+ ContactMapperMultipleAnnotation.class);
+ }
+ }
+
+ processor.consolidateRecompilationDependencies(new CombinedIndexBuildItem(index, index), buildItems);
+
+ Map> recompilationDependencies = ((TestRuntimeUpdatesProcessor) RuntimeUpdatesProcessor.INSTANCE).recompilationDependencies;
+
+ // In the end only one entry in the resulting dependency map
+ // Address -> ContactMapperMultipleAnnotation, ContactMapper
+ assertEquals(1, recompilationDependencies.size());
+ assertAffectedClasses(recompilationDependencies, Address.class, ContactMapperMultipleAnnotation.class,
+ ContactMapper.class);
+ }
+
+ private void assertAffectedClasses(Map> dependencies, Class> clazz,
+ Class>... affectedClasses) {
+ DotName className = DotName.createSimple(clazz.getName());
+ assertTrue(dependencies.containsKey(className));
+ assertEquals(affectedClasses.length, dependencies.get(className).size());
+ for (Class> affectedClass : affectedClasses) {
+ DotName affectedClassName = DotName.createSimple(affectedClass.getName());
+ assertTrue(dependencies.get(className).contains(affectedClassName));
+ }
+ }
+
+ private Index buildIndex(Class>... classes) throws IOException {
+ assertTrue(classes.length > 0);
+
+ Indexer indexer = new Indexer();
+
+ for (Class> clazz : classes) {
+ indexer.indexClass(clazz);
+ }
+
+ return indexer.complete();
+ }
+
+ private static class TestRuntimeUpdatesProcessor extends RuntimeUpdatesProcessor {
+
+ private Map> recompilationDependencies = new ConcurrentHashMap<>();
+
+ public TestRuntimeUpdatesProcessor() {
+ super(null, null, null, null, null, null, null, null, null);
+ }
+
+ public RuntimeUpdatesProcessor setRecompilationDependencies(Map> recompilationDependencies) {
+ this.recompilationDependencies = recompilationDependencies;
+ return this;
+ }
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/APMarker.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/APMarker.java
new file mode 100644
index 0000000000000..aa1093851e11a
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/APMarker.java
@@ -0,0 +1,4 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+public @interface APMarker {
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Address.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Address.java
new file mode 100644
index 0000000000000..b500d81269010
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Address.java
@@ -0,0 +1,38 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+import java.util.List;
+
+public class Address extends ModelBase {
+ private String city;
+
+ private String streetName;
+
+ public LocalizationInfo localizationInfo;
+
+ private List contacts;
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public List getContacts() {
+ return contacts;
+ }
+
+ public void setContacts(List contacts) {
+ this.contacts = contacts;
+ }
+
+ public static class LocalizationInfo {
+
+ public static class LocalizationInfo2 {
+ public static class LocalizationInfo3 {
+
+ }
+ }
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Contact.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Contact.java
new file mode 100644
index 0000000000000..c7857c7d7fe01
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Contact.java
@@ -0,0 +1,23 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+public class Contact extends ModelBase {
+ private String name;
+
+ private Email email;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Email getEmail() {
+ return email;
+ }
+
+ public void setEmail(Email email) {
+ this.email = email;
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapper.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapper.java
new file mode 100644
index 0000000000000..d184ad6028b96
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapper.java
@@ -0,0 +1,6 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+@APMarker
+public interface ContactMapper {
+ void mapToData(Address contact);
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapperImpl.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapperImpl.java
new file mode 100644
index 0000000000000..8d8ccfd67a349
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapperImpl.java
@@ -0,0 +1,11 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+public class ContactMapperImpl implements ContactMapper {
+ private MapperHelperImpl mapperHelper = new MapperHelperImpl();
+
+ private DefaultEmailCreator defaultEmailCreator;
+
+ @Override
+ public void mapToData(Address contact) {
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapperMultipleAnnotation.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapperMultipleAnnotation.java
new file mode 100644
index 0000000000000..2002e8a50dcaa
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ContactMapperMultipleAnnotation.java
@@ -0,0 +1,6 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+@Marker1
+@Marker2
+public interface ContactMapperMultipleAnnotation extends ContactMapper {
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/DefaultEmailCreator.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/DefaultEmailCreator.java
new file mode 100644
index 0000000000000..b5e840a6c40ee
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/DefaultEmailCreator.java
@@ -0,0 +1,6 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+@APMarker
+public interface DefaultEmailCreator {
+ Email createDefault();
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Email.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Email.java
new file mode 100644
index 0000000000000..179987bfae6c1
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Email.java
@@ -0,0 +1,14 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+// embeddable, with validation etc
+public class Email {
+ private String email;
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/MapperHelper.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/MapperHelper.java
new file mode 100644
index 0000000000000..030eaa21495c3
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/MapperHelper.java
@@ -0,0 +1,13 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+import java.util.Calendar;
+import java.util.Date;
+
+public abstract class MapperHelper {
+ Calendar mapToCalendar(Date date) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(date);
+
+ return calendar;
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/MapperHelperImpl.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/MapperHelperImpl.java
new file mode 100644
index 0000000000000..f7001f7f28056
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/MapperHelperImpl.java
@@ -0,0 +1,4 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+public class MapperHelperImpl extends MapperHelper {
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Marker1.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Marker1.java
new file mode 100644
index 0000000000000..9ebdf837f6612
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Marker1.java
@@ -0,0 +1,4 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+public @interface Marker1 {
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Marker2.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Marker2.java
new file mode 100644
index 0000000000000..431ae9a933ff2
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/Marker2.java
@@ -0,0 +1,4 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+public @interface Marker2 {
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ModelBase.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ModelBase.java
new file mode 100644
index 0000000000000..0e0d7b89a6fee
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/ModelBase.java
@@ -0,0 +1,6 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+// base class that every model in the system should implement
+public class ModelBase {
+ public Long id;
+}
diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/PackagePrivateData.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/PackagePrivateData.java
new file mode 100644
index 0000000000000..5a2dde4140b26
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/annotation_dependent_classes/model/PackagePrivateData.java
@@ -0,0 +1,20 @@
+package io.quarkus.deployment.dev.annotation_dependent_classes.model;
+
+public class PackagePrivateData {
+ Address address;
+ protected Contact contact;
+
+ Address help2Address() {
+ return null;
+ }
+
+ protected Contact help3Contact() {
+ return null;
+ }
+
+ void help2(Address d) {
+ }
+
+ protected void help3(Contact e) {
+ }
+}
diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AddressData.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AddressData.java
new file mode 100644
index 0000000000000..d149c96c68ea3
--- /dev/null
+++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AddressData.java
@@ -0,0 +1,7 @@
+package io.quarkus.test.reload;
+
+public class AddressData {
+ public String name1;
+
+ public ContactData contactData;
+}
diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AddressMapper.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AddressMapper.java
new file mode 100644
index 0000000000000..903f19b969fb5
--- /dev/null
+++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AddressMapper.java
@@ -0,0 +1,12 @@
+package io.quarkus.test.reload;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+@AnnotationProcessorMarker
+@ApplicationScoped
+public class AddressMapper {
+
+ public String map(AddressData address) {
+ return address.name1;
+ }
+}
diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationDependentReloadDevModeTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationDependentReloadDevModeTest.java
new file mode 100644
index 0000000000000..f6fc4b403330f
--- /dev/null
+++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationDependentReloadDevModeTest.java
@@ -0,0 +1,59 @@
+package io.quarkus.test.reload;
+
+import static org.hamcrest.Matchers.is;
+
+import java.util.logging.LogRecord;
+
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusDevModeTest;
+import io.restassured.RestAssured;
+
+class AnnotationDependentReloadDevModeTest {
+
+ @RegisterExtension
+ static final QuarkusDevModeTest TEST = new QuarkusDevModeTest()
+ .withApplicationRoot((jar) -> jar
+ .addClass(AnnotationProcessorMarker.class)
+ .addClass(AddressData.class)
+ .addClass(ContactData.class)
+ .addClass(AddressMapper.class)
+ .addClass(AnnotationDependentReloadResource.class)
+ .addAsResource(new StringAsset(
+ """
+ quarkus.dev.recompile-annotations=io.quarkus.test.reload.AnnotationProcessorMarker
+ quarkus.grpc.dev-mode.force-server-start=false
+ """),
+ "application.properties"))
+ .setLogRecordPredicate(r -> true);
+
+ // mostly meant as smoke test to ensure the recompile-annotations has any effect
+ // more detailed testing is done as Unit Tests in AnnotationDependentClassesProcessorTest
+ @Test
+ void testDependencyChangeTriggersAnnotatedClassRecompilation() {
+ RestAssured.get("/annotation-dependent-reload/test").then().body(is("hello"));
+
+ // just a file change to make quarkus hot reload on next rest call
+ TEST.modifySourceFile(ContactData.class, oldSource -> oldSource.replace(
+ "}",
+ "public String email;}"));
+
+ RestAssured.get("/annotation-dependent-reload/test").then().body(is("hello"));
+
+ // ContactData -> AddressMapper recompile
+ // but not AdressData
+ // since AdressData is not annotated
+ boolean found = false;
+ for (LogRecord logRecord : TEST.getLogRecords()) {
+ if (logRecord.getLoggerName().equals("io.quarkus.deployment.dev.RuntimeUpdatesProcessor")
+ && (logRecord.getParameters()[0].equals("AddressMapper.class, ContactData.class")
+ || logRecord.getParameters()[0].equals("ContactData.class, AddressMapper.class"))) {
+ found = true;
+ }
+ }
+ Assertions.assertTrue(found, "Did not find a log record from RuntimeUpdatesProcessor for AddressMapper class");
+ }
+}
diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationDependentReloadResource.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationDependentReloadResource.java
new file mode 100644
index 0000000000000..fc94b7d0eb941
--- /dev/null
+++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationDependentReloadResource.java
@@ -0,0 +1,23 @@
+package io.quarkus.test.reload;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@ApplicationScoped
+@Path("/annotation-dependent-reload")
+public class AnnotationDependentReloadResource {
+
+ @Inject
+ AddressMapper addressMapper;
+
+ @GET
+ @Path("/test")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String test() {
+ return "hello";
+ }
+}
diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationProcessorMarker.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationProcessorMarker.java
new file mode 100644
index 0000000000000..c805e9b61f3b3
--- /dev/null
+++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AnnotationProcessorMarker.java
@@ -0,0 +1,11 @@
+package io.quarkus.test.reload;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AnnotationProcessorMarker {
+}
diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/ContactData.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/ContactData.java
new file mode 100644
index 0000000000000..6b383dbb4c270
--- /dev/null
+++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/ContactData.java
@@ -0,0 +1,4 @@
+package io.quarkus.test.reload;
+
+public class ContactData {
+}