Skip to content

Commit 916f029

Browse files
authored
Fix runtime type reflection in nf-lang (#7077)
1 parent d96f6de commit 916f029

14 files changed

Lines changed: 221 additions & 150 deletions

File tree

modules/nextflow/src/main/groovy/nextflow/plugin/spec/ConfigSpec.groovy

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616
package nextflow.plugin.spec
1717

18+
import java.lang.reflect.ParameterizedType
19+
import java.lang.reflect.Type
20+
1821
import groovy.transform.CompileStatic
1922
import nextflow.config.spec.SpecNode
20-
import nextflow.script.types.Types
21-
import org.codehaus.groovy.ast.ClassNode
23+
import nextflow.script.dsl.Types
2224

2325
/**
2426
* Generate specs for config scopes.
@@ -44,7 +46,7 @@ class ConfigSpec {
4446

4547
private static Map<String,?> fromOption(SpecNode.Option node, String name) {
4648
final description = node.description().stripIndent(true).trim()
47-
final types = node.types().collect { t -> fromType(new ClassNode(t)) }
49+
final types = node.types().collect { t -> fromType(t) }
4850

4951
return [
5052
type: 'ConfigOption',
@@ -89,14 +91,13 @@ class ConfigSpec {
8991
]
9092
}
9193

92-
private static Object fromType(ClassNode cn) {
93-
final name = Types.getName(cn.getTypeClass())
94-
if( !cn.isGenericsPlaceHolder() && cn.getGenericsTypes() != null ) {
95-
final typeArguments = cn.getGenericsTypes().collect { gt -> fromType(gt.getType()) }
94+
private static Object fromType(Type type) {
95+
if( type instanceof ParameterizedType ) {
96+
final name = Types.getName(type.getRawType())
97+
final typeArguments = type.getActualTypeArguments().collect { t -> fromType(t) }
9698
return [ name: name, typeArguments: typeArguments ]
9799
}
98-
else {
99-
return name
100-
}
100+
101+
return Types.getName(type)
101102
}
102103
}

modules/nextflow/src/main/groovy/nextflow/plugin/spec/FunctionSpec.groovy

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
package nextflow.plugin.spec
1717

1818
import java.lang.reflect.Method
19+
import java.lang.reflect.ParameterizedType
20+
import java.lang.reflect.Type
1921

2022
import groovy.transform.CompileStatic
2123
import nextflow.script.dsl.Description
22-
import nextflow.script.types.Types
23-
import org.codehaus.groovy.ast.ClassNode
24+
import nextflow.script.dsl.Types
2425

2526
/**
2627
* Generate specs for functions, channel factories, and operators.
@@ -37,7 +38,7 @@ class FunctionSpec {
3738
final parameters = method.getParameters().collect { param ->
3839
[
3940
name: param.getName(),
40-
type: fromType(param.getType())
41+
type: fromType(param.getParameterizedType())
4142
]
4243
}
4344

@@ -52,18 +53,13 @@ class FunctionSpec {
5253
]
5354
}
5455

55-
private static Object fromType(Class c) {
56-
return fromType(new ClassNode(c))
57-
}
58-
59-
private static Object fromType(ClassNode cn) {
60-
final name = Types.getName(cn.getTypeClass())
61-
if( !cn.isGenericsPlaceHolder() && cn.getGenericsTypes() != null ) {
62-
final typeArguments = cn.getGenericsTypes().collect { gt -> fromType(gt.getType()) }
56+
private static Object fromType(Type type) {
57+
if( type instanceof ParameterizedType ) {
58+
final name = Types.getName(type.getRawType())
59+
final typeArguments = type.getActualTypeArguments().collect { t -> fromType(t) }
6360
return [ name: name, typeArguments: typeArguments ]
6461
}
65-
else {
66-
return name
67-
}
62+
63+
return Types.getName(type)
6864
}
6965
}

modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import nextflow.script.ProcessConfigV2
7575
import nextflow.script.ScriptMeta
7676
import nextflow.script.ScriptType
7777
import nextflow.script.bundle.ResourcesBundle
78+
import nextflow.script.dsl.Types
7879
import nextflow.script.params.BaseOutParam
7980
import nextflow.script.params.CmdEvalParam
8081
import nextflow.script.params.DefaultOutParam
@@ -97,7 +98,6 @@ import nextflow.script.params.v2.ProcessInput
9798
import nextflow.script.params.v2.ProcessTupleInput
9899
import nextflow.script.types.Record
99100
import nextflow.script.types.Tuple
100-
import nextflow.script.types.Types
101101
import nextflow.trace.TraceRecord
102102
import nextflow.util.Escape
103103
import nextflow.util.HashBuilder

modules/nextflow/src/main/groovy/nextflow/script/ParamsDsl.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import groovy.util.logging.Slf4j
2626
import nextflow.Session
2727
import nextflow.file.FileHelper
2828
import nextflow.exception.ScriptRuntimeException
29+
import nextflow.script.dsl.Types
2930
import nextflow.script.types.Bag
30-
import nextflow.script.types.Types
3131
import nextflow.splitter.CsvSplitter
3232
import nextflow.util.ArrayBag
3333
import nextflow.util.Duration

modules/nextflow/src/main/groovy/nextflow/script/TypeDef.groovy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class TypeDef extends ComponentDef {
3939
this.alias = alias
4040
}
4141

42+
Class getTarget() { target }
43+
4244
@Override
4345
String getType() { 'type' }
4446

modules/nextflow/src/test/groovy/nextflow/plugin/spec/PluginSpecTest.groovy

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -47,57 +47,65 @@ class PluginSpecTest extends Specification {
4747
expect:
4848
definitions.size() == 4
4949
and:
50-
definitions[0] == [
51-
type: 'ConfigScope',
52-
spec: [
53-
name: 'hello',
54-
description: 'The `hello` scope controls the behavior of the `nf-hello` plugin.',
55-
children: [
56-
[
57-
type: 'ConfigOption',
58-
spec: [
59-
name: 'message',
60-
description: 'Message to print to standard output when the plugin is enabled.',
61-
type: 'String',
62-
additionalTypes: []
63-
]
64-
]
50+
definitions[0].type == 'ConfigScope'
51+
definitions[0].spec.name == 'hello'
52+
definitions[0].spec.description == 'The `hello` scope controls the behavior of the `nf-hello` plugin.'
53+
definitions[0].spec.children.sort { it.spec.name } == [
54+
[
55+
type: 'ConfigOption',
56+
spec: [
57+
name: 'message',
58+
description: 'Message to print to standard output when the plugin is enabled.',
59+
type: 'String',
60+
additionalTypes: []
61+
]
62+
],
63+
[
64+
type: 'ConfigOption',
65+
spec: [
66+
name: 'names',
67+
description: 'Names to address when the plugin is enabled.',
68+
type: [
69+
name: 'List',
70+
typeArguments: [ 'String' ]
71+
],
72+
additionalTypes: []
6573
]
6674
]
6775
]
68-
definitions[1] == [
69-
type: 'Factory',
70-
spec: [
71-
name: 'helloFactory',
72-
description: null,
73-
returnType: 'DataflowWriteChannel',
74-
parameters: []
75-
]
76+
and:
77+
definitions[1].type == 'Factory'
78+
definitions[1].spec == [
79+
name: 'helloFactory',
80+
description: null,
81+
returnType: 'DataflowWriteChannel',
82+
parameters: []
7683
]
77-
definitions[2] == [
78-
type: 'Operator',
79-
spec: [
80-
name: 'helloOperator',
81-
description: null,
82-
returnType: 'DataflowWriteChannel',
83-
parameters: [
84-
[
85-
name: 'arg0',
86-
type: 'DataflowReadChannel'
87-
]
84+
and:
85+
definitions[2].type == 'Operator'
86+
definitions[2].spec == [
87+
name: 'helloOperator',
88+
description: null,
89+
returnType: 'DataflowWriteChannel',
90+
parameters: [
91+
[
92+
name: 'arg0',
93+
type: 'DataflowReadChannel'
8894
]
8995
]
9096
]
91-
definitions[3] == [
92-
type: 'Function',
93-
spec: [
94-
name: 'sayHello',
95-
description: 'Say hello to the given targets',
96-
returnType: 'void',
97-
parameters: [
98-
[
99-
name: 'arg0',
100-
type: 'List'
97+
and:
98+
definitions[3].type == 'Function'
99+
definitions[3].spec == [
100+
name: 'sayHello',
101+
description: 'Say hello to the given targets',
102+
returnType: 'void',
103+
parameters: [
104+
[
105+
name: 'arg0',
106+
type: [
107+
name: 'List',
108+
typeArguments: [ 'String' ]
101109
]
102110
]
103111
]
@@ -115,6 +123,12 @@ class TestConfig implements ConfigScope {
115123
Message to print to standard output when the plugin is enabled.
116124
''')
117125
String message
126+
127+
@ConfigOption
128+
@Description('''
129+
Names to address when the plugin is enabled.
130+
''')
131+
List<String> names
118132
}
119133

120134

modules/nextflow/src/test/groovy/nextflow/script/ScriptTypesTest.groovy

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package nextflow.script
1818

19+
import java.lang.reflect.ParameterizedType
1920
import java.nio.file.Files
21+
import java.nio.file.Path
2022

2123
import nextflow.exception.ScriptCompilationException
2224
import test.Dsl2Spec
@@ -195,4 +197,49 @@ class ScriptTypesTest extends Dsl2Spec {
195197
cleanup:
196198
folder?.deleteDir()
197199
}
200+
201+
def 'should expose type annotations via reflection'() {
202+
when:
203+
def script = loadScript(
204+
'''\
205+
record Sample {
206+
id: String
207+
reads: List<Path>
208+
}
209+
''',
210+
module: true
211+
)
212+
def meta = ScriptMeta.get(script)
213+
def typeDef = meta.getComponent('Sample') as TypeDef
214+
def type = typeDef.getTarget()
215+
then:
216+
type.getField('id').getType() == String
217+
type.getField('id').getGenericType() instanceof Class
218+
type.getField('id').getGenericType() == String
219+
type.getField('reads').getType() == List
220+
type.getField('reads').getGenericType() instanceof ParameterizedType
221+
type.getField('reads').getGenericType().getRawType() == List
222+
type.getField('reads').getGenericType().getActualTypeArguments()[0] instanceof Class
223+
type.getField('reads').getGenericType().getActualTypeArguments()[0] == Path
224+
225+
when:
226+
script = loadScript(
227+
'''\
228+
def greet(message: String, names: List<String>) {
229+
}
230+
''',
231+
module: true
232+
)
233+
def method = script.getClass().getDeclaredMethods().find { m -> m.name == 'greet' }
234+
def params = method.getParameters()
235+
then:
236+
params[0].getType() == String
237+
params[0].getParameterizedType() instanceof Class
238+
params[0].getParameterizedType() == String
239+
params[1].getType() == List
240+
params[1].getParameterizedType() instanceof ParameterizedType
241+
params[1].getParameterizedType().getRawType() == List
242+
params[1].getParameterizedType().getActualTypeArguments()[0] instanceof Class
243+
params[1].getParameterizedType().getActualTypeArguments()[0] == String
244+
}
198245
}

modules/nf-lang/src/main/java/nextflow/config/control/ConfigParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import nextflow.config.parser.ConfigParserPluginFactory;
2222
import nextflow.script.control.Compiler;
2323
import nextflow.script.control.LazyErrorCollector;
24-
import nextflow.script.types.Types;
24+
import nextflow.script.dsl.Types;
2525
import org.codehaus.groovy.control.CompilerConfiguration;
2626
import org.codehaus.groovy.control.SourceUnit;
2727
import org.codehaus.groovy.control.messages.WarningMessage;

modules/nf-lang/src/main/java/nextflow/config/spec/SpecNode.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.reflect.Field;
2020
import java.lang.reflect.Method;
2121
import java.lang.reflect.ParameterizedType;
22+
import java.lang.reflect.Type;
2223
import java.util.ArrayList;
2324
import java.util.HashMap;
2425
import java.util.List;
@@ -109,10 +110,10 @@ private static String annotatedDescription(AnnotatedElement el, String defaultVa
109110
return annot != null ? annot.value() : defaultValue;
110111
}
111112

112-
private static List<Class> optionTypes(Field field) {
113-
var result = new ArrayList<Class>();
113+
private static List<Type> optionTypes(Field field) {
114+
var result = new ArrayList<Type>();
114115
// use the field type
115-
result.add(field.getType());
116+
result.add(field.getGenericType());
116117
// append types from ConfigOption annotation if specified
117118
var annot = field.getAnnotation(ConfigOption.class);
118119
if( annot != null ) {
@@ -122,6 +123,14 @@ private static List<Class> optionTypes(Field field) {
122123
return result;
123124
}
124125

126+
private static Class rawType(Type type) {
127+
if( type instanceof Class c )
128+
return c;
129+
if( type instanceof ParameterizedType pt )
130+
return (Class) pt.getRawType();
131+
throw new IllegalStateException();
132+
}
133+
125134
/**
126135
* Models a config option that is defined through a DSL
127136
* instead of an assignment (i.e. `plugins`).
@@ -136,7 +145,7 @@ public static record DslOption(
136145
*/
137146
public static record Option(
138147
String description,
139-
List<Class> types
148+
List<Type> types
140149
) implements SpecNode {}
141150

142151
/**
@@ -212,23 +221,23 @@ public static Scope of(Class<? extends ConfigScope> scope, String description) {
212221
var children = new HashMap<String, SpecNode>();
213222
for( var field : scope.getDeclaredFields() ) {
214223
var name = field.getName();
215-
var type = field.getType();
224+
var type = field.getGenericType();
225+
var rawType = rawType(type);
216226
var desc = annotatedDescription(field, description);
217227
var placeholderName = field.getAnnotation(PlaceholderName.class);
218228
// fields annotated with @ConfigOption are config options
219229
if( field.getAnnotation(ConfigOption.class) != null ) {
220-
if( DslScope.class.isAssignableFrom(type) )
221-
children.put(name, new DslOption(desc, type));
230+
if( DslScope.class.isAssignableFrom(rawType) )
231+
children.put(name, new DslOption(desc, rawType));
222232
else
223233
children.put(name, new Option(desc, optionTypes(field)));
224234
}
225-
// fields of type ConfigScope are nested config scopes
226-
else if( ConfigScope.class.isAssignableFrom(type) ) {
227-
children.put(name, Scope.of((Class<? extends ConfigScope>) type, desc));
235+
// fields of rawType ConfigScope are nested config scopes
236+
else if( ConfigScope.class.isAssignableFrom(rawType) ) {
237+
children.put(name, Scope.of((Class<? extends ConfigScope>) rawType, desc));
228238
}
229239
// fields of type Map<String, ConfigScope> are placeholder scopes
230-
else if( Map.class.isAssignableFrom(type) && placeholderName != null ) {
231-
var pt = (ParameterizedType)field.getGenericType();
240+
else if( Map.class.isAssignableFrom(rawType) && type instanceof ParameterizedType pt && placeholderName != null ) {
232241
var valueType = (Class<? extends ConfigScope>)pt.getActualTypeArguments()[1];
233242
children.put(name, new Placeholder(desc, placeholderName.value(), Scope.of(valueType, desc)));
234243
}

0 commit comments

Comments
 (0)