Skip to content

Commit cc6447b

Browse files
authored
Break out a helper class from GenerateVisitor annotation processor. (#3060)
* Break out a helper class from GenerateVisitor annotation processor. This allows service loading all annotation processors and even determining that this one does not apply, without having javapoet in the classpath.
1 parent 646a698 commit cc6447b

File tree

3 files changed

+276
-236
lines changed

3 files changed

+276
-236
lines changed

docs/ReleaseNotes.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Our API stability annotations have been updated to reflect greater API instabili
1919
// begin next release
2020
### NEXT_RELEASE
2121
22-
* **Bug fix** Fix 1 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
22+
* **Bug fix** Break out a helper class from GenerateVisitor annotation processor [(Issue #3060)](https://github.com/FoundationDB/fdb-record-layer/issues/3060)
2323
* **Bug fix** Fix 2 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
2424
* **Bug fix** Fix 3 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
2525
* **Bug fix** Fix 4 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/*
2+
* GenerateVisitorAnnotationHelper.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.annotation;
22+
23+
import com.squareup.javapoet.AnnotationSpec;
24+
import com.squareup.javapoet.ClassName;
25+
import com.squareup.javapoet.CodeBlock;
26+
import com.squareup.javapoet.FieldSpec;
27+
import com.squareup.javapoet.JavaFile;
28+
import com.squareup.javapoet.MethodSpec;
29+
import com.squareup.javapoet.ParameterSpec;
30+
import com.squareup.javapoet.ParameterizedTypeName;
31+
import com.squareup.javapoet.TypeName;
32+
import com.squareup.javapoet.TypeSpec;
33+
import com.squareup.javapoet.TypeVariableName;
34+
import com.squareup.javapoet.WildcardTypeName;
35+
36+
import javax.annotation.Nonnull;
37+
import javax.annotation.processing.Filer;
38+
import javax.annotation.processing.Messager;
39+
import javax.annotation.processing.ProcessingEnvironment;
40+
import javax.annotation.processing.RoundEnvironment;
41+
import javax.lang.model.element.Element;
42+
import javax.lang.model.element.ElementKind;
43+
import javax.lang.model.element.Modifier;
44+
import javax.lang.model.element.PackageElement;
45+
import javax.lang.model.element.TypeElement;
46+
import javax.lang.model.type.TypeKind;
47+
import javax.lang.model.type.TypeMirror;
48+
import javax.lang.model.util.Types;
49+
import javax.tools.Diagnostic;
50+
import java.io.IOException;
51+
import java.util.List;
52+
import java.util.Locale;
53+
import java.util.Map;
54+
import java.util.Objects;
55+
import java.util.Set;
56+
import java.util.function.BiFunction;
57+
import java.util.stream.Collectors;
58+
59+
/**
60+
* A separate class to support (@link GenerateVisitorAnnotationProcessor) so that dependency on javapoet does not leak to anyone
61+
* just service loading all annotation processors in the class path.
62+
*/
63+
@SuppressWarnings("PMD.GuardLogStatement") // confused by error invocation
64+
class GenerateVisitorAnnotationHelper {
65+
private static final String parameterName = "element";
66+
67+
private GenerateVisitorAnnotationHelper() {
68+
}
69+
70+
static boolean process(final ProcessingEnvironment processingEnv, Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
71+
final var elementUtils = processingEnv.getElementUtils();
72+
final var typeUtils = processingEnv.getTypeUtils();
73+
final var messager = processingEnv.getMessager();
74+
final var filer = processingEnv.getFiler();
75+
76+
for (final Element annotatedElement : roundEnv.getElementsAnnotatedWith(GenerateVisitor.class)) {
77+
if (!annotatedElement.getKind().isClass() && !annotatedElement.getKind().isInterface()) {
78+
error(messager, annotatedElement, "only classes and interfaces can be annotated with %s", GenerateVisitor.class.getSimpleName());
79+
return true;
80+
}
81+
82+
final var rootTypeElement = (TypeElement)annotatedElement;
83+
84+
final var moduleElement = elementUtils.getModuleOf(annotatedElement);
85+
if (moduleElement == null) {
86+
error(messager, annotatedElement, "cannot annotate class with %s in null-module", GenerateVisitor.class.getSimpleName());
87+
return true;
88+
}
89+
90+
if (!isValidClass(rootTypeElement)) {
91+
error(messager,
92+
rootTypeElement, "The class %s cannot be annotated with this annotation.",
93+
rootTypeElement.getQualifiedName().toString());
94+
return true;
95+
}
96+
97+
final var generateVisitor = annotatedElement.getAnnotation(GenerateVisitor.class);
98+
final var rootTypeMirror = rootTypeElement.asType();
99+
100+
final var packageOfRoot = elementUtils.getPackageOf(rootTypeElement);
101+
final var subClassTypeMirrors = moduleElement
102+
.getEnclosedElements()
103+
.stream()
104+
.flatMap(packageElement -> packageElement.getEnclosedElements().stream())
105+
.filter(element -> element.getKind() == ElementKind.CLASS &&
106+
!element.getModifiers().contains(Modifier.ABSTRACT))
107+
.map(Element::asType)
108+
.filter(mirror -> mirror.getKind() == TypeKind.DECLARED)
109+
.filter(mirror -> typeUtils.isSubtype(mirror, rootTypeMirror))
110+
.collect(Collectors.toList());
111+
112+
try {
113+
generateCode(typeUtils, filer, generateVisitor, packageOfRoot, rootTypeElement, subClassTypeMirrors);
114+
} catch (final Exception exception) {
115+
Objects.requireNonNull(messager)
116+
.printMessage(Diagnostic.Kind.ERROR,
117+
"unable to generate visitor in " + packageOfRoot.getQualifiedName() + "[" + exception.getMessage() + "]");
118+
}
119+
}
120+
121+
return true;
122+
}
123+
124+
private static void generateCode(@Nonnull final Types typeUtils,
125+
@Nonnull final Filer filer,
126+
@Nonnull GenerateVisitor generateVisitor,
127+
@Nonnull final PackageElement packageElement,
128+
@Nonnull final TypeElement rootTypeElement,
129+
@Nonnull final List<TypeMirror> subClassTypeMirrors) throws IOException {
130+
final var rootTypeMirror = rootTypeElement.asType();
131+
final var interfaceName = rootTypeElement.getSimpleName() + generateVisitor.classSuffix();
132+
final var typeVariableName = TypeVariableName.get("T");
133+
final var defaultMethodName = generateVisitor.methodPrefix() + "Default";
134+
135+
generateInterface(typeUtils, filer, generateVisitor, packageElement, subClassTypeMirrors, rootTypeMirror, interfaceName, typeVariableName, defaultMethodName);
136+
137+
final var className = rootTypeElement.getSimpleName() + generateVisitor.classSuffix() + "WithDefaults";
138+
generateImplementationWithDefaults(typeUtils, filer, generateVisitor, packageElement, subClassTypeMirrors, className, interfaceName, typeVariableName, defaultMethodName);
139+
}
140+
141+
private static void generateInterface(@Nonnull final Types typeUtils,
142+
@Nonnull final Filer filer,
143+
@Nonnull final GenerateVisitor generateVisitor,
144+
@Nonnull final PackageElement packageElement,
145+
@Nonnull final List<TypeMirror> subClassTypeMirrors,
146+
@Nonnull final TypeMirror rootTypeMirror,
147+
@Nonnull final String interfaceName,
148+
@Nonnull final TypeVariableName typeVariableName,
149+
@Nonnull final String defaultMethodName) throws IOException {
150+
final TypeSpec.Builder typeBuilder =
151+
TypeSpec.interfaceBuilder(interfaceName)
152+
.addModifiers(Modifier.PUBLIC)
153+
.addTypeVariable(typeVariableName);
154+
155+
final var jumpMapBuilder = FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(Map.class),
156+
ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(Object.class)),
157+
ParameterizedTypeName.get(ClassName.get(BiFunction.class),
158+
ParameterizedTypeName.get(ClassName.get(packageElement.getQualifiedName().toString(), interfaceName), WildcardTypeName.subtypeOf(Object.class)),
159+
TypeName.get(rootTypeMirror),
160+
WildcardTypeName.subtypeOf(Object.class))),
161+
"jumpMap", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);
162+
163+
final var initializerStrings = subClassTypeMirrors.stream()
164+
.map(typeMirror -> {
165+
final var typeElement = (TypeElement)typeUtils.asElement(typeMirror);
166+
return "Map.entry(" + typeElement.getSimpleName() + ".class, (visitor, element) -> visitor." + methodNameOfVisitMethod(generateVisitor, typeElement) + "((" + typeElement.getSimpleName() + ")element))";
167+
})
168+
.collect(Collectors.joining(", \n"));
169+
170+
final var initializerBlock = CodeBlock.builder()
171+
.add("$T.ofEntries(" + initializerStrings + ")", ClassName.get(Map.class))
172+
.build();
173+
174+
typeBuilder.addField(jumpMapBuilder
175+
.initializer(initializerBlock)
176+
.build());
177+
178+
for (final var typeMirror : subClassTypeMirrors) {
179+
final var typeElement = (TypeElement)typeUtils.asElement(typeMirror);
180+
final var methodName = methodNameOfVisitMethod(generateVisitor, typeElement);
181+
final MethodSpec.Builder specificVisitMethodBuilder =
182+
MethodSpec
183+
.methodBuilder(methodName)
184+
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
185+
.addAnnotation(Nonnull.class)
186+
.addParameter(ParameterSpec.builder(TypeName.get(typeMirror), parameterName).addAnnotation(Nonnull.class).build())
187+
.returns(typeVariableName);
188+
typeBuilder.addMethod(specificVisitMethodBuilder.build());
189+
}
190+
191+
final MethodSpec.Builder visitDefaultMethodBuilder =
192+
MethodSpec
193+
.methodBuilder(defaultMethodName)
194+
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
195+
.addAnnotation(Nonnull.class)
196+
.addParameter(ParameterSpec.builder(TypeName.get(rootTypeMirror), parameterName).addAnnotation(Nonnull.class).build())
197+
.returns(typeVariableName);
198+
typeBuilder.addMethod(visitDefaultMethodBuilder.build());
199+
200+
final MethodSpec.Builder visitMethodBuilder =
201+
MethodSpec
202+
.methodBuilder(generateVisitor.methodPrefix())
203+
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
204+
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "$S", "unchecked").build())
205+
.addParameter(ParameterSpec.builder(TypeName.get(rootTypeMirror), parameterName).addAnnotation(Nonnull.class).build())
206+
.returns(typeVariableName)
207+
.addCode(CodeBlock.builder()
208+
.addStatement("final var visitFn = jumpMap.get(" + parameterName + ".getClass())")
209+
.addStatement("return visitFn == null ? visitDefault(" + parameterName + ") : (" + typeVariableName + ")visitFn.apply(this, " + parameterName + ")")
210+
.build());
211+
typeBuilder.addMethod(visitMethodBuilder.build());
212+
213+
JavaFile.builder(packageElement.getQualifiedName().toString(), typeBuilder.build())
214+
.skipJavaLangImports(true)
215+
.build()
216+
.writeTo(Objects.requireNonNull(filer));
217+
}
218+
219+
private static void generateImplementationWithDefaults(@Nonnull final Types typeUtils,
220+
@Nonnull final Filer filer,
221+
@Nonnull final GenerateVisitor generateVisitor,
222+
@Nonnull final PackageElement packageElement,
223+
@Nonnull final List<TypeMirror> subClassTypeMirrors,
224+
@Nonnull final String className,
225+
@Nonnull final String interfaceName,
226+
@Nonnull final TypeVariableName typeVariableName,
227+
@Nonnull final String defaultMethodName) throws IOException {
228+
final TypeSpec.Builder typeBuilder =
229+
TypeSpec.interfaceBuilder(className)
230+
.addModifiers(Modifier.PUBLIC)
231+
.addTypeVariable(typeVariableName)
232+
.addSuperinterface(ParameterizedTypeName.get(ClassName.get(packageElement.getQualifiedName().toString(), interfaceName), typeVariableName));
233+
234+
for (final var typeMirror : subClassTypeMirrors) {
235+
final var typeElement = (TypeElement)typeUtils.asElement(typeMirror);
236+
final var methodName = methodNameOfVisitMethod(generateVisitor, typeElement);
237+
final MethodSpec.Builder specificVisitMethodBuilder =
238+
MethodSpec
239+
.methodBuilder(methodName)
240+
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
241+
.addAnnotation(Nonnull.class)
242+
.addAnnotation(Override.class)
243+
.addParameter(ParameterSpec.builder(TypeName.get(typeMirror), parameterName).addAnnotation(Nonnull.class).build())
244+
.returns(typeVariableName)
245+
.addCode(CodeBlock.builder()
246+
.addStatement("return " + defaultMethodName + "(" + parameterName + ")")
247+
.build());
248+
typeBuilder.addMethod(specificVisitMethodBuilder.build());
249+
}
250+
251+
JavaFile.builder(packageElement.getQualifiedName().toString(), typeBuilder.build())
252+
.skipJavaLangImports(true)
253+
.build()
254+
.writeTo(Objects.requireNonNull(filer));
255+
}
256+
257+
private static String methodNameOfVisitMethod(@Nonnull final GenerateVisitor generateVisitor, @Nonnull TypeElement typeElement) {
258+
return generateVisitor.methodPrefix() + typeElement.getSimpleName().toString().replace(generateVisitor.stripPrefix(), "");
259+
}
260+
261+
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
262+
private static boolean isValidClass(final TypeElement annotatedClassElement) {
263+
return annotatedClassElement.getModifiers().contains(Modifier.PUBLIC);
264+
}
265+
266+
private static void error(final Messager messager,
267+
final Element e,
268+
final String msg,
269+
final Object... args) {
270+
Objects.requireNonNull(messager).printMessage(Diagnostic.Kind.ERROR,
271+
String.format(Locale.ROOT, msg, args),
272+
e);
273+
}
274+
}

0 commit comments

Comments
 (0)