Skip to content

Commit fdeba02

Browse files
authored
Fixes incremental compilation for Pico services and running unit tests from IDE (#6863)
1 parent 2d928e4 commit fdeba02

File tree

15 files changed

+384
-45
lines changed

15 files changed

+384
-45
lines changed

pico/processor/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
<artifactId>hamcrest-all</artifactId>
5858
<scope>test</scope>
5959
</dependency>
60+
<dependency>
61+
<groupId>org.mockito</groupId>
62+
<artifactId>mockito-core</artifactId>
63+
<scope>test</scope>
64+
</dependency>
6065
</dependencies>
6166

6267
</project>

pico/processor/src/main/java/io/helidon/pico/processor/ActiveProcessorUtils.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ public void error(String message,
117117
out(System.Logger.Level.ERROR, Diagnostic.Kind.ERROR, message, null);
118118
}
119119

120-
void out(System.Logger.Level level, Diagnostic.Kind kind, String message, Throwable t) {
120+
void out(System.Logger.Level level,
121+
Diagnostic.Kind kind,
122+
String message,
123+
Throwable t) {
121124
if (logger.isLoggable(level)) {
122125
logger.log(level, getClass().getSimpleName() + ": " + message, t);
123126
}
@@ -191,10 +194,6 @@ Optional<TypeInfo> toTypeInfo(TypeElement element,
191194
return typeInfoCreatorProvider.createTypeInfo(element, mirror, processingEnv, isOneWeCareAbout);
192195
}
193196

194-
System.Logger.Level loggerLevel() {
195-
return (Options.isOptionEnabled(Options.TAG_DEBUG)) ? System.Logger.Level.INFO : System.Logger.Level.DEBUG;
196-
}
197-
198197
RoundEnvironment roundEnv() {
199198
return roundEnv;
200199
}

pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java

+15-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Optional;
25+
import java.util.ServiceConfigurationError;
2526
import java.util.ServiceLoader;
2627
import java.util.Set;
2728
import java.util.concurrent.ConcurrentHashMap;
@@ -72,9 +73,7 @@ public CustomAnnotationProcessor() {
7273
}
7374

7475
static List<CustomAnnotationTemplateCreator> initialize() {
75-
// note: it is important to use this class' CL since maven will not give us the "right" one.
76-
List<CustomAnnotationTemplateCreator> creators = HelidonServiceLoader.create(ServiceLoader.load(
77-
CustomAnnotationTemplateCreator.class, CustomAnnotationTemplateCreator.class.getClassLoader())).asList();
76+
List<CustomAnnotationTemplateCreator> creators = HelidonServiceLoader.create(loader()).asList();
7877
creators.forEach(creator -> {
7978
try {
8079
Set<String> annoTypes = creator.annoTypes();
@@ -237,6 +236,19 @@ CustomAnnotationTemplateRequestDefault.Builder toRequestBuilder(TypeName annoTyp
237236
.enclosingTypeInfo(enclosingClassTypeInfo);
238237
}
239238

239+
private static ServiceLoader<CustomAnnotationTemplateCreator> loader() {
240+
try {
241+
// note: it is important to use this class' CL since maven will not give us the "right" one.
242+
return ServiceLoader.load(
243+
CustomAnnotationTemplateCreator.class, CustomAnnotationTemplateCreator.class.getClassLoader());
244+
} catch (ServiceConfigurationError e) {
245+
// see issue #6261 - running inside the IDE?
246+
// this version will use the thread ctx classloader
247+
System.getLogger(CustomAnnotationProcessor.class.getName()).log(System.Logger.Level.WARNING, e.getMessage(), e);
248+
return ServiceLoader.load(CustomAnnotationTemplateCreator.class);
249+
}
250+
}
251+
240252
private static TypeElement toEnclosingClassTypeElement(Element typeToProcess) {
241253
while (typeToProcess != null && !(typeToProcess instanceof TypeElement)) {
242254
typeToProcess = typeToProcess.getEnclosingElement();

pico/processor/src/main/java/io/helidon/pico/processor/PicoAnnotationProcessor.java

+62-21
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package io.helidon.pico.processor;
1818

19+
import java.io.IOException;
20+
import java.nio.file.Path;
1921
import java.util.ArrayList;
2022
import java.util.Collection;
2123
import java.util.LinkedHashMap;
@@ -40,6 +42,7 @@
4042
import io.helidon.common.types.AnnotationAndValueDefault;
4143
import io.helidon.common.types.TypeInfo;
4244
import io.helidon.common.types.TypeName;
45+
import io.helidon.common.types.TypeNameDefault;
4346
import io.helidon.common.types.TypedElementInfo;
4447
import io.helidon.pico.api.Activator;
4548
import io.helidon.pico.api.Contract;
@@ -51,9 +54,11 @@
5154
import io.helidon.pico.api.ServiceInfoBasics;
5255
import io.helidon.pico.runtime.Dependencies;
5356
import io.helidon.pico.tools.ActivatorCreatorCodeGen;
57+
import io.helidon.pico.tools.ActivatorCreatorCodeGenDefault;
5458
import io.helidon.pico.tools.ActivatorCreatorConfigOptionsDefault;
5559
import io.helidon.pico.tools.ActivatorCreatorDefault;
5660
import io.helidon.pico.tools.ActivatorCreatorRequest;
61+
import io.helidon.pico.tools.ActivatorCreatorRequestDefault;
5762
import io.helidon.pico.tools.ActivatorCreatorResponse;
5863
import io.helidon.pico.tools.InterceptionPlan;
5964
import io.helidon.pico.tools.InterceptorCreatorProvider;
@@ -68,6 +73,7 @@
6873
import jakarta.annotation.PreDestroy;
6974
import jakarta.inject.Inject;
7075

76+
import static io.helidon.builder.processor.tools.BeanUtils.isBuiltInJavaType;
7177
import static io.helidon.builder.processor.tools.BuilderTypeTools.createTypeNameFromElement;
7278
import static io.helidon.common.types.TypeNameDefault.createFromTypeName;
7379
import static io.helidon.pico.processor.ActiveProcessorUtils.MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR;
@@ -81,6 +87,10 @@
8187
import static io.helidon.pico.processor.GeneralProcessorUtils.toScopeNames;
8288
import static io.helidon.pico.processor.GeneralProcessorUtils.toServiceTypeHierarchy;
8389
import static io.helidon.pico.processor.GeneralProcessorUtils.toWeight;
90+
import static io.helidon.pico.processor.ProcessingTracker.DEFAULT_SCRATCH_FILE_NAME;
91+
import static io.helidon.pico.processor.ProcessingTracker.initializeFrom;
92+
import static io.helidon.pico.tools.CodeGenFiler.scratchClassOutputPath;
93+
import static io.helidon.pico.tools.CodeGenFiler.targetClassOutputPath;
8494
import static io.helidon.pico.tools.TypeTools.createTypedElementInfoFromElement;
8595
import static io.helidon.pico.tools.TypeTools.toAccess;
8696
import static java.util.Objects.requireNonNull;
@@ -112,6 +122,7 @@ public class PicoAnnotationProcessor extends BaseAnnotationProcessor {
112122

113123
private final Set<TypedElementInfo> allElementsOfInterestInThisModule = new LinkedHashSet<>();
114124
private final Map<TypeName, TypeInfo> typeInfoToCreateActivatorsForInThisModule = new LinkedHashMap<>();
125+
private ProcessingTracker tracker;
115126
private CreatorHandler creator;
116127
private boolean autoAddInterfaces;
117128

@@ -149,10 +160,7 @@ public void init(ProcessingEnvironment processingEnv) {
149160
super.init(processingEnv);
150161
this.autoAddInterfaces = Options.isOptionEnabled(Options.TAG_AUTO_ADD_NON_CONTRACT_INTERFACES);
151162
this.creator = new CreatorHandler(getClass().getSimpleName(), processingEnv, utils());
152-
// if (BaseAnnotationProcessor.ENABLED) {
153-
// // we are is simulation mode when the base one is operating...
154-
// this.creator.activateSimulationMode();
155-
// }
163+
this.tracker = initializeFrom(trackerStatePath(), processingEnv);
156164
}
157165

158166
@Override
@@ -165,7 +173,6 @@ public boolean process(Set<? extends TypeElement> annotations,
165173
}
166174

167175
ServicesToProcess.onBeginProcessing(utils(), getSupportedAnnotationTypes(), roundEnv);
168-
// ServicesToProcess.addOnDoneRunnable(CreatorHandler.reporting());
169176

170177
try {
171178
// build the model
@@ -243,6 +250,7 @@ protected Set<String> supportedElementTargetAnnotations() {
243250
* Code generate these {@link io.helidon.pico.api.Activator}'s ad {@link io.helidon.pico.api.ModuleComponent}'s.
244251
*
245252
* @param services the services to code generate
253+
* @throws ToolsException if there is problem code generating sources or resources
246254
*/
247255
protected void doFiler(ServicesToProcess services) {
248256
ActivatorCreatorCodeGen codeGen = ActivatorCreatorDefault.createActivatorCreatorCodeGen(services).orElse(null);
@@ -257,8 +265,28 @@ protected void doFiler(ServicesToProcess services) {
257265
.build();
258266
ActivatorCreatorRequest req = ActivatorCreatorDefault
259267
.createActivatorCreatorRequest(services, codeGen, configOptions, creator.filer(), false);
268+
Set<TypeName> allActivatorTypeNames = tracker.remainingTypeNames().stream()
269+
.map(TypeNameDefault::createFromTypeName)
270+
.collect(Collectors.toSet());
271+
if (!allActivatorTypeNames.isEmpty()) {
272+
req = ActivatorCreatorRequestDefault.toBuilder(req)
273+
.codeGen(ActivatorCreatorCodeGenDefault.toBuilder(req.codeGen())
274+
.allModuleActivatorTypeNames(allActivatorTypeNames)
275+
.build())
276+
.build();
277+
}
260278
ActivatorCreatorResponse res = creator.createModuleActivators(req);
261-
if (!res.success()) {
279+
if (res.success()) {
280+
res.activatorTypeNamesPutInComponentModule()
281+
.forEach(it -> tracker.processing(it.name()));
282+
if (processingOver) {
283+
try {
284+
tracker.close();
285+
} catch (IOException e) {
286+
throw new ToolsException(e.getMessage(), e);
287+
}
288+
}
289+
} else {
262290
ToolsException exc = new ToolsException("Error during codegen", res.error().orElse(null));
263291
utils().error(exc.getMessage(), exc);
264292
// should not get here since the error above should halt further processing
@@ -504,14 +532,11 @@ private void gatherContracts(Set<TypeName> contracts,
504532
if (fqProviderTypeName != null) {
505533
if (!genericTypeName.generic()) {
506534
providerForSet.add(genericTypeName);
507-
508-
Optional<String> moduleName = filterModuleName(typeInfo.moduleNameOf(genericTypeName));
509-
moduleName.ifPresent(externalModuleNamesRequired::add);
510-
if (moduleName.isPresent()) {
511-
externalContracts.add(genericTypeName);
512-
} else {
513-
contracts.add(genericTypeName);
514-
}
535+
extractModuleAndContract(contracts,
536+
externalContracts,
537+
externalModuleNamesRequired,
538+
typeInfo,
539+
genericTypeName);
515540
}
516541

517542
// if we are dealing with a Provider<> then we should add those too as module dependencies
@@ -529,13 +554,11 @@ private void gatherContracts(Set<TypeName> contracts,
529554
|| !isTypeAnInterface
530555
|| AnnotationAndValueDefault.findFirst(Contract.class, typeInfo.annotations()).isPresent();
531556
if (isTypeAContract) {
532-
Optional<String> moduleName = filterModuleName(typeInfo.moduleNameOf(genericTypeName));
533-
moduleName.ifPresent(externalModuleNamesRequired::add);
534-
if (moduleName.isPresent()) {
535-
externalContracts.add(genericTypeName);
536-
} else {
537-
contracts.add(genericTypeName);
538-
}
557+
extractModuleAndContract(contracts,
558+
externalContracts,
559+
externalModuleNamesRequired,
560+
typeInfo,
561+
genericTypeName);
539562
}
540563
}
541564
}
@@ -574,6 +597,20 @@ private void gatherContracts(Set<TypeName> contracts,
574597
true));
575598
}
576599

600+
private void extractModuleAndContract(Set<TypeName> contracts,
601+
Set<TypeName> externalContracts,
602+
Set<String> externalModuleNamesRequired,
603+
TypeInfo typeInfo,
604+
TypeName genericTypeName) {
605+
Optional<String> moduleName = filterModuleName(typeInfo.moduleNameOf(genericTypeName));
606+
moduleName.ifPresent(externalModuleNamesRequired::add);
607+
if (moduleName.isPresent() || isBuiltInJavaType(genericTypeName)) {
608+
externalContracts.add(genericTypeName);
609+
} else {
610+
contracts.add(genericTypeName);
611+
}
612+
}
613+
577614
private Optional<String> filterModuleName(Optional<String> moduleName) {
578615
String name = moduleName.orElse(null);
579616
if (name != null && (name.startsWith("java.") || name.startsWith("jdk"))) {
@@ -727,4 +764,8 @@ private void gatherTypeInfosToProcessInThisModule(Map<TypeName, TypeInfo> result
727764
});
728765
}
729766

767+
private Path trackerStatePath() {
768+
return scratchClassOutputPath(targetClassOutputPath(processingEnv.getFiler())).resolve(DEFAULT_SCRATCH_FILE_NAME);
769+
}
770+
730771
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.pico.processor;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.util.LinkedHashSet;
25+
import java.util.List;
26+
import java.util.Objects;
27+
import java.util.Set;
28+
import java.util.function.Function;
29+
30+
import javax.annotation.processing.ProcessingEnvironment;
31+
import javax.lang.model.element.TypeElement;
32+
33+
import io.helidon.pico.tools.ToolsException;
34+
35+
/**
36+
* This class adds persistent tracking (typically under ./target/XXX) to allow seamless full and/or incremental processing of
37+
* types to be tracked over repeated compilation cycles over time. It is expected to be integrated into a host annotation
38+
* processor implementation.
39+
* <p>
40+
* For example, when incremental processing occurs, the elements passed to process in all rounds will just be a subset of
41+
* all of the annotated services since the compiler (from the IDE) only recompiles the files that have been changed. This is
42+
* typically different from how maven invokes compilation (doing a full compile where all types will be seen in the round). The
43+
* {@link PicoAnnotationProcessor}, for example, would see this reduced subset of types in the round and would otherwise have
44+
* created a {@link io.helidon.pico.api.ModuleComponent} only representative of the reduced subset of classes. This would be
45+
* incorrect and lead to an invalid module component source file to have been generated.
46+
* <p>
47+
* We use this tracker to persist the list of generated activators much in the same way that
48+
* {@code META-INF/services} are tracked. A target scratch directory (i.e., target/pico in this case) is used instead - in order
49+
* to keep it out of the build jar.
50+
* <p>
51+
* Usage:
52+
* <ol>
53+
* <li>{@link #initializeFrom} - during the APT initialization phase</li>
54+
* <li>{@link #processing(String)} - during each processed type that the annotation processor visits in the round</li>
55+
* <li>{@link #removedTypeNames()} or {@link #remainingTypeNames()} as needed - to see the changes over time</li>
56+
* <li>{@link #close()} - during final lifecycle of the APT in order to persist state to be (re)written out to disk</li>
57+
* </ol>
58+
*
59+
* @see PicoAnnotationProcessor
60+
*/
61+
class ProcessingTracker implements AutoCloseable {
62+
static final String DEFAULT_SCRATCH_FILE_NAME = "activators.lst";
63+
64+
private final Path path;
65+
private final Set<String> allTypeNames;
66+
private final TypeElementFinder typeElementFinder;
67+
private final Set<String> foundOrProcessed = new LinkedHashSet<>();
68+
69+
/**
70+
* Creates an instance using the given path to keep persistent state.
71+
*
72+
* @param persistentScratchPath the fully qualified path to carry the state
73+
* @param allLines all lines read at initialization
74+
* @param typeElementFinder the type element finder (e.g., {@link ProcessingEnvironment#getElementUtils})
75+
*/
76+
ProcessingTracker(Path persistentScratchPath,
77+
List<String> allLines,
78+
TypeElementFinder typeElementFinder) {
79+
this.path = persistentScratchPath;
80+
this.allTypeNames = new LinkedHashSet<>(allLines);
81+
this.typeElementFinder = typeElementFinder;
82+
}
83+
84+
public static ProcessingTracker initializeFrom(Path persistentScratchPath,
85+
ProcessingEnvironment processingEnv) {
86+
List<String> allLines = List.of();
87+
File file = persistentScratchPath.toFile();
88+
if (file.exists() && file.canRead()) {
89+
try {
90+
allLines = Files.readAllLines(persistentScratchPath, StandardCharsets.UTF_8);
91+
} catch (IOException e) {
92+
throw new ToolsException(e.getMessage(), e);
93+
}
94+
}
95+
return new ProcessingTracker(persistentScratchPath, allLines, toTypeElementFinder(processingEnv));
96+
}
97+
98+
public ProcessingTracker processing(String typeName) {
99+
foundOrProcessed.add(Objects.requireNonNull(typeName));
100+
return this;
101+
}
102+
103+
public Set<String> allTypeNamesFromInitialization() {
104+
return allTypeNames;
105+
}
106+
107+
public Set<String> removedTypeNames() {
108+
Set<String> typeNames = new LinkedHashSet<>(allTypeNamesFromInitialization());
109+
typeNames.removeAll(remainingTypeNames());
110+
return typeNames;
111+
}
112+
113+
public Set<String> remainingTypeNames() {
114+
Set<String> typeNames = new LinkedHashSet<>(allTypeNamesFromInitialization());
115+
typeNames.addAll(foundOrProcessed);
116+
typeNames.removeIf(typeName -> !found(typeName));
117+
return typeNames;
118+
}
119+
120+
@Override
121+
public void close() throws IOException {
122+
Path parent = path.getParent();
123+
if (parent == null) {
124+
throw new ToolsException("bad path: " + path);
125+
}
126+
Files.createDirectories(parent);
127+
Files.write(path, remainingTypeNames(), StandardCharsets.UTF_8);
128+
}
129+
130+
private boolean found(String typeName) {
131+
return (typeElementFinder.apply(typeName) != null);
132+
}
133+
134+
private static TypeElementFinder toTypeElementFinder(ProcessingEnvironment processingEnv) {
135+
return typeName -> processingEnv.getElementUtils().getTypeElement(typeName);
136+
}
137+
138+
@FunctionalInterface
139+
interface TypeElementFinder extends Function<CharSequence, TypeElement> {
140+
}
141+
142+
}

0 commit comments

Comments
 (0)