add nextflow d30e48d
This commit is contained in:
39
nextflow/modules/nf-lang/build.gradle
Normal file
39
nextflow/modules/nf-lang/build.gradle
Normal 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')
|
||||
}
|
||||
826
nextflow/modules/nf-lang/src/main/antlr/ConfigLexer.g4
Normal file
826
nextflow/modules/nf-lang/src/main/antlr/ConfigLexer.g4
Normal 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); }
|
||||
;
|
||||
580
nextflow/modules/nf-lang/src/main/antlr/ConfigParser.g4
Normal file
580
nextflow/modules/nf-lang/src/main/antlr/ConfigParser.g4
Normal 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)+
|
||||
;
|
||||
855
nextflow/modules/nf-lang/src/main/antlr/ScriptLexer.g4
Normal file
855
nextflow/modules/nf-lang/src/main/antlr/ScriptLexer.g4
Normal 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); }
|
||||
;
|
||||
837
nextflow/modules/nf-lang/src/main/antlr/ScriptParser.g4
Normal file
837
nextflow/modules/nf-lang/src/main/antlr/ScriptParser.g4
Normal 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)+
|
||||
;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user