Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/migrations/25-10.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,49 @@ This syntax allows you to declare all parameters in one place with explicit type

See {ref}`workflow-params-def` for details.

<h3>Type annotations</h3>

Type annotations are a way to denote the *type* of a variable. They help document and validate pipeline code.

```nextflow
workflow RNASEQ {
take:
reads: Channel<Path>
index: Channel<Path>

main:
samples_ch = QUANT( reads.combine(index) )

emit:
samples: Channel<Path> = samples_ch
}

def isSraId(id: String) -> Boolean {
return id.startsWith('SRA')
}
```

The following declarations can be annotated with types:

- Pipeline parameters (the `params` block)
- Workflow takes and emits
- Function parameters and returns
- Local variables
- Closure parameters
- Workflow outputs (the `output` block)

Type annotations can refer to any of the {ref}`standard types <stdlib-types>`.

Type annotations can be appended with `?` to denote that the value can be `null`:

```nextflow
def x_opt: String? = null
```

:::{note}
Nextflow supports Groovy-style type annotations using the `<type> <name>` syntax, but this approach is deprecated in {ref}`strict syntax <strict-syntax-page>`. While Groovy-style annotations remain valid for functions and local variables, the language server and `nextflow lint` automatically convert them to Nextflow-style annotations during code formatting.
:::

## Enhancements

<h3>New syntax for workflow handlers</h3>
Expand Down
16 changes: 14 additions & 2 deletions docs/strict-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,22 @@ def str = 'hello'
def meta = [:]
```

:::{note}
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.
:::{versionadded} 25.10.0
:::

Local variables can be declared with a type annotation:

```nextflow
def a: Integer = 1
def b: Integer = 2
def (c: Integer, d: Integer) = [3, 4]
def (e: Integer, f: Integer) = [5, 6]
def str: String = 'hello'
def meta: Map = [:]
```

Groovy-style type annotations are still supported. However, the language server and `nextflow lint` will automatically convert them to Nextflow-style type annotations when formatting code. Groovy-style type annotations will not be supported in a future version.

### Strings

Groovy supports a wide variety of strings, including multi-line strings, dynamic strings, slashy strings, multi-line dynamic slashy strings, and more.
Expand Down
56 changes: 43 additions & 13 deletions modules/nf-lang/src/main/antlr/ScriptParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -263,15 +263,25 @@ workflowBody
;

workflowTakes
: identifier (sep identifier)*
: workflowTake (sep workflowTake)*
;

workflowTake
: identifier (COLON type)?
| statement
;

workflowEmits
: statement (sep statement)*
: workflowEmit (sep workflowEmit)*
;

workflowEmit
: nameTypePair (ASSIGN expression)?
| statement
;

workflowPublishers
: statement (sep statement)*
: workflowEmit (sep workflowEmit)*
;

// -- output definition
Expand All @@ -286,14 +296,19 @@ outputBody
;

outputDeclaration
: identifier LBRACE nls blockStatements? RBRACE
: identifier (COLON type)? LBRACE nls blockStatements? RBRACE
| statement
;

// -- function definition
functionDef
: (DEF | legacyType | DEF legacyType) identifier LPAREN nls (formalParameterList nls)? rparen nls LBRACE
nls blockStatements? RBRACE
: DEF
identifier LPAREN nls (formalParameterList nls)? rparen (ARROW type)?
nls LBRACE nls blockStatements? RBRACE

| (legacyType | DEF legacyType)
identifier LPAREN nls (formalParameterList nls)? rparen
nls LBRACE nls blockStatements? RBRACE
;

// -- incomplete script declaration
Expand Down Expand Up @@ -338,7 +353,12 @@ tryCatchStatement
;

catchClause
: CATCH LPAREN catchTypes? identifier rparen nls statementOrBlock
: CATCH LPAREN catchVariable rparen nls statementOrBlock
;

catchVariable
: identifier (COLON catchTypes)?
| catchTypes identifier
;

catchTypes
Expand All @@ -352,19 +372,28 @@ assertStatement

// -- variable declaration
variableDeclaration
: (DEF | legacyType | DEF legacyType) identifier (nls ASSIGN nls initializer=expression)?
| DEF variableNames nls ASSIGN nls initializer=expression
: DEF nameTypePair (nls ASSIGN nls initializer=expression)?
| DEF nameTypePairs nls ASSIGN nls initializer=expression
| (legacyType | DEF legacyType) identifier (nls ASSIGN nls initializer=expression)?
;

variableNames
: LPAREN identifier (COMMA identifier)+ rparen
nameTypePairs
: LPAREN nameTypePair (COMMA nameTypePair)+ rparen
;

nameTypePair
: identifier (COLON type)?
;

// -- assignment statement
multipleAssignmentStatement
: variableNames nls ASSIGN nls expression
;

variableNames
: LPAREN identifier (COMMA identifier)+ rparen
;

assignmentStatement
: target=expression nls
op=(ASSIGN
Expand Down Expand Up @@ -595,7 +624,8 @@ formalParameterList
;

formalParameter
: DEF? legacyType? identifier (nls ASSIGN nls expression)?
: identifier (COLON type)? (nls ASSIGN nls expression)?
| DEF? legacyType? identifier (nls ASSIGN nls expression)?
;

closureWithLabels
Expand Down Expand Up @@ -667,7 +697,7 @@ namedArg
//
type
: primitiveType
| qualifiedClassName typeArguments?
| qualifiedClassName typeArguments? QUESTION?
;

primitiveType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public enum ASTNodeMarker {
// the MethodNode targeted by a variable expression (PropertyNode)
METHOD_VARIABLE_TARGET,

// denotes a nullable type annotation (ClassNode)
NULLABLE,

// the starting quote sequence of a string literal or gstring expression
QUOTE_CHAR,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package nextflow.script.ast;

import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.stmt.Statement;

/**
Expand All @@ -25,10 +26,12 @@
*/
public class OutputNode extends ASTNode {
public final String name;
public final ClassNode type;
public final Statement body;

public OutputNode(String name, Statement body) {
public OutputNode(String name, ClassNode type, Statement body) {
this.name = name;
this.type = type;
this.body = body;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ public void visitParamV1(ParamNodeV1 node) {

@Override
public void visitWorkflow(WorkflowNode node) {
visit(node.takes);
visit(node.main);
visit(node.emits);
visit(node.publishers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,14 @@
* @author Ben Sherman <[email protected]>
*/
public class WorkflowNode extends MethodNode {
public final Statement takes;
public final Statement main;
public final Statement emits;
public final Statement publishers;
public final Statement onComplete;
public final Statement onError;

public WorkflowNode(String name, Statement takes, Statement main, Statement emits, Statement publishers, Statement onComplete, Statement onError) {
super(name, 0, dummyReturnType(emits), dummyParams(takes), ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE);
this.takes = takes;
public WorkflowNode(String name, Parameter[] takes, Statement main, Statement emits, Statement publishers, Statement onComplete, Statement onError) {
super(name, 0, dummyReturnType(emits), takes, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE);
this.main = main;
this.emits = emits;
this.publishers = publishers;
Expand All @@ -56,7 +54,7 @@ public WorkflowNode(String name, Statement takes, Statement main, Statement emit
}

public WorkflowNode(String name, Statement main) {
this(name, EmptyStatement.INSTANCE, main, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE);
this(name, Parameter.EMPTY_ARRAY, main, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE);
}

public boolean isEntry() {
Expand All @@ -67,13 +65,6 @@ public boolean isCodeSnippet() {
return getLineNumber() == -1;
}

private static Parameter[] dummyParams(Statement takes) {
return asBlockStatements(takes)
.stream()
.map((stmt) -> new Parameter(ClassHelper.dynamicType(), ""))
.toArray(Parameter[]::new);
}

private static ClassNode dummyReturnType(Statement emits) {
var cn = new ClassNode(Record.class);
asBlockStatements(emits).stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import groovy.lang.Tuple2;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.types.Bag;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
Expand Down Expand Up @@ -60,9 +61,16 @@
*/
public class ResolveVisitor extends ClassCodeExpressionTransformer {

public static final String[] DEFAULT_PACKAGE_PREFIXES = { "java.lang.", "java.util.", "java.io.", "java.net.", "groovy.lang.", "groovy.util." };

public static final String[] EMPTY_STRING_ARRAY = new String[0];
public static final ClassNode[] STANDARD_TYPES = {
ClassHelper.makeCached(Bag.class),
ClassHelper.Boolean_TYPE,
ClassHelper.Integer_TYPE,
ClassHelper.Number_TYPE,
ClassHelper.STRING_TYPE,
ClassHelper.LIST_TYPE,
ClassHelper.MAP_TYPE,
ClassHelper.SET_TYPE
};

private SourceUnit sourceUnit;

Expand Down Expand Up @@ -113,12 +121,16 @@ public boolean resolve(ClassNode type) {
return true;
if( type.isResolved() )
return true;
if( resolveFromModule(type) )
if( !type.hasPackageName() && resolveFromModule(type) )
return true;
if( !type.hasPackageName() && resolveFromStandardTypes(type) )
return true;
if( resolveFromLibImports(type) )
return true;
if( !type.hasPackageName() && resolveFromDefaultImports(type) )
return true;
if( !type.hasPackageName() && resolveFromGroovyImports(type) )
return true;
return resolveFromClassResolver(type.getName()) != null;
}

Expand Down Expand Up @@ -156,10 +168,19 @@ protected boolean resolveFromModule(ClassNode type) {
return false;
}

protected boolean resolveFromStandardTypes(ClassNode type) {
for( var cn : STANDARD_TYPES ) {
if( cn.getNameWithoutPackage().equals(type.getName()) ) {
type.setRedirect(cn);
return true;
}
}
return false;
}

protected boolean resolveFromLibImports(ClassNode type) {
var name = type.getName();
for( var cn : libImports ) {
if( name.equals(cn.getName()) ) {
if( cn.getName().equals(type.getName()) ) {
type.setRedirect(cn);
return true;
}
Expand All @@ -168,22 +189,29 @@ protected boolean resolveFromLibImports(ClassNode type) {
}

protected boolean resolveFromDefaultImports(ClassNode type) {
// resolve from script imports
var typeName = type.getName();
for( var cn : defaultImports ) {
if( typeName.equals(cn.getNameWithoutPackage()) ) {
if( cn.getNameWithoutPackage().equals(type.getName()) ) {
type.setRedirect(cn);
return true;
}
}
// resolve from default imports cache
return false;
}

private static final String[] DEFAULT_PACKAGE_PREFIXES = { "java.lang.", "java.util.", "java.io.", "java.net.", "groovy.lang.", "groovy.util." };

private static final String[] EMPTY_STRING_ARRAY = new String[0];

protected boolean resolveFromGroovyImports(ClassNode type) {
var typeName = type.getName();
// resolve from Groovy imports cache
var packagePrefixSet = DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE.get(typeName);
if( packagePrefixSet != null ) {
if( resolveFromDefaultImports(type, packagePrefixSet.toArray(EMPTY_STRING_ARRAY)) )
if( resolveFromGroovyImports(type, packagePrefixSet.toArray(EMPTY_STRING_ARRAY)) )
return true;
}
// resolve from default imports
if( resolveFromDefaultImports(type, DEFAULT_PACKAGE_PREFIXES) ) {
// resolve from Groovy imports
if( resolveFromGroovyImports(type, DEFAULT_PACKAGE_PREFIXES) ) {
return true;
}
if( "BigInteger".equals(typeName) ) {
Expand All @@ -202,7 +230,7 @@ protected boolean resolveFromDefaultImports(ClassNode type) {
DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE.putAll(VMPluginFactory.getPlugin().getDefaultImportClasses(DEFAULT_PACKAGE_PREFIXES));
}

protected boolean resolveFromDefaultImports(ClassNode type, String[] packagePrefixes) {
protected boolean resolveFromGroovyImports(ClassNode type, String[] packagePrefixes) {
var typeName = type.getName();
for( var packagePrefix : packagePrefixes ) {
var redirect = resolveFromClassResolver(packagePrefix + typeName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public void visitParamV1(ParamNodeV1 node) {

@Override
public void visitWorkflow(WorkflowNode node) {
for( var take : node.getParameters() )
resolver.resolveOrFail(take.getType(), take);
resolver.visit(node.main);
resolver.visit(node.emits);
resolver.visit(node.publishers);
Expand Down
Loading