Skip to content

Commit 839717c

Browse files
committed
GROOVY-11973: Allow ASTTransformationCustomizer to support annotations with multiple AST transforms (e.g. @Sealed, @recordbase) and @AnnotationCollector aliases (e.g. @AutoExternalize) via a new forAnnotation factory method
1 parent 981cb53 commit 839717c

3 files changed

Lines changed: 129 additions & 9 deletions

File tree

src/main/groovy/org/codehaus/groovy/control/customizers/ASTTransformationCustomizer.groovy

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.codehaus.groovy.control.customizers
2020

21+
import groovy.transform.AnnotationCollector
2122
import groovy.transform.AutoFinal
2223
import groovy.transform.CompilationUnitAware
2324
import groovy.transform.CompileStatic
@@ -195,22 +196,87 @@ class ASTTransformationCustomizer extends CompilationCustomizer implements Compi
195196

196197
@SuppressWarnings('ClassForName')
197198
private static Class<ASTTransformation> findASTTransformationClass(Class<? extends Annotation> anAnnotationClass, ClassLoader transformationClassLoader) {
199+
List<Class<ASTTransformation>> classes = findASTTransformationClasses(anAnnotationClass, transformationClassLoader)
200+
if (classes.size() > 1) {
201+
throw new IllegalArgumentException("AST transformation customizer doesn't support AST transforms with multiple classes; use ASTTransformationCustomizer.forAnnotation(...) to obtain one customizer per transform class")
202+
}
203+
classes[0]
204+
}
205+
206+
@SuppressWarnings('ClassForName')
207+
private static List<Class<ASTTransformation>> findASTTransformationClasses(Class<? extends Annotation> anAnnotationClass, ClassLoader transformationClassLoader) {
198208
GroovyASTTransformationClass annotation = anAnnotationClass.getAnnotation(GroovyASTTransformationClass)
199209
if (annotation == null) throw new IllegalArgumentException("Provided class doesn't look like an AST @interface")
200210

201-
Class[] classes = annotation.classes()
202-
String[] classesAsStrings = annotation.value()
203-
if (classes.length + classesAsStrings.length > 1) {
204-
throw new IllegalArgumentException("AST transformation customizer doesn't support AST transforms with multiple classes")
211+
ClassLoader loader = transformationClassLoader ?: anAnnotationClass.classLoader
212+
List<Class<ASTTransformation>> result = []
213+
annotation.classes().each { Class c -> result << (c as Class<ASTTransformation>) }
214+
annotation.value().each { String name -> result << (Class.forName(name, true, loader) as Class<ASTTransformation>) }
215+
if (result.isEmpty()) {
216+
throw new IllegalArgumentException("No AST transformation class found for ${anAnnotationClass.name}")
205217
}
206-
classes ? classes[0] : (Class) Class.forName(classesAsStrings[0], true, transformationClassLoader ?: anAnnotationClass.classLoader)
218+
result
207219
}
208220

209221
@SuppressWarnings('ClassForName')
210222
private static Class<ASTTransformation> findASTTransformationClass(Class<? extends Annotation> anAnnotationClass, String astTransformationClassName, ClassLoader transformationClassLoader) {
211223
Class.forName(astTransformationClassName, true, transformationClassLoader ?: anAnnotationClass.classLoader) as Class<ASTTransformation>
212224
}
213225

226+
/**
227+
* Creates one {@link ASTTransformationCustomizer} per AST transformation class declared by the
228+
* given annotation's {@link GroovyASTTransformationClass} list. This is the way to use the
229+
* customizer with annotations whose implementation is split across multiple transforms running
230+
* at different compile phases (e.g. {@link groovy.transform.Sealed}, {@link groovy.transform.RecordBase}).
231+
* <p>
232+
* Spread the result into {@link org.codehaus.groovy.control.CompilerConfiguration#addCompilationCustomizers}:
233+
* <pre>
234+
* configuration.addCompilationCustomizers(*ASTTransformationCustomizer.forAnnotation(Sealed))
235+
* </pre>
236+
*
237+
* @since 6.0.0
238+
*/
239+
static List<ASTTransformationCustomizer> forAnnotation(Class<? extends Annotation> transformationAnnotation) {
240+
forAnnotation([:], transformationAnnotation, transformationAnnotation.classLoader)
241+
}
242+
243+
/**
244+
* @see #forAnnotation(Class)
245+
* @since 6.0.0
246+
*/
247+
static List<ASTTransformationCustomizer> forAnnotation(Class<? extends Annotation> transformationAnnotation, ClassLoader transformationClassLoader) {
248+
forAnnotation([:], transformationAnnotation, transformationClassLoader)
249+
}
250+
251+
/**
252+
* @see #forAnnotation(Class)
253+
* @since 6.0.0
254+
*/
255+
static List<ASTTransformationCustomizer> forAnnotation(Map annotationParams, Class<? extends Annotation> transformationAnnotation) {
256+
forAnnotation(annotationParams, transformationAnnotation, transformationAnnotation.classLoader)
257+
}
258+
259+
/**
260+
* @see #forAnnotation(Class)
261+
* @since 6.0.0
262+
*/
263+
static List<ASTTransformationCustomizer> forAnnotation(Map annotationParams, Class<? extends Annotation> transformationAnnotation, ClassLoader transformationClassLoader) {
264+
// expand @AnnotationCollector aliases (e.g. @AutoExternalize -> @ExternalizeMethods + @ExternalizeVerifier)
265+
AnnotationCollector collector = transformationAnnotation.getAnnotation(AnnotationCollector)
266+
if (collector != null) {
267+
List<ASTTransformationCustomizer> result = []
268+
for (Class<? extends Annotation> aliased : collector.value()) {
269+
result.addAll(forAnnotation(annotationParams, aliased, transformationClassLoader))
270+
}
271+
return result
272+
}
273+
findASTTransformationClasses(transformationAnnotation, transformationClassLoader).collect { Class<ASTTransformation> txClass ->
274+
annotationParams
275+
? new ASTTransformationCustomizer(annotationParams, transformationAnnotation, txClass.name, transformationClassLoader)
276+
: new ASTTransformationCustomizer(transformationAnnotation, txClass.name, transformationClassLoader)
277+
}
278+
}
279+
214280
private static CompilePhase findPhase(ASTTransformation transformation) {
215281
if (transformation == null) throw new IllegalArgumentException('Provided transformation must not be null')
216282
Class<?> clazz = transformation.class

src/main/java/org/codehaus/groovy/transform/SealedCompletionASTTransformation.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ public void visit(ASTNode[] nodes, SourceUnit source) {
5252
if (!SEALED_TYPE.equals(anno.getClassNode())) return;
5353

5454
if (parent instanceof ClassNode) {
55-
addDetectedSealedClasses((ClassNode) parent);
55+
addDetectedSealedClasses((ClassNode) parent, anno);
5656
}
5757
}
5858

59-
private void addDetectedSealedClasses(ClassNode node) {
59+
private void addDetectedSealedClasses(ClassNode node, AnnotationNode anno) {
6060
boolean sealed = Boolean.TRUE.equals(node.getNodeMetaData(SEALED_CLASS));
6161
List<ClassNode> permitted = node.getPermittedSubclasses();
6262
if (!sealed || !permitted.isEmpty() || node.getModule() == null) return;
@@ -70,11 +70,11 @@ private void addDetectedSealedClasses(ClassNode node) {
7070
}
7171
}
7272
}
73+
if (permitted.isEmpty()) return;
7374
List<Expression> names = new ArrayList<>();
7475
for (ClassNode next : permitted) {
7576
names.add(classX(ClassHelper.make(next.getName())));
7677
}
77-
AnnotationNode an = node.getAnnotations(SEALED_TYPE).get(0);
78-
an.addMember("permittedSubclasses", new ListExpression(names));
78+
anno.addMember("permittedSubclasses", new ListExpression(names));
7979
}
8080
}

src/test/groovy/org/codehaus/groovy/control/customizers/ASTTransformationCustomizerTest.groovy

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,60 @@ final class ASTTransformationCustomizerTest {
177177
'''
178178
}
179179

180+
@Test
181+
void testForAnnotationSealed() {
182+
def config = new CompilerConfiguration()
183+
config.addCompilationCustomizers(*ASTTransformationCustomizer.forAnnotation(Sealed))
184+
def shell = new GroovyShell(config)
185+
def result = shell.evaluate '''
186+
class Shape { }
187+
class Circle extends Shape { }
188+
class Square extends Shape { }
189+
[Shape, Circle, Square]
190+
'''
191+
assert result[0].permittedSubclasses*.simpleName.toSet() == ['Circle', 'Square'] as Set
192+
}
193+
194+
@Test
195+
void testForAnnotationWithSingleTransform() {
196+
// single-transform annotations should also work via the factory
197+
def config = new CompilerConfiguration()
198+
config.addCompilationCustomizers(*ASTTransformationCustomizer.forAnnotation(Log))
199+
def shell = new GroovyShell(config)
200+
def result = shell.evaluate '''
201+
class MyClass { }
202+
new MyClass()
203+
'''
204+
assert result.log.class == java.util.logging.Logger
205+
}
206+
207+
@Test
208+
void testForAnnotationCollector() {
209+
// @AutoExternalize is an @AnnotationCollector bundling @ExternalizeMethods + @ExternalizeVerifier
210+
def config = new CompilerConfiguration()
211+
config.addCompilationCustomizers(*ASTTransformationCustomizer.forAnnotation(AutoExternalize))
212+
def shell = new GroovyShell(config)
213+
def result = shell.evaluate '''
214+
class Person {
215+
String first, last
216+
}
217+
new Person(first: 'a', last: 'b')
218+
'''
219+
assert result instanceof Externalizable
220+
}
221+
222+
@Test
223+
void testForAnnotationWithParams() {
224+
def config = new CompilerConfiguration()
225+
config.addCompilationCustomizers(*ASTTransformationCustomizer.forAnnotation([value: 'logger'], Log))
226+
def shell = new GroovyShell(config)
227+
def result = shell.evaluate '''
228+
class MyClass { }
229+
new MyClass()
230+
'''
231+
assert result.logger.class == java.util.logging.Logger
232+
}
233+
180234
//--------------------------------------------------------------------------
181235

182236
@Test

0 commit comments

Comments
 (0)