Skip to content

Commit 4c520f9

Browse files
committed
Type annotations
Signed-off-by: Ben Sherman <[email protected]>
1 parent a76b760 commit 4c520f9

File tree

18 files changed

+563
-214
lines changed

18 files changed

+563
-214
lines changed

docs/migrations/25-10.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,51 @@ 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>Type annotations</h3>
14+
15+
Type annotations are a way to denote the *type* of a variable. They are useful both for documenting and validating your pipeline code.
16+
17+
```nextflow
18+
workflow RNASEQ {
19+
take:
20+
reads: Channel<Path>
21+
index: Channel<Path>
22+
23+
main:
24+
samples_ch = QUANT( reads.combine(index) )
25+
26+
emit:
27+
samples: Channel<Path> = samples_ch
28+
}
29+
30+
def isSraId(id: String) -> Boolean {
31+
return id.startsWith('SRA')
32+
}
33+
```
34+
35+
The following declarations can be annotated with types:
36+
37+
- Pipeline parameters (the `params` block)
38+
- Workflow takes and emits
39+
- Function parameters and returns
40+
- Local variables
41+
- Closure parameters
42+
- Workflow outputs (the `output` block)
43+
44+
Type annotations can refer to any of the {ref}`standard types <stdlib-types>`.
45+
46+
Type annotations can be appended with `?` to denote that the value can be `null`:
47+
48+
```nextflow
49+
def x_opt: String? = null
50+
```
51+
52+
:::{note}
53+
While Nextflow inherited type annotations of the form `<type> <name>` from Groovy, this syntax was deprecated in the {ref}`strict syntax <strict-syntax-page>`. Groovy-style type annotations are still allowed for functions and local variables, but will be automatically converted to Nextflow-stype type annotations when formatting code with the language server or `nextflow lint`.
54+
:::
55+
1156
## Enhancements
1257

1358
<h3>New syntax for workflow handlers</h3>

docs/strict-syntax.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,22 @@ def str = 'hello'
299299
def meta = [:]
300300
```
301301

302-
:::{note}
303-
Because type annotations are useful for providing type checking at runtime, the language server will not report errors for Groovy-style type annotations at this time. Type annotations will be addressed in a future version of the Nextflow language specification.
302+
:::{versionadded} 25.10.0
304303
:::
305304

305+
Local variables can be declared with a type annotation:
306+
307+
```nextflow
308+
def a: Integer = 1
309+
def b: Integer = 2
310+
def (c: Integer, d: Integer) = [3, 4]
311+
def (e: Integer, f: Integer) = [5, 6]
312+
def str: String = 'hello'
313+
def meta: Map = [:]
314+
```
315+
316+
While Groovy-style type annotations are still supported, the linter and language server will automatically convert them to Nextflow-style type annotations when formatting code. Groovy-style type annotations will not be supported in a future version.
317+
306318
### Strings
307319

308320
Groovy supports a wide variety of strings, including multi-line strings, dynamic strings, slashy strings, multi-line dynamic slashy strings, and more.

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

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,25 @@ workflowBody
246246
;
247247

248248
workflowTakes
249-
: identifier (sep identifier)*
249+
: workflowTake (sep workflowTake)*
250+
;
251+
252+
workflowTake
253+
: identifier (COLON type)?
254+
| statement
250255
;
251256

252257
workflowEmits
253-
: statement (sep statement)*
258+
: workflowEmit (sep workflowEmit)*
259+
;
260+
261+
workflowEmit
262+
: nameTypePair (ASSIGN expression)?
263+
| statement
254264
;
255265

256266
workflowPublishers
257-
: statement (sep statement)*
267+
: workflowEmit (sep workflowEmit)*
258268
;
259269

260270
// -- output definition
@@ -269,14 +279,19 @@ outputBody
269279
;
270280

271281
outputDeclaration
272-
: identifier LBRACE nls blockStatements? RBRACE
282+
: identifier (COLON type)? LBRACE nls blockStatements? RBRACE
273283
| statement
274284
;
275285

276286
// -- function definition
277287
functionDef
278-
: (DEF | legacyType | DEF legacyType) identifier LPAREN nls (formalParameterList nls)? rparen nls LBRACE
279-
nls blockStatements? RBRACE
288+
: DEF
289+
identifier LPAREN nls (formalParameterList nls)? rparen (ARROW type)?
290+
nls LBRACE nls blockStatements? RBRACE
291+
292+
| (legacyType | DEF legacyType)
293+
identifier LPAREN nls (formalParameterList nls)? rparen
294+
nls LBRACE nls blockStatements? RBRACE
280295
;
281296

282297
// -- incomplete script declaration
@@ -321,7 +336,12 @@ tryCatchStatement
321336
;
322337

323338
catchClause
324-
: CATCH LPAREN catchTypes? identifier rparen nls statementOrBlock
339+
: CATCH LPAREN catchVariable rparen nls statementOrBlock
340+
;
341+
342+
catchVariable
343+
: identifier (COLON catchTypes)?
344+
| catchTypes identifier
325345
;
326346

327347
catchTypes
@@ -335,19 +355,28 @@ assertStatement
335355

336356
// -- variable declaration
337357
variableDeclaration
338-
: (DEF | legacyType | DEF legacyType) identifier (nls ASSIGN nls initializer=expression)?
339-
| DEF variableNames nls ASSIGN nls initializer=expression
358+
: DEF nameTypePair (nls ASSIGN nls initializer=expression)?
359+
| DEF nameTypePairs nls ASSIGN nls initializer=expression
360+
| (legacyType | DEF legacyType) identifier (nls ASSIGN nls initializer=expression)?
340361
;
341362

342-
variableNames
343-
: LPAREN identifier (COMMA identifier)+ rparen
363+
nameTypePairs
364+
: LPAREN nameTypePair (COMMA nameTypePair)+ rparen
365+
;
366+
367+
nameTypePair
368+
: identifier (COLON type)?
344369
;
345370

346371
// -- assignment statement
347372
multipleAssignmentStatement
348373
: variableNames nls ASSIGN nls expression
349374
;
350375

376+
variableNames
377+
: LPAREN identifier (COMMA identifier)+ rparen
378+
;
379+
351380
assignmentStatement
352381
: target=expression nls
353382
op=(ASSIGN
@@ -578,7 +607,8 @@ formalParameterList
578607
;
579608

580609
formalParameter
581-
: DEF? legacyType? identifier (nls ASSIGN nls expression)?
610+
: identifier (COLON type)? (nls ASSIGN nls expression)?
611+
| DEF? legacyType? identifier (nls ASSIGN nls expression)?
582612
;
583613

584614
closureWithLabels
@@ -650,7 +680,7 @@ namedArg
650680
//
651681
type
652682
: primitiveType
653-
| qualifiedClassName typeArguments?
683+
| qualifiedClassName typeArguments? QUESTION?
654684
;
655685

656686
primitiveType

modules/nf-lang/src/main/java/nextflow/script/ast/ASTNodeMarker.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public enum ASTNodeMarker {
4545
// the MethodNode targeted by a variable expression (PropertyNode)
4646
METHOD_VARIABLE_TARGET,
4747

48+
// denotes a nullable type annotation (ClassNode)
49+
NULLABLE,
50+
4851
// the starting quote sequence of a string literal or gstring expression
4952
QUOTE_CHAR,
5053

modules/nf-lang/src/main/java/nextflow/script/ast/OutputNode.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package nextflow.script.ast;
1717

1818
import org.codehaus.groovy.ast.ASTNode;
19+
import org.codehaus.groovy.ast.ClassNode;
1920
import org.codehaus.groovy.ast.stmt.Statement;
2021

2122
/**
@@ -25,10 +26,12 @@
2526
*/
2627
public class OutputNode extends ASTNode {
2728
public final String name;
29+
public final ClassNode type;
2830
public final Statement body;
2931

30-
public OutputNode(String name, Statement body) {
32+
public OutputNode(String name, ClassNode type, Statement body) {
3133
this.name = name;
34+
this.type = type;
3235
this.body = body;
3336
}
3437
}

modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ public void visitParam(ParamNode node) {
6363

6464
@Override
6565
public void visitWorkflow(WorkflowNode node) {
66-
visit(node.takes);
6766
visit(node.main);
6867
visit(node.emits);
6968
visit(node.publishers);

modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,14 @@
3838
* @author Ben Sherman <[email protected]>
3939
*/
4040
public class WorkflowNode extends MethodNode {
41-
public final Statement takes;
4241
public final Statement main;
4342
public final Statement emits;
4443
public final Statement publishers;
4544
public final Statement onComplete;
4645
public final Statement onError;
4746

48-
public WorkflowNode(String name, Statement takes, Statement main, Statement emits, Statement publishers, Statement onComplete, Statement onError) {
49-
super(name, 0, dummyReturnType(emits), dummyParams(takes), ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE);
50-
this.takes = takes;
47+
public WorkflowNode(String name, Parameter[] takes, Statement main, Statement emits, Statement publishers, Statement onComplete, Statement onError) {
48+
super(name, 0, dummyReturnType(emits), takes, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE);
5149
this.main = main;
5250
this.emits = emits;
5351
this.publishers = publishers;
@@ -56,7 +54,7 @@ public WorkflowNode(String name, Statement takes, Statement main, Statement emit
5654
}
5755

5856
public WorkflowNode(String name, Statement main) {
59-
this(name, EmptyStatement.INSTANCE, main, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE);
57+
this(name, Parameter.EMPTY_ARRAY, main, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE);
6058
}
6159

6260
public boolean isEntry() {
@@ -67,13 +65,6 @@ public boolean isCodeSnippet() {
6765
return getLineNumber() == -1;
6866
}
6967

70-
private static Parameter[] dummyParams(Statement takes) {
71-
return asBlockStatements(takes)
72-
.stream()
73-
.map((stmt) -> new Parameter(ClassHelper.dynamicType(), ""))
74-
.toArray(Parameter[]::new);
75-
}
76-
7768
private static ClassNode dummyReturnType(Statement emits) {
7869
var cn = new ClassNode(Record.class);
7970
asBlockStatements(emits).stream()

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

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import groovy.lang.Tuple2;
2424
import nextflow.script.ast.ASTNodeMarker;
25+
import nextflow.script.types.Bag;
2526
import org.codehaus.groovy.GroovyBugError;
2627
import org.codehaus.groovy.ast.ASTNode;
2728
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
@@ -60,9 +61,16 @@
6061
*/
6162
public class ResolveVisitor extends ClassCodeExpressionTransformer {
6263

63-
public static final String[] DEFAULT_PACKAGE_PREFIXES = { "java.lang.", "java.util.", "java.io.", "java.net.", "groovy.lang.", "groovy.util." };
64-
65-
public static final String[] EMPTY_STRING_ARRAY = new String[0];
64+
public static final ClassNode[] STANDARD_TYPES = {
65+
ClassHelper.makeCached(Bag.class),
66+
ClassHelper.Boolean_TYPE,
67+
ClassHelper.Integer_TYPE,
68+
ClassHelper.Number_TYPE,
69+
ClassHelper.STRING_TYPE,
70+
ClassHelper.LIST_TYPE,
71+
ClassHelper.MAP_TYPE,
72+
ClassHelper.SET_TYPE
73+
};
6674

6775
private SourceUnit sourceUnit;
6876

@@ -113,12 +121,16 @@ public boolean resolve(ClassNode type) {
113121
return true;
114122
if( type.isResolved() )
115123
return true;
116-
if( resolveFromModule(type) )
124+
if( !type.hasPackageName() && resolveFromModule(type) )
125+
return true;
126+
if( !type.hasPackageName() && resolveFromStandardTypes(type) )
117127
return true;
118128
if( resolveFromLibImports(type) )
119129
return true;
120130
if( !type.hasPackageName() && resolveFromDefaultImports(type) )
121131
return true;
132+
if( !type.hasPackageName() && resolveFromGroovyImports(type) )
133+
return true;
122134
return resolveFromClassResolver(type.getName()) != null;
123135
}
124136

@@ -156,10 +168,19 @@ protected boolean resolveFromModule(ClassNode type) {
156168
return false;
157169
}
158170

171+
protected boolean resolveFromStandardTypes(ClassNode type) {
172+
for( var cn : STANDARD_TYPES ) {
173+
if( cn.getNameWithoutPackage().equals(type.getName()) ) {
174+
type.setRedirect(cn);
175+
return true;
176+
}
177+
}
178+
return false;
179+
}
180+
159181
protected boolean resolveFromLibImports(ClassNode type) {
160-
var name = type.getName();
161182
for( var cn : libImports ) {
162-
if( name.equals(cn.getName()) ) {
183+
if( cn.getName().equals(type.getName()) ) {
163184
type.setRedirect(cn);
164185
return true;
165186
}
@@ -168,22 +189,29 @@ protected boolean resolveFromLibImports(ClassNode type) {
168189
}
169190

170191
protected boolean resolveFromDefaultImports(ClassNode type) {
171-
// resolve from script imports
172-
var typeName = type.getName();
173192
for( var cn : defaultImports ) {
174-
if( typeName.equals(cn.getNameWithoutPackage()) ) {
193+
if( cn.getNameWithoutPackage().equals(type.getName()) ) {
175194
type.setRedirect(cn);
176195
return true;
177196
}
178197
}
179-
// resolve from default imports cache
198+
return false;
199+
}
200+
201+
private static final String[] DEFAULT_PACKAGE_PREFIXES = { "java.lang.", "java.util.", "java.io.", "java.net.", "groovy.lang.", "groovy.util." };
202+
203+
private static final String[] EMPTY_STRING_ARRAY = new String[0];
204+
205+
protected boolean resolveFromGroovyImports(ClassNode type) {
206+
var typeName = type.getName();
207+
// resolve from Groovy imports cache
180208
var packagePrefixSet = DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE.get(typeName);
181209
if( packagePrefixSet != null ) {
182-
if( resolveFromDefaultImports(type, packagePrefixSet.toArray(EMPTY_STRING_ARRAY)) )
210+
if( resolveFromGroovyImports(type, packagePrefixSet.toArray(EMPTY_STRING_ARRAY)) )
183211
return true;
184212
}
185-
// resolve from default imports
186-
if( resolveFromDefaultImports(type, DEFAULT_PACKAGE_PREFIXES) ) {
213+
// resolve from Groovy imports
214+
if( resolveFromGroovyImports(type, DEFAULT_PACKAGE_PREFIXES) ) {
187215
return true;
188216
}
189217
if( "BigInteger".equals(typeName) ) {
@@ -202,7 +230,7 @@ protected boolean resolveFromDefaultImports(ClassNode type) {
202230
DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE.putAll(VMPluginFactory.getPlugin().getDefaultImportClasses(DEFAULT_PACKAGE_PREFIXES));
203231
}
204232

205-
protected boolean resolveFromDefaultImports(ClassNode type, String[] packagePrefixes) {
233+
protected boolean resolveFromGroovyImports(ClassNode type, String[] packagePrefixes) {
206234
var typeName = type.getName();
207235
for( var packagePrefix : packagePrefixes ) {
208236
var redirect = resolveFromClassResolver(packagePrefix + typeName);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public void visitParam(ParamNode node) {
8585

8686
@Override
8787
public void visitWorkflow(WorkflowNode node) {
88+
for( var take : node.getParameters() )
89+
resolver.resolveOrFail(take.getType(), take);
8890
resolver.visit(node.main);
8991
resolver.visit(node.emits);
9092
resolver.visit(node.publishers);

0 commit comments

Comments
 (0)