Skip to content

Commit e48aa31

Browse files
committed
Type annotations for params
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
1 parent d4d8716 commit e48aa31

14 files changed

Lines changed: 164 additions & 30 deletions

File tree

docs/migrations/25-10.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,31 @@ This page summarizes the upcoming changes in Nextflow 25.10, which will be relea
88
This page is a work in progress and will be updated as features are finalized. It should not be considered complete until the 25.10 release.
99
:::
1010

11+
## New features
12+
13+
<h3>Workflow params</h3>
14+
15+
The `params` block is a new way to declare pipeline parameters in a Nextflow script:
16+
17+
```nextflow
18+
params {
19+
// Path to input data.
20+
input: Path
21+
22+
// Whether to save intermediate files.
23+
save_intermeds: Boolean = false
24+
}
25+
26+
workflow {
27+
println "params.input = ${params.input}"
28+
println "params.save_intermeds = ${params.save_intermeds}"
29+
}
30+
```
31+
32+
This syntax allows you to declare all parameters in one place and allows Nextflow to validate them at runtime.
33+
34+
See {ref}`workflow-params-def` for details.
35+
1136
## Breaking changes
1237

1338
- The AWS Java SDK used by Nextflow was upgraded from v1 to v2, which introduced some breaking changes to the `aws.client` config options. See {ref}`the guide <aws-java-sdk-v2-page>` for details.

docs/reference/syntax.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ The params block consists of one or more *parameter declarations*. A parameter d
114114

115115
```nextflow
116116
params {
117-
input
118-
save_intermeds = false
117+
input: Path
118+
save_intermeds: Boolean = false
119119
}
120120
```
121121

docs/workflow.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ workflow {
2222
}
2323
```
2424

25+
(workflow-params-def)=
26+
2527
## Parameters
2628

2729
Parameters can be declared in a Nextflow script with the `params` block or with *legacy* parameter declarations.
@@ -42,15 +44,23 @@ params {
4244
/**
4345
* Path to input data.
4446
*/
45-
input
47+
input: Path
4648
4749
/**
4850
* Whether to save intermediate files.
4951
*/
50-
save_intermeds = false
52+
save_intermeds: Boolean = false
5153
}
5254
```
5355

56+
The following types can be used for parameters:
57+
58+
- Boolean
59+
- Integer
60+
- Number
61+
- {ref}`stdlib-types-path`
62+
- {ref}`stdlib-types-string`
63+
5464
Parameters can be used in the entry workflow:
5565

5666
```nextflow

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

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616

1717
package nextflow.script
1818

19+
import java.nio.file.Path
20+
21+
import groovy.transform.Canonical
1922
import groovy.transform.CompileStatic
2023
import groovy.util.logging.Slf4j
2124
import nextflow.Session
25+
import nextflow.file.FileHelper
2226
import nextflow.exception.ScriptRuntimeException
27+
import nextflow.script.types.Types
2328
/**
2429
* Implements the DSL for defining workflow params
2530
*
@@ -29,14 +34,14 @@ import nextflow.exception.ScriptRuntimeException
2934
@CompileStatic
3035
class ParamsDsl {
3136

32-
private Map<String,Optional<?>> declarations = [:]
37+
private Map<String,Param> declarations = [:]
3338

34-
void declare(String name) {
35-
declarations[name] = Optional.empty()
39+
void declare(String name, Class type) {
40+
declarations[name] = new Param(name, type, Optional.empty())
3641
}
3742

38-
void declare(String name, Object defaultValue) {
39-
declarations[name] = Optional.of(defaultValue)
43+
void declare(String name, Class type, Object defaultValue) {
44+
declarations[name] = new Param(name, type, Optional.of(defaultValue))
4045
}
4146

4247
void apply(Session session) {
@@ -50,18 +55,70 @@ class ParamsDsl {
5055

5156
final params = new HashMap<String,?>()
5257
for( final name : declarations.keySet() ) {
53-
final defaultValue = declarations[name]
58+
final decl = declarations[name]
5459
if( cliParams.containsKey(name) )
55-
params[name] = cliParams[name]
60+
params[name] = resolveFromCli(decl, cliParams[name])
5661
else if( configParams.containsKey(name) )
57-
params[name] = configParams[name]
58-
else if( defaultValue.isPresent() )
59-
params[name] = defaultValue.get()
62+
params[name] = resolveFromCode(decl, configParams[name])
63+
else if( decl.defaultValue.isPresent() )
64+
params[name] = resolveFromCode(decl, decl.defaultValue.get())
6065
else
6166
throw new ScriptRuntimeException("Parameter `$name` is required but was not specified on the command line, params file, or config")
67+
68+
final actualType = params[name].getClass()
69+
if( !Types.isAssignableFrom(decl.type, actualType) )
70+
throw new ScriptRuntimeException("Parameter `$name` with type ${Types.getName(decl.type)} cannot be assigned to ${params[name]} [${Types.getName(actualType)}]")
6271
}
6372

6473
session.binding.setParams(params)
6574
}
6675

76+
private Object resolveFromCli(Param decl, Object value) {
77+
if( value == null )
78+
return null
79+
80+
if( value !instanceof CharSequence )
81+
return value
82+
83+
final str = value.toString()
84+
85+
if( decl.type == Boolean ) {
86+
if( str.toLowerCase() == 'true' ) return Boolean.TRUE
87+
if( str.toLowerCase() == 'false' ) return Boolean.FALSE
88+
}
89+
90+
if( decl.type == Integer || decl.type == Number ) {
91+
if( str.isInteger() ) return str.toInteger()
92+
if( str.isLong() ) return str.toLong()
93+
}
94+
95+
if( decl.type == Number ) {
96+
if( str.isFloat() ) return str.toFloat()
97+
if( str.isDouble() ) return str.toDouble()
98+
}
99+
100+
if( decl.type == Path ) {
101+
return FileHelper.asPath(str)
102+
}
103+
104+
return value
105+
}
106+
107+
private Object resolveFromCode(Param decl, Object value) {
108+
if( value == null )
109+
return null
110+
111+
if( decl.type == Path && value instanceof CharSequence )
112+
return FileHelper.asPath(value.toString())
113+
114+
return value
115+
}
116+
117+
@Canonical
118+
private static class Param {
119+
String name
120+
Class type
121+
Optional<?> defaultValue
122+
}
123+
67124
}

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

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package nextflow.script
22

3+
import java.nio.file.Path
4+
35
import nextflow.Session
6+
import nextflow.file.FileHelper
47
import nextflow.exception.ScriptRuntimeException
58
import spock.lang.Specification
69
/**
@@ -18,11 +21,11 @@ class ParamsDslTest extends Specification {
1821

1922
when:
2023
def dsl = new ParamsDsl()
21-
dsl.declare('input')
22-
dsl.declare('save_intermeds', false)
24+
dsl.declare('input', Path)
25+
dsl.declare('save_intermeds', Boolean, false)
2326
dsl.apply(session)
2427
then:
25-
session.binding.getParams() == [input: './data', save_intermeds: false]
28+
session.binding.getParams() == [input: FileHelper.asPath('./data'), save_intermeds: false]
2629
}
2730

2831
def 'should report error for missing required param'() {
@@ -34,8 +37,8 @@ class ParamsDslTest extends Specification {
3437

3538
when:
3639
def dsl = new ParamsDsl()
37-
dsl.declare('input')
38-
dsl.declare('save_intermeds', false)
40+
dsl.declare('input', Path)
41+
dsl.declare('save_intermeds', Boolean, false)
3942
dsl.apply(session)
4043
then:
4144
def e = thrown(ScriptRuntimeException)
@@ -51,12 +54,29 @@ class ParamsDslTest extends Specification {
5154

5255
when:
5356
def dsl = new ParamsDsl()
54-
dsl.declare('input')
55-
dsl.declare('save_intermeds', false)
57+
dsl.declare('input', Path)
58+
dsl.declare('save_intermeds', Boolean, false)
5659
dsl.apply(session)
5760
then:
5861
def e = thrown(ScriptRuntimeException)
5962
e.message == 'Parameter `inputs` was specified on the command line or params file but is not declared in the script or config'
6063
}
6164

65+
def 'should report error for invalid type'() {
66+
given:
67+
def cliParams = [input: './data', save_intermeds: 42]
68+
def configParams = [:]
69+
def session = new Session()
70+
session.init(null, null, cliParams, configParams)
71+
72+
when:
73+
def dsl = new ParamsDsl()
74+
dsl.declare('input', Path)
75+
dsl.declare('save_intermeds', Boolean, false)
76+
dsl.apply(session)
77+
then:
78+
def e = thrown(ScriptRuntimeException)
79+
e.message == 'Parameter `save_intermeds` with type Boolean cannot be assigned to 42 [Integer]'
80+
}
81+
6282
}

modules/nf-lang/src/main/antlr/ScriptParser.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ paramsBody
160160
;
161161

162162
paramDeclaration
163-
: identifier (ASSIGN expression)?
163+
: identifier (COLON type)? (ASSIGN expression)?
164164
| statement
165165
;
166166

modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public void visit() {
8484
@Override
8585
public void visitParam(Parameter node) {
8686
node.setInitialExpression(resolver.transform(node.getInitialExpression()));
87+
resolver.resolveOrFail(node.getType(), node);
8788
}
8889

8990
@Override

modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,10 @@ public void visitParams(ParamBlockNode node) {
124124
var statements = Arrays.stream(node.declarations)
125125
.map((param) -> {
126126
var name = constX(param.getName());
127+
var type = classX(param.getType());
127128
var arguments = param.hasInitialExpression()
128-
? args(name, param.getInitialExpression())
129-
: args(name);
129+
? args(name, type, param.getInitialExpression())
130+
: args(name, type);
130131
return stmt(callThisX("declare", arguments));
131132
})
132133
.toList();

modules/nf-lang/src/main/java/nextflow/script/control/TypeCheckingVisitor.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import nextflow.script.types.Types;
2727
import org.codehaus.groovy.ast.ASTNode;
2828
import org.codehaus.groovy.ast.MethodNode;
29+
import org.codehaus.groovy.ast.Parameter;
2930
import org.codehaus.groovy.ast.Variable;
3031
import org.codehaus.groovy.ast.expr.BinaryExpression;
3132
import org.codehaus.groovy.ast.expr.DeclarationExpression;
@@ -83,6 +84,16 @@ public void visitFeatureFlag(FeatureFlagNode node) {
8384
addError("Type mismatch for feature flag '" + node.name + "' -- expected a " + Types.getName(expectedType) + " but received a " + Types.getName(actualType), node);
8485
}
8586

87+
@Override
88+
public void visitParam(Parameter node) {
89+
if( !node.hasInitialExpression() )
90+
return;
91+
var expectedType = node.getType();
92+
var actualType = node.getInitialExpression().getType();
93+
if( !Types.isAssignableFrom(expectedType, actualType) )
94+
addError("Parameter '" + node.getName() + "' with type " + Types.getName(expectedType) + " cannot be assigned to default value with type " + Types.getName(actualType), node);
95+
}
96+
8697
// statements
8798

8899
@Override

modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.codehaus.groovy.ast.ClassNode;
2525
import org.codehaus.groovy.ast.CodeVisitorSupport;
2626
import org.codehaus.groovy.ast.Parameter;
27+
import org.codehaus.groovy.ast.Variable;
2728
import org.codehaus.groovy.ast.expr.BinaryExpression;
2829
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression;
2930
import org.codehaus.groovy.ast.expr.CastExpression;
@@ -713,6 +714,10 @@ private static boolean hasTrailingComma(Expression node) {
713714
return node.getNodeMetaData(ASTNodeMarker.TRAILING_COMMA) != null;
714715
}
715716

717+
public static boolean hasType(Variable variable) {
718+
return !variable.isDynamicTyped() || isLegacyType(variable.getType());
719+
}
720+
716721
public static boolean isLegacyType(ClassNode cn) {
717722
return cn.getNodeMetaData(ASTNodeMarker.LEGACY_TYPE) != null;
718723
}

0 commit comments

Comments
 (0)