add nextflow d30e48d

This commit is contained in:
2026-04-29 23:01:54 +02:00
parent d0b12d668d
commit 97cc9058d3
2840 changed files with 730250 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id 'antlr'
}
compileJava {
options.compilerArgs << '-parameters'
}
dependencies {
antlr 'me.sunlan:antlr4:4.13.2.6'
api 'org.apache.groovy:groovy:4.0.31'
api 'org.pf4j:pf4j:3.14.1'
testFixturesApi 'com.google.jimfs:jimfs:1.2'
testImplementation(testFixtures(project(":nextflow")))
}
generateGrammarSource {
arguments += ['-no-listener', '-no-visitor']
}
tasks.named('sourcesJar') {
dependsOn tasks.named('generateGrammarSource')
}

View File

@@ -0,0 +1,826 @@
/*
* Copyright 2024-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This file is adapted from the Antlr4 Java grammar which has the following license
*
* Copyright (c) 2013 Terence Parr, Sam Harwell
* All rights reserved.
* [The "BSD licence"]
*
* http://www.opensource.org/licenses/bsd-license.php
*
* Subsequent modifications by the Groovy community have been done under the Apache License v2:
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Grammar specification for the Nextflow configuration language.
*
* Based on the official grammar for Groovy:
* https://github.com/apache/groovy/blob/GROOVY_4_0_X/src/antlr/GroovyLexer.g4
*/
lexer grammar ConfigLexer;
options {
superClass = AbstractLexer;
}
@header {
package nextflow.config.parser;
import java.util.*;
import java.util.regex.Pattern;
import nextflow.script.parser.AbstractLexer;
import org.antlr.v4.runtime.CharStream;
import org.apache.groovy.parser.antlr4.GroovySyntaxError;
import static nextflow.script.parser.SemanticPredicates.*;
}
@members {
private boolean errorIgnored;
private int lastTokenType;
private int invalidDigitCount;
/**
* Record the index and token type of the current token while emitting tokens.
*/
@Override
public void emit(Token token) {
int tokenType = token.getType();
if (Token.DEFAULT_CHANNEL == token.getChannel()) {
this.lastTokenType = tokenType;
}
super.emit(token);
}
private static final int[] REGEX_CHECK_ARRAY = {
// DEC,
// INC,
// THIS,
RBRACE,
RBRACK,
RPAREN,
GStringEnd,
TdqGStringEnd,
NullLiteral,
StringLiteral,
BooleanLiteral,
IntegerLiteral,
FloatingPointLiteral,
Identifier, CapitalizedIdentifier
};
static {
Arrays.sort(REGEX_CHECK_ARRAY);
}
private boolean isRegexAllowed() {
return (Arrays.binarySearch(REGEX_CHECK_ARRAY, this.lastTokenType) < 0);
}
@Override
public int getSyntaxErrorSource() {
return GroovySyntaxError.LEXER;
}
@Override
public int getErrorLine() {
return getLine();
}
@Override
public int getErrorColumn() {
return getCharPositionInLine() + 1;
}
private static boolean isJavaIdentifierStartAndNotIdentifierIgnorable(int codePoint) {
return Character.isJavaIdentifierStart(codePoint) && !Character.isIdentifierIgnorable(codePoint);
}
private static boolean isJavaIdentifierPartAndNotIdentifierIgnorable(int codePoint) {
return Character.isJavaIdentifierPart(codePoint) && !Character.isIdentifierIgnorable(codePoint);
}
}
//
// §3.10.5 String Literals
//
StringLiteral
: DqStringQuotationMark DqStringCharacter* DqStringQuotationMark
| SqStringQuotationMark SqStringCharacter* SqStringQuotationMark
| Slash { this.isRegexAllowed() && _input.LA(1) != '*' }? SlashyStringCharacter+ Slash
| TdqStringQuotationMark TdqStringCharacter* TdqStringQuotationMark
| TsqStringQuotationMark TsqStringCharacter* TsqStringQuotationMark
;
GStringBegin
: DqStringQuotationMark -> pushMode(DQ_GSTRING_MODE)
;
TdqGStringBegin
: TdqStringQuotationMark -> pushMode(TDQ_GSTRING_MODE)
;
mode DQ_GSTRING_MODE;
GStringEnd
: DqStringQuotationMark -> popMode
;
GStringPath
: Dollar IdentifierInGString (Dot IdentifierInGString)*
;
GStringText
: DqStringCharacter+
;
GStringExprStart
: '${' -> pushMode(DEFAULT_MODE)
;
mode TDQ_GSTRING_MODE;
TdqGStringEnd
: TdqStringQuotationMark -> popMode
;
TdqGStringPath
: Dollar IdentifierInGString (Dot IdentifierInGString)*
;
TdqGStringText
: TdqStringCharacter+
;
TdqGStringExprStart
: '${' -> pushMode(DEFAULT_MODE)
;
mode DEFAULT_MODE;
// character in the double quotation string. e.g. "a"
fragment
DqStringCharacter
: ~["\r\n\\$]
| EscapeSequence
;
// character in the single quotation string. e.g. 'a'
fragment
SqStringCharacter
: ~['\r\n\\]
| EscapeSequence
;
// character in the triple double quotation string. e.g. """a"""
fragment
TdqStringCharacter
: ~["\\$]
| DqStringQuotationMark { _input.LA(1) != '"' || _input.LA(2) != '"' || _input.LA(3) == '"' && (_input.LA(4) != '"' || _input.LA(5) != '"') }?
| EscapeSequence
;
// character in the triple single quotation string. e.g. '''a'''
fragment
TsqStringCharacter
: ~['\\]
| SqStringQuotationMark { _input.LA(1) != '\'' || _input.LA(2) != '\'' || _input.LA(3) == '\'' && (_input.LA(4) != '\'' || _input.LA(5) != '\'') }?
| EscapeSequence
;
// character in the slashy string. e.g. /a/
fragment
SlashyStringCharacter
: SlashEscape
| Dollar { !isFollowedByJavaLetterInGString(_input) }?
| ~[/$\u0000]
;
// Groovy keywords
AS : 'as';
DEF : 'def';
IN : 'in';
// TRAIT : 'trait';
// THREADSAFE : 'threadsafe'; // reserved keyword
// the reserved type name of Java10
// VAR : 'var';
//
// §3.9 Keywords
//
BuiltInPrimitiveType
: BOOLEAN
| CHAR
| BYTE
| SHORT
| INT
| LONG
| FLOAT
| DOUBLE
;
// ABSTRACT : 'abstract';
ASSERT : 'assert';
fragment
BOOLEAN : 'boolean';
// BREAK : 'break';
// YIELD : 'yield';
fragment
BYTE : 'byte';
// CASE : 'case';
CATCH : 'catch';
fragment
CHAR : 'char';
// CLASS : 'class';
// CONST : 'const';
// CONTINUE : 'continue';
// DEFAULT : 'default';
// DO : 'do';
fragment
DOUBLE : 'double';
ELSE : 'else';
// ENUM : 'enum';
// EXTENDS : 'extends';
// FINAL : 'final';
// FINALLY : 'finally';
fragment
FLOAT : 'float';
// FOR : 'for';
IF : 'if';
// GOTO : 'goto';
// IMPLEMENTS : 'implements';
// IMPORT : 'import';
INSTANCEOF : 'instanceof';
fragment
INT : 'int';
// INTERFACE : 'interface';
fragment
LONG : 'long';
// NATIVE : 'native';
NEW : 'new';
// NON_SEALED : 'non-sealed';
// PACKAGE : 'package';
// PERMITS : 'permits';
// PRIVATE : 'private';
// PROTECTED : 'protected';
// PUBLIC : 'public';
// RECORD : 'record';
RETURN : 'return';
// SEALED : 'sealed';
fragment
SHORT : 'short';
// STATIC : 'static';
// STRICTFP : 'strictfp';
// SUPER : 'super';
// SWITCH : 'switch';
// SYNCHRONIZED : 'synchronized';
// THIS : 'this';
THROW : 'throw';
// THROWS : 'throws';
// TRANSIENT : 'transient';
TRY : 'try';
// VOID : 'void';
// VOLATILE : 'volatile';
// WHILE : 'while';
// -- include statement
INCLUDE_CONFIG : 'includeConfig';
//
// §3.10.1 Integer Literals
//
IntegerLiteral
: ( DecimalIntegerLiteral
| HexIntegerLiteral
| OctalIntegerLiteral
| BinaryIntegerLiteral
)
(Underscore { require(errorIgnored, "Number ending with underscores is invalid", -1, true); })?
// !!! Error Alternative !!!
| Zero ([0-9] { invalidDigitCount++; })+ { require(errorIgnored, "Invalid octal number", -(invalidDigitCount + 1), true); } IntegerTypeSuffix?
;
fragment
Zero
: '0'
;
fragment
DecimalIntegerLiteral
: DecimalNumeral IntegerTypeSuffix?
;
fragment
HexIntegerLiteral
: HexNumeral IntegerTypeSuffix?
;
fragment
OctalIntegerLiteral
: OctalNumeral IntegerTypeSuffix?
;
fragment
BinaryIntegerLiteral
: BinaryNumeral IntegerTypeSuffix?
;
fragment
IntegerTypeSuffix
: [lLiIgG]
;
fragment
DecimalNumeral
: Zero
| NonZeroDigit (Digits? | Underscores Digits)
;
fragment
Digits
: Digit (DigitOrUnderscore* Digit)?
;
fragment
Digit
: Zero
| NonZeroDigit
;
fragment
NonZeroDigit
: [1-9]
;
fragment
DigitOrUnderscore
: Digit
| Underscore
;
fragment
Underscores
: Underscore+
;
fragment
Underscore
: '_'
;
fragment
HexNumeral
: Zero [xX] HexDigits
;
fragment
HexDigits
: HexDigit (HexDigitOrUnderscore* HexDigit)?
;
fragment
HexDigit
: [0-9a-fA-F]
;
fragment
HexDigitOrUnderscore
: HexDigit
| Underscore
;
fragment
OctalNumeral
: Zero Underscores? OctalDigits
;
fragment
OctalDigits
: OctalDigit (OctalDigitOrUnderscore* OctalDigit)?
;
fragment
OctalDigit
: [0-7]
;
fragment
OctalDigitOrUnderscore
: OctalDigit
| Underscore
;
fragment
BinaryNumeral
: Zero [bB] BinaryDigits
;
fragment
BinaryDigits
: BinaryDigit (BinaryDigitOrUnderscore* BinaryDigit)?
;
fragment
BinaryDigit
: [01]
;
fragment
BinaryDigitOrUnderscore
: BinaryDigit
| Underscore
;
//
// §3.10.2 Floating-Point Literals
//
FloatingPointLiteral
: ( DecimalFloatingPointLiteral
| HexadecimalFloatingPointLiteral
)
(Underscore { require(errorIgnored, "Number ending with underscores is invalid", -1, true); })?
;
fragment
DecimalFloatingPointLiteral
: Digits? Dot Digits ExponentPart? FloatTypeSuffix?
| Digits ExponentPart FloatTypeSuffix?
| Digits FloatTypeSuffix
;
fragment
ExponentPart
: ExponentIndicator SignedInteger
;
fragment
ExponentIndicator
: [eE]
;
fragment
SignedInteger
: Sign? Digits
;
fragment
Sign
: [+\-]
;
fragment
FloatTypeSuffix
: [fFdDgG]
;
fragment
HexadecimalFloatingPointLiteral
: HexSignificand BinaryExponent FloatTypeSuffix?
;
fragment
HexSignificand
: HexNumeral Dot?
| Zero [xX] HexDigits? Dot HexDigits
;
fragment
BinaryExponent
: BinaryExponentIndicator SignedInteger
;
fragment
BinaryExponentIndicator
: [pP]
;
fragment
Dot : '.'
;
//
// §3.10.3 Boolean Literals
//
BooleanLiteral
: 'true'
| 'false'
;
//
// §3.10.6 Escape Sequences for Character and String Literals
//
fragment
EscapeSequence
: Backslash [btnfrs"'\\]
| OctalEscape
| UnicodeEscape
| DollarEscape
| LineEscape
;
fragment
OctalEscape
: Backslash OctalDigit
| Backslash OctalDigit OctalDigit
| Backslash ZeroToThree OctalDigit OctalDigit
;
// Groovy allows 1 or more u's after the backslash
fragment
UnicodeEscape
: Backslash 'u' HexDigit HexDigit HexDigit HexDigit
;
fragment
ZeroToThree
: [0-3]
;
// Groovy Escape Sequences
fragment
DollarEscape
: Backslash Dollar
;
fragment
LineEscape
: Backslash LineTerminator
;
fragment
LineTerminator
: '\r'? '\n' | '\r'
;
fragment
SlashEscape
: Backslash Slash
;
fragment
Backslash
: '\\'
;
fragment
Slash
: '/'
;
fragment
Dollar
: '$'
;
fragment
DqStringQuotationMark
: '"'
;
fragment
SqStringQuotationMark
: '\''
;
fragment
TdqStringQuotationMark
: '"""'
;
fragment
TsqStringQuotationMark
: '\'\'\''
;
//
// §3.10.7 The Null Literal
//
NullLiteral
: 'null'
;
//
// Groovy Operators
//
RANGE_INCLUSIVE : '..';
// RANGE_EXCLUSIVE_LEFT : '<..';
RANGE_EXCLUSIVE_RIGHT : '..<';
// RANGE_EXCLUSIVE_FULL : '<..<';
SPREAD_DOT : '*.';
SAFE_DOT : '?.';
// SAFE_INDEX : '?[' { this.enterParen(); } -> pushMode(DEFAULT_MODE);
// SAFE_CHAIN_DOT : '??.';
ELVIS : '?:';
// METHOD_POINTER : '.&';
// METHOD_REFERENCE : '::';
REGEX_FIND : '=~';
REGEX_MATCH : '==~';
POWER : '**';
SPACESHIP : '<=>';
// IDENTICAL : '===';
// NOT_IDENTICAL : '!==';
ARROW : '->';
// !internalPromise will be parsed as !in ternalPromise, so semantic predicates are necessary
NOT_INSTANCEOF : '!instanceof' { isFollowedBy(_input, ' ', '\t', '\r', '\n') }?;
NOT_IN : '!in' { isFollowedBy(_input, ' ', '\t', '\r', '\n', '[', '(', '{') }?;
//
// §3.11 Separators
//
LPAREN : '(' /* { this.enterParen(); } */ -> pushMode(DEFAULT_MODE);
RPAREN : ')' /* { this.exitParen(); } */ -> popMode;
LBRACE : '{' /* { this.enterParen(); } */ -> pushMode(DEFAULT_MODE);
RBRACE : '}' /* { this.exitParen(); } */ -> popMode;
LBRACK : '[' /* { this.enterParen(); } */ -> pushMode(DEFAULT_MODE);
RBRACK : ']' /* { this.exitParen(); } */ -> popMode;
SEMI : ';';
COMMA : ',';
DOT : Dot;
//
// §3.12 Operators
//
ASSIGN : '=';
GT : '>';
LT : '<';
NOT : '!';
BITNOT : '~';
QUESTION : '?';
COLON : ':';
EQUAL : '==';
LE : '<=';
GE : '>=';
NOTEQUAL : '!=';
AND : '&&';
OR : '||';
// INC : '++';
// DEC : '--';
ADD : '+';
SUB : '-';
MUL : '*';
DIV : Slash;
BITAND : '&';
BITOR : '|';
XOR : '^';
MOD : '%';
ADD_ASSIGN : '+=';
SUB_ASSIGN : '-=';
MUL_ASSIGN : '*=';
DIV_ASSIGN : '/=';
AND_ASSIGN : '&=';
OR_ASSIGN : '|=';
XOR_ASSIGN : '^=';
MOD_ASSIGN : '%=';
LSHIFT_ASSIGN : '<<=';
RSHIFT_ASSIGN : '>>=';
URSHIFT_ASSIGN : '>>>=';
ELVIS_ASSIGN : '?=';
POWER_ASSIGN : '**=';
//
// §3.8 Identifiers (must appear after all keywords in the grammar)
//
CapitalizedIdentifier
: JavaLetter { Character.isUpperCase(_input.LA(-1)) }? JavaLetterOrDigit*
;
Identifier
: JavaLetter JavaLetterOrDigit*
;
fragment
IdentifierInGString
: JavaLetterInGString JavaLetterOrDigitInGString*
;
fragment
JavaLetter
: [a-zA-Z$_] // these are the "java letters" below 0x7F
| // covers all characters above 0x7F which are not a surrogate
~[\u0000-\u007F\uD800-\uDBFF]
{ isJavaIdentifierStartAndNotIdentifierIgnorable(_input.LA(-1)) }?
| // covers UTF-16 surrogate pairs encodings for U+10000 to U+10FFFF
[\uD800-\uDBFF] [\uDC00-\uDFFF]
{ Character.isJavaIdentifierStart(Character.toCodePoint((char) _input.LA(-2), (char) _input.LA(-1))) }?
;
fragment
JavaLetterInGString
: JavaLetter { _input.LA(-1) != '$' }?
;
fragment
JavaLetterOrDigit
: [a-zA-Z0-9$_] // these are the "java letters or digits" below 0x7F
| // covers all characters above 0x7F which are not a surrogate
~[\u0000-\u007F\uD800-\uDBFF]
{ isJavaIdentifierPartAndNotIdentifierIgnorable(_input.LA(-1)) }?
| // covers UTF-16 surrogate pairs encodings for U+10000 to U+10FFFF
[\uD800-\uDBFF] [\uDC00-\uDFFF]
{ Character.isJavaIdentifierPart(Character.toCodePoint((char) _input.LA(-2), (char) _input.LA(-1))) }?
;
fragment
JavaLetterOrDigitInGString
: JavaLetterOrDigit { _input.LA(-1) != '$' }?
;
// fragment
// ShCommand
// : ~[\r\n\uFFFF]*
// ;
// Additional symbols not defined in the lexical specification
// AT : '@';
// ELLIPSIS : '...';
// Whitespace, line escape and comments
WS : ([ \t]+ | LineEscape+) -> skip
;
// Inside (...) and [...] but not {...}, ignore newlines.
NL : LineTerminator /* { this.ignoreTokenInsideParens(); } */
;
// Multiple-line comments (including groovydoc comments)
ML_COMMENT
: '/*' .*? '*/' /* { this.ignoreMultiLineCommentConditionally(); } */ -> type(NL)
;
// Single-line comments
SL_COMMENT
: '//' ~[\r\n\uFFFF]* /* { this.ignoreTokenInsideParens(); } */ -> type(NL)
;
// Script-header comments.
// The very first characters of the file may be "#!". If so, ignore the first line.
// SH_COMMENT
// : '#!' { require(errorIgnored || 0 == this.tokenIndex, "Shebang comment should appear at the first line", -2, true); } ShCommand (LineTerminator '#!' ShCommand)* -> skip
// ;
// Unexpected characters will be handled by groovy parser later.
UNEXPECTED_CHAR
: . { require(errorIgnored, "Unexpected character: '" + getText().replace("'", "\\'") + "'", -1, false); }
;

View File

@@ -0,0 +1,580 @@
/*
* Copyright 2024-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This file is adapted from the Antlr4 Java grammar which has the following license
*
* Copyright (c) 2013 Terence Parr, Sam Harwell
* All rights reserved.
* [The "BSD licence"]
*
* http://www.opensource.org/licenses/bsd-license.php
*
* Subsequent modifications by the Groovy community have been done under the Apache License v2:
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Grammar specification for the Nextflow scripting language.
*
* Based on the official grammar for Groovy:
* https://github.com/apache/groovy/blob/GROOVY_4_0_X/src/antlr/GroovyParser.g4
*/
parser grammar ConfigParser;
options {
superClass = AbstractParser;
tokenVocab = ConfigLexer;
}
@header {
package nextflow.config.parser;
import nextflow.script.parser.AbstractParser;
import org.apache.groovy.parser.antlr4.GroovySyntaxError;
import static nextflow.script.parser.SemanticPredicates.*;
}
@members {
@Override
public int getSyntaxErrorSource() {
return GroovySyntaxError.PARSER;
}
@Override
public int getErrorLine() {
Token token = _input.LT(-1);
if (null == token) {
return -1;
}
return token.getLine();
}
@Override
public int getErrorColumn() {
Token token = _input.LT(-1);
if (null == token) {
return -1;
}
return token.getCharPositionInLine() + 1 + token.getText().length();
}
}
compilationUnit
: nls (configStatement (sep configStatement)* sep?)? EOF
;
//
// top-level statements
//
configStatement
: configInclude #configIncludeStmtAlt
| configAssign #configAssignStmtAlt
| configBlock #configBlockStmtAlt
| configApplyBlock #configApplyBlockStmtAlt
| configIncomplete #configIncompleteStmtAlt
| invalidStatement #configInvalidStmtAlt
;
// -- include statement
configInclude
: INCLUDE_CONFIG expression
;
// -- config assignment
configAssign
: configAssignPath nls ASSIGN nls expression
;
configAssignPath
: configPrimary (DOT configPrimary)*
;
configPrimary
: identifier
| stringLiteral
| builtInType
;
// -- config block
configBlock
: configPrimary nls LBRACE nls (configBlockStatement (sep configBlockStatement)* sep?)? RBRACE
;
configBlockStatement
: configInclude #configIncludeBlockStmtAlt
| configAssign #configAssignBlockStmtAlt
| configBlock #configBlockBlockStmtAlt
| configApplyBlock #configApplyBlockBlockStmtAlt
| configSelector #configSelectorBlockStmtAlt
| configIncomplete #configIncompleteBlockStmtAlt
| invalidStatement #configInvalidBlockStmtAlt
;
configSelector
: kind=Identifier COLON target=configPrimary nls LBRACE nls (configBlockStatement (sep configBlockStatement)* sep?)? RBRACE
;
// -- config "apply" block (e.g. plugins)
configApplyBlock
: configPrimary nls LBRACE nls (configApply (sep configApply)* sep?)? RBRACE
;
configApply
: identifier argumentList
;
// -- incomplete config statement
configIncomplete
: configPrimary (DOT configPrimary)* DOT?
;
//
// -- invalid statements
//
invalidStatement
: ifElseStatement
| tryCatchStatement
| variableDeclaration
;
//
// statements
//
statement
: ifElseStatement #ifElseStmtAlt
| tryCatchStatement #tryCatchStmtAlt
| RETURN expression? #returnStmtAlt
| THROW expression #throwStmtAlt
| assertStatement #assertStmtAlt
| variableDeclaration #variableDeclarationStmtAlt
| multipleAssignmentStatement #multipleAssignmentStmtAlt
| assignmentStatement #assignmentStmtAlt
| expressionStatement #expressionStmtAlt
| SEMI #emptyStmtAlt
;
// -- if/else statement
ifElseStatement
: IF parExpression nls tb=statementOrBlock (nls ELSE nls fb=statementOrBlock)?
;
statementOrBlock
: LBRACE nls blockStatements? RBRACE
| statement
;
blockStatements
: statement (sep statement)* sep?
;
// -- try/catch statement
tryCatchStatement
: TRY nls statementOrBlock (nls catchClause)*
;
catchClause
: CATCH LPAREN catchTypes? identifier rparen nls statementOrBlock
;
catchTypes
: qualifiedClassName (BITOR qualifiedClassName)*
;
// -- assert statement
assertStatement
: ASSERT condition=expression (nls COLON nls message=expression)?
;
// -- variable declaration
variableDeclaration
: (DEF | legacyType | DEF legacyType) identifier (nls ASSIGN nls initializer=expression)?
| DEF variableNames nls ASSIGN nls initializer=expression
;
variableNames
: LPAREN identifier (COMMA identifier)+ rparen
;
// -- assignment statement
multipleAssignmentStatement
: variableNames nls ASSIGN nls expression
;
assignmentStatement
: target=expression nls
op=(ASSIGN
| ADD_ASSIGN
| SUB_ASSIGN
| MUL_ASSIGN
| DIV_ASSIGN
| AND_ASSIGN
| OR_ASSIGN
| XOR_ASSIGN
| RSHIFT_ASSIGN
| URSHIFT_ASSIGN
| LSHIFT_ASSIGN
| MOD_ASSIGN
| POWER_ASSIGN
| ELVIS_ASSIGN
) nls
source=expression
;
// -- expression statement
expressionStatement
: expression
(
{ isValidDirective($expression.ctx) }? argumentList
|
/* only certain expressions can be called as a directive (no parens) */
)
;
//
// expressions
//
expression
// identifiers, literals, closures, lists, maps, method calls, index/property expressions
: primary pathElement* #pathExprAlt
// bitwise not (~) / logical not (!) (level 1)
| op=(BITNOT | NOT) nls expression #unaryNotExprAlt
// math power operator (**) (level 2)
| left=expression op=POWER nls right=expression #powerExprAlt
// unary (+/-) (level 3)
| op=(ADD | SUB) expression #unaryAddExprAlt
// multiplication/division/modulo (level 4)
| left=expression nls op=(MUL | DIV | MOD) nls right=expression #multDivExprAlt
// binary addition/subtraction (level 5)
| left=expression op=(ADD | SUB) nls right=expression #addSubExprAlt
// bit shift, range (level 6)
| left=expression nls
(( dlOp=LT LT
| tgOp=GT GT GT
| dgOp=GT GT
)
|( riOp=RANGE_INCLUSIVE
| reOp=RANGE_EXCLUSIVE_RIGHT
)) nls
right=expression #shiftExprAlt
// boolean relational expressions (level 7)
| left=expression nls op=AS nls type #relationalCastExprAlt
| left=expression nls op=(INSTANCEOF | NOT_INSTANCEOF) nls type #relationalTypeExprAlt
| left=expression nls op=(LE | GE | GT | LT | IN | NOT_IN) nls right=expression #relationalExprAlt
// equality/inequality (==/!=) (level 8)
| left=expression nls
op=(EQUAL
| NOTEQUAL
| SPACESHIP
) nls
right=expression #equalityExprAlt
// regex find and match (=~ and ==~) (level 8.5)
| left=expression nls op=(REGEX_FIND | REGEX_MATCH) nls right=expression #regexExprAlt
// bitwise and (&) (level 9)
| left=expression nls op=BITAND nls right=expression #bitwiseAndExprAlt
// exclusive or (^) (level 10)
| left=expression nls op=XOR nls right=expression #exclusiveOrExprAlt
// bitwise or (|) (level 11)
| left=expression nls op=BITOR nls right=expression #bitwiseOrExprAlt
// logical and (&&) (level 12)
| left=expression nls op=AND nls right=expression #logicalAndExprAlt
// logical or (||) (level 13)
| left=expression nls op=OR nls right=expression #logicalOrExprAlt
// ternary, elvis (level 14)
| <assoc=right>
condition=expression nls
( QUESTION nls tb=expression nls COLON nls
| ELVIS nls
)
fb=expression #conditionalExprAlt
;
primary
: identifier #identifierPrmrAlt
| literal #literalPrmrAlt
| gstring #gstringPrmrAlt
| NEW creator #newPrmrAlt
| parExpression #parenPrmrAlt
| closure #closurePrmrAlt
| list #listPrmrAlt
| map #mapPrmrAlt
| builtInType #builtInTypePrmrAlt
;
pathElement
// property expression
: nls
( DOT
| SPREAD_DOT // spread dot: xs*.y == xs?.collect { x -> x.y }
| SAFE_DOT // safe dot: x?.y == (x != null) ? x.y : null
)
namedProperty #propertyPathExprAlt
// method call expression (with closure)
| closure #closurePathExprAlt
| closureWithLabels #closureWithLabelsPathExprAlt
// method call expression
| arguments #argumentsPathExprAlt
// index expression
| indexPropertyArgs #indexPathExprAlt
;
namedProperty
: identifier
| stringLiteral
| keywords
;
indexPropertyArgs
: LBRACK expressionList RBRACK
;
// -- variable, function, type identifiers
identifier
: Identifier
| CapitalizedIdentifier
| IN
| INCLUDE_CONFIG
;
// -- primitive literals
literal
: IntegerLiteral #integerLiteralAlt
| FloatingPointLiteral #floatingPointLiteralAlt
| stringLiteral #stringLiteralAlt
| BooleanLiteral #booleanLiteralAlt
| NullLiteral #nullLiteralAlt
;
stringLiteral
: StringLiteral
;
// -- gstring expression
gstring
: GStringBegin gstringDqPart* GStringEnd
| TdqGStringBegin gstringTdqPart* TdqGStringEnd
;
gstringDqPart
: GStringText #gstringDqTextAlt
| GStringPath #gstringDqPathAlt
| GStringExprStart expression RBRACE #gstringDqExprAlt
;
gstringTdqPart
: TdqGStringText #gstringTdqTextAlt
| TdqGStringPath #gstringTdqPathAlt
| TdqGStringExprStart expression RBRACE #gstringTdqExprAlt
;
// -- constructor method call
creator
: createdName arguments
;
createdName
: primitiveType
| qualifiedClassName typeArguments?
;
// -- parenthetical expression
parExpression
: LPAREN nls expression nls rparen
;
// -- closure expression
closure
: LBRACE (nls (formalParameterList nls)? ARROW)? nls blockStatements? RBRACE
;
formalParameterList
: formalParameter (COMMA nls formalParameter)*
;
formalParameter
: DEF? legacyType? identifier (nls ASSIGN nls expression)?
;
closureWithLabels
: LBRACE (nls (formalParameterList nls)? ARROW)? nls blockStatementsWithLabels RBRACE
;
blockStatementsWithLabels
: statementOrLabeled (sep statementOrLabeled)* sep?
;
statementOrLabeled
: identifier COLON nls statementOrLabeled
| statement
;
// -- list expression
list
: LBRACK nls expressionList? COMMA? nls RBRACK
;
expressionList
: expression (nls COMMA nls expression)*
;
// -- map expression
map
: LBRACK nls mapEntryList COMMA? nls RBRACK
| LBRACK COLON RBRACK
;
mapEntryList
: mapEntry (nls COMMA nls mapEntry)*
;
mapEntry
: mapEntryLabel COLON expression
;
mapEntryLabel
: keywords
| primary
;
// -- primitive type
builtInType
: BuiltInPrimitiveType
;
// -- argument list
arguments
: LPAREN nls argumentList? COMMA? nls rparen
;
argumentList
: argumentListElement (nls COMMA nls argumentListElement)*
;
argumentListElement
: expression
| namedArg
;
namedArg
: namedProperty COLON expression
;
//
// types
//
type
: primitiveType
| qualifiedClassName typeArguments?
;
primitiveType
: BuiltInPrimitiveType
;
qualifiedClassName
: qualifiedNameElements className
;
qualifiedNameElements
: (qualifiedNameElement DOT)*
;
qualifiedNameElement
: identifier
| AS
| DEF
| IN
;
className
: CapitalizedIdentifier
;
typeArguments
: LT type (COMMA type)* GT
;
legacyType
: type (LBRACK RBRACK)*
;
//
// keywords, whitespace
//
keywords
: AS
| DEF
| IN
| INSTANCEOF
| RETURN
| NullLiteral
| BooleanLiteral
| BuiltInPrimitiveType
;
rparen
: RPAREN
;
nls
: NL*
;
sep : (NL | SEMI)+
;

View File

@@ -0,0 +1,855 @@
/*
* Copyright 2024-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This file is adapted from the Antlr4 Java grammar which has the following license
*
* Copyright (c) 2013 Terence Parr, Sam Harwell
* All rights reserved.
* [The "BSD licence"]
*
* http://www.opensource.org/licenses/bsd-license.php
*
* Subsequent modifications by the Groovy community have been done under the Apache License v2:
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Grammar specification for the Nextflow scripting language.
*
* Based on the official grammar for Groovy:
* https://github.com/apache/groovy/blob/GROOVY_4_0_X/src/antlr/GroovyLexer.g4
*/
lexer grammar ScriptLexer;
options {
superClass = AbstractLexer;
}
@header {
package nextflow.script.parser;
import java.util.*;
import java.util.regex.Pattern;
import org.antlr.v4.runtime.CharStream;
import org.apache.groovy.parser.antlr4.GroovySyntaxError;
import static nextflow.script.parser.SemanticPredicates.*;
}
@members {
private boolean errorIgnored;
private long tokenIndex;
private int lastTokenType;
private int invalidDigitCount;
/**
* Record the index and token type of the current token while emitting tokens.
*/
@Override
public void emit(Token token) {
this.tokenIndex++;
int tokenType = token.getType();
if (Token.DEFAULT_CHANNEL == token.getChannel()) {
this.lastTokenType = tokenType;
}
super.emit(token);
}
private static final int[] REGEX_CHECK_ARRAY = {
// DEC,
// INC,
// THIS,
RBRACE,
RBRACK,
RPAREN,
GStringEnd,
TdqGStringEnd,
NullLiteral,
StringLiteral,
BooleanLiteral,
IntegerLiteral,
FloatingPointLiteral,
Identifier, CapitalizedIdentifier
};
static {
Arrays.sort(REGEX_CHECK_ARRAY);
}
private boolean isRegexAllowed() {
return (Arrays.binarySearch(REGEX_CHECK_ARRAY, this.lastTokenType) < 0);
}
@Override
public int getSyntaxErrorSource() {
return GroovySyntaxError.LEXER;
}
@Override
public int getErrorLine() {
return getLine();
}
@Override
public int getErrorColumn() {
return getCharPositionInLine() + 1;
}
private static boolean isJavaIdentifierStartAndNotIdentifierIgnorable(int codePoint) {
return Character.isJavaIdentifierStart(codePoint) && !Character.isIdentifierIgnorable(codePoint);
}
private static boolean isJavaIdentifierPartAndNotIdentifierIgnorable(int codePoint) {
return Character.isJavaIdentifierPart(codePoint) && !Character.isIdentifierIgnorable(codePoint);
}
}
//
// §3.10.5 String Literals
//
StringLiteral
: DqStringQuotationMark DqStringCharacter* DqStringQuotationMark
| SqStringQuotationMark SqStringCharacter* SqStringQuotationMark
| Slash { this.isRegexAllowed() && _input.LA(1) != '*' }? SlashyStringCharacter+ Slash
| TdqStringQuotationMark TdqStringCharacter* TdqStringQuotationMark
| TsqStringQuotationMark TsqStringCharacter* TsqStringQuotationMark
;
GStringBegin
: DqStringQuotationMark -> pushMode(DQ_GSTRING_MODE)
;
TdqGStringBegin
: TdqStringQuotationMark -> pushMode(TDQ_GSTRING_MODE)
;
mode DQ_GSTRING_MODE;
GStringEnd
: DqStringQuotationMark -> popMode
;
GStringPath
: Dollar IdentifierInGString (Dot IdentifierInGString)*
;
GStringText
: DqStringCharacter+
;
GStringExprStart
: '${' -> pushMode(DEFAULT_MODE)
;
mode TDQ_GSTRING_MODE;
TdqGStringEnd
: TdqStringQuotationMark -> popMode
;
TdqGStringPath
: Dollar IdentifierInGString (Dot IdentifierInGString)*
;
TdqGStringText
: TdqStringCharacter+
;
TdqGStringExprStart
: '${' -> pushMode(DEFAULT_MODE)
;
mode DEFAULT_MODE;
// character in the double quotation string. e.g. "a"
fragment
DqStringCharacter
: ~["\r\n\\$]
| EscapeSequence
;
// character in the single quotation string. e.g. 'a'
fragment
SqStringCharacter
: ~['\r\n\\]
| EscapeSequence
;
// character in the triple double quotation string. e.g. """a"""
fragment
TdqStringCharacter
: ~["\\$]
| DqStringQuotationMark { _input.LA(1) != '"' || _input.LA(2) != '"' || _input.LA(3) == '"' && (_input.LA(4) != '"' || _input.LA(5) != '"') }?
| EscapeSequence
;
// character in the triple single quotation string. e.g. '''a'''
fragment
TsqStringCharacter
: ~['\\]
| SqStringQuotationMark { _input.LA(1) != '\'' || _input.LA(2) != '\'' || _input.LA(3) == '\'' && (_input.LA(4) != '\'' || _input.LA(5) != '\'') }?
| EscapeSequence
;
// character in the slashy string. e.g. /a/
fragment
SlashyStringCharacter
: SlashEscape
| Dollar { !isFollowedByJavaLetterInGString(_input) }?
| ~[/$\u0000]
;
// Groovy keywords
AS : 'as';
DEF : 'def';
IN : 'in';
// TRAIT : 'trait';
// THREADSAFE : 'threadsafe'; // reserved keyword
// the reserved type name of Java10
// VAR : 'var';
//
// §3.9 Keywords
//
BuiltInPrimitiveType
: BOOLEAN
| CHAR
| BYTE
| SHORT
| INT
| LONG
| FLOAT
| DOUBLE
;
// ABSTRACT : 'abstract';
ASSERT : 'assert';
fragment
BOOLEAN : 'boolean';
// BREAK : 'break';
// YIELD : 'yield';
fragment
BYTE : 'byte';
// CASE : 'case';
CATCH : 'catch';
fragment
CHAR : 'char';
// CLASS : 'class';
// CONST : 'const';
// CONTINUE : 'continue';
// DEFAULT : 'default';
// DO : 'do';
fragment
DOUBLE : 'double';
ELSE : 'else';
ENUM : 'enum';
// EXTENDS : 'extends';
// FINAL : 'final';
// FINALLY : 'finally';
fragment
FLOAT : 'float';
// FOR : 'for';
IF : 'if';
// GOTO : 'goto';
// IMPLEMENTS : 'implements';
IMPORT : 'import';
INSTANCEOF : 'instanceof';
fragment
INT : 'int';
// INTERFACE : 'interface';
fragment
LONG : 'long';
// NATIVE : 'native';
NEW : 'new';
// NON_SEALED : 'non-sealed';
// PACKAGE : 'package';
// PERMITS : 'permits';
// PRIVATE : 'private';
// PROTECTED : 'protected';
// PUBLIC : 'public';
RECORD : 'record';
RETURN : 'return';
// SEALED : 'sealed';
fragment
SHORT : 'short';
// STATIC : 'static';
// STRICTFP : 'strictfp';
// SUPER : 'super';
// SWITCH : 'switch';
// SYNCHRONIZED : 'synchronized';
// THIS : 'this';
THROW : 'throw';
// THROWS : 'throws';
// TRANSIENT : 'transient';
TRY : 'try';
// VOID : 'void';
// VOLATILE : 'volatile';
// WHILE : 'while';
// -- feature flag, param declarations
NEXTFLOW : 'nextflow';
PARAMS : 'params';
// -- include declaration
INCLUDE : 'include';
FROM : 'from';
// -- process definition
PROCESS : 'process';
EXEC : 'exec';
INPUT : 'input';
OUTPUT : 'output';
SCRIPT : 'script';
SHELL : 'shell';
STAGE : 'stage';
STUB : 'stub';
TOPIC : 'topic';
TUPLE : 'tuple';
WHEN : 'when';
// -- workflow definition
WORKFLOW : 'workflow';
EMIT : 'emit';
MAIN : 'main';
ONCOMPLETE : 'onComplete';
ONERROR : 'onError';
PUBLISH : 'publish';
TAKE : 'take';
//
// §3.10.1 Integer Literals
//
IntegerLiteral
: ( DecimalIntegerLiteral
| HexIntegerLiteral
| OctalIntegerLiteral
| BinaryIntegerLiteral
)
(Underscore { require(errorIgnored, "Number ending with underscores is invalid", -1, true); })?
// !!! Error Alternative !!!
| Zero ([0-9] { invalidDigitCount++; })+ { require(errorIgnored, "Invalid octal number", -(invalidDigitCount + 1), true); } IntegerTypeSuffix?
;
fragment
Zero
: '0'
;
fragment
DecimalIntegerLiteral
: DecimalNumeral IntegerTypeSuffix?
;
fragment
HexIntegerLiteral
: HexNumeral IntegerTypeSuffix?
;
fragment
OctalIntegerLiteral
: OctalNumeral IntegerTypeSuffix?
;
fragment
BinaryIntegerLiteral
: BinaryNumeral IntegerTypeSuffix?
;
fragment
IntegerTypeSuffix
: [lLiIgG]
;
fragment
DecimalNumeral
: Zero
| NonZeroDigit (Digits? | Underscores Digits)
;
fragment
Digits
: Digit (DigitOrUnderscore* Digit)?
;
fragment
Digit
: Zero
| NonZeroDigit
;
fragment
NonZeroDigit
: [1-9]
;
fragment
DigitOrUnderscore
: Digit
| Underscore
;
fragment
Underscores
: Underscore+
;
fragment
Underscore
: '_'
;
fragment
HexNumeral
: Zero [xX] HexDigits
;
fragment
HexDigits
: HexDigit (HexDigitOrUnderscore* HexDigit)?
;
fragment
HexDigit
: [0-9a-fA-F]
;
fragment
HexDigitOrUnderscore
: HexDigit
| Underscore
;
fragment
OctalNumeral
: Zero Underscores? OctalDigits
;
fragment
OctalDigits
: OctalDigit (OctalDigitOrUnderscore* OctalDigit)?
;
fragment
OctalDigit
: [0-7]
;
fragment
OctalDigitOrUnderscore
: OctalDigit
| Underscore
;
fragment
BinaryNumeral
: Zero [bB] BinaryDigits
;
fragment
BinaryDigits
: BinaryDigit (BinaryDigitOrUnderscore* BinaryDigit)?
;
fragment
BinaryDigit
: [01]
;
fragment
BinaryDigitOrUnderscore
: BinaryDigit
| Underscore
;
//
// §3.10.2 Floating-Point Literals
//
FloatingPointLiteral
: ( DecimalFloatingPointLiteral
| HexadecimalFloatingPointLiteral
)
(Underscore { require(errorIgnored, "Number ending with underscores is invalid", -1, true); })?
;
fragment
DecimalFloatingPointLiteral
: Digits? Dot Digits ExponentPart? FloatTypeSuffix?
| Digits ExponentPart FloatTypeSuffix?
| Digits FloatTypeSuffix
;
fragment
ExponentPart
: ExponentIndicator SignedInteger
;
fragment
ExponentIndicator
: [eE]
;
fragment
SignedInteger
: Sign? Digits
;
fragment
Sign
: [+\-]
;
fragment
FloatTypeSuffix
: [fFdDgG]
;
fragment
HexadecimalFloatingPointLiteral
: HexSignificand BinaryExponent FloatTypeSuffix?
;
fragment
HexSignificand
: HexNumeral Dot?
| Zero [xX] HexDigits? Dot HexDigits
;
fragment
BinaryExponent
: BinaryExponentIndicator SignedInteger
;
fragment
BinaryExponentIndicator
: [pP]
;
fragment
Dot : '.'
;
//
// §3.10.3 Boolean Literals
//
BooleanLiteral
: 'true'
| 'false'
;
//
// §3.10.6 Escape Sequences for Character and String Literals
//
fragment
EscapeSequence
: Backslash [btnfrs"'\\]
| OctalEscape
| UnicodeEscape
| DollarEscape
| LineEscape
;
fragment
OctalEscape
: Backslash OctalDigit
| Backslash OctalDigit OctalDigit
| Backslash ZeroToThree OctalDigit OctalDigit
;
// Groovy allows 1 or more u's after the backslash
fragment
UnicodeEscape
: Backslash 'u' HexDigit HexDigit HexDigit HexDigit
;
fragment
ZeroToThree
: [0-3]
;
// Groovy Escape Sequences
fragment
DollarEscape
: Backslash Dollar
;
fragment
LineEscape
: Backslash LineTerminator
;
fragment
LineTerminator
: '\r'? '\n' | '\r'
;
fragment
SlashEscape
: Backslash Slash
;
fragment
Backslash
: '\\'
;
fragment
Slash
: '/'
;
fragment
Dollar
: '$'
;
fragment
DqStringQuotationMark
: '"'
;
fragment
SqStringQuotationMark
: '\''
;
fragment
TdqStringQuotationMark
: '"""'
;
fragment
TsqStringQuotationMark
: '\'\'\''
;
//
// §3.10.7 The Null Literal
//
NullLiteral
: 'null'
;
//
// Groovy Operators
//
RANGE_INCLUSIVE : '..';
// RANGE_EXCLUSIVE_LEFT : '<..';
RANGE_EXCLUSIVE_RIGHT : '..<';
// RANGE_EXCLUSIVE_FULL : '<..<';
SPREAD_DOT : '*.';
SAFE_DOT : '?.';
// SAFE_INDEX : '?[' { this.enterParen(); } -> pushMode(DEFAULT_MODE);
// SAFE_CHAIN_DOT : '??.';
ELVIS : '?:';
// METHOD_POINTER : '.&';
// METHOD_REFERENCE : '::';
REGEX_FIND : '=~';
REGEX_MATCH : '==~';
POWER : '**';
SPACESHIP : '<=>';
// IDENTICAL : '===';
// NOT_IDENTICAL : '!==';
ARROW : '->';
// !internalPromise will be parsed as !in ternalPromise, so semantic predicates are necessary
NOT_INSTANCEOF : '!instanceof' { isFollowedBy(_input, ' ', '\t', '\r', '\n') }?;
NOT_IN : '!in' { isFollowedBy(_input, ' ', '\t', '\r', '\n', '[', '(', '{') }?;
//
// §3.11 Separators
//
LPAREN : '(' /* { this.enterParen(); } */ -> pushMode(DEFAULT_MODE);
RPAREN : ')' /* { this.exitParen(); } */ -> popMode;
LBRACE : '{' /* { this.enterParen(); } */ -> pushMode(DEFAULT_MODE);
RBRACE : '}' /* { this.exitParen(); } */ -> popMode;
LBRACK : '[' /* { this.enterParen(); } */ -> pushMode(DEFAULT_MODE);
RBRACK : ']' /* { this.exitParen(); } */ -> popMode;
SEMI : ';';
COMMA : ',';
DOT : Dot;
//
// §3.12 Operators
//
ASSIGN : '=';
GT : '>';
LT : '<';
NOT : '!';
BITNOT : '~';
QUESTION : '?';
COLON : ':';
EQUAL : '==';
LE : '<=';
GE : '>=';
NOTEQUAL : '!=';
AND : '&&';
OR : '||';
// INC : '++';
// DEC : '--';
ADD : '+';
SUB : '-';
MUL : '*';
DIV : Slash;
BITAND : '&';
BITOR : '|';
XOR : '^';
MOD : '%';
ADD_ASSIGN : '+=';
SUB_ASSIGN : '-=';
MUL_ASSIGN : '*=';
DIV_ASSIGN : '/=';
AND_ASSIGN : '&=';
OR_ASSIGN : '|=';
XOR_ASSIGN : '^=';
MOD_ASSIGN : '%=';
LSHIFT_ASSIGN : '<<=';
RSHIFT_ASSIGN : '>>=';
URSHIFT_ASSIGN : '>>>=';
ELVIS_ASSIGN : '?=';
POWER_ASSIGN : '**=';
//
// §3.8 Identifiers (must appear after all keywords in the grammar)
//
CapitalizedIdentifier
: JavaLetter { Character.isUpperCase(_input.LA(-1)) }? JavaLetterOrDigit*
;
Identifier
: JavaLetter JavaLetterOrDigit*
;
fragment
IdentifierInGString
: JavaLetterInGString JavaLetterOrDigitInGString*
;
fragment
JavaLetter
: [a-zA-Z$_] // these are the "java letters" below 0x7F
| // covers all characters above 0x7F which are not a surrogate
~[\u0000-\u007F\uD800-\uDBFF]
{ isJavaIdentifierStartAndNotIdentifierIgnorable(_input.LA(-1)) }?
| // covers UTF-16 surrogate pairs encodings for U+10000 to U+10FFFF
[\uD800-\uDBFF] [\uDC00-\uDFFF]
{ Character.isJavaIdentifierStart(Character.toCodePoint((char) _input.LA(-2), (char) _input.LA(-1))) }?
;
fragment
JavaLetterInGString
: JavaLetter { _input.LA(-1) != '$' }?
;
fragment
JavaLetterOrDigit
: [a-zA-Z0-9$_] // these are the "java letters or digits" below 0x7F
| // covers all characters above 0x7F which are not a surrogate
~[\u0000-\u007F\uD800-\uDBFF]
{ isJavaIdentifierPartAndNotIdentifierIgnorable(_input.LA(-1)) }?
| // covers UTF-16 surrogate pairs encodings for U+10000 to U+10FFFF
[\uD800-\uDBFF] [\uDC00-\uDFFF]
{ Character.isJavaIdentifierPart(Character.toCodePoint((char) _input.LA(-2), (char) _input.LA(-1))) }?
;
fragment
JavaLetterOrDigitInGString
: JavaLetterOrDigit { _input.LA(-1) != '$' }?
;
fragment
ShCommand
: ~[\r\n\uFFFF]*
;
// Additional symbols not defined in the lexical specification
// AT : '@';
// ELLIPSIS : '...';
// Whitespace, line escape and comments
WS : ([ \t]+ | LineEscape+) -> skip
;
// Inside (...) and [...] but not {...}, ignore newlines.
NL : LineTerminator /* { this.ignoreTokenInsideParens(); } */
;
// Multiple-line comments (including groovydoc comments)
ML_COMMENT
: '/*' .*? '*/' /* { this.ignoreMultiLineCommentConditionally(); } */ -> type(NL)
;
// Single-line comments
SL_COMMENT
: '//' ~[\r\n\uFFFF]* /* { this.ignoreTokenInsideParens(); } */ -> type(NL)
;
// Script-header comments.
// The very first characters of the file may be "#!". If so, ignore the first line.
SH_COMMENT
: '#!' { require(errorIgnored || 0 == this.tokenIndex, "Shebang comment should appear at the first line", -2, true); } ShCommand (LineTerminator '#!' ShCommand)* -> type(NL)
;
// Unexpected characters will be handled by groovy parser later.
UNEXPECTED_CHAR
: . { require(errorIgnored, "Unexpected character: '" + getText().replace("'", "\\'") + "'", -1, false); }
;

View File

@@ -0,0 +1,837 @@
/*
* Copyright 2024-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This file is adapted from the Antlr4 Java grammar which has the following license
*
* Copyright (c) 2013 Terence Parr, Sam Harwell
* All rights reserved.
* [The "BSD licence"]
*
* http://www.opensource.org/licenses/bsd-license.php
*
* Subsequent modifications by the Groovy community have been done under the Apache License v2:
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Grammar specification for the Nextflow scripting language.
*
* Based on the official grammar for Groovy:
* https://github.com/apache/groovy/blob/GROOVY_4_0_X/src/antlr/GroovyParser.g4
*/
parser grammar ScriptParser;
options {
superClass = AbstractParser;
tokenVocab = ScriptLexer;
}
@header {
package nextflow.script.parser;
import org.apache.groovy.parser.antlr4.GroovySyntaxError;
import static nextflow.script.parser.SemanticPredicates.*;
}
@members {
@Override
public int getSyntaxErrorSource() {
return GroovySyntaxError.PARSER;
}
@Override
public int getErrorLine() {
Token token = _input.LT(-1);
if (null == token) {
return -1;
}
return token.getLine();
}
@Override
public int getErrorColumn() {
Token token = _input.LT(-1);
if (null == token) {
return -1;
}
return token.getCharPositionInLine() + 1 + token.getText().length();
}
}
compilationUnit
: nls (scriptDeclarationOrStatement (sep scriptDeclarationOrStatement)* sep?)? EOF
;
scriptDeclarationOrStatement
: scriptDeclaration
| statement
;
//
// script declarations
//
scriptDeclaration
: featureFlagDeclaration #featureFlagDeclAlt
| includeDeclaration #includeDeclAlt
| importDeclaration #importDeclAlt
| paramsDef #paramsDefAlt
| paramDeclarationV1 #paramDeclV1Alt
| recordDef #recordDefAlt
| enumDef #enumDefAlt
| processDef #processDefAlt
| workflowDef #workflowDefAlt
| outputDef #outputDefAlt
| functionDef #functionDefAlt
| incompleteScriptDeclaration #incompleteScriptDeclAlt
;
// -- feature flag declaration
featureFlagDeclaration
: featureFlagName nls ASSIGN nls expression
;
featureFlagName
: NEXTFLOW (DOT identifier)+
;
// -- include declaration
includeDeclaration
: INCLUDE includeNames FROM stringLiteral
;
includeNames
: LBRACE nls includeName (sep includeName)* sep? RBRACE
;
includeName
: name=identifier
| name=identifier AS alias=identifier
;
// -- import declaration (legacy)
importDeclaration
: IMPORT qualifiedClassName
;
// -- params definition
paramsDef
: PARAMS nls LBRACE
paramsBody?
sep? RBRACE
;
paramsBody
: sep? paramDeclaration (sep paramDeclaration)*
;
paramDeclaration
: identifier (COLON type)? (ASSIGN expression)?
| statement
;
// -- legacy parameter declaration
paramDeclarationV1
: PARAMS (DOT identifier)+ nls ASSIGN nls expression
;
// -- record definition
recordDef
: RECORD identifier nls LBRACE
nls recordBody?
nls RBRACE
;
recordBody
: nameTypePair (sep nameTypePair)*
;
// -- enum definition
enumDef
: ENUM identifier nls LBRACE
nls enumBody? COMMA?
nls RBRACE
;
enumBody
: identifier (nls COMMA nls identifier)*
;
// -- process definition
processDef
: PROCESS name=identifier nls LBRACE
body=processBody?
sep? RBRACE
;
processBody
// explicit script/exec body with optional stub
: (sep processDirectives)?
(sep processInputs)?
(sep processStage)?
(sep processOutputs)?
(sep processTopics)?
(sep processWhen)?
sep processExec
(sep processStub)?
// explicit "Mahesh" form
| (sep processDirectives)?
(sep processInputs)?
(sep processStage)?
(sep processWhen)?
sep processExec
(sep processStub)?
(sep processOutputs)?
(sep processTopics)?
// implicit script/exec body
| (sep processDirectives)?
(sep processInputs)?
(sep processStage)?
(sep processOutputs)?
(sep processTopics)?
(sep processWhen)?
sep blockStatements
;
processDirectives
: statement (sep statement)*
;
processInputs
: INPUT COLON nls processInput (sep processInput)*
;
processInput
: identifier (COLON type)?
| processRecordInput
| processTupleInput
| statement
;
processRecordInput
: RECORD LPAREN nls nameTypePair (COMMA nls nameTypePair)* COMMA? nls rparen
;
processTupleInput
: TUPLE LPAREN nls nameTypePair (COMMA nls nameTypePair)* COMMA? nls rparen
;
processStage
: STAGE COLON nls statement (sep statement)*
;
processOutputs
: OUTPUT COLON nls processOutput (sep processOutput)*
;
processOutput
: nameTypePair (ASSIGN expression)?
| statement
;
processTopics
: TOPIC COLON nls statement (sep statement)*
;
processWhen
: WHEN COLON nls expression
;
processExec
: (SCRIPT | SHELL | EXEC) COLON nls blockStatements
;
processStub
: STUB COLON nls blockStatements
;
// -- workflow definition
workflowDef
: WORKFLOW name=identifier? nls LBRACE
body=workflowBody?
sep? RBRACE
;
workflowBody
// explicit main block with optional take/emit blocks
: (sep TAKE COLON nls take=workflowTakes)?
sep MAIN COLON nls main=blockStatements
(sep EMIT COLON nls emit=workflowEmits)?
(sep PUBLISH COLON nls publish=workflowPublishers)?
(sep ONCOMPLETE COLON nls onComplete=blockStatements)?
(sep ONERROR COLON nls onError=blockStatements)?
// explicit emit block with optional take/main blocks
| (sep TAKE COLON nls take=workflowTakes)?
(sep MAIN COLON nls main=blockStatements)?
sep EMIT COLON nls emit=workflowEmits
(sep PUBLISH COLON nls publish=workflowPublishers)?
// implicit main block
| sep? main=blockStatements
;
workflowTakes
: workflowTake (sep workflowTake)*
;
workflowTake
: identifier (COLON type)?
| statement
;
workflowEmits
: workflowEmit (sep workflowEmit)*
;
workflowEmit
: nameTypePair (ASSIGN expression)?
| statement
;
workflowPublishers
: workflowEmit (sep workflowEmit)*
;
// -- output definition
outputDef
: OUTPUT nls LBRACE
outputBody?
sep? RBRACE
;
outputBody
: sep? outputDeclaration (sep outputDeclaration)*
;
outputDeclaration
: identifier (COLON type)? LBRACE nls blockStatements? RBRACE
| statement
;
// -- function definition
functionDef
: 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
incompleteScriptDeclaration
: identifier (DOT identifier)* DOT
;
//
// statements
//
statement
: ifElseStatement #ifElseStmtAlt
| tryCatchStatement #tryCatchStmtAlt
| RETURN expression? #returnStmtAlt
| THROW expression #throwStmtAlt
| assertStatement #assertStmtAlt
| variableDeclaration #variableDeclarationStmtAlt
| multipleAssignmentStatement #multipleAssignmentStmtAlt
| assignmentStatement #assignmentStmtAlt
| expressionStatement #expressionStmtAlt
| SEMI #emptyStmtAlt
;
// -- if/else statement
ifElseStatement
: IF parExpression nls tb=statementOrBlock (nls ELSE nls fb=statementOrBlock)?
;
statementOrBlock
: LBRACE nls blockStatements? RBRACE
| statement
;
blockStatements
: statement (sep statement)* sep?
;
// -- try/catch statement
tryCatchStatement
: TRY nls statementOrBlock (nls catchClause)*
;
catchClause
: CATCH LPAREN catchVariable rparen nls statementOrBlock
;
catchVariable
: identifier (COLON catchTypes)?
| catchTypes identifier
;
catchTypes
: qualifiedClassName (BITOR qualifiedClassName)*
;
// -- assert statement
assertStatement
: ASSERT condition=expression (nls COLON nls message=expression)?
;
// -- variable declaration
variableDeclaration
: DEF nameTypePair (nls ASSIGN nls initializer=expression)?
| DEF nameTypePairs nls ASSIGN nls initializer=expression
| (legacyType | DEF legacyType) identifier (nls ASSIGN nls initializer=expression)?
;
nameTypePairs
: LPAREN nls nameTypePair (COMMA nls nameTypePair)+ nls rparen
;
nameTypePair
: identifier (COLON type)?
;
// -- assignment statement
multipleAssignmentStatement
: variableNames nls ASSIGN nls expression
;
variableNames
: LPAREN nls identifier (COMMA nls identifier)+ nls rparen
;
assignmentStatement
: target=expression nls
op=(ASSIGN
| ADD_ASSIGN
| SUB_ASSIGN
| MUL_ASSIGN
| DIV_ASSIGN
| AND_ASSIGN
| OR_ASSIGN
| XOR_ASSIGN
| RSHIFT_ASSIGN
| URSHIFT_ASSIGN
| LSHIFT_ASSIGN
| MOD_ASSIGN
| POWER_ASSIGN
| ELVIS_ASSIGN
) nls
source=expression
;
// -- expression statement
expressionStatement
: expression
(
{ isValidDirective($expression.ctx) }? argumentList
|
/* only certain expressions can be called as a directive (no parens) */
)
;
//
// expressions
//
expression
// identifiers, literals, closures, lists, maps, method calls, index/property expressions
: primary pathElement* #pathExprAlt
// bitwise not (~) / logical not (!) (level 1)
| op=(BITNOT | NOT) nls expression #unaryNotExprAlt
// math power operator (**) (level 2)
| left=expression op=POWER nls right=expression #powerExprAlt
// unary (+/-) (level 3)
| op=(ADD | SUB) expression #unaryAddExprAlt
// multiplication/division/modulo (level 4)
| left=expression nls op=(MUL | DIV | MOD) nls right=expression #multDivExprAlt
// binary addition/subtraction (level 5)
| left=expression op=(ADD | SUB) nls right=expression #addSubExprAlt
// bit shift, range (level 6)
| left=expression nls
(( dlOp=LT LT
| tgOp=GT GT GT
| dgOp=GT GT
)
|( riOp=RANGE_INCLUSIVE
| reOp=RANGE_EXCLUSIVE_RIGHT
)) nls
right=expression #shiftExprAlt
// boolean relational expressions (level 7)
| left=expression nls op=AS nls type #relationalCastExprAlt
| left=expression nls op=(INSTANCEOF | NOT_INSTANCEOF) nls type #relationalTypeExprAlt
| left=expression nls op=(LE | GE | GT | LT | IN | NOT_IN) nls right=expression #relationalExprAlt
// equality/inequality (==/!=) (level 8)
| left=expression nls
op=(EQUAL
| NOTEQUAL
| SPACESHIP
) nls
right=expression #equalityExprAlt
// regex find and match (=~ and ==~) (level 8.5)
| left=expression nls op=(REGEX_FIND | REGEX_MATCH) nls right=expression #regexExprAlt
// bitwise and (&) (level 9)
| left=expression nls op=BITAND nls right=expression #bitwiseAndExprAlt
// exclusive or (^) (level 10)
| left=expression nls op=XOR nls right=expression #exclusiveOrExprAlt
// bitwise or (|) (level 11)
| left=expression nls op=BITOR nls right=expression #bitwiseOrExprAlt
// logical and (&&) (level 12)
| left=expression nls op=AND nls right=expression #logicalAndExprAlt
// logical or (||) (level 13)
| left=expression nls op=OR nls right=expression #logicalOrExprAlt
// ternary, elvis (level 14)
| <assoc=right>
condition=expression nls
( QUESTION nls tb=expression nls COLON nls
| ELVIS nls
)
fb=expression #conditionalExprAlt
// incomplete expression
| expression nls (DOT | SPREAD_DOT | SAFE_DOT) #incompleteExprAlt
;
primary
: identifier #identifierPrmrAlt
| literal #literalPrmrAlt
| gstring #gstringPrmrAlt
| NEW creator #newPrmrAlt
| parExpression #parenPrmrAlt
| closure #closurePrmrAlt
| list #listPrmrAlt
| map #mapPrmrAlt
| builtInType #builtInTypePrmrAlt
;
pathElement
// property expression
: nls
( DOT
| SPREAD_DOT // spread dot: xs*.y == xs?.collect { x -> x.y }
| SAFE_DOT // safe dot: x?.y == (x != null) ? x.y : null
)
namedProperty #propertyPathExprAlt
// method call expression (with closure)
| closure #closurePathExprAlt
| closureWithLabels #closureWithLabelsPathExprAlt
// method call expression
| arguments #argumentsPathExprAlt
// index expression
| indexPropertyArgs #indexPathExprAlt
;
namedProperty
: identifier
| stringLiteral
| keywords
;
indexPropertyArgs
: LBRACK expressionList RBRACK
;
// -- variable, function, type identifiers
identifier
: Identifier
| CapitalizedIdentifier
| IN
| NEXTFLOW
| PARAMS
| FROM
| RECORD
| PROCESS
| EXEC
| INPUT
| OUTPUT
| SCRIPT
| SHELL
| STAGE
| STUB
| TOPIC
| TUPLE
| WHEN
| WORKFLOW
| EMIT
| MAIN
| ONCOMPLETE
| ONERROR
| PUBLISH
| TAKE
;
// -- primitive literals
literal
: IntegerLiteral #integerLiteralAlt
| FloatingPointLiteral #floatingPointLiteralAlt
| stringLiteral #stringLiteralAlt
| BooleanLiteral #booleanLiteralAlt
| NullLiteral #nullLiteralAlt
;
stringLiteral
: StringLiteral
;
// -- gstring expression
gstring
: GStringBegin gstringDqPart* GStringEnd
| TdqGStringBegin gstringTdqPart* TdqGStringEnd
;
gstringDqPart
: GStringText #gstringDqTextAlt
| GStringPath #gstringDqPathAlt
| GStringExprStart expression RBRACE #gstringDqExprAlt
;
gstringTdqPart
: TdqGStringText #gstringTdqTextAlt
| TdqGStringPath #gstringTdqPathAlt
| TdqGStringExprStart expression RBRACE #gstringTdqExprAlt
;
// -- constructor method call
creator
: createdName arguments
;
createdName
: primitiveType
| qualifiedClassName typeArguments?
;
// -- parenthetical expression
parExpression
: LPAREN nls expression nls rparen
;
// -- closure expression
closure
: LBRACE (nls (formalParameterList nls)? ARROW)? nls blockStatements? RBRACE
;
formalParameterList
: formalParameter (COMMA nls formalParameter)*
;
formalParameter
: identifier (COLON type)? (nls ASSIGN nls expression)?
| DEF? legacyType? identifier (nls ASSIGN nls expression)?
;
closureWithLabels
: LBRACE (nls (formalParameterList nls)? ARROW)? nls blockStatementsWithLabels RBRACE
;
blockStatementsWithLabels
: statementOrLabeled (sep statementOrLabeled)* sep?
;
statementOrLabeled
: identifier COLON nls statementOrLabeled
| statement
;
// -- list expression
list
: LBRACK nls expressionList? COMMA? nls RBRACK
;
expressionList
: expression (nls COMMA nls expression)*
;
// -- map expression
map
: LBRACK nls mapEntryList COMMA? nls RBRACK
| LBRACK COLON RBRACK
;
mapEntryList
: mapEntry (nls COMMA nls mapEntry)*
;
mapEntry
: mapEntryLabel COLON expression
;
mapEntryLabel
: keywords
| primary
;
// -- primitive type
builtInType
: BuiltInPrimitiveType
;
// -- argument list
arguments
: LPAREN nls argumentList? COMMA? nls rparen
;
argumentList
: argumentListElement (nls COMMA nls argumentListElement)*
;
argumentListElement
: expression
| namedArg
;
namedArg
: namedProperty COLON expression
;
//
// types
//
type
: primitiveType
| qualifiedClassName typeArguments? QUESTION?
;
primitiveType
: BuiltInPrimitiveType
;
qualifiedClassName
: qualifiedNameElements className
;
qualifiedNameElements
: (qualifiedNameElement DOT)*
;
qualifiedNameElement
: identifier
| AS
| DEF
| IN
;
className
: CapitalizedIdentifier
;
typeArguments
: LT typeArgument (COMMA typeArgument)* GT
;
typeArgument
: type
| QUESTION
;
legacyType
: type (LBRACK RBRACK)*
;
//
// keywords, whitespace
//
keywords
: AS
| DEF
| IMPORT
| IN
| INSTANCEOF
| RETURN
| NEXTFLOW
| PARAMS
| INCLUDE
| FROM
| RECORD
| PROCESS
| EXEC
| INPUT
| OUTPUT
| SCRIPT
| SHELL
| STAGE
| STUB
| TOPIC
| TUPLE
| WHEN
| WORKFLOW
| EMIT
| MAIN
| ONCOMPLETE
| ONERROR
| PUBLISH
| TAKE
| NullLiteral
| BooleanLiteral
| BuiltInPrimitiveType
;
rparen
: RPAREN
;
nls
: NL*
;
sep : (NL | SEMI)+
;

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import java.util.List;
/**
* A config block that defines a config option through a DSL.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigApplyBlockNode extends ConfigStatement {
public final String name;
public final List<ConfigApplyNode> statements;
public ConfigApplyBlockNode(String name, List<ConfigApplyNode> statements) {
this.name = name;
this.statements = statements;
}
@Override
public void visit(ConfigVisitor visitor) {
visitor.visitConfigApplyBlock(this);
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
/**
* A directive in a config "apply" block.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigApplyNode extends MethodCallExpression {
public ConfigApplyNode(Expression name, Expression arguments) {
super(VariableExpression.THIS_EXPRESSION, name, arguments);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import java.util.List;
import org.codehaus.groovy.ast.expr.Expression;
/**
* A config assignment statement.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigAssignNode extends ConfigStatement {
public final List<String> names;
public Expression value;
public ConfigAssignNode(List<String> names, Expression value) {
this.names = names;
this.value = value;
}
@Override
public void visit(ConfigVisitor visitor) {
visitor.visitConfigAssign(this);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import java.util.List;
/**
* A config block statement.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigBlockNode extends ConfigStatement {
public final String kind;
public final String name;
public final List<ConfigStatement> statements;
public ConfigBlockNode(String kind, String name, List<ConfigStatement> statements) {
this.kind = kind;
this.name = name;
this.statements = statements;
}
public ConfigBlockNode(String name, List<ConfigStatement> statements) {
this(null, name, statements);
}
@Override
public void visit(ConfigVisitor visitor) {
visitor.visitConfigBlock(this);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import org.codehaus.groovy.ast.expr.Expression;
/**
* A config include statement.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigIncludeNode extends ConfigStatement {
public Expression source;
public ConfigIncludeNode(Expression source) {
this.source = source;
}
@Override
public void visit(ConfigVisitor visitor) {
visitor.visitConfigInclude(this);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
/**
* An incomplete config statement, used to provide more
* contextual error messages and completions.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigIncompleteNode extends ConfigStatement {
public final String text;
public ConfigIncompleteNode(String text) {
this.text = text;
}
@Override
public void visit(ConfigVisitor visitor) {
visitor.visitConfigIncomplete(this);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import java.util.ArrayList;
import java.util.List;
import nextflow.config.spec.SpecNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.control.SourceUnit;
/**
* The top-level AST node for a config file.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigNode extends ModuleNode {
private List<ConfigStatement> configStatements = new ArrayList<>();
private SpecNode.Scope spec;
public ConfigNode(SourceUnit sourceUnit) {
super(sourceUnit);
}
public List<ConfigStatement> getConfigStatements() {
return configStatements;
}
public void addConfigStatement(ConfigStatement statement) {
configStatements.add(statement);
}
public SpecNode.Scope getSpec() {
return spec;
}
public void setSpec(SpecNode.Scope spec) {
this.spec = spec;
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import org.codehaus.groovy.ast.stmt.Statement;
public class ConfigStatement extends Statement {
public void visit(ConfigVisitor visitor) {
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import org.codehaus.groovy.ast.GroovyCodeVisitor;
public interface ConfigVisitor extends GroovyCodeVisitor {
void visit(ConfigNode node);
void visit(ConfigStatement node);
void visitConfigAssign(ConfigAssignNode node);
void visitConfigBlock(ConfigBlockNode node);
void visitConfigApplyBlock(ConfigApplyBlockNode node);
void visitConfigInclude(ConfigIncludeNode node);
void visitConfigIncomplete(ConfigIncompleteNode node);
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.ast;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
public abstract class ConfigVisitorSupport extends ClassCodeVisitorSupport implements ConfigVisitor {
//--------------------------------------------------------------------------
// config statements
@Override
public void visit(ConfigNode node) {
for( var statement : node.getConfigStatements() ) {
visit(statement);
}
}
@Override
public void visit(ConfigStatement node) {
node.visit(this);
}
@Override
public void visitConfigApplyBlock(ConfigApplyBlockNode node) {
for( var statement : node.statements ) {
visitConfigApply(statement);
}
}
public void visitConfigApply(ConfigApplyNode node) {
}
@Override
public void visitConfigAssign(ConfigAssignNode node) {
visit(node.value);
}
@Override
public void visitConfigBlock(ConfigBlockNode node) {
for( var statement : node.statements ) {
visit(statement);
}
}
@Override
public void visitConfigInclude(ConfigIncludeNode node) {
visit(node.source);
}
@Override
public void visitConfigIncomplete(ConfigIncompleteNode node) {
}
//--------------------------------------------------------------------------
// expressions
@Override
public void visitMethodCallExpression(MethodCallExpression node) {
if( !node.isImplicitThis() )
node.getObjectExpression().visit(this);
node.getMethod().visit(this);
node.getArguments().visit(this);
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.control;
import java.io.File;
import groovy.lang.GroovyClassLoader;
import nextflow.config.parser.ConfigParserPluginFactory;
import nextflow.script.control.Compiler;
import nextflow.script.control.LazyErrorCollector;
import nextflow.script.dsl.Types;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.WarningMessage;
/**
* Parse and analyze config files without compiling to Groovy.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigParser {
private Compiler compiler;
public ConfigParser() {
var config = getConfig();
var classLoader = new GroovyClassLoader();
compiler = new Compiler(config, classLoader);
}
public Compiler compiler() {
return compiler;
}
public SourceUnit parse(File file) {
var source = compiler.createSourceUnit(file);
compiler.addSource(source);
compiler.compile(source);
return source;
}
public SourceUnit parse(String name, String contents) {
var source = compiler.createSourceUnit(name, contents);
compiler.addSource(source);
compiler.compile(source);
return source;
}
public void analyze() {
for( var source : compiler.getSources().values() ) {
var includeResolver = new ResolveIncludeVisitor(source);
includeResolver.visit();
for( var error : includeResolver.getErrors() )
source.getErrorCollector().addErrorAndContinue(error);
new ConfigResolveVisitor(source, compiler.compilationUnit(), Types.DEFAULT_CONFIG_IMPORTS).visit();
}
}
private static CompilerConfiguration getConfig() {
var config = new CompilerConfiguration();
config.setPluginFactory(new ConfigParserPluginFactory());
config.setWarningLevel(WarningMessage.POSSIBLE_ERRORS);
return config;
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.control;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import nextflow.config.ast.ConfigAssignNode;
import nextflow.config.ast.ConfigIncludeNode;
import nextflow.config.ast.ConfigNode;
import nextflow.config.ast.ConfigVisitorSupport;
import nextflow.script.control.ResolveVisitor;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.SourceUnit;
/**
* Resolve variable names, function names, and type names in
* a config file.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigResolveVisitor extends ConfigVisitorSupport {
private SourceUnit sourceUnit;
private ResolveVisitor resolver;
public ConfigResolveVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit, List<ClassNode> defaultImports) {
this.sourceUnit = sourceUnit;
this.resolver = new ResolveVisitor(sourceUnit, compilationUnit, defaultImports, Collections.emptyList());
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ConfigNode cn ) {
// initialize variable scopes
new VariableScopeVisitor(sourceUnit).visit();
// resolve type names
super.visit(cn);
// report errors for any unresolved variable references
new DynamicVariablesVisitor().visit(cn);
}
}
@Override
public void visitConfigAssign(ConfigAssignNode node) {
node.value = resolver.transform(node.value);
}
@Override
public void visitConfigInclude(ConfigIncludeNode node) {
node.source = resolver.transform(node.source);
}
private class DynamicVariablesVisitor extends ConfigVisitorSupport {
private static final Pattern ENV_VAR_NAME = Pattern.compile("[A-Z_]+[A-Z0-9_]*");
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public void visitVariableExpression(VariableExpression node) {
var variable = node.getAccessedVariable();
if( variable instanceof DynamicVariable ) {
var message = "`" + node.getName() + "` is not defined";
if( ENV_VAR_NAME.matcher(variable.getName()).matches() )
message += " (hint: use `env('...')` to access environment variable)";
resolver.addError(message, node);
}
}
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.control;
import java.util.ArrayList;
import java.util.stream.Collectors;
import nextflow.config.ast.ConfigApplyNode;
import nextflow.config.ast.ConfigApplyBlockNode;
import nextflow.config.ast.ConfigAssignNode;
import nextflow.config.ast.ConfigBlockNode;
import nextflow.config.ast.ConfigIncludeNode;
import nextflow.config.ast.ConfigNode;
import nextflow.config.ast.ConfigVisitorSupport;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Transform a Nextflow config AST into a Groovy AST.
*
* @see nextflow.config.parser.v2.ConfigDsl
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigToGroovyVisitor extends ConfigVisitorSupport {
private SourceUnit sourceUnit;
private ConfigNode moduleNode;
public ConfigToGroovyVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
this.moduleNode = (ConfigNode) sourceUnit.getAST();
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
if( moduleNode == null )
return;
super.visit(moduleNode);
if( moduleNode.isEmpty() )
moduleNode.addStatement(ReturnStatement.RETURN_NULL_OR_VOID);
}
@Override
public void visitConfigApplyBlock(ConfigApplyBlockNode node) {
moduleNode.addStatement(transformConfigApplyBlock(node));
}
protected Statement transformConfigApplyBlock(ConfigApplyBlockNode node) {
var statements = new ArrayList<Statement>();
for( var call : node.statements )
statements.add(stmt(call));
var code = block(new VariableScope(), statements);
return stmt(callThisX("block", args(constX(node.name), closureX(null, code))));
}
@Override
public void visitConfigAssign(ConfigAssignNode node) {
moduleNode.addStatement(transformConfigAssign(node));
}
protected Statement transformConfigAssign(ConfigAssignNode node) {
var names = listX(
node.names.stream()
.map(name -> (Expression) constX(name))
.collect(Collectors.toList())
);
return stmt(callThisX("assign", args(names, node.value)));
}
@Override
public void visitConfigBlock(ConfigBlockNode node) {
moduleNode.addStatement(transformConfigBlock(node));
}
protected Statement transformConfigBlock(ConfigBlockNode node) {
var statements = new ArrayList<Statement>();
for( var stmt : node.statements ) {
if( stmt instanceof ConfigAssignNode can )
statements.add(transformConfigAssign(can));
else if( stmt instanceof ConfigBlockNode cbn )
statements.add(transformConfigBlock(cbn));
else if( stmt instanceof ConfigIncludeNode cin )
statements.add(transformConfigInclude(cin));
}
var code = block(new VariableScope(), statements);
var kind = node.kind != null ? node.kind : "block";
return stmt(callThisX(kind, args(constX(node.name), closureX(null, code))));
}
@Override
public void visitConfigInclude(ConfigIncludeNode node) {
moduleNode.addStatement(transformConfigInclude(node));
}
protected Statement transformConfigInclude(ConfigIncludeNode node) {
return stmt(callThisX("includeConfig", args(node.source)));
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.control;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import nextflow.config.ast.ConfigIncludeNode;
import nextflow.config.ast.ConfigNode;
import nextflow.config.ast.ConfigVisitorSupport;
import nextflow.script.control.PhaseAware;
import nextflow.script.control.Phases;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
/**
* Resolve includes against included config files.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ResolveIncludeVisitor extends ConfigVisitorSupport {
private SourceUnit sourceUnit;
private URI uri;
private List<SyntaxErrorMessage> errors = new ArrayList<>();
public ResolveIncludeVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
this.uri = sourceUnit.getSource().getURI();
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ConfigNode cn )
super.visit(cn);
}
@Override
public void visitConfigInclude(ConfigIncludeNode node) {
if( !(node.source instanceof ConstantExpression) )
return;
var source = node.source.getText();
var includeUri = getIncludeUri(uri, source);
if( !isIncludeLocal(includeUri) )
return;
if( !Files.exists(Path.of(includeUri)) ) {
addError("Invalid include source: '" + includeUri.getPath() + "'", node);
return;
}
}
protected static URI getIncludeUri(URI uri, String source) {
// return source URI if it is already an absolute URI (e.g. http URL)
try {
var sourceUri = new URI(source);
if( sourceUri.getScheme() != null )
return sourceUri;
}
catch( Exception e ) {
// ignore
}
// otherwise, resolve the source path against the including URI
return Path.of(uri).getParent().resolve(source).normalize().toUri();
}
protected static boolean isIncludeLocal(URI includeUri) {
return "file".equals(includeUri.getScheme());
}
@Override
public void addError(String message, ASTNode node) {
var cause = new ResolveIncludeError(message, node);
var errorMessage = new SyntaxErrorMessage(cause, sourceUnit);
errors.add(errorMessage);
}
public List<SyntaxErrorMessage> getErrors() {
return errors;
}
private class ResolveIncludeError extends SyntaxException implements PhaseAware {
public ResolveIncludeError(String message, ASTNode node) {
super(message, node);
}
@Override
public int getPhase() {
return Phases.INCLUDE_RESOLUTION;
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.control;
import java.net.URI;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.io.StringReaderSource;
public class StringReaderSourceWithURI extends StringReaderSource {
private URI uri;
public StringReaderSourceWithURI(String string, URI uri, CompilerConfiguration configuration) {
super(string, configuration);
this.uri = uri;
}
public URI getURI() {
return uri;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.control;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.control.SourceUnit;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Replace secret expressions with a string literal.
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
public class StripSecretsVisitor extends ClassCodeExpressionTransformer {
private SourceUnit sourceUnit;
private boolean isIncludeConfigArgument;
public StripSecretsVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public Expression transform(Expression node) {
if( node instanceof ClosureExpression ce ) {
ce.visit(this);
return ce;
}
if( node instanceof MethodCallExpression mce ) {
return transformMethodCall(mce);
}
if( node instanceof PropertyExpression pe ) {
return transformProperty(pe);
}
return super.transform(node);
}
/**
* Don't obfuscate secret references in a config include, since
* it would break the config inclusion and isn't needed anyway
* (config include source is not preserved in config map).
*
* @param node
*/
private Expression transformMethodCall(MethodCallExpression node) {
if( "includeConfig".equals(node.getMethodAsString()) ) {
isIncludeConfigArgument = true;
try {
return super.transform(node);
}
finally {
isIncludeConfigArgument = false;
}
}
else {
return super.transform(node);
}
}
/**
* Replace any reference to a secret with a string literal
* in order to not dislose the secret value when printin the config.
*
* @param node
*/
private Expression transformProperty(PropertyExpression node) {
if( isSecretProperty(node) && !isIncludeConfigArgument )
return constX("secrets." + node.getPropertyAsString());
else
return super.transform(node);
}
private boolean isSecretProperty(PropertyExpression node) {
return node.getObjectExpression() instanceof VariableExpression ve
&& "secrets".equals(ve.getText())
&& node.getProperty() instanceof ConstantExpression;
}
}

View File

@@ -0,0 +1,364 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.control;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import nextflow.config.ast.ConfigApplyBlockNode;
import nextflow.config.ast.ConfigApplyNode;
import nextflow.config.ast.ConfigAssignNode;
import nextflow.config.ast.ConfigBlockNode;
import nextflow.config.ast.ConfigIncludeNode;
import nextflow.config.ast.ConfigNode;
import nextflow.config.ast.ConfigVisitorSupport;
import nextflow.config.dsl.ConfigDsl;
import nextflow.config.spec.SpecNode;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.ast.ImplicitClosureParameter;
import nextflow.script.control.VariableScopeChecker;
import nextflow.script.dsl.ProcessDsl;
import nextflow.script.dsl.ScriptDsl;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.syntax.Types;
/**
* Initialize the variable scopes for an AST.
*
* @see org.codehaus.groovy.classgen.VariableScopeVisitor
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
class VariableScopeVisitor extends ConfigVisitorSupport {
private SourceUnit sourceUnit;
private VariableScopeChecker vsc;
private Stack<String> configScopes = new Stack<>();
public VariableScopeVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
this.vsc = new VariableScopeChecker(sourceUnit, new ClassNode(ConfigDsl.class));
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ConfigNode cn ) {
super.visit(cn);
vsc.checkUnusedVariables();
}
}
@Override
public void visitConfigApplyBlock(ConfigApplyBlockNode node) {
configScopes.add(node.name);
var names = currentConfigScopes();
var option = SpecNode.ROOT.getDslOption(names);
if( option != null ) {
vsc.pushScope(option.dsl());
super.visitConfigApplyBlock(node);
vsc.popScope();
}
else {
// invalid config apply block is handled by ScriptAstBuilder
// addError("Unrecognized config block '" + node.name + "'", node);
}
configScopes.pop();
}
@Override
public void visitConfigApply(ConfigApplyNode node) {
checkMethodCall(node);
}
private boolean inProcessScope;
private boolean inClosure;
@Override
public void visitConfigAssign(ConfigAssignNode node) {
for( int i = 0; i < node.names.size() - 1; i++ )
configScopes.add(node.names.get(i));
var scopes = currentConfigScopes();
inProcessScope = isProcessScope(scopes, node);
inClosure = node.value instanceof ClosureExpression;
if( isWorkflowHandler(scopes, node) )
vsc.addWarning("The use of workflow handlers in the config is deprecated -- use the entry workflow or a plugin instead", String.join(".", node.names), node);
if( inClosure ) {
vsc.pushScope(ScriptDsl.class);
if( inProcessScope )
vsc.pushScope(ProcessDsl.class);
}
super.visitConfigAssign(node);
if( inClosure ) {
if( inProcessScope )
vsc.popScope();
vsc.popScope();
}
inClosure = false;
inProcessScope = false;
for( int i = 0; i < node.names.size() - 1; i++ )
configScopes.pop();
}
/**
* Determine whether a config option can access the process
* DSL for dynamic settings.
*
* This includes options in the `process` config scope and `executor.jobName`.
*
* @param scopes
* @param node
*/
private static boolean isProcessScope(List<String> scopes, ConfigAssignNode node) {
if( scopes.isEmpty() )
return false;
if( "process".equals(scopes.get(0)) )
return true;
var option = node.names.get(node.names.size() - 1);
return scopes.size() == 1
&& "executor".equals(scopes.get(0))
&& "jobName".equals(option);
}
private static boolean isWorkflowHandler(List<String> scopes, ConfigAssignNode node) {
var option = node.names.get(node.names.size() - 1);
return scopes.size() == 1
&& "workflow".equals(scopes.get(0))
&& List.of("onComplete", "onError").contains(option);
}
@Override
public void visitMapEntryExpression(MapEntryExpression node) {
node.getKeyExpression().visit(this);
var ic = inClosure;
if( inProcessScope && node.getValueExpression() instanceof ClosureExpression )
inClosure = true;
node.getValueExpression().visit(this);
inClosure = ic;
}
@Override
public void visitConfigBlock(ConfigBlockNode node) {
var newScope = node.kind == null;
if( newScope )
configScopes.add(node.name);
super.visitConfigBlock(node);
if( newScope )
configScopes.remove(configScopes.size() - 1);
}
@Override
public void visitConfigInclude(ConfigIncludeNode node) {
checkConfigInclude(node);
visit(node.source);
}
private void checkConfigInclude(ConfigIncludeNode node) {
if( configScopes.isEmpty() )
return;
if( configScopes.size() == 2 && "profiles".equals(configScopes.get(0)) )
return;
vsc.addError("Config includes are only allowed at the top-level or in a profile", node);
}
// statements
@Override
public void visitBlockStatement(BlockStatement node) {
var newScope = node.getVariableScope() != null;
if( newScope ) vsc.pushScope();
node.setVariableScope(currentScope());
super.visitBlockStatement(node);
if( newScope ) vsc.popScope();
}
@Override
public void visitCatchStatement(CatchStatement node) {
vsc.pushScope();
vsc.declare(node.getVariable(), node);
super.visitCatchStatement(node);
vsc.popScope();
}
@Override
public void visitExpressionStatement(ExpressionStatement node) {
var exp = node.getExpression();
if( exp instanceof DeclarationExpression de ) {
visitDeclarationExpression(de);
return;
}
if( exp instanceof BinaryExpression be && Types.isAssignment(be.getOperation().getType()) ) {
var source = be.getRightExpression();
var target = be.getLeftExpression();
visit(source);
if( !visitAssignment(target) ) {
visit(target);
}
return;
}
super.visitExpressionStatement(node);
}
private boolean visitAssignment(Expression node) {
if( node instanceof TupleExpression te ) {
var result = false;
for( var el : te.getExpressions() )
result |= visitAssignedVariable((VariableExpression) el);
return result;
}
else if( node instanceof VariableExpression ve ) {
return visitAssignedVariable(ve);
}
return false;
}
private boolean visitAssignedVariable(VariableExpression ve) {
var variable = vsc.findVariableDeclaration(ve.getName(), ve);
if( variable != null ) {
ve.setAccessedVariable(variable);
return false;
}
else {
vsc.addError("`" + ve.getName() + "` was assigned but not declared", ve);
return true;
}
}
// expressions
private static final List<String> KEYWORDS = List.of(
"case",
"for",
"switch",
"while"
);
@Override
public void visitMethodCallExpression(MethodCallExpression node) {
checkMethodCall(node);
super.visitMethodCallExpression(node);
}
private void checkMethodCall(MethodCallExpression node) {
if( !node.isImplicitThis() )
return;
var name = node.getMethodAsString();
var methods = vsc.findDslFunction(name, node);
if( methods.size() == 1 )
node.putNodeMetaData(ASTNodeMarker.METHOD_TARGET, methods.get(0));
else if( !methods.isEmpty() )
node.putNodeMetaData(ASTNodeMarker.METHOD_OVERLOADS, methods);
else if( !KEYWORDS.contains(name) )
vsc.addError("`" + name + "` is not defined", node.getMethod());
}
@Override
public void visitDeclarationExpression(DeclarationExpression node) {
visit(node.getRightExpression());
if( node.isMultipleAssignmentDeclaration() ) {
for( var el : node.getTupleExpression() )
vsc.declare((VariableExpression) el);
}
else {
vsc.declare(node.getVariableExpression());
}
}
@Override
public void visitClosureExpression(ClosureExpression node) {
vsc.pushScope();
node.setVariableScope(currentScope());
if( node.isParameterSpecified() ) {
for( var parameter : node.getParameters() ) {
vsc.declare(parameter, parameter);
if( parameter.hasInitialExpression() )
parameter.getInitialExpression().visit(this);
}
}
else if( node.getParameters() != null ) {
var implicit = new ImplicitClosureParameter();
currentScope().putDeclaredVariable(implicit);
}
super.visitClosureExpression(node);
vsc.popScope();
}
@Override
public void visitVariableExpression(VariableExpression node) {
var name = node.getName();
Variable variable = vsc.findVariableDeclaration(name, node);
if( variable == null ) {
if( inProcessScope && inClosure ) {
// dynamic process directives can reference process inputs which are not known at this point
}
else {
variable = new DynamicVariable(name, false);
}
}
if( variable instanceof ImplicitClosureParameter ) {
vsc.addWarning("Implicit closure parameter is deprecated, declare an explicit parameter instead", variable.getName(), node);
}
if( variable != null ) {
node.setAccessedVariable(variable);
}
}
// helpers
private VariableScope currentScope() {
return vsc.getCurrentScope();
}
private List<String> currentConfigScopes() {
var names = new ArrayList<>(configScopes);
if( !names.isEmpty() && "profiles".equals(names.get(0)) ) {
if( !names.isEmpty() ) names.remove(0);
if( !names.isEmpty() ) names.remove(0);
}
return names;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.dsl;
import java.nio.file.Path;
import java.util.Map;
import nextflow.script.dsl.Constant;
import nextflow.script.dsl.Description;
import nextflow.script.dsl.DslScope;
/**
* The built-in constants and functions in a config file.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public interface ConfigDsl extends DslScope {
// constants
@Deprecated
@Constant("baseDir")
@Description("""
Alias of `projectDir`.
""")
Path getBaseDir();
@Constant("launchDir")
@Description("""
The directory where the workflow was launched.
""")
Path getLaunchDir();
@Constant("params")
@Description("""
Map of workflow parameters specified in the config file or as command line options.
""")
Map<String,Object> getParams();
@Constant("projectDir")
@Description("""
The directory where the main script is located.
""")
Path getProjectDir();
@Constant("secrets")
@Description("""
Map of pipeline secrets.
""")
Map<String,String> getSecrets();
// functions
@Description("""
Get the value of an environment variable from the launch environment.
""")
String env(String name);
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.formatter;
import java.util.regex.Pattern;
import nextflow.config.ast.ConfigApplyNode;
import nextflow.config.ast.ConfigApplyBlockNode;
import nextflow.config.ast.ConfigAssignNode;
import nextflow.config.ast.ConfigBlockNode;
import nextflow.config.ast.ConfigIncludeNode;
import nextflow.config.ast.ConfigNode;
import nextflow.config.ast.ConfigVisitorSupport;
import nextflow.script.formatter.FormattingOptions;
import nextflow.script.formatter.Formatter;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.IOGroovyMethods;
import static nextflow.script.ast.ASTUtils.*;
/**
* Format a config file.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigFormattingVisitor extends ConfigVisitorSupport {
private SourceUnit sourceUnit;
private FormattingOptions options;
private Formatter fmt;
public ConfigFormattingVisitor(SourceUnit sourceUnit, FormattingOptions options) {
this.sourceUnit = sourceUnit;
this.options = options;
this.fmt = new Formatter(options);
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ConfigNode cn )
super.visit(cn);
}
public String toString() {
return fmt.toString();
}
// config statements
@Override
public void visitConfigApplyBlock(ConfigApplyBlockNode node) {
fmt.appendLeadingComments(node);
fmt.appendIndent();
fmt.append(node.name);
fmt.append(" {");
fmt.appendNewLine();
fmt.incIndent();
super.visitConfigApplyBlock(node);
fmt.decIndent();
fmt.appendIndent();
fmt.append('}');
fmt.appendNewLine();
}
@Override
public void visitConfigApply(ConfigApplyNode node) {
fmt.appendLeadingComments(node);
fmt.visitDirective(node);
}
@Override
public void visitConfigAssign(ConfigAssignNode node) {
fmt.appendLeadingComments(node);
fmt.appendIndent();
var name = String.join(".", node.names);
fmt.append(name);
if( currentAlignmentWidth > 0 ) {
var padding = currentAlignmentWidth - name.length();
fmt.append(" ".repeat(padding));
}
fmt.append(" = ");
fmt.visit(node.value);
fmt.appendNewLine();
}
private static final Pattern IDENTIFIER = Pattern.compile("[a-zA-Z_]+[a-zA-Z0-9_]*");
private int currentAlignmentWidth = 0;
@Override
public void visitConfigBlock(ConfigBlockNode node) {
fmt.appendLeadingComments(node);
fmt.appendIndent();
if( node.kind != null ) {
fmt.append(node.kind);
fmt.append(": ");
}
var name = node.name;
if( IDENTIFIER.matcher(name).matches() ) {
fmt.append(name);
}
else {
fmt.append('\'');
fmt.append(name);
fmt.append('\'');
}
fmt.append(" {");
fmt.appendNewLine();
int caw = currentAlignmentWidth;
if( options.harshilAlignment() ) {
int maxWidth = 0;
for( var stmt : node.statements ) {
if( stmt instanceof ConfigAssignNode can ) {
var width = String.join(".", can.names).length();
if( maxWidth < width )
maxWidth = width;
}
}
currentAlignmentWidth = maxWidth;
}
fmt.incIndent();
super.visitConfigBlock(node);
fmt.decIndent();
if( options.harshilAlignment() )
currentAlignmentWidth = caw;
fmt.appendIndent();
fmt.append('}');
fmt.appendNewLine();
}
@Override
public void visitConfigInclude(ConfigIncludeNode node) {
fmt.appendLeadingComments(node);
fmt.appendIndent();
fmt.append("includeConfig ");
fmt.visit(node.source);
fmt.appendNewLine();
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.parser;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.control.ParserPlugin;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.io.StringReaderSource;
import org.codehaus.groovy.runtime.IOGroovyMethods;
import org.codehaus.groovy.syntax.Reduction;
import java.io.IOException;
import java.io.Reader;
/**
* Parser plugin for the Nextflow config parser.
*/
public class ConfigParserPlugin implements ParserPlugin {
@Override
public Reduction parseCST(SourceUnit sourceUnit, Reader reader) {
if (!sourceUnit.getSource().canReopenSource()) {
try {
sourceUnit.setSource(new StringReaderSource(
IOGroovyMethods.getText(reader),
sourceUnit.getConfiguration()
));
} catch (IOException e) {
throw new GroovyBugError("Failed to create StringReaderSource", e);
}
}
return null;
}
@Override
public ModuleNode buildAST(SourceUnit sourceUnit, ClassLoader classLoader, Reduction cst) {
return new ConfigAstBuilder(sourceUnit).buildAST();
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.parser;
import org.codehaus.groovy.control.ParserPlugin;
import org.codehaus.groovy.control.ParserPluginFactory;
public class ConfigParserPluginFactory extends ParserPluginFactory {
@Override
public ParserPlugin createParserPlugin() {
return new ConfigParserPlugin();
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.schema;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Deprecated alias for backwards compatibility.
*
* @deprecated Use {@link nextflow.config.spec.ConfigOption} instead.
* This package was renamed from config.schema to config.spec.
*/
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ConfigOption {
Class[] types() default {};
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.schema;
/**
* Deprecated alias for backwards compatibility.
*
* @deprecated Use {@link nextflow.config.spec.ConfigScope} instead.
* This package was renamed from config.schema to config.spec.
*/
@Deprecated
public interface ConfigScope {
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.schema;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Deprecated alias for backwards compatibility.
*
* @deprecated Use {@link nextflow.config.spec.PlaceholderName} instead.
* This package was renamed from config.schema to config.spec.
*/
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PlaceholderName {
String value();
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.schema;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Deprecated alias for backwards compatibility.
*
* @deprecated Use {@link nextflow.config.spec.ScopeName} instead.
* This package was renamed from config.schema to config.spec.
*/
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ScopeName {
String value();
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.scopes;
import nextflow.config.spec.ConfigOption;
import nextflow.config.spec.ConfigScope;
import nextflow.script.dsl.Description;
public class Config implements ConfigScope {
@Description("""
The `env` scope allows you to define environment variables that will be exported into the environment where workflow tasks are executed.
[Read more](https://nextflow.io/docs/latest/reference/config.html#env)
""")
public ConfigScope env;
// NOTE: `nextflow` config options are inferred from FeatureFlagDsl
public ConfigScope nextflow;
@Description("""
The `params` scope allows you to define parameters that will be accessible in the pipeline script.
[Read more](https://nextflow.io/docs/latest/reference/config.html#params)
""")
public ConfigScope params;
@ConfigOption
@Description("""
The `plugins` scope allows you to include plugins at runtime.
[Read more](https://nextflow.io/docs/latest/plugins.html)
""")
public PluginsDsl plugins;
// NOTE: `process` config options are inferred from ProcessDsl
public ConfigScope process;
@Description("""
The `profiles` block allows you to define configuration profiles. A profile is a set of configuration settings that can be applied at runtime with the `-profile` command line option.
[Read more](https://nextflow.io/docs/latest/config.html#config-profiles)
""")
public ConfigScope profiles;
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.scopes;
import nextflow.script.dsl.Description;
import nextflow.script.dsl.DslScope;
public interface PluginsDsl extends DslScope {
@Description("""
Specify a plugin to be used by the pipeline. The plugin id can be a name (e.g. `nf-hello`) or a name with a version (e.g. `nf-hello@0.5.0`).
""")
public void id(String value);
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.spec;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ConfigOption {
Class[] types() default {};
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.spec;
import org.pf4j.ExtensionPoint;
public interface ConfigScope extends ExtensionPoint {
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.spec;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface PlaceholderName {
String value();
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.spec;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for defining the name of a custom config scope. Used
* only by third-party plugins.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface ScopeName {
String value();
}

View File

@@ -0,0 +1,249 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.spec;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nextflow.config.scopes.Config;
import nextflow.script.dsl.Description;
import nextflow.script.dsl.DslScope;
import nextflow.script.dsl.FeatureFlag;
import nextflow.script.dsl.FeatureFlagDsl;
import nextflow.script.dsl.ProcessDsl;
/**
* Models a config spec.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public sealed interface SpecNode {
String description();
public static final Scope ROOT = rootScope();
private static Scope rootScope() {
var result = Scope.of(Config.class, "");
// derive `nextflow` config options from feature flags.
result.children().put("nextflow", nextflowScope());
// derive `process` config options from process directives.
result.children().put("process", processScope());
return result;
}
private static SpecNode nextflowScope() {
var enableOpts = new HashMap<String, SpecNode>();
var previewOpts = new HashMap<String, SpecNode>();
for( var field : FeatureFlagDsl.class.getDeclaredFields() ) {
var fqName = field.getAnnotation(FeatureFlag.class).value();
var names = fqName.split("\\.");
var simpleName = names[names.length - 1];
var desc = annotatedDescription(field, "");
if( fqName.startsWith("nextflow.enable.") )
enableOpts.put(simpleName, new Option(desc, optionTypes(field)));
else if( fqName.startsWith("nextflow.preview.") )
previewOpts.put(simpleName, new Option(desc, optionTypes(field)));
else
throw new IllegalArgumentException();
}
return new Scope(
"",
Map.ofEntries(
Map.entry("enable", (SpecNode) new Scope("", enableOpts)),
Map.entry("preview", (SpecNode) new Scope("", previewOpts))
)
);
}
/**
* Initialize the `process` config scope from the set of
* process directives.
*
* Directives with multiple method overloads are treated as
* options with multiple supported types. Method overloads with
* multiple parameters are ignored because they are not supported
* in the configuration.
*/
private static SpecNode processScope() {
var description = """
The `process` scope allows you to specify default directives for processes in your pipeline.
[Read more](https://nextflow.io/docs/latest/config.html#process-configuration)
""";
var children = new HashMap<String, SpecNode>();
for( var method : ProcessDsl.DirectiveDsl.class.getDeclaredMethods() ) {
if( method.getParameters().length != 1 )
continue;
if( !children.containsKey(method.getName()) ) {
var desc = annotatedDescription(method, "");
children.put(method.getName(), new Option(desc, new ArrayList<>()));
}
var option = (Option) children.get(method.getName());
var paramType = method.getParameterTypes()[0];
option.types.add(paramType);
}
return new Scope(description, children);
}
private static String annotatedDescription(AnnotatedElement el, String defaultValue) {
var annot = el.getAnnotation(Description.class);
return annot != null ? annot.value() : defaultValue;
}
private static List<Type> optionTypes(Field field) {
var result = new ArrayList<Type>();
// use the field type
result.add(field.getGenericType());
// append types from ConfigOption annotation if specified
var annot = field.getAnnotation(ConfigOption.class);
if( annot != null ) {
for( var type : annot.types() )
result.add(type);
}
return result;
}
private static Class rawType(Type type) {
if( type instanceof Class c )
return c;
if( type instanceof ParameterizedType pt )
return (Class) pt.getRawType();
throw new IllegalStateException();
}
/**
* Models a config option that is defined through a DSL
* instead of an assignment (i.e. `plugins`).
*/
public static record DslOption(
String description,
Class dsl
) implements SpecNode {}
/**
* Models a config option.
*/
public static record Option(
String description,
List<Type> types
) implements SpecNode {}
/**
* Models a config scope that contains custom named scopes
* (e.g. `azure.batch.pools.<name>`).
*/
public static record Placeholder(
String description,
String placeholderName,
Scope scope
) implements SpecNode {}
/**
* Models a config scope.
*/
public static record Scope(
String description,
Map<String, SpecNode> children
) implements SpecNode {
/**
* Get the spec node at the given path.
*
* @param names
*/
public SpecNode getChild(List<String> names) {
SpecNode node = this;
for( var name : names ) {
if( node instanceof Scope sn )
node = sn.children().get(name);
else if( node instanceof Placeholder pn )
node = pn.scope();
else
return null;
}
return node;
}
/**
* Get the config dsl option at the given path.
*
* @param names
*/
public DslOption getDslOption(List<String> names) {
return getChild(names) instanceof DslOption option ? option : null;
}
/**
* Get the config option at the given path.
*
* @param names
*/
public Option getOption(List<String> names) {
return getChild(names) instanceof Option option ? option : null;
}
/**
* Get the config scope at the given path.
*
* @param names
*/
public Scope getScope(List<String> names) {
return getChild(names) instanceof Scope scope ? scope : null;
}
/**
* Create a scope node from a ConfigScope class.
*
* @param scope
* @param description
*/
public static Scope of(Class<? extends ConfigScope> scope, String description) {
var children = new HashMap<String, SpecNode>();
for( var field : scope.getDeclaredFields() ) {
var name = field.getName();
var type = field.getGenericType();
var rawType = rawType(type);
var desc = annotatedDescription(field, description);
var placeholderName = field.getAnnotation(PlaceholderName.class);
// fields annotated with @ConfigOption are config options
if( field.getAnnotation(ConfigOption.class) != null ) {
if( DslScope.class.isAssignableFrom(rawType) )
children.put(name, new DslOption(desc, rawType));
else
children.put(name, new Option(desc, optionTypes(field)));
}
// fields of rawType ConfigScope are nested config scopes
else if( ConfigScope.class.isAssignableFrom(rawType) ) {
children.put(name, Scope.of((Class<? extends ConfigScope>) rawType, desc));
}
// fields of type Map<String, ConfigScope> are placeholder scopes
else if( Map.class.isAssignableFrom(rawType) && type instanceof ParameterizedType pt && placeholderName != null ) {
var valueType = (Class<? extends ConfigScope>)pt.getActualTypeArguments()[1];
children.put(name, new Placeholder(desc, placeholderName.value(), Scope.of(valueType, desc)));
}
}
return new Scope(description, children);
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.module.spi;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Fallback implementation of RemoteModuleResolver that is used when no other
* implementation is found via the SPI mechanism.
*
* <p>This implementation throws an exception with a helpful error message
* indicating that remote module resolution is not available.
*
* @author Jorge Ejarque <jorge.ejarque@seqera.io>
*/
public class FallbackRemoteModuleResolver implements RemoteModuleResolver {
@Override
public Path resolve(String moduleName, Path projectDir) {
var baseDir = projectDir != null ? projectDir : Path.of(".").toAbsolutePath();
var modulesDir = baseDir.resolve("modules").normalize();
var resolved = modulesDir.resolve(moduleName).normalize();
if( !resolved.startsWith(modulesDir) ) {
throw new IllegalStateException("Invalid module name '" + moduleName + "' -- path escapes the modules directory");
}
if( !Files.exists(resolved) ) {
throw new IllegalStateException("Module '" + moduleName + "' not found in 'modules' directory -- use 'nextflow module install' to install module first");
}
return resolved.resolve("main.nf");
}
@Override
public int getPriority() {
return Integer.MIN_VALUE; // Fallback has lowest possible priority
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.module.spi;
import java.nio.file.Path;
/**
* Service Provider Interface for resolving remote modules referenced with '@scope/name' syntax.
*
* <p>Implementations should handle:
* <ul>
* <li>Checking if a module is already installed locally</li>
* <li>Downloading modules from a registry if not present</li>
* <li>Version resolution and validation</li>
* </ul>
*
* <p>The interface follows the Java SPI pattern. Implementations should be registered
* in META-INF/services/nextflow.module.spi.RemoteModuleResolver
*
* @author Jorge Ejarque <jorge.ejarque@seqera.io>
*/
public interface RemoteModuleResolver {
/**
* Resolve a remote module reference (e.g., '@scope/name') to a local path.
*
* <p>This method should:
* <ol>
* <li>Parse the module reference</li>
* <li>Check if the module is already installed locally</li>
* <li>Download and install the module if not present (auto-install)</li>
* <li>Validate version constraints if specified</li>
* </ol>
*
* @param moduleName The module reference string (e.g., '@scope/name' or '@scope/name@version')
* @param projectDir The base directory for the project (used to locate the modules directory)
* @return Path to the resolved module's main.nf file
* @throws IllegalArgumentException if the module reference is invalid or resolution fails
*/
Path resolve(String moduleName, Path projectDir);
/**
* Get the priority of this resolver. Higher priority resolvers are tried first.
*
* <p>Use this to allow custom implementations to override the default resolver.
* The default implementation should return 0. Custom implementations can return
* positive values to take precedence.
*
* @return Priority value (higher = tried first), default should be 0
*/
default int getPriority() {
return 0;
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.module.spi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.ServiceLoader;
/**
* Provider for accessing RemoteModuleResolver implementations via SPI.
*
* <p>This class uses the Java ServiceLoader mechanism to discover and load
* implementations of RemoteModuleResolver. It selects the implementation
* with the highest priority.
*
* @author Jorge Ejarque <jorge.ejarque@seqera.io>
*/
public class RemoteModuleResolverProvider {
private static final Logger log = LoggerFactory.getLogger(RemoteModuleResolverProvider.class);
private static RemoteModuleResolver instance;
/**
* Get the RemoteModuleResolver instance with the highest priority.
*
* <p>This method lazily loads and caches the resolver. It discovers all
* implementations via ServiceLoader and selects the one with the highest
* priority value.
*
* <p>If no implementations are found, returns the FallbackRemoteModuleResolver
* which throws an informative exception.
*
* @return The RemoteModuleResolver instance with highest priority
*/
public static synchronized RemoteModuleResolver getInstance() {
if (instance == null) {
instance = loadResolver();
}
return instance;
}
private static RemoteModuleResolver loadResolver() {
List<RemoteModuleResolver> resolvers = new ArrayList<>();
ServiceLoader<RemoteModuleResolver> loader = ServiceLoader.load(RemoteModuleResolver.class);
// Collect all available resolvers
for (RemoteModuleResolver resolver : loader) {
resolvers.add(resolver);
log.debug("Discovered RemoteModuleResolver: {} with priority {}",
resolver.getClass().getName(), resolver.getPriority());
}
// Sort by priority (highest first)
resolvers.sort(Comparator.comparingInt(RemoteModuleResolver::getPriority).reversed());
if (resolvers.isEmpty()) {
log.warn("No RemoteModuleResolver implementations found via SPI, using fallback");
return new FallbackRemoteModuleResolver();
}
RemoteModuleResolver selected = resolvers.get(0);
log.debug("Selected RemoteModuleResolver: {} with priority {}",
selected.getClass().getName(), selected.getPriority());
return selected;
}
/**
* Reset the cached instance. Used primarily for testing.
*/
public static synchronized void reset() {
instance = null;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
/**
* Additional markers for AST nodes that are used for static analysis.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public enum ASTNodeMarker {
// denotes a fully-qualified type annotation (ClassNode)
FULLY_QUALIFIED,
// denotes that an assignment is an implicit declaration
IMPLICIT_DECLARATION,
// the inferred return type of a closure expression
INFERRED_RETURN_TYPE,
// the inferred type of an expression
INFERRED_TYPE,
// the number of enclosing parentheses around an expression
INSIDE_PARENTHESES_LEVEL,
// the comments preceding a statement or declaration
LEADING_COMMENTS,
// the verbatim text of a Groovy-style type annotation (ClassNode)
LEGACY_TYPE,
// the list of candidate MethodNode's for a MethodCallExpression
METHOD_OVERLOADS,
// the MethodNode targeted by a MethodCallExpression
METHOD_TARGET,
// the MethodNode targeted by a variable expression (PropertyNode)
METHOD_VARIABLE_TARGET,
// denotes a nullable type annotation (ClassNode)
NULLABLE,
// the FieldNode targeted by a PropertyExpression
PROPERTY_TARGET,
// the starting quote sequence of a string literal or gstring expression
QUOTE_CHAR,
// denotes that an expression list has a trailing comma
TRAILING_COMMA,
// the trailing comment on the same line as a statement or declaration
TRAILING_COMMENT,
// the verbatim text of a string literal or gstring expression
VERBATIM_TEXT
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import groovy.transform.NamedParams;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.AnnotationConstantExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MapExpression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MethodCall;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.NamedArgumentListExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.tools.GeneralUtils;
/**
* Utility functions for common AST operations.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ASTUtils {
public static Expression classX(String name) {
return GeneralUtils.classX(ClassHelper.makeWithoutCaching(name));
}
public static Expression createX(Class type, TupleExpression args) {
return GeneralUtils.ctorX(new ClassNode(type), args);
}
public static Expression createX(Class type, Expression... expressions) {
return GeneralUtils.ctorX(new ClassNode(type), GeneralUtils.args(expressions));
}
public static Expression createX(String name, TupleExpression args) {
return GeneralUtils.ctorX(ClassHelper.makeWithoutCaching(name), args);
}
public static Expression createX(String name, Expression... expressions) {
return GeneralUtils.ctorX(ClassHelper.makeWithoutCaching(name), GeneralUtils.args(expressions));
}
public static List<Statement> asBlockStatements(Statement statement) {
return statement instanceof BlockStatement block
? block.getStatements()
: Collections.emptyList();
}
public static ConstantExpression asConstX(Expression expression) {
return expression instanceof ConstantExpression ce ? ce : null;
}
/**
* Given a statement which represents a block statement of directives,
* iterate through each directive as a method call expression.
*
* @param statement
*/
public static Stream<MethodCallExpression> asDirectives(Statement statement) {
return asBlockStatements(statement)
.stream()
.map(stmt -> asMethodCallX(stmt))
.filter(mce -> mce != null);
}
/**
* Given a method call which represents a definition (i.e. DSL) block, get
* the definition body, which is the block statement of the last closure argument
* in the method call.
*
* @param call
* @param argsCount
*/
public static BlockStatement asDslBlock(MethodCallExpression call, int argsCount) {
var args = asMethodCallArguments(call);
if( args.size() != argsCount )
return null;
var lastArg = args.get(args.size() - 1);
if( !(lastArg instanceof ClosureExpression) )
return null;
var closure = (ClosureExpression) lastArg;
return (BlockStatement) closure.getCode();
}
public static Parameter[] asFlatParams(Parameter[] params) {
return Arrays.stream(params)
.flatMap((param) -> (
param instanceof TupleParameter tp
? Arrays.stream(tp.components)
: Stream.of(param)
))
.toArray(Parameter[]::new);
}
public static MethodCallExpression asMethodCallX(Statement stmt) {
if( !(stmt instanceof ExpressionStatement) )
return null;
var stmtX = (ExpressionStatement) stmt;
if( !(stmtX.getExpression() instanceof MethodCallExpression) )
return null;
return (MethodCallExpression) stmtX.getExpression();
}
public static List<Expression> asMethodCallArguments(MethodCall call) {
return ((TupleExpression) call.getArguments()).getExpressions();
}
public static List<MapEntryExpression> asNamedArgs(MethodCall call) {
var args = asMethodCallArguments(call);
return args.size() > 0 && args.get(0) instanceof NamedArgumentListExpression nale
? nale.getMapEntryExpressions()
: Collections.emptyList();
}
/**
* Given a parameter with a @NamedParams annotation,
* return the map of named params.
*
* @param parameter
*/
public static Map<String, AnnotationNode> asNamedParams(Parameter parameter) {
var namedParams = new LinkedHashMap<String, AnnotationNode>();
parameter.getAnnotations().stream()
.filter(an -> an.getClassNode().getName().equals(NamedParams.class.getName()))
.flatMap(an -> {
var value = an.getMember("value");
return value instanceof ListExpression le
? le.getExpressions().stream()
: Stream.empty();
})
.forEach((value) -> {
if( !(value instanceof AnnotationConstantExpression) )
return;
var ace = (AnnotationConstantExpression) value;
var namedParam = (AnnotationNode) ace.getValue();
var name = namedParam.getMember("value").getText();
namedParams.put(name, namedParam);
});
return namedParams;
}
public static Parameter asNamedParam(AnnotationNode node) {
var name = node.getMember("value").getText();
var typeX = (ClassExpression) node.getMember("type");
var type = typeX != null ? typeX.getType() : ClassHelper.dynamicType();
return new Parameter(type, name);
}
public static VariableExpression asVarX(Statement statement) {
return statement instanceof ExpressionStatement es ? asVarX(es.getExpression()) : null;
}
public static VariableExpression asVarX(Expression expression) {
return expression instanceof VariableExpression ve ? ve : null;
}
/**
* Given a variable which represents a method being accessed
* as a variable, return the underlying method.
*
* @param variable
*/
public static MethodNode asMethodVariable(Variable variable) {
if( variable instanceof PropertyNode pn ) {
if( pn.getNodeMetaData(ASTNodeMarker.METHOD_VARIABLE_TARGET) instanceof MethodNode mn )
return mn;
}
return null;
}
/**
* Given a property expression which represents a process or workflow
* output, return the underlying process or workflow.
*
* @param node
*/
public static MethodNode asMethodOutput(PropertyExpression node) {
if( node.getObjectExpression() instanceof VariableExpression ve && "out".equals(node.getPropertyAsString()) )
return asMethodVariable(ve.getAccessedVariable());
return null;
}
/**
* Given an annotated node (e.g. class, field, method), Find the first
* annotation of the given type in the node's list of annotations.
*
* @param node
* @param type
*/
public static Optional<AnnotationNode> findAnnotation(AnnotatedNode node, Class type) {
return node.getAnnotations().stream()
.filter(an -> an.getClassNode().getName().equals(type.getName()))
.findFirst();
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.syntax.Token;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ASSIGN;
/**
* Convenience class for assignment expressions.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class AssignmentExpression extends BinaryExpression {
public AssignmentExpression(Expression target, Token op, Expression value) {
super(target, op, value);
}
public AssignmentExpression(Expression target, Expression value) {
super(target, ASSIGN, value);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.Expression;
/**
* A feature flag declaration.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class FeatureFlagNode extends ASTNode {
public final String name;
public final Expression value;
public Variable target;
public FeatureFlagNode(String name, Expression value) {
this.name = name;
this.value = value;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.lang.reflect.Modifier;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.Statement;
/**
* A function definition.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class FunctionNode extends MethodNode {
public FunctionNode(String name, ClassNode returnType, Parameter[] parameters, Statement code) {
super(name, Modifier.PUBLIC, returnType, parameters, ClassNode.EMPTY_ARRAY, code);
}
public FunctionNode(String name) {
super(name, Modifier.PUBLIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE);
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.Parameter;
/**
* An implicit closure parameter (`it`). Used to discourage
* the use of implicit parameters.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ImplicitClosureParameter extends Parameter {
public ImplicitClosureParameter() {
super(ClassHelper.dynamicType(), "it");
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
/**
* An included process, workflow, or function.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class IncludeEntryNode extends ASTNode {
public final String name;
public final String alias;
public IncludeEntryNode(String name, String alias) {
this.name = name;
this.alias = alias;
}
public IncludeEntryNode(String name) {
this(name, null);
}
public String getNameOrAlias() {
return alias != null ? alias : name;
}
private AnnotatedNode target;
public void setTarget(AnnotatedNode target) {
this.target = target;
}
public AnnotatedNode getTarget() {
return target;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.util.List;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
/**
* An include declaration.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class IncludeNode extends ASTNode {
public final ConstantExpression source;
public final List<IncludeEntryNode> entries;
public IncludeNode(ConstantExpression source, List<IncludeEntryNode> entries) {
this.source = source;
this.entries = entries;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
/**
* An incomplete script declaration, used to provide more
* contextual error messages and completions.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class IncompleteNode extends ExpressionStatement {
public final String text;
public IncompleteNode(String text) {
super(EmptyExpression.INSTANCE);
this.text = text;
}
public IncompleteNode(Expression expression) {
super(expression);
this.text = null;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
/**
* An invalid script declaration. Denotes either a
* script declaration that was mixed with statements,
* or an invalid script declaration for which a more
* specific error could not be given.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class InvalidDeclaration extends EmptyStatement {
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.util.List;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.stmt.Statement;
/**
* A workflow output definition.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class OutputBlockNode extends ASTNode {
public final List<OutputNode> declarations;
public OutputBlockNode(List<OutputNode> declarations) {
this.declarations = declarations;
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.stmt.Statement;
/**
* An output declaration.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class OutputNode extends Parameter {
public final Statement body;
public OutputNode(String name, ClassNode type, Statement body) {
super(type, name);
this.body = body;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.Parameter;
/**
* A workflow params definition.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ParamBlockNode extends ASTNode {
public final Parameter[] declarations;
public ParamBlockNode(Parameter[] declarations) {
this.declarations = declarations;
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.expr.Expression;
/**
* A legacy parameter declaration.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ParamNodeV1 extends ASTNode {
public final Expression target;
public Expression value;
public ParamNodeV1(Expression target, Expression value) {
this.target = target;
this.value = value;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
public abstract class ProcessNode extends MethodNode {
public ProcessNode(String name, Parameter[] parameters, ClassNode returnType) {
super(name, 0, returnType, parameters, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE);
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.lang.reflect.Modifier;
import java.util.Optional;
import nextflow.script.types.Record;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.Statement;
import static nextflow.script.ast.ASTUtils.*;
/**
* A legacy process definition.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ProcessNodeV1 extends ProcessNode {
public final Statement directives;
public final Statement inputs;
public final Statement outputs;
public final Expression when;
public final String type;
public final Statement exec;
public final Statement stub;
public ProcessNodeV1(String name, Statement directives, Statement inputs, Statement outputs, Expression when, String type, Statement exec, Statement stub) {
super(name, dummyParams(inputs), dummyReturnType(outputs));
this.directives = directives;
this.inputs = inputs;
this.outputs = outputs;
this.when = when;
this.type = type;
this.exec = exec;
this.stub = stub;
}
private static Parameter[] dummyParams(Statement inputs) {
return asBlockStatements(inputs)
.stream()
.map((stmt) -> new Parameter(ClassHelper.dynamicType(), ""))
.toArray(Parameter[]::new);
}
private static ClassNode dummyReturnType(Statement outputs) {
var cn = new ClassNode(Record.class);
asDirectives(outputs)
.map(call -> emitName(call))
.filter(name -> name != null)
.forEach((name) -> {
var type = ClassHelper.dynamicType();
var fn = new FieldNode(name, Modifier.PUBLIC, type, cn, null);
fn.setDeclaringClass(cn);
cn.addField(fn);
});
return cn;
}
private static String emitName(MethodCallExpression output) {
return Optional.of(output)
.flatMap(call -> Optional.ofNullable(asNamedArgs(call)))
.flatMap(namedArgs ->
namedArgs.stream()
.filter(entry -> "emit".equals(entry.getKeyExpression().getText()))
.findFirst()
)
.flatMap(entry -> Optional.ofNullable(
entry.getValueExpression() instanceof VariableExpression ve ? ve.getName() : null
))
.orElse(null);
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.lang.reflect.Modifier;
import nextflow.script.types.Record;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import static nextflow.script.ast.ASTUtils.*;
/**
* A typed process definition.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ProcessNodeV2 extends ProcessNode {
public final Statement directives;
public final Parameter[] inputs;
public final Statement stagers;
public final Statement outputs;
public final Statement topics;
public final Expression when;
public final String type;
public final Statement exec;
public final Statement stub;
public ProcessNodeV2(String name, Statement directives, Parameter[] inputs, Statement stagers, Statement outputs, Statement topics, Expression when, String type, Statement exec, Statement stub) {
super(name, inputs, dummyReturnType(outputs));
this.directives = directives;
this.inputs = inputs;
this.stagers = stagers;
this.outputs = outputs;
this.topics = topics;
this.when = when;
this.type = type;
this.exec = exec;
this.stub = stub;
}
/**
* Process outputs are represented as a single record, or
* a value if there is a single output expression.
*
* @param block
*/
private static ClassNode dummyReturnType(Statement block) {
var outputs = asBlockStatements(block);
if( outputs.size() == 1 ) {
var first = outputs.get(0);
var output = ((ExpressionStatement) first).getExpression();
return output.getType();
}
var cn = new ClassNode(Record.class);
outputs.stream()
.map(stmt -> ((ExpressionStatement) stmt).getExpression())
.map(output -> outputTarget(output))
.filter(target -> target != null)
.forEach((target) -> {
var fn = new FieldNode(target.getName(), Modifier.PUBLIC, target.getType(), cn, null);
fn.setDeclaringClass(cn);
cn.addField(fn);
});
return cn;
}
private static VariableExpression outputTarget(Expression output) {
if( output instanceof VariableExpression ve ) {
return ve;
}
if( output instanceof AssignmentExpression ae ) {
return (VariableExpression)ae.getLeftExpression();
}
return null;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2024-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.lang.reflect.Modifier;
import nextflow.script.types.Record;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
/**
* A record type definition.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class RecordNode extends ClassNode {
public RecordNode(String name) {
super(name, Modifier.PUBLIC | Modifier.FINAL, ClassHelper.OBJECT_TYPE);
setInterfaces(new ClassNode[] { ClassHelper.makeCached(Record.class) });
}
@Override
public Class getTypeClass() {
return Record.class;
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.util.ArrayList;
import java.util.List;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.control.SourceUnit;
/**
* The top-level AST node for a script.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ScriptNode extends ModuleNode {
private String shebang;
private List<FeatureFlagNode> featureFlags = new ArrayList<>();
private List<IncludeNode> includes = new ArrayList<>();
private ParamBlockNode params;
private List<ParamNodeV1> paramsV1 = new ArrayList<>();
private WorkflowNode entry;
private OutputBlockNode outputs;
private List<WorkflowNode> workflows = new ArrayList<>();
private List<ProcessNode> processes = new ArrayList<>();
private List<FunctionNode> functions = new ArrayList<>();
public ScriptNode(SourceUnit sourceUnit) {
super(sourceUnit);
}
public String getShebang() {
return shebang;
}
/**
* Get the list of script declarations in canonical order.
*/
public List<ASTNode> getDeclarations() {
var declarations = new ArrayList<ASTNode>();
declarations.addAll(featureFlags);
declarations.addAll(includes);
if( params != null )
declarations.add(params);
declarations.addAll(paramsV1);
if( entry != null )
declarations.add(entry);
if( outputs != null )
declarations.add(outputs);
for( var wn : workflows ) {
if( !wn.isEntry() )
declarations.add(wn);
}
declarations.addAll(processes);
declarations.addAll(functions);
declarations.addAll(getTypes());
return declarations;
}
public List<FeatureFlagNode> getFeatureFlags() {
return featureFlags;
}
public List<IncludeNode> getIncludes() {
return includes;
}
public ParamBlockNode getParams() {
return params;
}
public List<ParamNodeV1> getParamsV1() {
return paramsV1;
}
public WorkflowNode getEntry() {
return entry;
}
public OutputBlockNode getOutputs() {
return outputs;
}
public List<WorkflowNode> getWorkflows() {
return workflows;
}
public List<ProcessNode> getProcesses() {
return processes;
}
public List<FunctionNode> getFunctions() {
return functions;
}
public List<ClassNode> getTypes() {
return getClasses().stream()
.filter(cn -> cn instanceof RecordNode || cn.isEnum())
.toList();
}
public void setShebang(String shebang) {
this.shebang = shebang;
}
public void addFeatureFlag(FeatureFlagNode featureFlag) {
featureFlags.add(featureFlag);
}
public void addInclude(IncludeNode includeNode) {
includes.add(includeNode);
}
public void setParams(ParamBlockNode params) {
this.params = params;
}
public void addParamV1(ParamNodeV1 paramNode) {
paramsV1.add(paramNode);
}
public void setEntry(WorkflowNode entry) {
this.entry = entry;
}
public void setOutputs(OutputBlockNode outputs) {
this.outputs = outputs;
}
public void addWorkflow(WorkflowNode workflowNode) {
workflows.add(workflowNode);
}
public void addProcess(ProcessNode processNode) {
processes.add(processNode);
}
public void addFunction(FunctionNode functionNode) {
functions.add(functionNode);
}
public boolean isTypingEnabled() {
return featureFlags.stream().anyMatch(ffn -> (
"nextflow.enable.types".equals(ffn.name)
&& ffn.value instanceof ConstantExpression ce
&& Boolean.TRUE.equals(ce.getValue())
));
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.GroovyCodeVisitor;
public interface ScriptVisitor extends GroovyCodeVisitor {
void visit(ScriptNode node);
void visitFeatureFlag(FeatureFlagNode node);
void visitInclude(IncludeNode node);
void visitParams(ParamBlockNode node);
void visitParam(Parameter node);
void visitParamV1(ParamNodeV1 node);
void visitWorkflow(WorkflowNode node);
void visitProcess(ProcessNode node);
void visitProcessV2(ProcessNodeV2 node);
void visitProcessV1(ProcessNodeV1 node);
void visitFunction(FunctionNode node);
void visitRecord(RecordNode node);
void visitEnum(ClassNode node);
void visitOutputs(OutputBlockNode node);
void visitOutput(OutputNode node);
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.ElvisOperatorExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
public abstract class ScriptVisitorSupport extends ClassCodeVisitorSupport implements ScriptVisitor {
//--------------------------------------------------------------------------
// script declarations
@Override
public void visit(ScriptNode script) {
for( var featureFlag : script.getFeatureFlags() )
visitFeatureFlag(featureFlag);
for( var includeNode : script.getIncludes() )
visitInclude(includeNode);
if( script.getParams() != null )
visitParams(script.getParams());
for( var paramNode : script.getParamsV1() )
visitParamV1(paramNode);
for( var workflowNode : script.getWorkflows() )
visitWorkflow(workflowNode);
for( var processNode : script.getProcesses() )
visitProcess(processNode);
for( var functionNode : script.getFunctions() )
visitFunction(functionNode);
for( var classNode : script.getClasses() ) {
if( classNode instanceof RecordNode rn )
visitRecord(rn);
else if( classNode.isEnum() )
visitEnum(classNode);
}
if( script.getOutputs() != null )
visitOutputs(script.getOutputs());
}
@Override
public void visitFeatureFlag(FeatureFlagNode node) {
}
@Override
public void visitInclude(IncludeNode node) {
visit(node.source);
}
@Override
public void visitParams(ParamBlockNode node) {
for( var param : node.declarations )
visitParam(param);
}
@Override
public void visitParam(Parameter node) {
}
@Override
public void visitParamV1(ParamNodeV1 node) {
visit(node.value);
}
@Override
public void visitWorkflow(WorkflowNode node) {
visit(node.main);
visit(node.emits);
visit(node.publishers);
visit(node.onComplete);
visit(node.onError);
}
@Override
public void visitProcess(ProcessNode node) {
if( node instanceof ProcessNodeV2 pn )
visitProcessV2(pn);
if( node instanceof ProcessNodeV1 pn )
visitProcessV1(pn);
}
@Override
public void visitProcessV2(ProcessNodeV2 node) {
visit(node.directives);
visit(node.stagers);
visit(node.outputs);
visit(node.topics);
visit(node.when);
visit(node.exec);
visit(node.stub);
}
@Override
public void visitProcessV1(ProcessNodeV1 node) {
visit(node.directives);
visit(node.inputs);
visit(node.outputs);
visit(node.when);
visit(node.exec);
visit(node.stub);
}
@Override
public void visitFunction(FunctionNode node) {
visit(node.getCode());
}
@Override
public void visitRecord(RecordNode node) {
for( var fn : node.getFields() )
visitField(fn);
}
@Override
public void visitEnum(ClassNode node) {
for( var fn : node.getFields() )
visitField(fn);
}
@Override
public void visitOutputs(OutputBlockNode node) {
for( var output : node.declarations )
visitOutput(output);
}
@Override
public void visitOutput(OutputNode node) {
visit(node.body);
}
//--------------------------------------------------------------------------
// expressions
@Override
public void visitMethodCallExpression(MethodCallExpression node) {
if( !node.isImplicitThis() )
node.getObjectExpression().visit(this);
node.getMethod().visit(this);
node.getArguments().visit(this);
}
@Override
public void visitShortTernaryExpression(ElvisOperatorExpression node) {
node.getTrueExpression().visit(this);
node.getFalseExpression().visit(this);
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.Parameter;
/**
* A parameter that destructures the components of a tuple
* by name.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class TupleParameter extends Parameter {
public final Parameter[] components;
public TupleParameter(ClassNode type, Parameter[] components) {
super(type, "");
this.components = components;
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.ast;
import java.lang.reflect.Modifier;
import nextflow.script.types.Record;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import static nextflow.script.ast.ASTUtils.*;
/**
* A workflow definition.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class WorkflowNode extends MethodNode {
public final Statement main;
public final Statement emits;
public final Statement publishers;
public final Statement onComplete;
public final Statement onError;
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;
this.onComplete = onComplete;
this.onError = onError;
}
public WorkflowNode(String name, Statement main) {
this(name, Parameter.EMPTY_ARRAY, main, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE, EmptyStatement.INSTANCE);
}
public boolean isEntry() {
return getName() == null;
}
public boolean isCodeSnippet() {
return getLineNumber() == -1;
}
private static ClassNode dummyReturnType(Statement block) {
var emits = asBlockStatements(block);
if( emits.size() == 1 ) {
var first = emits.get(0);
var emit = ((ExpressionStatement) first).getExpression();
return emit.getType();
}
var cn = new ClassNode(Record.class);
emits.stream()
.map(stmt -> ((ExpressionStatement) stmt).getExpression())
.map(emit -> emitTarget(emit))
.filter(target -> target != null)
.forEach((target) -> {
var fn = new FieldNode(target.getName(), Modifier.PUBLIC, target.getType(), cn, null);
fn.setDeclaringClass(cn);
cn.addField(fn);
});
return cn;
}
private static VariableExpression emitTarget(Expression emit) {
if( emit instanceof VariableExpression ve ) {
return ve;
}
if( emit instanceof AssignmentExpression ae ) {
return (VariableExpression)ae.getLeftExpression();
}
return null;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.util.IdentityHashMap;
import java.util.HashMap;
import java.util.Map;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.WorkflowNode;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.control.SourceUnit;
/**
* Collect call sites for each workflow in a script.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class CallSiteCollector {
public Map<WorkflowNode, Map<String, MethodNode>> apply(SourceUnit source) {
var callSites = new IdentityHashMap<WorkflowNode, Map<String, MethodNode>>();
if( source.getAST() instanceof ScriptNode sn ) {
for ( var wn : sn.getWorkflows() )
callSites.put(wn, new WorkflowVisitor().apply(wn));
}
return callSites;
}
public class WorkflowVisitor extends CodeVisitorSupport {
private Map<String, MethodNode> calls;
public Map<String, MethodNode> apply(WorkflowNode node) {
calls = new HashMap<>();
visit(node.main);
visit(node.emits);
visit(node.publishers);
return calls;
}
@Override
public void visitMethodCallExpression(MethodCallExpression node) {
visit(node.getObjectExpression());
visit(node.getArguments());
if( node.isImplicitThis() ) {
var mn = (MethodNode) node.getNodeMetaData(ASTNodeMarker.METHOD_TARGET);
if( mn instanceof WorkflowNode || mn instanceof ProcessNode )
calls.put(node.getMethodAsString(), mn);
}
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.io.File;
import java.net.URI;
import java.security.CodeSource;
import java.util.HashMap;
import java.util.Map;
import groovy.lang.GroovyClassLoader;
import org.antlr.v4.runtime.RecognitionException;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.ErrorCollector;
import org.codehaus.groovy.control.SourceUnit;
/**
* Compiler that can lookup source units by URI.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class Compiler {
private CompilationUnit compilationUnit;
private Map<URI, SourceUnit> sourcesByUri = new HashMap<>();
public Compiler(CompilerConfiguration configuration, GroovyClassLoader classLoader) {
this(new CompilationUnit(configuration, null, classLoader));
}
public Compiler(CompilationUnit compilationUnit) {
this.compilationUnit = compilationUnit;
}
public CompilationUnit compilationUnit() {
return compilationUnit;
}
protected CompilerConfiguration configuration() {
return compilationUnit().getConfiguration();
}
protected GroovyClassLoader classLoader() {
return compilationUnit().getClassLoader();
}
public SourceUnit createSourceUnit(File file) {
return new SourceUnit(
file,
configuration(),
classLoader(),
createErrorCollector());
}
public SourceUnit createSourceUnit(String name, String contents) {
return new SourceUnit(
name,
contents,
configuration(),
classLoader(),
createErrorCollector());
}
protected ErrorCollector createErrorCollector() {
return new LazyErrorCollector(configuration());
}
public void addSource(SourceUnit source) {
sourcesByUri.put(source.getSource().getURI(), source);
}
public Map<URI, SourceUnit> getSources() {
return sourcesByUri;
}
public SourceUnit getSource(URI uri) {
return sourcesByUri.get(uri);
}
public void compile(SourceUnit source) {
try {
source.parse();
source.buildAST();
}
catch( RecognitionException e ) {
}
catch( CompilationFailedException e ) {
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.GStringExpression;
import org.codehaus.groovy.control.SourceUnit;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Transform a GString to a Lazy GString.
*
* from
* "${foo} ${bar}"
* to
* "${->foo} ${->bar}"
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
public class GStringToLazyVisitor extends ClassCodeVisitorSupport {
private SourceUnit sourceUnit;
private boolean inClosure;
public GStringToLazyVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public void visitClosureExpression(ClosureExpression node) {
inClosure = true;
try {
super.visitClosureExpression(node);
}
finally {
inClosure = false;
}
}
@Override
public void visitGStringExpression(GStringExpression node) {
// gstrings in a closure will be lazily evaluated and therefore
// don't need to be lazy themselves
if( !inClosure ) {
transformToLazy(node);
}
}
private void transformToLazy(GStringExpression node) {
var values = node.getValues();
var lazyValues = new Expression[values.size()];
// wrap all non-closure expressions in a closure
for( int i = 0; i < values.size(); i++ ) {
var value = values.get(i);
if( value instanceof ClosureExpression ) {
// when the value is already a closure, skip the entire gstring
// because it is assumed to already be lazy
return;
}
lazyValues[i] = wrapExpressionInClosure(value);
}
for( int i = 0; i < values.size(); i++ ) {
values.set(i, lazyValues[i]);
}
}
protected ClosureExpression wrapExpressionInClosure(Expression node) {
// note: the closure parameter argument must be *null* to force the creation of a closure like {-> something}
// otherwise it creates a closure with an implicit parameter that is managed in a different manner by the
// GString -- see http://docs.groovy-lang.org/latest/html/documentation/#_special_case_of_interpolating_closure_expressions
return closureX(null, block(stmt(node)));
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.GStringExpression;
import org.codehaus.groovy.control.SourceUnit;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Coerce a GString into a String.
*
* from
* "${foo} ${bar}"
* to
* "${foo} ${bar}".toString()
*
* This enables equality checks between GStrings and Strings,
* e.g. `"${'hello'}" == 'hello'`.
*
* @author Ben Sherman <bentshermman@gmail.com>
*/
public class GStringToStringVisitor extends ClassCodeExpressionTransformer {
private SourceUnit sourceUnit;
public GStringToStringVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public Expression transform(Expression node) {
if( node instanceof ClosureExpression ce ) {
ce.visit(this);
return ce;
}
if( node instanceof GStringExpression gse ) {
return transformToString(gse);
}
return super.transform(node);
}
private Expression transformToString(GStringExpression node) {
return callX(node, "toString", new ArgumentListExpression());
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2024-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.io.File;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.Phases;
import org.codehaus.groovy.control.SourceUnit;
/**
* Load Groovy classes from the `lib` directory.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class GroovyCompiler {
public static List<ClassNode> compile(SourceUnit su) {
// create compilation unit
var config = new CompilerConfiguration();
config.getOptimizationOptions().put(CompilerConfiguration.GROOVYDOC, true);
var classLoader = new GroovyClassLoader();
var compilationUnit = new CompilationUnit(config, null, classLoader);
// create source units (or restore from cache)
var uri = su.getSource().getURI();
var sourceUnit = new SourceUnit(
new File(uri),
config,
classLoader,
new LazyErrorCollector(config));
compilationUnit.addSource(sourceUnit);
// compile source files
compilationUnit.compile(Phases.CANONICALIZATION);
// collect compiled classes
var moduleNode = sourceUnit.getAST();
if( moduleNode == null )
return Collections.emptyList();
return moduleNode.getClasses();
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.ErrorCollector;
/**
* Error collector that does not throw exceptions.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class LazyErrorCollector extends ErrorCollector {
public LazyErrorCollector(CompilerConfiguration configuration) {
super(configuration);
}
@Override
protected void failIfErrors() {
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import nextflow.module.spi.RemoteModuleResolverProvider;
import nextflow.script.ast.IncludeNode;
import nextflow.script.ast.ScriptNode;
import org.codehaus.groovy.control.SourceUnit;
/**
* Resolve and compile all modules included (directly or indirectly)
* by the main script.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ModuleResolver {
private Compiler compiler;
private Path projectDir;
public ModuleResolver(Path projectDir, Compiler compiler) {
this.compiler = compiler;
this.projectDir = projectDir;
}
/**
* Resolve all modules included by a script.
*
* @param entry the main script
* @param sourceResolver function that generates the source unit for a given file
*/
public Set<SourceUnit> resolve(SourceUnit entry, Function<URI,SourceUnit> sourceResolver) {
var modules = new HashSet<SourceUnit>();
var queuedSources = new LinkedList<SourceUnit>();
compiler.addSource(entry);
queuedSources.add(entry);
while( !queuedSources.isEmpty() ) {
var source = queuedSources.remove();
if( source.getAST() == null )
continue;
var sn = (ScriptNode) source.getAST();
for( var in : sn.getIncludes() ) {
var includeSource = resolveInclude(in, source, sourceResolver);
if( includeSource == null )
continue;
modules.add(includeSource);
queuedSources.add(includeSource);
}
}
return modules;
}
private SourceUnit resolveInclude(IncludeNode node, SourceUnit sourceUnit, Function<URI,SourceUnit> sourceResolver) {
var source = node.source.getText();
if( source.startsWith("plugin/") )
return null;
var uri = sourceUnit.getSource().getURI();
var includeUri = getIncludeUri(uri, source);
if( compiler.getSource(includeUri) != null )
return null;
if( !Files.exists(Path.of(includeUri)) )
return null;
var includeSource = sourceResolver.apply(includeUri);
compiler.addSource(includeSource);
compiler.compile(includeSource);
if( includeSource.getAST() == null )
return null;
return includeSource;
}
private URI getIncludeUri(URI uri, String source) {
if( isRemoteModule(source) ) {
return RemoteModuleResolverProvider.getInstance()
.resolve(source, projectDir)
.normalize()
.toUri();
}
else {
var parent = Path.of(uri).getParent();
return getLocalIncludeUri(parent, source);
}
}
/**
* Module name pattern matching the canonical format used by ModuleReference.
* Scope: lowercase alphanumeric with dots/underscores/hyphens.
* Name: one or more slash-separated segments, each lowercase alphanumeric with dots/underscores/hyphens.
*/
private static final String REMOTE_MODULE_PATTERN = "^[a-z0-9][a-z0-9._\\-]*/[a-z][a-z0-9._\\-]*(/[a-z][a-z0-9._\\-]*)*$";
static boolean isRemoteModule(String source) {
if( source.startsWith("/") || source.startsWith("./") || source.startsWith("../") )
return false;
return source.matches(REMOTE_MODULE_PATTERN);
}
private static URI getLocalIncludeUri(Path parent, String source) {
Path includePath = parent.resolve(source);
if( Files.isDirectory(includePath) )
includePath = includePath.resolve("main.nf");
else if( !source.endsWith(".nf") )
includePath = Path.of(includePath.toString() + ".nf");
return includePath.normalize().toUri();
}
}

View File

@@ -0,0 +1,375 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.IfStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.syntax.SyntaxException;
import static nextflow.script.ast.ASTUtils.*;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Transform closure arguments for branch and multiMap
* into the appropriate criteria objects.
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
public class OpCriteriaVisitor extends ClassCodeExpressionTransformer {
private SourceUnit sourceUnit;
public OpCriteriaVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public Expression transform(Expression node) {
if( node instanceof ClosureExpression ce ) {
ce.visit(this);
return ce;
}
if( node instanceof MethodCallExpression mce ) {
return transformMethodCall(mce);
}
return super.transform(node);
}
private Expression transformMethodCall(MethodCallExpression node) {
ClosureExpression body;
if( (body=asBranchOpClosure(node)) != null ) {
var criteria = new BranchTransformer(body).apply();
node.setArguments(args(criteria));
return node;
}
if( (body=asMultiMapOpClosure(node)) != null ) {
var criteria = new MultiMapTransformer(body).apply();
node.setArguments(args(criteria));
return node;
}
return super.transform(node);
}
private ClosureExpression asBranchOpClosure(MethodCallExpression node) {
var name = node.getMethodAsString();
var arguments = (TupleExpression) node.getArguments();
var argCount = arguments.getExpressions().size();
var closureArg = argCount > 0 && arguments.getExpression(argCount - 1) instanceof ClosureExpression ce ? ce : null;
if( "branch".equals(name) && argCount == 1 )
return closureArg;
if( node.isImplicitThis() && "branchCriteria".equals(name) && argCount == 1 )
return closureArg;
return null;
}
private ClosureExpression asMultiMapOpClosure(MethodCallExpression node) {
var name = node.getMethodAsString();
var arguments = (TupleExpression) node.getArguments();
var argCount = arguments.getExpressions().size();
var closureArg = argCount > 0 && arguments.getExpression(argCount - 1) instanceof ClosureExpression ce ? ce : null;
if( "multiMap".equals(name) && argCount == 1 )
return closureArg;
if( node.isImplicitThis() && "multiMapCriteria".equals(name) && argCount == 1 )
return closureArg;
return null;
}
private void syntaxError(String message, ASTNode node) {
sourceUnit.addError(new SyntaxException(message, node.getLineNumber(), node.getColumnNumber()));
}
class BranchTransformer {
private ClosureExpression body;
public BranchTransformer(ClosureExpression body) {
this.body = body;
}
Expression apply() {
if( body.getParameters() == null ) {
syntaxError("Branch criteria should declare at least one parameter or use the implicit `it` parameter", body);
return body;
}
// collect mapping of branch conditions
var code = body.getCode() instanceof BlockStatement block ? block : null;
if( code == null )
return body;
var allLabels = new LinkedHashSet<String>();
var allBlocks = new LinkedHashMap<String,BranchCondition>();
var statements = new ArrayList<Statement>();
String currentLabel = null;
for( var stmt : code.getStatements() ) {
var label = stmt.getStatementLabel();
if( label != null ) {
if( !allLabels.add(label) ) {
syntaxError("Branch label already declared: " + label, stmt);
break;
}
currentLabel = label;
if( stmt instanceof ExpressionStatement es ) {
var block = new BranchCondition(label, es.getExpression());
allBlocks.put(label, block);
}
else {
syntaxError("Unexpected statement in label " + label, stmt);
break;
}
}
else if( currentLabel != null ) {
var block = allBlocks.get(currentLabel);
block.code.add(stmt);
}
else {
statements.add(stmt);
}
}
if( allBlocks.isEmpty() ) {
syntaxError("Branch criteria should declare at least one branch", body);
return body;
}
// construct if statement for each branch condition
IfStatement ifStatement = null;
IfStatement prevIf = null;
for( var branch : allBlocks.values() ) {
var nextIf = ifS(boolX(branch.condition), branchBlock(branch));
nextIf.addStatementLabel(branch.label);
if( ifStatement == null )
ifStatement = nextIf;
if( prevIf != null )
prevIf.setElseBlock(nextIf);
prevIf = nextIf;
}
statements.add(ifStatement);
// construct branch criteria
var main = closureX(body.getParameters(), block(body.getVariableScope(), statements));
var newTokenBranchDef = createX("nextflow.script.TokenBranchDef", main, list2args(new ArrayList(allLabels)));
return closureX(null, block(body.getVariableScope(), stmt(newTokenBranchDef)));
}
private Statement branchBlock(BranchCondition branch) {
var choice = branch.label;
var statements = branch.code;
// return the closure param by default
if( statements.isEmpty() ) {
statements.add(branchReturn(paramX(body.getParameters()), choice));
return block(body.getVariableScope(), statements);
}
// otherwise transform any return statements
var returns = new BranchReturnCollector().collect(statements);
if( !returns.isEmpty() ) {
for( var stmt : returns )
stmt.setExpression(branchReturnX(stmt.getExpression(), choice));
return block(body.getVariableScope(), statements);
}
// otherwise transform the last expression statement
var last = statements.size() - 1;
if( statements.get(last) instanceof ExpressionStatement es ) {
statements.set(last, branchReturn(es.getExpression(), choice));
return block(body.getVariableScope(), statements);
}
syntaxError("Unexpected statement in branch condition", statements.get(last));
return EmptyStatement.INSTANCE;
}
private Expression paramX(Parameter[] params) {
if( params.length == 0 )
return varX("it");
if( params.length == 1 )
return varX(params[0].getName());
return listX(
Arrays.stream(params).map(p -> (Expression) varX(p.getName())).toList()
);
}
private Statement branchReturn(Expression value, String choice) {
return returnS(branchReturnX(value, choice));
}
private Expression branchReturnX(Expression value, String choice) {
return createX("nextflow.script.TokenBranchChoice", value, constX(choice));
}
private static class BranchReturnCollector extends CodeVisitorSupport {
private List<ReturnStatement> returns = new ArrayList<>();
public List<ReturnStatement> collect(List<Statement> statements) {
for( var stmt : statements )
stmt.visit(this);
return returns;
}
@Override
public void visitReturnStatement(ReturnStatement node) {
returns.add(node);
}
}
private static class BranchCondition {
String label;
Expression condition;
List<Statement> code = new ArrayList<>();
public BranchCondition(String label, Expression condition) {
this.label = label;
this.condition = condition;
}
}
}
class MultiMapTransformer {
private ClosureExpression body;
public MultiMapTransformer(ClosureExpression body) {
this.body = body;
}
Expression apply() {
// assign output variables for each multiMap block
var code = body.getCode() instanceof BlockStatement block ? block : null;
if( code == null )
return body;
var allLabels = new LinkedHashSet<String>();
var vars = new LinkedHashSet<String>();
var statements = new ArrayList<Statement>(code.getStatements().size() * 2);
List<String> labels = Collections.emptyList();
for( int i=0; i<code.getStatements().size(); i++ ) {
var stmt = code.getStatements().get(i);
var currentLabels = Optional.ofNullable(stmt.getStatementLabels()).orElse(labels);
if( currentLabels != null )
allLabels.addAll(DefaultGroovyMethods.asReversed(currentLabels));
if( DefaultGroovyMethods.equals(currentLabels, labels) ) {
statements.add(stmt);
continue;
}
if( DefaultGroovyMethods.asBoolean(labels) ) {
assignOutputVars(stmt, code.getStatements().get(i - 1), labels, vars, statements);
statements.add(stmt);
}
else {
statements.add(stmt);
}
labels = currentLabels;
}
if( DefaultGroovyMethods.asBoolean(labels) ) {
var last = code.getStatements().get(code.getStatements().size() - 1);
assignOutputVars(last, last, labels, vars, statements);
}
if( allLabels.isEmpty() ) {
syntaxError("multiMap criteria should declare at least two outputs", code);
return body;
}
// construct map entry for each output variable
statements.add(returnS(mapX(
vars.stream().map(v -> entryX(constX(v.substring(5)), varX(v))).toList()
)));
// construct multiMap criteria
var main = closureX(body.getParameters(), block(body.getVariableScope(), statements));
var newTokenMultiMapDef = createX("nextflow.script.TokenMultiMapDef", main, list2args(new ArrayList(allLabels)));
return closureX(null, block(body.getVariableScope(), stmt(newTokenMultiMapDef)));
}
/**
* Assign the last expression statement in a multiMap block to a variable
* for each label in the block.
*
* This is done to reuse the same result expression for multiple labels.
*
* @param current
* @param previous
* @param labels
* @param vars
* @param statements
*/
void assignOutputVars(Statement current, Statement previous, List<String> labels, Set<String> vars, List<Statement> statements) {
VariableExpression target = null;
for( var label : labels ) {
var varName = "$out_" + label;
if( !vars.add(varName) )
syntaxError("multiMap label already declared: " + label, current);
if( !(previous instanceof ExpressionStatement) )
syntaxError("multiMap block must end with an expression statement", previous);
if( target == null ) {
var source = ((ExpressionStatement) previous).getExpression();
target = varX(varName);
statements.add(declS(target, source));
}
else {
statements.add(declS(varX(varName), target));
}
}
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.WarningMessage;
import org.codehaus.groovy.syntax.CSTNode;
/**
* A warning that should only be reported when paranoid warnings
* are enabled.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ParanoidWarning extends WarningMessage implements RelatedInformationAware {
private String otherMessage;
private ASTNode otherNode;
public ParanoidWarning(int importance, String message, CSTNode context, SourceUnit owner) {
super(importance, message, context, owner);
}
public void setRelatedInformation(String otherMessage, ASTNode otherNode) {
this.otherMessage = otherMessage;
this.otherNode = otherNode;
}
@Override
public String getOtherMessage() {
return otherMessage;
}
@Override
public ASTNode getOtherNode() {
return otherNode;
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.syntax.Types;
import static nextflow.script.ast.ASTUtils.*;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Replace path comparisons with explicit method calls.
*
* This is required to correctly compare `Path` values, which are
* not supported by default because `Path` implements `Comparable`.
*
* @see https://stackoverflow.com/questions/28355773/in-groovy-why-does-the-behaviour-of-change-for-interfaces-extending-compar#comment45123447_28387391
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
public class PathCompareVisitor extends ClassCodeExpressionTransformer {
private SourceUnit sourceUnit;
public PathCompareVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public Expression transform(Expression node) {
if( node instanceof BinaryExpression be ) {
return transformBinaryExpression(be);
}
if( node instanceof ClosureExpression ce ) {
ce.visit(this);
return ce;
}
return super.transform(node);
}
/**
* Replace comparison operations with explicit calls to
* the appropriate {@link LangHelpers} method.
*
* @param node
*/
protected Expression transformBinaryExpression(BinaryExpression node) {
var left = node.getLeftExpression();
var right = node.getRightExpression();
return switch( node.getOperation().getType() ) {
case Types.COMPARE_EQUAL ->
call("compareEqual", left, right);
case Types.COMPARE_NOT_EQUAL ->
notX(call("compareEqual", left, right));
case Types.COMPARE_LESS_THAN ->
call("compareLessThan", left, right);
case Types.COMPARE_LESS_THAN_EQUAL ->
call("compareLessThanEqual", left, right);
case Types.COMPARE_GREATER_THAN ->
call("compareGreaterThan", left, right);
case Types.COMPARE_GREATER_THAN_EQUAL ->
call("compareGreaterThanEqual", left, right);
default -> super.transform(node);
};
}
private MethodCallExpression call(String method, Expression left, Expression right) {
return callX(
classX("nextflow.util.LangHelpers"),
method,
args(transform(left), transform(right)));
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
/**
* Interface used by errors that are associated with a
* compile phase.
*
* @see Phases
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public interface PhaseAware {
int getPhase();
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
/**
* Extended compilation phases for the Nextflow compiler.
*
* @see org.codehaus.groovy.control.Phases
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class Phases {
public static final int SYNTAX = 1;
public static final int INCLUDE_RESOLUTION = 2;
public static final int NAME_RESOLUTION = 3;
public static final int TYPE_CHECKING = 4;
public static final int ALL = TYPE_CHECKING;
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.WorkflowNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.control.SourceUnit;
/**
* Resolve all fully-qualified process names invoked
* (directly or indirectly) by an entry workflow.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ProcessNameResolver {
private Map<WorkflowNode, Map<String, MethodNode>> callSites;
public ProcessNameResolver(Map<WorkflowNode, Map<String, MethodNode>> callSites) {
this.callSites = callSites;
}
public Set<String> resolve(SourceUnit main) {
var result = new HashSet<String>();
var queue = new LinkedList<CallScope>();
if( main.getAST() instanceof ScriptNode sn && sn.getEntry() != null )
queue.add(new CallScope("", sn.getEntry()));
while( !queue.isEmpty() ) {
var scope = queue.remove();
var calls = callSites.get(scope.node());
calls.forEach((name, mn) -> {
if( mn instanceof WorkflowNode wn ) {
var workflowName = fullyQualifiedName(scope.name(), name);
queue.add(new CallScope(workflowName, wn));
}
else if( mn instanceof ProcessNode pn ) {
var processName = fullyQualifiedName(scope.name(), name);
result.add(processName);
}
});
}
return result;
}
private String fullyQualifiedName(String scope, String name) {
return scope.isEmpty()
? name
: scope + ":" + name;
}
private static record CallScope(
String name,
WorkflowNode node
) {}
}

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.util.List;
import nextflow.script.ast.ProcessNodeV1;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import static nextflow.script.ast.ASTUtils.*;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Transform a legacy process AST node into Groovy AST.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ProcessToGroovyVisitorV1 {
private SourceUnit sourceUnit;
private ScriptToGroovyHelper sgh;
public ProcessToGroovyVisitorV1(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
this.sgh = new ScriptToGroovyHelper(sourceUnit);
}
public Statement transform(ProcessNodeV1 node) {
visitProcessDirectives(node.directives);
visitProcessInputs(node.inputs);
visitProcessOutputs(node.outputs);
if( "script".equals(node.type) )
node.exec.visit(new TaskCmdXformVisitor(sourceUnit));
node.stub.visit(new TaskCmdXformVisitor(sourceUnit));
var closure = closureX(null, block(new VariableScope(), List.of(
node.directives,
node.inputs,
node.outputs,
processWhen(node.when),
processStub(node.stub),
stmt(createX(
"nextflow.script.BodyDef",
args(
closureX(null, node.exec),
constX(sgh.getSourceText(node.exec)),
constX(node.type),
sgh.getVariableRefs(node.exec)
)
))
)));
return stmt(callThisX("process", args(constX(node.getName()), closure)));
}
private static final List<String> NON_DYNAMIC_DIRECTIVES = List.of(
"executor",
"label",
"maxForks",
"module",
"pod",
"publishDir",
"secret"
);
private void visitProcessDirectives(Statement directives) {
asDirectives(directives).forEach((call) -> {
var name = call.getMethodAsString();
// don't wrap directives that can't be dynamic
if( NON_DYNAMIC_DIRECTIVES.contains(name) )
return;
// don't wrap directives with multiple arguments
var arguments = asMethodCallArguments(call);
if( arguments.size() != 1 )
return;
// don't wrap directives that already have a closure
var firstArg = arguments.get(0);
if( firstArg instanceof ClosureExpression )
return;
arguments.set(0, sgh.transformToLazy(firstArg));
});
}
private void visitProcessInputs(Statement inputs) {
asDirectives(inputs).forEach((call) -> {
var name = call.getMethodAsString();
varToConstX(call.getArguments(), "tuple".equals(name), "each".equals(name));
call.setMethod( constX("_in_" + name) );
});
}
private void visitProcessOutputs(Statement outputs) {
asDirectives(outputs).forEach((call) -> {
var name = call.getMethodAsString();
varToConstX(call.getArguments(), "tuple".equals(name), false);
call.setMethod( constX("_out_" + name) );
visitProcessOutputEmitAndTopic(call);
});
}
private static final List<String> EMIT_AND_TOPIC = List.of("emit", "topic");
private void visitProcessOutputEmitAndTopic(MethodCallExpression output) {
var namedArgs = asNamedArgs(output);
for( int i = 0; i < namedArgs.size(); i++ ) {
var entry = namedArgs.get(i);
var key = asConstX(entry.getKeyExpression());
var value = asVarX(entry.getValueExpression());
if( value != null && key != null && EMIT_AND_TOPIC.contains(key.getText()) ) {
namedArgs.set(i, entryX(key, constX(value.getText())));
}
}
}
private Expression varToConstX(Expression node, boolean withinTuple, boolean withinEach) {
if( node instanceof TupleExpression te ) {
var arguments = te.getExpressions();
for( int i = 0; i < arguments.size(); i++ )
arguments.set(i, varToConstX(arguments.get(i), withinTuple, withinEach));
return te;
}
if( node instanceof VariableExpression ve ) {
var name = ve.getName();
if( "stdin".equals(name) && withinTuple )
return createX( "nextflow.script.TokenStdinCall" );
if ( "stdout".equals(name) && withinTuple )
return createX( "nextflow.script.TokenStdoutCall" );
return createX( "nextflow.script.TokenVar", constX(name) );
}
if( node instanceof MethodCallExpression mce ) {
var name = mce.getMethodAsString();
var arguments = mce.getArguments();
if( "env".equals(name) && withinTuple )
return createX( "nextflow.script.TokenEnvCall", (TupleExpression) varToStrX(arguments) );
if( "eval".equals(name) && withinTuple )
return createX( "nextflow.script.TokenEvalCall", (TupleExpression) varToStrX(arguments) );
if( "file".equals(name) && (withinTuple || withinEach) )
return createX( "nextflow.script.TokenFileCall", (TupleExpression) varToConstX(arguments, withinTuple, withinEach) );
if( "path".equals(name) && (withinTuple || withinEach) )
return createX( "nextflow.script.TokenPathCall", (TupleExpression) varToConstX(arguments, withinTuple, withinEach) );
if( "val".equals(name) && withinTuple )
return createX( "nextflow.script.TokenValCall", (TupleExpression) varToStrX(arguments) );
}
return sgh.transformToLazy(node);
}
private Expression varToStrX(Expression node) {
if( node instanceof TupleExpression te ) {
var arguments = te.getExpressions();
for( int i = 0; i < arguments.size(); i++ )
arguments.set(i, varToStrX(arguments.get(i)));
return te;
}
if( node instanceof VariableExpression ve ) {
// before:
// val(x)
// after:
// val(TokenVar('x'))
var name = ve.getName();
return createX( "nextflow.script.TokenVar", constX(name) );
}
return sgh.transformToLazy(node);
}
private Statement processWhen(Expression when) {
if( when instanceof EmptyExpression )
return EmptyStatement.INSTANCE;
return stmt(callThisX("when", createX(
"nextflow.script.TaskClosure",
args(
closureX(null, block(stmt(when))),
constX(sgh.getSourceText(when))
)
)));
}
private Statement processStub(Statement stub) {
if( stub instanceof EmptyStatement )
return EmptyStatement.INSTANCE;
return stmt(callThisX("stub", createX(
"nextflow.script.TaskClosure",
args(
closureX(null, stub),
constX(sgh.getSourceText(stub))
)
)));
}
}

View File

@@ -0,0 +1,345 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.ast.AssignmentExpression;
import nextflow.script.ast.ProcessNodeV2;
import nextflow.script.ast.RecordNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.TupleParameter;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MapExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import static nextflow.script.ast.ASTUtils.*;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Transform a typed process AST node into Groovy AST.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ProcessToGroovyVisitorV2 {
private SourceUnit sourceUnit;
private ScriptNode moduleNode;
private ScriptToGroovyHelper sgh;
public ProcessToGroovyVisitorV2(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
this.moduleNode = (ScriptNode) sourceUnit.getAST();
this.sgh = new ScriptToGroovyHelper(sourceUnit);
}
public Statement transform(ProcessNodeV2 node) {
visitProcessDirectives(node.directives);
visitProcessStagers(node.stagers);
var stagers = node.stagers instanceof BlockStatement block ? block : new BlockStatement();
visitProcessInputs(node.inputs, stagers);
var unstagers = new BlockStatement();
var unstageVisitor = new ProcessUnstageVisitor(unstagers);
visitProcessUnstagers(node.outputs, unstageVisitor);
visitProcessUnstagers(node.topics, unstageVisitor);
if( "script".equals(node.type) )
node.exec.visit(new TaskCmdXformVisitor(sourceUnit));
node.stub.visit(new TaskCmdXformVisitor(sourceUnit));
var body = closureX(block(new VariableScope(), List.of(
node.directives,
stagers,
unstagers,
processInputs(node.inputs),
processOutputs(node.outputs),
processTopics(node.topics),
processWhen(node.when),
processStub(node.stub),
stmt(createX(
"nextflow.script.BodyDef",
args(
closureX(node.exec),
constX(sgh.getSourceText(node.exec)),
constX(node.type),
sgh.getVariableRefs(node.exec)
)
))
)));
return stmt(callThisX("processV2", args(constX(node.getName()), body)));
}
private void visitProcessDirectives(Statement directives) {
asDirectives(directives).forEach((call) -> {
var arguments = asMethodCallArguments(call);
if( arguments.size() != 1 )
return;
var firstArg = arguments.get(0);
if( firstArg instanceof ClosureExpression )
return;
arguments.set(0, sgh.transformToLazy(firstArg));
});
}
private void visitProcessStagers(Statement directives) {
asDirectives(directives).forEach((call) -> {
var arguments = asMethodCallArguments(call).stream()
.map(arg -> sgh.transformToLazy(arg))
.toList();
call.setArguments(args(arguments));
});
}
private void visitProcessInputs(Parameter[] inputs, BlockStatement stagers) {
for( var param : asFlatParams(inputs) ) {
visitProcessInputType(param, varX(param.getName()), stagers);
}
}
/**
* Add implicit staging directives that are inferred from
* the process input type:
*
* - Inputs with type Path or a Path collection (e.g. Set<Path>)
* are staged as input files.
*
* - Inputs with a record type are recursively inspected for nested
* file inputs based on the record type definition.
*
* @param param
* @param target
* @param stagers
*/
private void visitProcessInputType(Variable param, Expression target, BlockStatement stagers) {
var cn = param.getType();
if( isPathType(cn) ) {
var stager = stmt(callThisX("stageAs", args(closureX(stmt(target)))));
stagers.addStatement(stager);
}
else if( isRecordType(cn) ) {
for( var fn : cn.getFields() )
visitProcessInputType(fn, propX(target, fn.getName()), stagers);
}
}
private static boolean isPathType(ClassNode cn) {
if( !cn.isResolved() )
return false;
var clazz = cn.getTypeClass();
if( Path.class.isAssignableFrom(clazz) ) {
return true;
}
if( Collection.class.isAssignableFrom(clazz) && cn.isUsingGenerics() ) {
var elementType = cn.getGenericsTypes()[0].getType();
return Path.class.isAssignableFrom(elementType.getTypeClass());
}
return false;
}
private static boolean isRecordType(ClassNode cn) {
return cn.redirect() instanceof RecordNode;
}
private void visitProcessUnstagers(Statement outputs, ProcessUnstageVisitor visitor) {
for( var output : asBlockStatements(outputs) )
visitor.visit(output);
}
private static class ProcessUnstageVisitor extends CodeVisitorSupport {
private int evalCount = 0;
private int pathCount = 0;
private BlockStatement unstagers;
public ProcessUnstageVisitor(BlockStatement unstagers) {
this.unstagers = unstagers;
}
@Override
public void visitMethodCallExpression(MethodCallExpression node) {
extractUnstageDirective(node);
super.visitMethodCallExpression(node);
}
private void extractUnstageDirective(MethodCallExpression node) {
if( !node.isImplicitThis() )
return;
var name = node.getMethodAsString();
var arguments = asMethodCallArguments(node);
// env(<name>)
// emit: _unstage_env(<name>)
if( "env".equals(name) && arguments.size() == 1 ) {
var key = arguments.get(0);
var unstager = stmt(callThisX("_unstage_env", args(key)));
unstagers.addStatement(unstager);
// rename to _env() to prevent dispatch to equivalent ScriptDsl function
node.setMethod(constX("_" + name));
}
// eval(<cmd>) -> eval(<key>)
// emit: _unstage_eval(<key>, { <cmd> })
if( "eval".equals(name) && arguments.size() == 1 ) {
var key = constX("nxf_out_eval_" + (evalCount++));
var cmd = arguments.get(0);
var unstager = stmt(callThisX("_unstage_eval", args(key, closureX(stmt(cmd)))));
unstagers.addStatement(unstager);
node.setArguments(args(key));
}
// file(<opts>, <pattern>) -> file(<opts>, <key>)
// files(<opts>, <pattern>) -> files(<opts>, <key>)
// emit: _unstage_files(<key>, { <pattern> })
if( "file".equals(name) || "files".equals(name) ) {
Expression opts;
Expression pattern;
if( arguments.size() == 1 ) {
opts = new MapExpression();
pattern = arguments.get(0);
}
else if( arguments.size() == 2 ) {
opts = arguments.get(0);
pattern = arguments.get(1);
}
else {
return;
}
var key = constX("$path" + (pathCount++));
var unstager = stmt(callThisX("_unstage_files", args(key, closureX(stmt(pattern)))));
unstagers.addStatement(unstager);
// rename to _file() or _files() to prevent dispatch to equivalent ScriptDsl functions
node.setMethod(constX("_" + name));
node.setArguments(args(opts, key));
}
}
}
private Statement processInputs(Parameter[] inputs) {
var statements = Arrays.stream(inputs)
.map((input) -> {
if( input instanceof TupleParameter tp ) {
var components = Arrays.stream(tp.components)
.map(p -> processInputCtor(p))
.toList();
var type = input.getType();
return stmt(callThisX("_input_", args(listX(components), classX(type))));
}
else {
var name = input.getName();
var type = input.getType();
var optional = type.getNodeMetaData(ASTNodeMarker.NULLABLE) != null;
return stmt(callThisX("_input_", args(constX(name), classX(type), constX(optional))));
}
})
.toList();
return block(null, statements);
}
private Expression processInputCtor(Parameter param) {
return createX(
"nextflow.script.params.v2.ProcessInput",
args(
constX(param.getName()),
classX(param.getType()),
constX(param.getType().getNodeMetaData(ASTNodeMarker.NULLABLE) != null)
)
);
}
private Statement processOutputs(Statement outputs) {
var statements = asBlockStatements(outputs).stream()
.map(stmt -> ((ExpressionStatement) stmt).getExpression())
.map((output) -> {
if( output instanceof VariableExpression target ) {
return stmt(callThisX("_output_", args(constX(target.getName()), classX(target.getType()), closureX(stmt(target)))));
}
else if( output instanceof AssignmentExpression ae ) {
var target = (VariableExpression)ae.getLeftExpression();
return stmt(callThisX("_output_", args(constX(target.getName()), classX(target.getType()), closureX(stmt(ae.getRightExpression())))));
}
else {
return stmt(callThisX("_output_", args(constX("$out"), classX(ClassHelper.dynamicType()), closureX(stmt(output)))));
}
})
.toList();
return block(null, statements);
}
private Statement processTopics(Statement topics) {
var statements = asBlockStatements(topics).stream()
.map((stmt) -> {
var es = (ExpressionStatement) stmt;
var be = (BinaryExpression) es.getExpression();
return stmt(callThisX("_topic_", args(closureX(stmt(be.getLeftExpression())), be.getRightExpression())));
})
.toList();
return block(null, statements);
}
private Statement processWhen(Expression when) {
if( when instanceof EmptyExpression )
return EmptyStatement.INSTANCE;
return stmt(callThisX("when", createX(
"nextflow.script.TaskClosure",
args(
closureX(null, block(stmt(when))),
constX(sgh.getSourceText(when))
)
)));
}
private Statement processStub(Statement stub) {
if( stub instanceof EmptyStatement )
return EmptyStatement.INSTANCE;
return stmt(callThisX("stub", createX(
"nextflow.script.TaskClosure",
args(
closureX(null, stub),
constX(sgh.getSourceText(stub))
)
)));
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import org.codehaus.groovy.ast.ASTNode;
/**
* Interface used by errors that have related information,
* such as the "already defined here" in a "variable already declared"
* error.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public interface RelatedInformationAware {
String getOtherMessage();
ASTNode getOtherNode();
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import nextflow.module.spi.RemoteModuleResolverProvider;
import nextflow.script.ast.FunctionNode;
import nextflow.script.ast.IncludeNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.ScriptVisitorSupport;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
/**
* Resolve includes against included source files.
*
* This visitor should be applied only after all source files
* have been parsed.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ResolveIncludeVisitor extends ScriptVisitorSupport {
private SourceUnit sourceUnit;
private URI uri;
private Path projectDir;
private Compiler compiler;
private Set<URI> changedUris;
private List<SyntaxErrorMessage> errors = new ArrayList<>();
private boolean changed;
public ResolveIncludeVisitor(SourceUnit sourceUnit, Path projectDir, Compiler compiler, Set<URI> changedUris) {
this.sourceUnit = sourceUnit;
this.uri = sourceUnit.getSource().getURI();
this.compiler = compiler;
this.changedUris = changedUris;
this.projectDir = projectDir;
}
public ResolveIncludeVisitor(SourceUnit sourceUnit, Path projectDir, Compiler compiler) {
this(sourceUnit, projectDir, compiler, null);
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ScriptNode sn )
super.visit(sn);
}
@Override
public void visitInclude(IncludeNode node) {
var source = node.source.getText();
if( source.startsWith("plugin/") ) {
setPlaceholderTargets(node);
return;
}
URI includeUri;
try {
includeUri = getIncludeUri(source);
}
catch( Exception e ) {
addError(e.getMessage(), node);
return;
}
if( !isIncludeStale(node, includeUri) )
return;
changed = true;
for( var entry : node.entries )
entry.setTarget(null);
var includeUnit = compiler.getSource(includeUri);
if( includeUnit == null ) {
addError("Invalid include source: '" + includeUri.getPath() + "'", node);
return;
}
if( includeUnit.getAST() == null ) {
addError("Module could not be parsed: '" + includeUri.getPath() + "'", node);
return;
}
var definitions = getDefinitions(includeUri);
for( var entry : node.entries ) {
var includedName = entry.name;
var includedNode = definitions.stream()
.filter(defNode -> includedName.equals(definitionName(defNode)))
.findFirst();
if( !includedNode.isPresent() ) {
addError("Included name '" + includedName + "' is not defined in module '" + includeUri.getPath() + "'", node);
continue;
}
entry.setTarget(includedNode.get());
}
}
private static void setPlaceholderTargets(IncludeNode node) {
for( var entry : node.entries ) {
if( entry.getTarget() == null ) {
var target = new FunctionNode(entry.getNameOrAlias());
entry.setTarget(target);
}
}
}
private URI getIncludeUri(String source) {
if( ModuleResolver.isRemoteModule(source) ) {
return RemoteModuleResolverProvider.getInstance()
.resolve(source, projectDir)
.normalize()
.toUri();
}
else {
var parent = Path.of(uri).getParent();
return getLocalIncludeUri(parent, source);
}
}
private static URI getLocalIncludeUri(Path parent, String source) {
Path includePath = parent.resolve(source);
if( Files.isDirectory(includePath) )
includePath = includePath.resolve("main.nf");
else if( !source.endsWith(".nf") )
includePath = Path.of(includePath.toString() + ".nf");
return includePath.normalize().toUri();
}
private boolean isIncludeStale(IncludeNode node, URI includeUri) {
if( changedUris == null || changedUris.contains(uri) || changedUris.contains(includeUri) )
return true;
for( var entry : node.entries ) {
if( entry.getTarget() == null )
return true;
}
return false;
}
private List<AnnotatedNode> getDefinitions(URI uri) {
var scriptNode = (ScriptNode) compiler.getSource(uri).getAST();
var result = new ArrayList<AnnotatedNode>();
result.addAll(scriptNode.getWorkflows());
result.addAll(scriptNode.getProcesses());
result.addAll(scriptNode.getFunctions());
result.addAll(scriptNode.getTypes());
return result;
}
private static String definitionName(AnnotatedNode node) {
return
node instanceof ClassNode cn ? cn.getNameWithoutPackage() :
node instanceof MethodNode mn ? mn.getName() :
null;
}
@Override
public void addError(String message, ASTNode node) {
var cause = new ResolveIncludeError(message, node);
var errorMessage = new SyntaxErrorMessage(cause, sourceUnit);
errors.add(errorMessage);
}
public List<SyntaxErrorMessage> getErrors() {
return errors;
}
public boolean isChanged() {
return changed;
}
private class ResolveIncludeError extends SyntaxException implements PhaseAware {
public ResolveIncludeError(String message, ASTNode node) {
super(message, node);
}
@Override
public int getPhase() {
return Phases.INCLUDE_RESOLUTION;
}
}
}

View File

@@ -0,0 +1,510 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import groovy.lang.Tuple2;
import nextflow.script.ast.ASTNodeMarker;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.control.ClassNodeResolver;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.runtime.memoize.UnlimitedConcurrentCache;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Types;
import org.codehaus.groovy.vmplugin.VMPluginFactory;
import static groovy.lang.Tuple.tuple;
import static org.codehaus.groovy.ast.tools.ClosureUtils.getParametersSafe;
/**
* Resolve the names of symbols.
*
* @see org.codehaus.groovy.control.ResolveVisitor
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ResolveVisitor extends ClassCodeExpressionTransformer {
public static final ClassNode[] STANDARD_TYPES = {
ClassHelper.makeCached(nextflow.script.types.Bag.class),
ClassHelper.Boolean_TYPE,
ClassHelper.Float_TYPE,
ClassHelper.Integer_TYPE,
ClassHelper.LIST_TYPE,
ClassHelper.MAP_TYPE,
ClassHelper.makeCached(java.nio.file.Path.class),
ClassHelper.makeCached(nextflow.script.types.Record.class),
ClassHelper.SET_TYPE,
ClassHelper.STRING_TYPE,
ClassHelper.makeCached(nextflow.script.types.Tuple.class)
};
private SourceUnit sourceUnit;
private CompilationUnit compilationUnit;
private ClassNodeResolver classNodeResolver = new ClassNodeResolver();
/**
* Default imports can be accessed only by their simple name.
*/
private List<ClassNode> defaultImports;
/**
* Lib imports can be accessed only by their fully-qualified name.
*/
private List<ClassNode> libImports;
public ResolveVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit, List<ClassNode> defaultImports, List<ClassNode> libImports) {
this.sourceUnit = sourceUnit;
this.compilationUnit = compilationUnit;
this.defaultImports = defaultImports;
this.libImports = libImports;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public void visitCatchStatement(CatchStatement cs) {
resolveOrFail(cs.getExceptionType(), cs);
if( ClassHelper.isDynamicTyped(cs.getExceptionType()) )
cs.getVariable().setType(ClassHelper.make(Exception.class));
super.visitCatchStatement(cs);
}
public void resolveOrFail(ClassNode type, ASTNode node) {
var unresolvedTypes = new LinkedList<ClassNode>();
resolve(type, unresolvedTypes);
for( var ut : unresolvedTypes )
addError("`" + ut.toString(false) + "` is not defined", node);
}
private boolean resolve(ClassNode type) {
var unresolvedTypes = new LinkedList<ClassNode>();
resolve(type, unresolvedTypes);
return unresolvedTypes.isEmpty();
}
/**
* Resolve a type annotation, including generic type arguments.
*
* Returns the list of types that could not be resolved.
*
* @param type
* @param unresolvedTypes
*/
private void resolve(ClassNode type, List<ClassNode> unresolvedTypes) {
if( !resolveType(type) )
unresolvedTypes.add(type);
var gts = type.getGenericsTypes();
if( gts == null )
return;
for( var gt : gts ) {
if( gt.isResolved() )
continue;
resolve(gt.getType(), unresolvedTypes);
gt.setResolved(gt.getType().isResolved());
}
}
private boolean resolveType(ClassNode type) {
if( type.isPrimaryClassNode() )
return true;
if( type.isResolved() )
return true;
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;
if( resolveFromClassResolver(type.getName()) != null )
return true;
if( resolveAsInnerClass(type) )
return true;
return false;
}
protected boolean resolveFromModule(ClassNode type) {
var name = type.getName();
var module = sourceUnit.getAST();
for( var cn : module.getClasses() ) {
if( name.equals(cn.getNameWithoutPackage()) ) {
if( cn != type )
type.setRedirect(cn);
return true;
}
}
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) {
for( var cn : libImports ) {
if( cn.getName().equals(type.getName()) ) {
type.setRedirect(cn);
return true;
}
}
return false;
}
protected boolean resolveFromDefaultImports(ClassNode type) {
for( var cn : defaultImports ) {
if( cn.getNameWithoutPackage().equals(type.getName()) ) {
type.setRedirect(cn);
return true;
}
}
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( resolveFromGroovyImports(type, packagePrefixSet.toArray(EMPTY_STRING_ARRAY)) )
return true;
}
// resolve from Groovy imports
if( resolveFromGroovyImports(type, DEFAULT_PACKAGE_PREFIXES) ) {
return true;
}
if( "BigInteger".equals(typeName) ) {
type.setRedirect(ClassHelper.BigInteger_TYPE);
return true;
}
if( "BigDecimal".equals(typeName) ) {
type.setRedirect(ClassHelper.BigDecimal_TYPE);
return true;
}
return false;
}
private static final Map<String, Set<String>> DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE = new UnlimitedConcurrentCache<>();
static {
DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE.putAll(VMPluginFactory.getPlugin().getDefaultImportClasses(DEFAULT_PACKAGE_PREFIXES));
}
protected boolean resolveFromGroovyImports(ClassNode type, String[] packagePrefixes) {
var typeName = type.getName();
for( var packagePrefix : packagePrefixes ) {
var redirect = resolveFromClassResolver(packagePrefix + typeName);
if( redirect != null ) {
type.setRedirect(redirect);
// don't update cache when using a cached lookup
if( packagePrefixes == DEFAULT_PACKAGE_PREFIXES ) {
var packagePrefixSet = DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE.computeIfAbsent(typeName, key -> new HashSet<>(2));
packagePrefixSet.add(packagePrefix);
}
return true;
}
}
return false;
}
protected ClassNode resolveFromClassResolver(String name) {
var lookupResult = classNodeResolver.resolveName(name, compilationUnit);
if( lookupResult == null )
return null;
if( lookupResult.isClassNode() )
return lookupResult.getClassNode();
// When a Groovy class from the lib directory is used, the class
// loader returns the URI of the Groovy file. We only need to compile
// the Groovy file enough to resolve the class definition for the purpose
// of name checking.
var su = lookupResult.getSourceUnit();
return GroovyCompiler.compile(su).stream()
.filter(cn -> cn.getName().equals(name))
.findFirst().orElse(null);
}
/**
* Try to resolve a ClassNode as an inner class by replacing dots with $.
* For example, "groovy.json.JsonGenerator.Options" becomes "groovy.json.JsonGenerator$Options".
* This method tries all possible combinations from right to left.
*
* @param type
*/
protected boolean resolveAsInnerClass(ClassNode type) {
var className = type.getName();
int lastDot = className.lastIndexOf('.');
while( lastDot > 0 ) {
var innerClassName = className.substring(0, lastDot) + '$' + className.substring(lastDot + 1);
var redirect = resolveFromClassResolver(innerClassName);
if( redirect != null ) {
type.setRedirect(redirect);
return true;
}
lastDot = className.lastIndexOf('.', lastDot - 1);
}
return false;
}
@Override
public Expression transform(Expression exp) {
if( exp == null )
return null;
Expression result;
if( exp instanceof VariableExpression ve ) {
result = transformVariableExpression(ve);
}
else if( exp instanceof PropertyExpression pe ) {
result = transformPropertyExpression(pe);
}
else if( exp instanceof DeclarationExpression de ) {
result = transformDeclarationExpression(de);
}
else if( exp instanceof BinaryExpression be ) {
result = transformBinaryExpression(be);
}
else if( exp instanceof MethodCallExpression mce ) {
result = transformMethodCallExpression(mce);
}
else if( exp instanceof ClosureExpression ce ) {
result = transformClosureExpression(ce);
}
else if( exp instanceof ConstructorCallExpression cce ) {
result = transformConstructorCallExpression(cce);
}
else {
resolveOrFail(exp.getType(), exp);
result = exp.transformExpression(this);
}
if( result != null && result != exp ) {
result.setSourcePosition(exp);
}
return result;
}
protected Expression transformVariableExpression(VariableExpression ve) {
var v = ve.getAccessedVariable();
if( v instanceof DynamicVariable ) {
// attempt to resolve variable as type name
var name = ve.getName();
var type = ClassHelper.make(name);
var isClass = type.isResolved();
if( !isClass )
isClass = resolve(type);
if( isClass )
return new ClassExpression(type);
}
if( inVariableDeclaration ) {
// resolve type of variable declaration
resolveOrFail(ve);
}
// if the variable is still dynamic (i.e. unresolved), it will be handled by DynamicVariablesVisitor
return ve;
}
public void resolveOrFail(VariableExpression ve) {
resolveOrFail(ve.getType(), ve);
var origin = ve.getOriginType();
if( origin != ve.getType() )
resolveOrFail(origin, ve);
}
protected Expression transformPropertyExpression(PropertyExpression pe) {
Expression objectExpression;
Expression property;
objectExpression = transform(pe.getObjectExpression());
property = transform(pe.getProperty());
var result = new PropertyExpression(objectExpression, property, pe.isSafe());
result.setSpreadSafe(pe.isSpreadSafe());
result.copyNodeMetaData(pe);
// attempt to resolve property expression as a fully-qualified class name
var className = lookupClassName(result);
if( className != null ) {
var type = ClassHelper.make(className);
if( resolve(type) ) {
type.putNodeMetaData(ASTNodeMarker.FULLY_QUALIFIED, true);
return new ClassExpression(type);
}
}
return result;
}
private static String lookupClassName(PropertyExpression node) {
boolean doInitialClassTest = true;
StringBuilder name = new StringBuilder(32);
Expression expr = node;
while( expr != null && name != null ) {
if( expr instanceof VariableExpression ve ) {
var varName = ve.getName();
var classNameInfo = makeClassName(doInitialClassTest, name, varName);
name = classNameInfo.getV1();
doInitialClassTest = classNameInfo.getV2();
break;
}
if( expr instanceof PropertyExpression pe ) {
var property = pe.getPropertyAsString();
var classNameInfo = makeClassName(doInitialClassTest, name, property);
name = classNameInfo.getV1();
doInitialClassTest = classNameInfo.getV2();
expr = pe.getObjectExpression();
}
else {
return null;
}
}
if( name == null || name.length() == 0 )
return null;
return name.toString();
}
private static Tuple2<StringBuilder, Boolean> makeClassName(boolean doInitialClassTest, StringBuilder name, String varName) {
if( doInitialClassTest ) {
return isValidClassName(varName)
? tuple(new StringBuilder(varName), Boolean.FALSE)
: tuple(null, Boolean.TRUE);
}
name.insert(0, varName + ".");
return tuple(name, Boolean.FALSE);
}
private static boolean isValidClassName(String name) {
if( name == null || name.length() == 0 )
return false;
return !Character.isLowerCase(name.charAt(0));
}
private boolean inVariableDeclaration;
protected Expression transformDeclarationExpression(DeclarationExpression de) {
inVariableDeclaration = true;
var left = transform(de.getLeftExpression());
inVariableDeclaration = false;
if( left instanceof ClassExpression ) {
addError("`" + left.getType().getName() + "` is already defined as a type", de.getLeftExpression());
return de;
}
var right = transform(de.getRightExpression());
if( right == de.getRightExpression() )
return de;
var result = new DeclarationExpression(left, de.getOperation(), right);
result.setDeclaringClass(de.getDeclaringClass());
return result;
}
protected Expression transformBinaryExpression(BinaryExpression be) {
var left = transform(be.getLeftExpression());
if( Types.isAssignment(be.getOperation().getType()) && left instanceof ClassExpression ) {
addError("`" + left.getType().getName() + "` is already defined as a type", be.getLeftExpression());
return be;
}
be.setLeftExpression(left);
be.setRightExpression(transform(be.getRightExpression()));
return be;
}
protected Expression transformMethodCallExpression(MethodCallExpression mce) {
var args = transform(mce.getArguments());
var method = transform(mce.getMethod());
var object = mce.getObjectExpression();
if( !mce.isImplicitThis() )
object = transform(object);
var result = new MethodCallExpression(object, method, args);
result.setMethodTarget(mce.getMethodTarget());
result.setImplicitThis(mce.isImplicitThis());
result.setSpreadSafe(mce.isSpreadSafe());
result.setSafe(mce.isSafe());
result.copyNodeMetaData(mce);
return result;
}
protected Expression transformClosureExpression(ClosureExpression ce) {
for( var param : getParametersSafe(ce) ) {
resolveOrFail(param.getType(), ce);
if( param.hasInitialExpression() )
param.setInitialExpression(transform(param.getInitialExpression()));
}
visit(ce.getCode());
return ce;
}
protected Expression transformConstructorCallExpression(ConstructorCallExpression cce) {
var cceType = cce.getType();
resolveOrFail(cceType, cce);
if( cceType.isAbstract() )
addError("`" + cceType.getName() + "` is an abstract type and cannot be constructed directly", cce);
return cce.transformExpression(this);
}
@Override
public void addError(String message, ASTNode node) {
var cause = new UnresolvedNameError(message, node);
var errorMessage = new SyntaxErrorMessage(cause, sourceUnit);
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
}
private class UnresolvedNameError extends SyntaxException implements PhaseAware {
public UnresolvedNameError(String message, ASTNode node) {
super(message, node);
}
@Override
public int getPhase() {
return Phases.NAME_RESOLUTION;
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import groovy.lang.GroovyClassLoader;
import nextflow.script.dsl.Types;
import nextflow.script.parser.ScriptParserPluginFactory;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.WarningMessage;
/**
* Parse and analyze scripts without compiling to Groovy.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ScriptParser {
private Path projectDir;
private Compiler compiler;
public ScriptParser(Path projectDir, GroovyClassLoader classLoader) {
this.projectDir = projectDir;
this.compiler = new Compiler(getConfig(), classLoader);
}
public ScriptParser(Path projectDir) {
this(projectDir, new GroovyClassLoader());
}
public ScriptParser() {
this(null);
}
public Compiler compiler() {
return compiler;
}
public SourceUnit parse(File file) {
var source = compiler.createSourceUnit(file);
parse0(source);
return source;
}
public SourceUnit parse(String name, String contents) {
var source = compiler.createSourceUnit(name, contents);
parse0(source);
return source;
}
private void parse0(SourceUnit source) {
var uri = source.getSource().getURI();
if( compiler.getSource(uri) != null )
return;
compiler.addSource(source);
compiler.compile(source);
}
public void analyze() {
var sources = new ArrayList<>(compiler.getSources().values());
for( var source : sources ) {
new ModuleResolver(projectDir, compiler()).resolve(source, (uri) -> compiler.createSourceUnit(new File(uri)));
}
for( var source : compiler.getSources().values() ) {
var includeResolver = new ResolveIncludeVisitor(source, projectDir, compiler);
includeResolver.visit();
for( var error : includeResolver.getErrors() )
source.getErrorCollector().addErrorAndContinue(error);
new ScriptResolveVisitor(source, compiler.compilationUnit(), Types.DEFAULT_SCRIPT_IMPORTS, Collections.emptyList()).visit();
if( source.getErrorCollector().hasErrors() )
continue;
new TypeCheckingVisitor(source).visit();
}
}
private static CompilerConfiguration getConfig() {
var config = new CompilerConfiguration();
config.setPluginFactory(new ScriptParserPluginFactory());
config.setWarningLevel(WarningMessage.POSSIBLE_ERRORS);
return config;
}
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import nextflow.script.ast.AssignmentExpression;
import nextflow.script.ast.FunctionNode;
import nextflow.script.ast.IncludeNode;
import nextflow.script.ast.OutputNode;
import nextflow.script.ast.ParamNodeV1;
import nextflow.script.ast.ProcessNodeV1;
import nextflow.script.ast.ProcessNodeV2;
import nextflow.script.ast.RecordNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.ScriptVisitorSupport;
import nextflow.script.ast.TupleParameter;
import nextflow.script.ast.WorkflowNode;
import nextflow.script.types.Record;
import nextflow.script.types.Tuple;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.SourceUnit;
import static nextflow.script.ast.ASTUtils.*;
/**
* Resolve variable names, function names, and type names in
* a script.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ScriptResolveVisitor extends ScriptVisitorSupport {
private static final ClassNode RECORD_TYPE = ClassHelper.makeCached(Record.class);
private static final ClassNode TUPLE_TYPE = ClassHelper.makeCached(Tuple.class);
private SourceUnit sourceUnit;
private List<ClassNode> imports;
private ResolveVisitor resolver;
public ScriptResolveVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit, List<ClassNode> defaultImports, List<ClassNode> libImports) {
this.sourceUnit = sourceUnit;
this.imports = new ArrayList<>(defaultImports);
this.resolver = new ResolveVisitor(sourceUnit, compilationUnit, imports, libImports);
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ScriptNode sn ) {
// initialize variable scopes
var variableScopeVisitor = new VariableScopeVisitor(sourceUnit);
variableScopeVisitor.declare();
variableScopeVisitor.visit();
// append included types to default imports
for( var includeNode : sn.getIncludes() ) {
for( var entry : includeNode.entries ) {
if( entry.getTarget() instanceof ClassNode cn )
imports.add(cn);
}
}
// resolve type names
if( sn.getParams() != null )
visitParams(sn.getParams());
for( var paramNode : sn.getParamsV1() )
visitParamV1(paramNode);
for( var workflowNode : sn.getWorkflows() )
visitWorkflow(workflowNode);
for( var processNode : sn.getProcesses() )
visitProcess(processNode);
for( var functionNode : sn.getFunctions() )
visitFunction(functionNode);
for( var type : sn.getClasses() ) {
if( type instanceof RecordNode rn )
visitRecord(rn);
else if( type.isEnum() )
visitEnum(type);
}
if( sn.getOutputs() != null )
visitOutputs(sn.getOutputs());
// report errors for any unresolved variable references
new DynamicVariablesVisitor().visit(sn);
}
}
@Override
public void visitParam(Parameter node) {
node.setInitialExpression(resolver.transform(node.getInitialExpression()));
resolver.resolveOrFail(node.getType(), node);
}
@Override
public void visitParamV1(ParamNodeV1 node) {
node.value = resolver.transform(node.value);
}
@Override
public void visitWorkflow(WorkflowNode node) {
for( var take : node.getParameters() )
resolver.resolveOrFail(take.getType(), take);
resolver.visit(node.main);
resolveTypedOutputs(node.emits);
resolver.visit(node.emits);
resolver.visit(node.publishers);
resolver.visit(node.onComplete);
resolver.visit(node.onError);
}
private void resolveTypedOutputs(Statement block) {
for( var stmt : asBlockStatements(block) ) {
var stmtX = (ExpressionStatement)stmt;
var output = stmtX.getExpression();
var target =
output instanceof AssignmentExpression ae ? ae.getLeftExpression() :
output instanceof VariableExpression ve ? ve :
null;
if( target instanceof VariableExpression ve )
resolver.resolveOrFail(ve);
}
}
@Override
public void visitProcessV2(ProcessNodeV2 node) {
for( var input : asFlatParams(node.inputs) ) {
resolver.resolveOrFail(input.getType(), input);
}
for( var input : node.inputs ) {
var type = input.getType();
if( input instanceof TupleParameter tp && RECORD_TYPE.equals(type) )
resolveRecordInput(tp);
if( input instanceof TupleParameter tp && TUPLE_TYPE.equals(type) )
resolveTupleInput(tp);
}
resolver.visit(node.directives);
resolver.visit(node.stagers);
resolveTypedOutputs(node.outputs);
resolver.visit(node.outputs);
resolver.visit(node.topics);
resolver.visit(node.when);
resolver.visit(node.exec);
resolver.visit(node.stub);
}
private void resolveRecordInput(TupleParameter tp) {
var type = tp.getType();
for( var param : tp.components ) {
var fn = new FieldNode(param.getName(), Modifier.PUBLIC, param.getType(), type, null);
fn.setDeclaringClass(type);
type.addField(fn);
}
}
private void resolveTupleInput(TupleParameter tp) {
var genericsTypes = Arrays.stream(tp.components)
.map(p -> new GenericsType(p.getType()))
.toArray(GenericsType[]::new);
tp.getType().setGenericsTypes(genericsTypes);
}
@Override
public void visitProcessV1(ProcessNodeV1 node) {
resolver.visit(node.directives);
resolver.visit(node.inputs);
resolver.visit(node.outputs);
resolver.visit(node.when);
resolver.visit(node.exec);
resolver.visit(node.stub);
}
@Override
public void visitFunction(FunctionNode node) {
for( var param : node.getParameters() ) {
param.setInitialExpression(resolver.transform(param.getInitialExpression()));
resolver.resolveOrFail(param.getType(), param.getType());
}
resolver.resolveOrFail(node.getReturnType(), node);
resolver.visit(node.getCode());
}
@Override
public void visitField(FieldNode node) {
resolver.resolveOrFail(node.getType(), node);
}
@Override
public void visitOutput(OutputNode node) {
resolver.resolveOrFail(node.getType(), node.getType());
resolver.visit(node.body);
}
private class DynamicVariablesVisitor extends ScriptVisitorSupport {
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public void visitVariableExpression(VariableExpression node) {
var variable = node.getAccessedVariable();
if( variable instanceof DynamicVariable )
resolver.addError("`" + node.getName() + "` is not defined", node);
}
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import static nextflow.script.ast.ASTUtils.*;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Utility functions for ScriptToGroovyVisitor.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ScriptToGroovyHelper {
private SourceUnit sourceUnit;
public ScriptToGroovyHelper(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
/**
* Get the list of variable references in a statement.
*
* This method is used to collect references to task ext
* properties (e.g. `task.ext.args`) in the process body, so that
* they are included in the task hash.
*
* These properties are typically used like inputs, but are not
* explicitly declared, so they must be identified by their usage.
*
* The resulting list expression should be provided as the fourth
* argument of the BodyDef constructor.
*
* @param node
*/
public Expression getVariableRefs(Statement node) {
var refs = new VariableRefCollector().collect(node).stream()
.map(name -> createX("nextflow.script.TokenValRef", constX(name)))
.toList();
return listX(refs);
}
private class VariableRefCollector extends CodeVisitorSupport {
private Set<String> variableRefs;
public Set<String> collect(Statement node) {
variableRefs = new HashSet<>();
visit(node);
return variableRefs;
}
@Override
public void visitPropertyExpression(PropertyExpression node) {
if( !isPropertyChain(node) ) {
super.visitPropertyExpression(node);
return;
}
var name = asPropertyChain(node);
if( name.startsWith("task.ext.") )
variableRefs.add(name);
}
private static boolean isPropertyChain(PropertyExpression node) {
var target = node.getObjectExpression();
while( target instanceof PropertyExpression pe )
target = pe.getObjectExpression();
return target instanceof VariableExpression;
}
private static String asPropertyChain(PropertyExpression node) {
var list = new ArrayList<String>();
list.add(node.getPropertyAsString());
var target = node.getObjectExpression();
while( target instanceof PropertyExpression pe ) {
list.add(pe.getPropertyAsString());
target = pe.getObjectExpression();
}
list.add(target.getText());
Collections.reverse(list);
return String.join(".", list);
}
}
/**
* Transform an expression into a lazy expression by
* wrapping it in a closure if it references variables.
*
* @param node
*/
public Expression transformToLazy(Expression node) {
if( node instanceof ClosureExpression )
return node;
var vars = new VariableCollector().collect(node);
if( !vars.isEmpty() )
return closureX(stmt(node));
return node;
}
private class VariableCollector extends CodeVisitorSupport {
private Set<Variable> vars;
private Set<Variable> declaredParams;
public Set<Variable> collect(Expression node) {
vars = new HashSet<>();
declaredParams = new HashSet<>();
visit(node);
return vars;
}
@Override
public void visitClosureExpression(ClosureExpression node) {
if( node.getParameters() != null ) {
for( var param : node.getParameters() )
declaredParams.add(param);
}
}
@Override
public void visitVariableExpression(VariableExpression node) {
var variable = node.getAccessedVariable();
if( variable != null && !declaredParams.contains(variable) )
vars.add(variable);
}
}
/**
* Get the source text for a statement.
*
* @param node
*/
public String getSourceText(Statement node) {
var builder = new StringBuilder();
var colx = node.getColumnNumber();
var colz = node.getLastColumnNumber();
var first = node.getLineNumber();
var last = node.getLastLineNumber();
for( int i = first; i <= last; i++ ) {
var line = sourceUnit.getSource().getLine(i, null);
// prepend first-line indent
if( i == first ) {
int k = 0;
while( k < line.length() && line.charAt(k) == ' ' )
k++;
builder.append( line.substring(0, k) );
}
// determine range of current line
var begin = (i == first) ? colx - 1 : 0;
var end = (i == last) ? colz - 1 : line.length();
// skip trailing newline (e.g. for block statements)
if( i == last && begin == end )
continue;
builder.append( line.substring(begin, end) ).append('\n');
}
return builder.toString();
}
/**
* Get the source text for an expression.
*
* @param node
*/
public String getSourceText(Expression node) {
var stm = stmt(node);
stm.setSourcePosition(node);
return getSourceText(stm);
}
}

View File

@@ -0,0 +1,403 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.ast.AssignmentExpression;
import nextflow.script.ast.FeatureFlagNode;
import nextflow.script.ast.FunctionNode;
import nextflow.script.ast.IncludeNode;
import nextflow.script.ast.OutputBlockNode;
import nextflow.script.ast.ParamBlockNode;
import nextflow.script.ast.ParamNodeV1;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.ProcessNodeV1;
import nextflow.script.ast.ProcessNodeV2;
import nextflow.script.ast.RecordNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.ScriptVisitorSupport;
import nextflow.script.ast.WorkflowNode;
import nextflow.script.dsl.Nullable;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Types;
import static nextflow.script.ast.ASTUtils.*;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Transform a Nextflow script AST into a Groovy AST.
*
* @see nextflow.script.BaseScript
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ScriptToGroovyVisitor extends ScriptVisitorSupport {
private static Set<String> RESERVED_NAMES = Set.of("main", "run", "runScript");
private SourceUnit sourceUnit;
private ScriptNode moduleNode;
private ScriptToGroovyHelper sgh;
public ScriptToGroovyVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
this.moduleNode = (ScriptNode) sourceUnit.getAST();
this.sgh = new ScriptToGroovyHelper(sourceUnit);
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
if( moduleNode == null )
return;
if( moduleNode.isTypingEnabled() )
moduleNode.addStatement(stmt(callThisX("enableTyping", new ArgumentListExpression())));
var declarations = moduleNode.getDeclarations();
declarations.sort(Comparator.comparing(node -> node.getLineNumber()));
for( var decl : declarations ) {
if( decl instanceof ClassNode cn && cn.isEnum() )
visitEnum(cn);
else if( decl instanceof FeatureFlagNode ffn )
visitFeatureFlag(ffn);
else if( decl instanceof FunctionNode fn )
visitFunction(fn);
else if( decl instanceof IncludeNode in )
visitInclude(in);
else if( decl instanceof OutputBlockNode obn )
visitOutputs(obn);
else if( decl instanceof ParamBlockNode pbn )
visitParams(pbn);
else if( decl instanceof ParamNodeV1 pn )
visitParamV1(pn);
else if( decl instanceof ProcessNode pn )
visitProcess(pn);
else if( decl instanceof RecordNode rn )
visitRecord(rn);
else if( decl instanceof WorkflowNode wn )
visitWorkflow(wn);
}
if( moduleNode.isEmpty() )
moduleNode.addStatement(ReturnStatement.RETURN_NULL_OR_VOID);
}
@Override
public void visitFeatureFlag(FeatureFlagNode node) {
// static typing is enabled per-script rather than globally
if( "nextflow.enable.types".equals(node.name) )
return;
var names = node.name.split("\\.");
Expression target = varX(DefaultGroovyMethods.head(names));
for( var name : DefaultGroovyMethods.tail(names) )
target = propX(target, name);
var result = stmt(assignX(target, node.value));
moduleNode.addStatement(result);
}
@Override
public void visitInclude(IncludeNode node) {
var entries = (List<Expression>) node.entries.stream()
.map((entry) -> {
var name = constX(entry.name);
return entry.alias != null
? createX("nextflow.script.IncludeDef.Module", args(name, constX(entry.alias)))
: createX("nextflow.script.IncludeDef.Module", args(name));
})
.collect(Collectors.toList());
var include = callThisX("include", args(createX("nextflow.script.IncludeDef", args(listX(entries)))));
var from = callX(include, "from", args(node.source));
var result = stmt(callX(from, "load0", args(varX("params"))));
moduleNode.addStatement(result);
}
@Override
public void visitParams(ParamBlockNode node) {
var paramsType = new RecordNode(packageName(moduleNode) + "." + "__Params");
for( var param : node.declarations ) {
var fn = new FieldNode(
param.getName(),
Modifier.PUBLIC,
param.getType(),
paramsType,
param.getInitialExpression()
);
paramsType.addField(fn);
}
moduleNode.addClass(paramsType);
var statements = Arrays.stream(node.declarations)
.map((param) -> {
var name = param.getName();
var optional = param.getType().getNodeMetaData(ASTNodeMarker.NULLABLE) != null;
var arguments = param.hasInitialExpression()
? args(constX(name), constX(optional), param.getInitialExpression())
: args(constX(name), constX(optional));
return stmt(callThisX("declare", arguments));
})
.toList();
var closure = closureX(block(new VariableScope(), statements));
var result = stmt(callThisX("params", args(classX(paramsType), closure)));
moduleNode.addStatement(result);
}
private static String packageName(ScriptNode moduleNode) {
var scriptClass = moduleNode.getClasses().get(0);
return scriptClass.getNameWithoutPackage();
}
@Override
public void visitParamV1(ParamNodeV1 node) {
var result = stmt(assignX(node.target, node.value));
moduleNode.addStatement(result);
}
@Override
public void visitWorkflow(WorkflowNode node) {
if( !node.isEntry() )
checkReservedMethodName(node, "workflow");
var main = node.main instanceof BlockStatement block ? block : new BlockStatement();
visitWorkflowEmits(node.emits, main);
visitWorkflowPublishers(node.publishers, main);
visitWorkflowHandler(node.onComplete, "setOnComplete", main);
visitWorkflowHandler(node.onError, "setOnError", main);
var bodyDef = stmt(createX(
"nextflow.script.BodyDef",
args(
closureX(null, main),
constX(null),
constX("workflow")
)
));
var closure = closureX(null, block(new VariableScope(), List.of(
workflowTakes(node.getParameters()),
node.emits,
bodyDef
)));
var arguments = node.isEntry()
? args(closure)
: args(constX(node.getName()), closure);
var result = stmt(callThisX("workflow", arguments));
moduleNode.addStatement(result);
}
private Statement workflowTakes(Parameter[] takes) {
var statements = Arrays.stream(takes)
.map((take) ->
stmt(callThisX("_take_", args(constX(take.getName()))))
)
.toList();
return block(null, statements);
}
private void visitWorkflowEmits(Statement emits, BlockStatement main) {
for( var stmt : asBlockStatements(emits) ) {
var es = (ExpressionStatement)stmt;
var emit = es.getExpression();
if( emit instanceof VariableExpression ve ) {
es.setExpression(callThisX("_emit_", args(constX(ve.getName()))));
}
else if( emit instanceof AssignmentExpression ae ) {
var target = (VariableExpression)ae.getLeftExpression();
main.addStatement(assignS(target, emit));
es.setExpression(callThisX("_emit_", args(constX(target.getName()))));
main.addStatement(es);
}
else {
var target = varX("$out");
main.addStatement(assignS(target, emit));
es.setExpression(callThisX("_emit_", args(constX(target.getName()))));
main.addStatement(es);
}
}
}
private void visitWorkflowPublishers(Statement publishers, BlockStatement main) {
for( var stmt : asBlockStatements(publishers) ) {
var es = (ExpressionStatement)stmt;
var publish = (BinaryExpression)es.getExpression();
var target = asVarX(publish.getLeftExpression());
es.setExpression(callThisX("_publish_", args(constX(target.getName()), publish.getRightExpression())));
main.addStatement(es);
}
}
private void visitWorkflowHandler(Statement code, String name, BlockStatement main) {
if( code instanceof BlockStatement block )
main.addStatement(stmt(callX(varX("workflow"), name, args(closureX(null, block)))));
}
@Override
public void visitProcessV2(ProcessNodeV2 node) {
checkReservedMethodName(node, "process");
var result = new ProcessToGroovyVisitorV2(sourceUnit).transform(node);
moduleNode.addStatement(result);
}
@Override
public void visitProcessV1(ProcessNodeV1 node) {
checkReservedMethodName(node, "process");
var result = new ProcessToGroovyVisitorV1(sourceUnit).transform(node);
moduleNode.addStatement(result);
}
@Override
public void visitFunction(FunctionNode node) {
checkReservedMethodName(node, "function");
moduleNode.getScriptClassDummy().addMethod(node);
}
private void checkReservedMethodName(MethodNode node, String typeLabel) {
if( RESERVED_NAMES.contains(node.getName()) )
syntaxError(node, "`" + node.getName() + "` is not allowed as a " + typeLabel + " name because it is reserved for internal use");
}
@Override
public void visitOutputs(OutputBlockNode node) {
var statements = node.declarations.stream()
.map((output) -> {
new PublishDslVisitor().visit(output.body);
var name = constX(output.getName());
var body = closureX(null, output.body);
return stmt(callThisX("declare", args(name, body)));
})
.toList();
var closure = closureX(null, block(new VariableScope(), statements));
var result = stmt(callThisX("output", args(closure)));
moduleNode.addStatement(result);
}
private static final ClassNode NULLABLE = ClassHelper.makeCached(Nullable.class);
@Override
public void visitRecord(RecordNode node) {
for( var fn : node.getFields() ) {
if( fn.getType().getNodeMetaData(ASTNodeMarker.NULLABLE) != null )
fn.addAnnotation(NULLABLE);
}
var result = stmt(callThisX("declareType", args(classX(node))));
moduleNode.addStatement(result);
}
@Override
public void visitEnum(ClassNode node) {
var result = stmt(callThisX("declareType", args(classX(node))));
moduleNode.addStatement(result);
}
private void syntaxError(ASTNode node, String message) {
sourceUnit.addError(new SyntaxException(message, node));
}
/**
* Transform publish statements in a workflow output:
*
* path { sample ->
* sample.foo >> 'foo/'
* sample.bar >> 'bar/'
* }
*
* becomes:
*
* path { sample ->
* publish(sample.foo, 'foo/')
* publish(sample.bar, 'bar/')
* }
*/
private class PublishDslVisitor extends CodeVisitorSupport {
private boolean hasPublishStatements;
private boolean hasNonPublishStatements;
@Override
public void visitMethodCallExpression(MethodCallExpression node) {
if( "path".equals(node.getMethodAsString()) )
visitPathDirective(node);
}
private void visitPathDirective(MethodCallExpression node) {
var code = asDslBlock(node, 1);
if( code == null )
return;
for( var stmt : code.getStatements() ) {
if( visitPublishStatement(stmt) )
hasPublishStatements = true;
else
hasNonPublishStatements = true;
}
if( hasPublishStatements && hasNonPublishStatements )
syntaxError(node, "Publish statements cannot be mixed with other statements in a dynamic publish path");
}
private boolean visitPublishStatement(Statement node) {
if( !(node instanceof ExpressionStatement) )
return false;
var es = (ExpressionStatement) node;
if( !(es.getExpression() instanceof BinaryExpression) )
return false;
var be = (BinaryExpression) es.getExpression();
if( be.getOperation().getType() != Types.RIGHT_SHIFT )
return false;
var source = be.getLeftExpression();
var target = be.getRightExpression();
es.setExpression(callThisX("publish", args(source, target)));
return true;
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.util.List;
import nextflow.script.ast.RecordNode;
import nextflow.script.types.Channel;
import nextflow.script.types.Record;
import nextflow.script.types.Tuple;
import nextflow.script.types.Value;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.expr.CastExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.control.SourceUnit;
/**
* Strip type annotations that are used by the Nextflow type checker
* but not supported by the Groovy runtime.
*
* For example, Nextflow allows the Tuple type to be specified
* with variable type arguments, which is not supported by the JVM,
* so these type annotations must be removed.
*
* @author Ben Sherman <bentshermman@gmail.com>
*/
public class StripTypesVisitor extends ClassCodeExpressionTransformer {
private static final List<ClassNode> STRIP_TYPES = List.of(
ClassHelper.makeWithoutCaching("nextflow.Channel"),
ClassHelper.makeCached(Channel.class),
ClassHelper.makeCached(Record.class),
ClassHelper.makeCached(Tuple.class),
ClassHelper.makeCached(Value.class)
);
private SourceUnit sourceUnit;
public StripTypesVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public void visitMethod(MethodNode node) {
// Erase record type parameters so that records (with type Record)
// can be dispatched to these methods at runtime
for( var param : node.getParameters() ) {
if( param.getType().redirect() instanceof RecordNode )
param.setType(ClassHelper.dynamicType());
}
super.visitMethod(node);
}
@Override
public Expression transform(Expression node) {
if( node instanceof CastExpression ce ) {
return stripTypeAnnotation(ce);
}
if( node instanceof ClosureExpression ce ) {
ce.visit(this);
return ce;
}
if( node instanceof DeclarationExpression de ) {
stripTypeAnnotation(de);
}
return super.transform(node);
}
private Expression stripTypeAnnotation(CastExpression node) {
return STRIP_TYPES.contains(node.getType())
? node.getExpression()
: node;
}
private Expression stripTypeAnnotation(DeclarationExpression node) {
if( node.getLeftExpression() instanceof VariableExpression ve ) {
if( STRIP_TYPES.contains(ve.getType()) )
node.setLeftExpression(new VariableExpression(ve.getName()));
}
return node;
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.GStringExpression;
import org.codehaus.groovy.control.SourceUnit;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
/**
* Transform a process script to use Bash-aware path escaping.
*
* This way, the user can reference files in a Bash script
* without needing to e.g. escape spaces in filenames.
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
public class TaskCmdXformVisitor extends ClassCodeVisitorSupport {
private SourceUnit sourceUnit;
public TaskCmdXformVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
@Override
public void visitGStringExpression(GStringExpression node) {
var values = node.getValues();
for( int i = 0; i < values.size(); i++ )
values.set(i, applyEscape(values.get(i)));
super.visitGStringExpression(node);
}
/**
* @see LangHelpers.applyPathEscapeAware()
*/
private static Expression applyEscape(Expression node) {
var cn = ClassHelper.makeWithoutCaching("nextflow.util.LangHelpers");
return callX(classX(cn), "applyPathEscapeAware", args(node));
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.ScriptVisitorSupport;
import nextflow.script.ast.WorkflowNode;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import static nextflow.script.ast.ASTUtils.*;
/**
* Resolve and validate the types of expressions.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class TypeCheckingVisitor extends ScriptVisitorSupport {
private SourceUnit sourceUnit;
public TypeCheckingVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ScriptNode sn )
visit(sn);
}
// expressions
@Override
public void visitMethodCallExpression(MethodCallExpression node) {
var defNode = (MethodNode) node.getNodeMetaData(ASTNodeMarker.METHOD_TARGET);
if( defNode instanceof ProcessNode || defNode instanceof WorkflowNode )
checkMethodCallArguments(node, defNode);
super.visitMethodCallExpression(node);
}
private void checkMethodCallArguments(MethodCallExpression node, MethodNode defNode) {
var argsCount = asMethodCallArguments(node).size();
var paramsCount = defNode.getParameters().length;
if( argsCount != paramsCount )
addError(String.format("Incorrect number of call arguments, expected %d but received %d", paramsCount, argsCount), node);
}
@Override
public void addError(String message, ASTNode node) {
var cause = new TypeError(message, node);
var errorMessage = new SyntaxErrorMessage(cause, sourceUnit);
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
}
private class TypeError extends SyntaxException implements PhaseAware {
public TypeError(String message, ASTNode node) {
super(message, node);
}
@Override
public int getPhase() {
return Phases.TYPE_CHECKING;
}
}
}

View File

@@ -0,0 +1,359 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.dsl.Constant;
import nextflow.script.dsl.Operator;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.WorkflowNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.control.messages.WarningMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import static nextflow.script.ast.ASTUtils.*;
/**
* Resolve variable and function names.
*
* @see org.codehaus.groovy.classgen.VariableScopeVisitor
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class VariableScopeChecker {
private SourceUnit sourceUnit;
private Map<String,AnnotatedNode> includes = new HashMap<>();
private VariableScope currentScope;
private Set<Variable> unusedVariables = Collections.newSetFromMap(new IdentityHashMap<>());
public VariableScopeChecker(SourceUnit sourceUnit, ClassNode classScope) {
this.sourceUnit = sourceUnit;
this.currentScope = new VariableScope();
this.currentScope.setClassScope(classScope);
}
public void setCurrentScope(VariableScope scope) {
currentScope = scope;
}
public VariableScope getCurrentScope() {
return currentScope;
}
public void include(String name, AnnotatedNode variable) {
includes.put(name, variable);
}
public AnnotatedNode getInclude(String name) {
return includes.get(name);
}
public void checkUnusedVariables() {
for( var variable : unusedVariables ) {
if( variable instanceof ASTNode node && !variable.getName().startsWith("_") ) {
var message = variable instanceof Parameter
? "Parameter was not used -- prefix with `_` to suppress warning"
: "Variable was declared but not used";
addWarning(message, variable.getName(), node);
}
}
}
public void pushScope(ClassNode classScope) {
currentScope = new VariableScope(currentScope);
if( classScope != null )
currentScope.setClassScope(classScope);
}
public void pushScope(Class classScope) {
pushScope(ClassHelper.makeCached(classScope));
}
public void pushScope() {
pushScope((ClassNode) null);
}
public void popScope() {
currentScope = currentScope.getParent();
}
public void declare(VariableExpression variable) {
declare(variable, variable);
variable.setAccessedVariable(variable);
}
public void declare(Variable variable, ASTNode context) {
var name = variable.getName();
for( var scope = currentScope; scope != null; scope = scope.getParent() ) {
var other = scope.getDeclaredVariable(name);
if( other != null ) {
addError("`" + name + "` is already declared", context, "First declared here", (ASTNode) other);
break;
}
}
currentScope.putDeclaredVariable(variable);
unusedVariables.add(variable);
}
/**
* Find the declaration of a given variable.
*
* @param name
* @param node
*/
public Variable findVariableDeclaration(String name, ASTNode node) {
Variable variable = null;
VariableScope scope = currentScope;
boolean isClassVariable = false;
while( scope != null ) {
variable = scope.getDeclaredVariable(name);
if( variable != null )
break;
variable = scope.getReferencedLocalVariable(name);
if( variable != null )
break;
variable = scope.getReferencedClassVariable(name);
if( variable != null ) {
isClassVariable = true;
break;
}
variable = findDslVariable(scope.getClassScope(), name, node);
if( variable != null ) {
isClassVariable = true;
break;
}
scope = scope.getParent();
}
if( variable == null )
return null;
VariableScope end = scope;
scope = currentScope;
while( true ) {
if( isClassVariable )
scope.putReferencedClassVariable(variable);
else
scope.putReferencedLocalVariable(variable);
if( scope == end )
break;
scope = scope.getParent();
}
unusedVariables.remove(variable);
return variable;
}
/**
* Find the definition of a built-in variable.
*
* @param cn
* @param name
* @param node
*/
private Variable findDslVariable(ClassNode cn, String name, ASTNode node) {
while( cn != null ) {
for( var mn : cn.getMethods() ) {
// processes, workflows, and operators can be accessed as variables, e.g. with pipes
if( isDataflowMethod(mn) && name.equals(mn.getName()) ) {
return wrapMethodAsVariable(mn, name);
}
// built-in constants and namespaces are methods annotated as @Constant
var an = findAnnotation(mn, Constant.class);
if( !an.isPresent() )
continue;
if( !name.equals(an.get().getMember("value").getText()) )
continue;
if( findAnnotation(mn, Deprecated.class).isPresent() )
addParanoidWarning("`" + name + "` is deprecated and will be removed in a future version", node);
return wrapMethodAsVariable(mn, name);
}
cn = cn.getInterfaces().length > 0
? cn.getInterfaces()[0]
: null;
}
if( includes.get(name) instanceof MethodNode mn )
return wrapMethodAsVariable(mn, name);
return null;
}
public static boolean isDataflowMethod(MethodNode mn) {
return mn instanceof ProcessNode || mn instanceof WorkflowNode || isOperator(mn);
}
public static boolean isOperator(MethodNode mn) {
return findAnnotation(mn, Operator.class).isPresent();
}
private static PropertyNode wrapMethodAsVariable(MethodNode mn, String name) {
var cn = mn.getDeclaringClass();
var fn = new FieldNode(name, mn.getModifiers() & 0xF, methodOutputType(mn), cn, null);
fn.setHasNoRealSourcePosition(true);
fn.setDeclaringClass(cn);
fn.setSynthetic(true);
var pn = new PropertyNode(fn, fn.getModifiers(), null, null);
pn.putNodeMetaData(ASTNodeMarker.METHOD_VARIABLE_TARGET, mn);
pn.setDeclaringClass(cn);
return pn;
}
private static ClassNode methodOutputType(MethodNode mn) {
if( mn instanceof ProcessNode || mn instanceof WorkflowNode )
return ClassHelper.dynamicType();
return mn.getReturnType();
}
/**
* Find the definition of a built-in function.
*
* @param name
* @param node
* @param directive
*/
public List<MethodNode> findDslFunction(String name, ASTNode node, boolean directive) {
VariableScope scope = currentScope;
while( scope != null ) {
ClassNode cn = scope.getClassScope();
while( cn != null ) {
// built-in functions are methods not annotated as @Constant
var methods = cn.getDeclaredMethods(name).stream()
.filter(mn -> !findAnnotation(mn, Constant.class).isPresent())
.toList();
if( methods.size() == 1 && findAnnotation(methods.get(0), Deprecated.class).isPresent() )
addParanoidWarning("`" + name + "` is deprecated and will be removed in a future version", node);
if( !methods.isEmpty() )
return methods;
// directives can only come from the immediate dsl scope
if( directive && scope == currentScope )
return Collections.emptyList();
cn = cn.getInterfaces().length > 0
? cn.getInterfaces()[0]
: null;
}
scope = scope.getParent();
}
return includes.get(name) instanceof MethodNode mn
? List.of(mn)
: Collections.emptyList();
}
public List<MethodNode> findDslFunction(String name, ASTNode node) {
return findDslFunction(name, node, false);
}
public void addWarning(String message, String tokenText, ASTNode node) {
var token = new Token(0, tokenText, node.getLineNumber(), node.getColumnNumber()); // ASTNode to CSTNode
sourceUnit.getErrorCollector().addWarning(WarningMessage.POSSIBLE_ERRORS, message, token, sourceUnit);
}
public void addParanoidWarning(String message, String tokenText, ASTNode node, String otherMessage, ASTNode otherNode) {
var token = new Token(0, tokenText, node.getLineNumber(), node.getColumnNumber()); // ASTNode to CSTNode
var warning = new ParanoidWarning(WarningMessage.POSSIBLE_ERRORS, message, token, sourceUnit);
if( otherNode != null )
warning.setRelatedInformation(otherMessage, otherNode);
sourceUnit.getErrorCollector().addWarning(warning);
}
public void addParanoidWarning(String message, ASTNode node, String otherMessage, ASTNode otherNode) {
addParanoidWarning(message, "", node, otherMessage, otherNode);
}
public void addParanoidWarning(String message, String tokenText, ASTNode node) {
addParanoidWarning(message, tokenText, node, null, null);
}
public void addParanoidWarning(String message, ASTNode node) {
addParanoidWarning(message, "", node, null, null);
}
public void addError(String message, ASTNode node) {
addError(new VariableScopeError(message, node));
}
public void addError(String message, ASTNode node, String otherMessage, ASTNode otherNode) {
var cause = new VariableScopeError(message, node);
if( otherNode != null )
cause.setRelatedInformation(otherMessage, otherNode);
addError(cause);
}
public void addError(SyntaxException cause) {
var errorMessage = new SyntaxErrorMessage(cause, sourceUnit);
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
}
private class VariableScopeError extends SyntaxException implements PhaseAware, RelatedInformationAware {
private String otherMessage;
private ASTNode otherNode;
public VariableScopeError(String message, ASTNode node) {
super(message, node);
}
public void setRelatedInformation(String otherMessage, ASTNode otherNode) {
this.otherMessage = otherMessage;
this.otherNode = otherNode;
}
@Override
public int getPhase() {
return Phases.NAME_RESOLUTION;
}
@Override
public String getOtherMessage() {
return otherMessage;
}
@Override
public ASTNode getOtherNode() {
return otherNode;
}
}
}

View File

@@ -0,0 +1,867 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.control;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import groovy.lang.groovydoc.GroovydocHolder;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.ast.AssignmentExpression;
import nextflow.script.ast.FeatureFlagNode;
import nextflow.script.ast.FunctionNode;
import nextflow.script.ast.ImplicitClosureParameter;
import nextflow.script.ast.IncludeNode;
import nextflow.script.ast.OutputBlockNode;
import nextflow.script.ast.OutputNode;
import nextflow.script.ast.ParamBlockNode;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.ProcessNodeV1;
import nextflow.script.ast.ProcessNodeV2;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.ScriptVisitorSupport;
import nextflow.script.ast.WorkflowNode;
import nextflow.script.dsl.Constant;
import nextflow.script.dsl.EntryWorkflowDsl;
import nextflow.script.dsl.FeatureFlag;
import nextflow.script.dsl.FeatureFlagDsl;
import nextflow.script.dsl.OutputDsl;
import nextflow.script.dsl.ProcessDsl;
import nextflow.script.dsl.ScriptDsl;
import nextflow.script.dsl.WorkflowDsl;
import nextflow.script.dsl.WorkflowDslV1;
import nextflow.script.types.ParamsMap;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Types;
import static nextflow.script.ast.ASTUtils.*;
/**
* Initialize the variable scopes for an AST.
*
* @see org.codehaus.groovy.classgen.VariableScopeVisitor
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
class VariableScopeVisitor extends ScriptVisitorSupport {
private SourceUnit sourceUnit;
private VariableScopeChecker vsc;
private boolean typingEnabled;
private MethodNode currentDefinition;
public VariableScopeVisitor(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit;
this.vsc = new VariableScopeChecker(sourceUnit, new ClassNode(ScriptDsl.class));
}
@Override
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
public void declare() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ScriptNode sn ) {
typingEnabled = sn.isTypingEnabled();
for( var includeNode : sn.getIncludes() )
declareInclude(includeNode);
declareParams(sn);
for( var workflowNode : sn.getWorkflows() ) {
if( !workflowNode.isEntry() )
declareMethod(workflowNode);
}
for( var processNode : sn.getProcesses() )
declareMethod(processNode);
for( var functionNode : sn.getFunctions() )
declareMethod(functionNode);
declareTypes(sn);
}
}
private void declareInclude(IncludeNode node) {
for( var entry : node.entries ) {
if( entry.getTarget() == null )
continue;
var name = entry.getNameOrAlias();
var otherInclude = vsc.getInclude(name);
if( otherInclude != null )
vsc.addError("`" + name + "` is already included", node, "First included here", otherInclude);
vsc.include(name, entry.getTarget());
}
}
private ClassNode paramsType;
private void declareParams(ScriptNode sn) {
var params = sn.getParams();
var entry = sn.getEntry();
if( params == null || entry == null )
return;
var cn = new ClassNode(ParamsMap.class);
for( var param : params.declarations ) {
var name = param.getName();
var type = param.getType();
var fn = new FieldNode(name, Modifier.PUBLIC, type, cn, null);
fn.setHasNoRealSourcePosition(true);
fn.setDeclaringClass(cn);
fn.setSynthetic(true);
fn.putNodeMetaData(GroovydocHolder.DOC_COMMENT, param.getGroovydoc());
cn.addField(fn);
}
this.paramsType = cn;
}
private void declareMethod(MethodNode mn) {
var cn = currentScope().getClassScope();
var name = mn.getName();
var otherInclude = vsc.getInclude(name);
if( otherInclude != null ) {
vsc.addError("`" + name + "` is already included", mn, "First included here", otherInclude);
}
var other = firstConflictingMethod(mn, cn);
if( other != null ) {
var first = mn.getLineNumber() < other.getLineNumber() ? mn : other;
var second = mn.getLineNumber() < other.getLineNumber() ? other : mn;
vsc.addError("`" + name + "` is already declared", second, "First declared here", first);
return;
}
cn.addMethod(mn);
}
private static MethodNode firstConflictingMethod(MethodNode mn, ClassNode cn) {
return cn.getDeclaredMethods(mn.getName()).stream()
.filter(other -> !(mn instanceof FunctionNode) || !(other instanceof FunctionNode))
.findFirst()
.orElse(null);
}
private void declareTypes(ScriptNode sn) {
var types = sn.getTypes();
for( int i = 0; i < types.size(); i++ ) {
var second = types.get(i);
// check includes
var name = second.getName();
var otherInclude = vsc.getInclude(name);
if( otherInclude != null ) {
vsc.addError("`" + name + "` is already included", second, "First included here", otherInclude);
}
// check declarations
for( int j = 0; j < i; j++ ) {
var first = types.get(j);
if( !first.getName().equals(second.getName()) )
continue;
vsc.addError("`" + second.getName() + "` is already declared", second, "First declared here", first);
}
}
}
public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ScriptNode sn ) {
super.visit(sn);
vsc.checkUnusedVariables();
}
}
@Override
public void visitFeatureFlag(FeatureFlagNode node) {
var cn = ClassHelper.makeCached(FeatureFlagDsl.class);
var result = cn.getFields().stream()
.filter(fn ->
findAnnotation(fn, FeatureFlag.class)
.map(an -> an.getMember("value").getText())
.map(name -> name.equals(node.name))
.orElse(false)
)
.findFirst();
if( result.isPresent() ) {
var ffn = result.get();
if( findAnnotation(ffn, Deprecated.class).isPresent() )
vsc.addParanoidWarning("`" + node.name + "` is deprecated and will be removed in a future version", node.name, node);
node.target = ffn;
}
else {
vsc.addError("Unrecognized feature flag '" + node.name + "'", node);
}
}
@Override
public void visitParams(ParamBlockNode node) {
var declaredParams = new HashMap<String,ASTNode>();
for( var param : node.declarations ) {
var name = param.getName();
var other = declaredParams.get(name);
if( other != null )
vsc.addError("Parameter `" + name + "` is already declared", param, "First declared here", other);
else
declaredParams.put(name, param);
if( param.hasInitialExpression() )
visit(param.getInitialExpression());
}
}
private boolean inWorkflowEmit;
@Override
public void visitWorkflow(WorkflowNode node) {
var classScope = workflowDsl(node.isEntry());
if( node.isEntry() && paramsType != null ) {
classScope = new ClassNode(classScope.getTypeClass());
var paramsMethod = classScope.getDeclaredMethods("getParams").get(0);
paramsMethod.setReturnType(paramsType);
}
vsc.pushScope(classScope);
currentDefinition = node;
node.setVariableScope(currentScope());
for( var take : node.getParameters() )
vsc.declare(take, take);
visit(node.main);
if( node.main instanceof BlockStatement block )
copyVariableScope(block.getVariableScope());
visitTypedOutputs(node.emits, "Workflow emit");
visitTypedOutputs(node.publishers, "Workflow output");
visit(node.onComplete);
visit(node.onError);
currentDefinition = null;
vsc.popScope();
}
private ClassNode workflowDsl(boolean entry) {
var result = new ClassNode(entry ? EntryWorkflowDsl.class : WorkflowDsl.class);
if( !typingEnabled ) {
var v1 = ClassHelper.makeCached(WorkflowDslV1.class);
for( var mn : v1.getMethods() ) {
if( vsc.isOperator(mn) )
result.addMethod(mn);
}
}
return result;
}
private void copyVariableScope(VariableScope source) {
for( var it = source.getDeclaredVariablesIterator(); it.hasNext(); ) {
var variable = it.next();
currentScope().putDeclaredVariable(variable);
}
}
private void visitTypedOutputs(Statement outputs, String typeLabel) {
var declaredOutputs = new HashMap<String,ASTNode>();
for( var stmt : asBlockStatements(outputs) ) {
var es = (ExpressionStatement)stmt;
var output = es.getExpression();
VariableExpression target;
if( output instanceof VariableExpression ve ) {
target = ve;
}
else if( output instanceof AssignmentExpression assign ) {
visit(assign.getRightExpression());
target = (VariableExpression)assign.getLeftExpression();
}
else {
visit(output);
target = null;
}
if( target != null ) {
var name = target.getName();
var other = declaredOutputs.get(name);
if( other != null )
vsc.addError(typeLabel + " `" + name + "` is already declared", target, "First declared here", other);
else
declaredOutputs.put(name, target);
}
}
}
@Override
public void visitProcessV2(ProcessNodeV2 node) {
vsc.pushScope(ProcessDsl.class);
currentDefinition = node;
node.setVariableScope(currentScope());
for( var input : asFlatParams(node.inputs) ) {
vsc.declare(input, input);
// suppress "unused variable" warnings since Path inputs are implicity staged
vsc.findVariableDeclaration(input.getName(), input);
}
vsc.pushScope(ProcessDsl.StageDsl.class);
visitDirectives(node.stagers, "stage directive", false);
vsc.popScope();
// deprecation warning reported during ast construction
visit(node.when);
visit(node.exec);
visit(node.stub);
vsc.pushScope(ProcessDsl.DirectiveDsl.class);
visitDirectives(node.directives, "process directive", false);
vsc.popScope();
vsc.pushScope(ProcessDsl.OutputDslV2.class);
visitTypedOutputs(node.outputs, "Process output");
visit(node.topics);
vsc.popScope();
currentDefinition = null;
vsc.popScope();
}
@Override
public void visitProcessV1(ProcessNodeV1 node) {
vsc.pushScope(ProcessDsl.class);
currentDefinition = node;
node.setVariableScope(currentScope());
declareProcessInputsV1(node.inputs);
vsc.pushScope(ProcessDsl.InputDslV1.class);
visitDirectives(node.inputs, "process input qualifier", false);
vsc.popScope();
if( !(node.when instanceof EmptyExpression) )
vsc.addParanoidWarning("Process `when` section will not be supported in a future version", node.when);
visit(node.when);
visit(node.exec);
visit(node.stub);
vsc.pushScope(ProcessDsl.DirectiveDsl.class);
visitDirectives(node.directives, "process directive", false);
vsc.popScope();
vsc.pushScope(ProcessDsl.OutputDslV1.class);
visitDirectives(node.outputs, "process output qualifier", false);
vsc.popScope();
currentDefinition = null;
vsc.popScope();
}
private void declareProcessInputsV1(Statement inputs) {
for( var stmt : asBlockStatements(inputs) ) {
var call = asMethodCallX(stmt);
if( call == null )
continue;
if( "tuple".equals(call.getMethodAsString()) ) {
for( var arg : asMethodCallArguments(call) ) {
if( arg instanceof MethodCallExpression mce )
declareProcessInput(mce);
}
}
else if( "each".equals(call.getMethodAsString()) ) {
var args = asMethodCallArguments(call);
if( args.size() != 1 )
continue;
var firstArg = args.get(0);
if( firstArg instanceof MethodCallExpression mce )
declareProcessInput(mce);
else if( firstArg instanceof VariableExpression ve )
vsc.declare(ve);
}
else {
declareProcessInput(call);
}
}
}
private static final List<String> DECLARING_INPUT_TYPES = List.of("val", "file", "path");
private void declareProcessInput(MethodCallExpression call) {
if( !DECLARING_INPUT_TYPES.contains(call.getMethodAsString()) )
return;
var args = asMethodCallArguments(call);
if( args.isEmpty() )
return;
if( args.get(args.size() - 1) instanceof VariableExpression ve )
vsc.declare(ve);
}
private void visitDirectives(Statement node, String typeLabel, boolean checkSyntaxErrors) {
if( node instanceof BlockStatement block )
block.setVariableScope(currentScope());
for( var stmt : asBlockStatements(node) ) {
var call = checkDirective(stmt, typeLabel, checkSyntaxErrors);
if( call != null )
super.visitMethodCallExpression(call);
}
}
private MethodCallExpression checkDirective(Statement node, String typeLabel, boolean checkSyntaxErrors) {
var call = asMethodCallX(node);
if( call == null ) {
if( checkSyntaxErrors )
addSyntaxError("Invalid " + typeLabel, node);
return null;
}
var name = call.getMethodAsString();
var methods = vsc.findDslFunction(name, call, true);
if( methods.size() == 1 )
call.putNodeMetaData(ASTNodeMarker.METHOD_TARGET, methods.get(0));
else if( !methods.isEmpty() )
call.putNodeMetaData(ASTNodeMarker.METHOD_OVERLOADS, methods);
else
vsc.addError("Unrecognized " + typeLabel + " `" + name + "`", node);
return call;
}
private static final List<String> EMIT_AND_TOPIC = List.of("emit", "topic");
@Override
public void visitMapEntryExpression(MapEntryExpression node) {
var classScope = currentScope().getClassScope();
if( classScope != null && classScope.getTypeClass() == ProcessDsl.OutputDslV1.class ) {
var key = node.getKeyExpression();
if( key instanceof ConstantExpression && EMIT_AND_TOPIC.contains(key.getText()) )
return;
}
super.visitMapEntryExpression(node);
}
@Override
public void visitFunction(FunctionNode node) {
vsc.pushScope();
currentDefinition = node;
node.setVariableScope(currentScope());
for( var parameter : node.getParameters() ) {
if( parameter.hasInitialExpression() )
visit(parameter.getInitialExpression());
vsc.declare(parameter, parameter);
}
visit(node.getCode());
currentDefinition = null;
vsc.popScope();
}
@Override
public void visitOutputs(OutputBlockNode node) {
var classScope = ClassHelper.makeCached(OutputDsl.class);
if( paramsType != null ) {
classScope = new ClassNode(classScope.getTypeClass());
var paramsMethod = classScope.getDeclaredMethods("getParams").get(0);
paramsMethod.setReturnType(paramsType);
}
vsc.pushScope(classScope);
super.visitOutputs(node);
vsc.popScope();
}
@Override
public void visitOutput(OutputNode node) {
var block = (BlockStatement) node.body;
block.setVariableScope(currentScope());
asBlockStatements(block).forEach((stmt) -> {
// validate output directive
var call = checkDirective(stmt, "output directive", true);
if( call == null )
return;
// treat as index definition
var name = call.getMethodAsString();
if( "index".equals(name) ) {
var code = asDslBlock(call, 1);
if( code != null ) {
vsc.pushScope(OutputDsl.IndexDsl.class);
visitDirectives(code, "output index directive", true);
vsc.popScope();
return;
}
}
// treat as regular directive
super.visitMethodCallExpression(call);
});
}
// statements
@Override
public void visitBlockStatement(BlockStatement node) {
var newScope = node.getVariableScope() != null;
if( newScope ) vsc.pushScope();
node.setVariableScope(currentScope());
super.visitBlockStatement(node);
if( newScope ) vsc.popScope();
}
@Override
public void visitCatchStatement(CatchStatement node) {
vsc.pushScope();
vsc.declare(node.getVariable(), node);
super.visitCatchStatement(node);
vsc.popScope();
}
@Override
public void visitExpressionStatement(ExpressionStatement node) {
var exp = node.getExpression();
if( exp instanceof AssignmentExpression ae ) {
var source = ae.getRightExpression();
var target = ae.getLeftExpression();
visit(source);
if( checkImplicitDeclaration(target) ) {
ae.putNodeMetaData(ASTNodeMarker.IMPLICIT_DECLARATION, Boolean.TRUE);
}
else {
visitMutatedVariable(target);
visit(target);
}
return;
}
super.visitExpressionStatement(node);
}
/**
* In processes and workflows, variables can be declared without `def`
* and are treated as variables scoped to the process or workflow.
*
* @param target
*/
private boolean checkImplicitDeclaration(Expression target) {
if( target instanceof TupleExpression te ) {
var result = false;
for( var el : te.getExpressions() )
result |= declareAssignedVariable((VariableExpression) el);
return result;
}
else if( target instanceof VariableExpression ve ) {
return declareAssignedVariable(ve);
}
return false;
}
private boolean declareAssignedVariable(VariableExpression ve) {
var variable = vsc.findVariableDeclaration(ve.getName(), ve);
if( variable != null ) {
if( isDslVariable(variable) )
vsc.addError("Built-in constant or namespace cannot be re-assigned", ve);
ve.setAccessedVariable(variable);
return false;
}
else if( currentDefinition instanceof ProcessNode || currentDefinition instanceof WorkflowNode ) {
if( currentClosure != null )
vsc.addError("Variables in a closure should be declared with `def`", ve);
var scope = currentScope();
currentScope(currentDefinition.getVariableScope());
vsc.declare(ve);
currentScope(scope);
return true;
}
else {
vsc.addError("`" + ve.getName() + "` was assigned but not declared", ve);
return true;
}
}
private boolean isDslVariable(Variable variable) {
var mn = asMethodVariable(variable);
return mn != null && findAnnotation(mn, Constant.class).isPresent();
}
private void visitMutatedVariable(Expression node) {
VariableExpression target = null;
while( true ) {
// e.g. obj.prop = 123
if( node instanceof PropertyExpression pe ) {
node = pe.getObjectExpression();
}
// e.g. list[1] = 123 OR map['a'] = 123
else if( node instanceof BinaryExpression be && be.getOperation().getType() == Types.LEFT_SQUARE_BRACKET ) {
node = be.getLeftExpression();
}
else {
if( node instanceof VariableExpression ve )
target = ve;
break;
}
}
if( target == null )
return;
var variable = vsc.findVariableDeclaration(target.getName(), target);
if( isDslVariable(variable) ) {
if( "params".equals(variable.getName()) )
vsc.addWarning("Params should be declared at the top-level (i.e. outside the workflow)", target.getName(), target);
// TODO: re-enable after workflow.onComplete bug is fixed
// else
// vsc.addError("Built-in constant or namespace cannot be mutated", target);
}
else if( variable != null ) {
checkExternalWriteInAsyncClosure(target, variable);
}
}
private void checkExternalWriteInAsyncClosure(VariableExpression target, Variable variable) {
if( !(currentDefinition instanceof WorkflowNode) )
return;
if( currentClosure == null )
return;
var scope = currentClosure.getVariableScope();
var name = variable.getName();
if( inOperatorCall && scope.isReferencedLocalVariable(name) && scope.getDeclaredVariable(name) == null )
vsc.addWarning("Mutating an external variable in an operator closure can lead to a race condition", target.getName(), target);
}
// expressions
private static final List<String> KEYWORDS = List.of(
"case",
"for",
"switch",
"while"
);
private boolean inOperatorCall;
@Override
public void visitMethodCallExpression(MethodCallExpression node) {
var target = checkSetAssignment(node);
if( target != null ) {
visit(node.getObjectExpression());
declareAssignedVariable(target);
return;
}
if( node.getObjectExpression() instanceof VariableExpression ve )
checkClassNamespaces(ve);
checkMethodCall(node);
var ioc = inOperatorCall;
inOperatorCall = isOperatorCall(node);
super.visitMethodCallExpression(node);
inOperatorCall = ioc;
}
private void checkClassNamespaces(VariableExpression node) {
if( "Channel".equals(node.getName()) )
vsc.addWarning("The use of `Channel` to access channel factories is deprecated -- use `channel` instead", "CHannel", node);
}
private static boolean isOperatorCall(MethodCallExpression node) {
return node.getNodeMetaData(ASTNodeMarker.METHOD_TARGET) instanceof MethodNode mn
&& VariableScopeChecker.isOperator(mn);
}
/**
* Treat `set` and `tap` operators as assignments.
*/
private VariableExpression checkSetAssignment(MethodCallExpression node) {
if( !(currentDefinition instanceof WorkflowNode) )
return null;
var name = node.getMethodAsString();
if( !"set".equals(name) && !"tap".equals(name) )
return null;
var code = asDslBlock(node, 1);
if( code == null || code.getStatements().size() != 1 )
return null;
return asVarX(code.getStatements().get(0));
}
private void checkMethodCall(MethodCallExpression node) {
if( !node.isImplicitThis() )
return;
var name = node.getMethodAsString();
var methods = vsc.findDslFunction(name, node);
if( methods.size() == 1 ) {
var mn = methods.get(0);
if( VariableScopeChecker.isDataflowMethod(mn) )
checkDataflowMethod(node, mn);
node.putNodeMetaData(ASTNodeMarker.METHOD_TARGET, mn);
}
else if( !methods.isEmpty() ) {
node.putNodeMetaData(ASTNodeMarker.METHOD_OVERLOADS, methods);
}
else if( !KEYWORDS.contains(name) ) {
vsc.addError("`" + name + "` is not defined", node.getMethod());
}
}
private void checkDataflowMethod(MethodCallExpression node, MethodNode mn) {
if( !(currentDefinition instanceof WorkflowNode) ) {
var type = dataflowMethodType(mn);
vsc.addError(type + " can only be called from a workflow", node);
return;
}
if( currentClosure != null ) {
var type = dataflowMethodType(mn);
vsc.addError(type + " cannot be called from within a closure", node);
return;
}
}
private static String dataflowMethodType(MethodNode mn) {
if( mn instanceof ProcessNode )
return "Processes";
if( mn instanceof WorkflowNode )
return "Workflows";
return "Operators";
}
@Override
public void visitDeclarationExpression(DeclarationExpression node) {
visit(node.getRightExpression());
if( node.isMultipleAssignmentDeclaration() ) {
for( var el : node.getTupleExpression() )
vsc.declare((VariableExpression) el);
}
else {
vsc.declare(node.getVariableExpression());
}
}
private ClosureExpression currentClosure;
@Override
public void visitClosureExpression(ClosureExpression node) {
var cl = currentClosure;
currentClosure = node;
vsc.pushScope();
node.setVariableScope(currentScope());
if( node.isParameterSpecified() ) {
for( var parameter : node.getParameters() ) {
vsc.declare(parameter, parameter);
if( parameter.hasInitialExpression() )
parameter.getInitialExpression().visit(this);
}
}
else if( node.getParameters() != null ) {
var implicit = new ImplicitClosureParameter();
currentScope().putDeclaredVariable(implicit);
}
super.visitClosureExpression(node);
vsc.popScope();
currentClosure = cl;
}
@Override
public void visitVariableExpression(VariableExpression node) {
var name = node.getName();
Variable variable = vsc.findVariableDeclaration(name, node);
if( variable == null ) {
if( "args".equals(name) ) {
vsc.addParanoidWarning("The use of `args` outside the entry workflow will not be supported in a future version", node);
}
else if( "params".equals(name) ) {
vsc.addParanoidWarning("The use of `params` outside the entry workflow will not be supported in a future version", node);
}
else if( isStdinStdout(name) ) {
// stdin, stdout can be declared without parentheses
}
else {
variable = new DynamicVariable(name, false);
}
}
if( variable instanceof ImplicitClosureParameter ) {
vsc.addWarning("Implicit closure parameter is deprecated, declare an explicit parameter instead", variable.getName(), node);
}
if( variable != null ) {
checkGlobalVariableInProcess(variable, node);
node.setAccessedVariable(variable);
}
}
private boolean isStdinStdout(String name) {
var classScope = currentScope().getClassScope();
if( classScope != null ) {
if( "stdin".equals(name) && classScope.getTypeClass() == ProcessDsl.InputDslV1.class )
return true;
if( "stdout".equals(name) && classScope.getTypeClass() == ProcessDsl.OutputDslV1.class )
return true;
}
return false;
}
private static final List<String> WARN_GLOBALS = List.of(
"baseDir",
"launchDir",
"projectDir",
"workDir"
);
private void checkGlobalVariableInProcess(Variable variable, ASTNode context) {
if( !(currentDefinition instanceof ProcessNode) )
return;
var mn = asMethodVariable(variable);
if( mn != null && mn.getDeclaringClass().getTypeClass() == ScriptDsl.class ) {
if( WARN_GLOBALS.contains(variable.getName()) )
vsc.addWarning("The use of `" + variable.getName() + "` in a process is discouraged -- input files should be provided as process inputs", variable.getName(), context);
}
}
// helpers
private VariableScope currentScope() {
return vsc.getCurrentScope();
}
private void currentScope(VariableScope scope) {
vsc.setCurrentScope(scope);
}
public void addSyntaxError(String message, ASTNode node) {
var cause = new SyntaxException(message, node);
var errorMessage = new SyntaxErrorMessage(cause, sourceUnit);
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for built-in constants in a {@code DslScope}.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Constant {
String value();
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD })
public @interface Description {
String value();
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
/**
* Marker interface for DSL scopes, which define the built-in
* constants and functions for a particular context.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public interface DslScope {
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
import java.util.List;
import nextflow.script.types.ParamsMap;
/**
* DSL scope for the entry workflow.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public interface EntryWorkflowDsl extends WorkflowDsl {
@Constant("args")
@Description("""
List of positional arguments specified on the command line.
""")
List<String> getArgs();
@Constant("params")
@Description("""
Record of pipeline parameters specified in the config file or on the command line.
""")
ParamsMap getParams();
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FeatureFlag {
String value();
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
public class FeatureFlagDsl {
@Deprecated
@FeatureFlag("nextflow.enable.configProcessNamesValidation")
@Description("""
When `true`, prints a warning for every `withName:` process selector that doesn't match a process in the pipeline (default: `true`).
""")
public boolean configProcessNamesValidation;
@Deprecated
@FeatureFlag("nextflow.enable.dsl")
@Description("""
Defines the DSL version (`1` or `2`).
""")
public float dsl;
@FeatureFlag("nextflow.enable.moduleBinaries")
@Description("""
When `true`, enables the use of module-scoped executable scripts via [module resources](https://nextflow.io/docs/latest/module.html#module-resources).
""")
public boolean moduleBinaries;
@Deprecated
@FeatureFlag("nextflow.enable.strict")
@Description("""
When `true`, the pipeline is executed in [strict mode](https://nextflow.io/docs/latest/reference/feature-flags.html).
""")
public boolean strict;
@FeatureFlag("nextflow.enable.types")
@Description("""
When `true`, enables the use of [typed processes](https://nextflow.io/docs/latest/process-typed.html) and [typed workflows](https://nextflow.io/docs/latest/workflow-typed.html).
This feature flag must be enabled in every script that uses typed processes/workflows. Legacy processes/workflows can not be defined in scripts that enable this feature flag.
""")
public boolean types;
@FeatureFlag("nextflow.preview.recursion")
@Description("""
When `true`, enables the use of [process and workflow recursion](https://nextflow.io/docs/latest/workflow.html#process-and-workflow-recursion).
""")
public boolean previewRecursion;
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
/**
* Marker interface for namespaces.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public interface Namespace {
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for denoting that a field or method return value
* can be null (equivalent to `?` suffix in a Nextflow type annotation).
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface Nullable {
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.script.dsl;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for channel operators.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Operator {
}

Some files were not shown because too many files have changed in this diff Show More