diff --git a/docs/migrations/25-10.md b/docs/migrations/25-10.md
index dcb07a64be..67b43914ed 100644
--- a/docs/migrations/25-10.md
+++ b/docs/migrations/25-10.md
@@ -8,6 +8,31 @@ This page summarizes the upcoming changes in Nextflow 25.10, which will be relea
This page is a work in progress and will be updated as features are finalized. It should not be considered complete until the 25.10 release.
:::
+## New features
+
+
Workflow params
+
+The `params` block is a new way to declare pipeline parameters in a Nextflow script:
+
+```nextflow
+params {
+ // Path to input data.
+ input: Path
+
+ // Whether to save intermediate files.
+ save_intermeds: Boolean = false
+}
+
+workflow {
+ println "params.input = ${params.input}"
+ println "params.save_intermeds = ${params.save_intermeds}"
+}
+```
+
+This syntax allows you to declare all parameters in one place with explicit type annotations, and it allows Nextflow to validate parameters at runtime.
+
+See {ref}`workflow-params-def` for details.
+
## Enhancements
New syntax for workflow handlers
@@ -74,4 +99,6 @@ This feature addresses previous inconsistencies in timestamp representations.
## Deprecations
+- The legacy type detection of CLI parameters is disabled when using the strict syntax (`NXF_SYNTAX_PARSER=v2`). {ref}`Legacy parameters ` in the strict syntax should not rely on legacy type detection. Alternatively, use the new `params` block to convert CLI parameters based on their type annotations. Legacy type detection can be disabled globally by setting the environment variable `NXF_DISABLE_PARAMS_TYPE_DETECTION=true`.
+
- The use of workflow handlers in the configuration file has been deprecated. You should define workflow handlers in the pipeline script or a plugin instead. See {ref}`config-workflow-handlers` for details.
diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md
index 696ef6b42a..5d794b1d62 100644
--- a/docs/reference/syntax.md
+++ b/docs/reference/syntax.md
@@ -28,7 +28,8 @@ A Nextflow script may contain the following top-level declarations:
- Shebang
- Feature flags
- Include declarations
-- Parameter declarations
+- Params block
+- Parameter declarations (legacy)
- Workflow definitions
- Process definitions
- Function definitions
@@ -107,9 +108,22 @@ The following definitions can be included:
- Processes
- Named workflows
-### Parameter
+### Params block
-A parameter declaration is an assignment. The target should be a pipeline parameter and the source should be an expression:
+The params block consists of one or more *parameter declarations*. A parameter declaration consists of a name and an optional default value:
+
+```nextflow
+params {
+ input: Path
+ save_intermeds: Boolean = false
+}
+```
+
+Only one params block may be defined in a script.
+
+### Parameter (legacy)
+
+A legacy parameter declaration is an assignment. The target should be a pipeline parameter and the source should be an expression:
```nextflow
params.message = 'Hello world!'
diff --git a/docs/vscode.md b/docs/vscode.md
index 76f1367594..ac3a716edf 100644
--- a/docs/vscode.md
+++ b/docs/vscode.md
@@ -26,7 +26,7 @@ The language server parses scripts and config files according to the {ref}`Nextf
When you hover over certain source code elements, such as variable names and function calls, the extension provides a tooltip with related information, such as the definition and/or documentation for the element.
-If a [Javadoc](https://en.wikipedia.org/wiki/Javadoc) comment is defined above a workflow, process, or function, the extension will include the contents of the comment in hover hints. The following is an example Javadoc comment:
+If a [Javadoc](https://en.wikipedia.org/wiki/Javadoc) comment is defined above a workflow, process, function, or parameter in a `params` block, the extension will include the contents of the comment in hover hints. The following is an example Javadoc comment:
```nextflow
/**
diff --git a/docs/workflow.md b/docs/workflow.md
index d44f92289a..6cfc61b11c 100644
--- a/docs/workflow.md
+++ b/docs/workflow.md
@@ -22,7 +22,60 @@ workflow {
}
```
-### Parameters
+(workflow-params-def)=
+
+## Parameters
+
+Parameters can be declared in a Nextflow script with the `params` block or with *legacy* parameter declarations.
+
+### Params block
+
+:::{versionadded} 25.10.0
+:::
+
+:::{note}
+This feature requires the {ref}`strict syntax ` to be enabled (`NXF_SYNTAX_PARSER=v2`).
+:::
+
+A script can declare parameters using the `params` block:
+
+```nextflow
+params {
+ // Path to input data.
+ input: Path
+
+ // Whether to save intermediate files.
+ save_intermeds: Boolean = false
+}
+```
+
+The following types can be used for parameters:
+
+- {ref}`stdlib-types-boolean`
+- {ref}`stdlib-types-float`
+- {ref}`stdlib-types-integer`
+- {ref}`stdlib-types-path`
+- {ref}`stdlib-types-string`
+
+Parameters can be used in the entry workflow:
+
+```nextflow
+workflow {
+ analyze(params.input, params.save_intermeds)
+}
+```
+
+:::{note}
+As a best practice, parameters should only be used directly in the entry workflow and passed to workflows and processes as explicit inputs.
+:::
+
+The default value can be overridden by the command line, params file, or config file. Parameters from multiple sources are resolved in the order described in {ref}`cli-params`. Parameters specified on the command line are converted to the appropriate type based on the corresponding type annotation.
+
+A parameter that doesn't specify a default value is a *required* param. If a required param is not given a value at runtime, the run will fail.
+
+(workflow-params-legacy)=
+
+### Legacy parameters
Parameters can be declared by assigning a `params` property to a default value:
@@ -38,10 +91,6 @@ workflow {
}
```
-:::{note}
-As a best practice, `params` should be used only in the entry workflow and passed to workflows and processes as explicit inputs.
-:::
-
The default value can be overridden by the command line, params file, or config file. Parameters from multiple sources are resolved in the order described in {ref}`cli-params`.
## Named workflows
diff --git a/modules/nextflow/src/main/groovy/nextflow/Session.groovy b/modules/nextflow/src/main/groovy/nextflow/Session.groovy
index 0e8ae74108..2a07576f09 100644
--- a/modules/nextflow/src/main/groovy/nextflow/Session.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/Session.groovy
@@ -120,6 +120,16 @@ class Session implements ISession {
*/
ScriptBinding binding
+ /**
+ * Params that were specified on the command line.
+ */
+ Map cliParams
+
+ /**
+ * Params that were specified in the configuration.
+ */
+ Map configParams
+
/**
* Holds the configuration object
*/
@@ -425,7 +435,7 @@ class Session implements ISession {
/**
* Initialize the session workDir, libDir, baseDir and scriptName variables
*/
- Session init( ScriptFile scriptFile, List args=null ) {
+ Session init( ScriptFile scriptFile, List args=null, Map cliParams=null, Map configParams=null ) {
if(!workDir.mkdirs()) throw new AbortOperationException("Cannot create work-dir: $workDir -- Make sure you have write permissions or specify a different directory by using the `-w` command line option")
log.debug "Work-dir: ${workDir.toUriString()} [${FileHelper.getPathFsType(workDir)}]"
@@ -452,6 +462,8 @@ class Session implements ISession {
this.workflowMetadata = new WorkflowMetadata(this, scriptFile)
// configure script params
+ this.cliParams = cliParams
+ this.configParams = configParams
binding.setParams( (Map)config.params )
binding.setArgs( new ScriptRunner.ArgsList(args) )
diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy
index d096e678e6..838bd173fe 100644
--- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy
@@ -325,12 +325,18 @@ class CmdRun extends CmdBase implements HubOptions {
// -- specify the arguments
final scriptFile = getScriptFile(pipeline)
+ // -- load command line params
+ final baseDir = scriptFile.parent
+ final cliParams = parsedParams(ConfigBuilder.getConfigVars(baseDir))
+
// create the config object
final builder = new ConfigBuilder()
.setOptions(launcher.options)
.setCmdRun(this)
- .setBaseDir(scriptFile.parent)
- final config = builder .build()
+ .setBaseDir(baseDir)
+ .setCliParams(cliParams)
+ final config = builder.build()
+ final configParams = builder.getConfigParams()
// check DSL syntax in the config
launchInfo(config, scriptFile)
@@ -376,7 +382,7 @@ class CmdRun extends CmdBase implements HubOptions {
}
// -- run it!
- runner.execute(scriptArgs, this.entryName)
+ runner.execute(scriptArgs, cliParams, configParams, this.entryName)
}
protected void printBanner() {
@@ -698,7 +704,7 @@ class CmdRun extends CmdBase implements HubOptions {
}
static protected parseParamValue(String str) {
- if ( SysEnv.get('NXF_DISABLE_PARAMS_TYPE_DETECTION') )
+ if ( SysEnv.get('NXF_DISABLE_PARAMS_TYPE_DETECTION') || NF.isSyntaxParserV2() )
return str
if ( str == null ) return null
diff --git a/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy
index c37e40591c..a3bfc4f27d 100644
--- a/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy
@@ -58,6 +58,8 @@ class ConfigBuilder {
Path currentDir
+ Map cliParams
+
boolean showAllProfiles
String profile = DEFAULT_PROFILE
@@ -78,9 +80,11 @@ class ConfigBuilder {
Map env = new HashMap<>(SysEnv.get())
- List warnings = new ArrayList<>(10);
+ List warnings = new ArrayList<>(10)
+
+ Map declaredParams = [:]
- {
+ ConfigBuilder() {
setHomeDir(Const.APP_HOME_DIR)
setCurrentDir(Paths.get('.'))
}
@@ -111,6 +115,11 @@ class ConfigBuilder {
return this
}
+ ConfigBuilder setCliParams( Map cliParams ) {
+ this.cliParams = cliParams
+ return this
+ }
+
ConfigBuilder setBaseDir( Path path ) {
this.baseDir = path.complete()
return this
@@ -159,6 +168,10 @@ class ConfigBuilder {
return this
}
+ Map getConfigParams() {
+ return declaredParams
+ }
+
static private wrapValue( value ) {
if( !value )
return ''
@@ -324,11 +337,11 @@ class ConfigBuilder {
// this is needed to make sure to reuse the same
// instance of the config vars across different instances of the ConfigBuilder
// and prevent multiple parsing of the same params file (which can even be remote resource)
- return cacheableConfigVars(baseDir)
+ return getConfigVars(baseDir)
}
@Memoized
- static private Map cacheableConfigVars(Path base) {
+ static Map getConfigVars(Path base) {
final binding = new HashMap(10)
binding.put('baseDir', base)
binding.put('projectDir', base)
@@ -348,8 +361,8 @@ class ConfigBuilder {
.setIgnoreIncludes(ignoreIncludes)
ConfigObject result = new ConfigObject()
- if( cmdRun && (cmdRun.hasParams()) )
- parser.setParams(cmdRun.parsedParams(configVars()))
+ if( cliParams )
+ parser.setParams(cliParams)
// add the user specified environment to the session env
env.sort().each { name, value -> result.env.put(name,value) }
@@ -380,7 +393,7 @@ class ConfigBuilder {
}
if( validateProfile ) {
- checkValidProfile(parser.getProfiles())
+ checkValidProfile(parser.getDeclaredProfiles())
}
}
@@ -414,6 +427,7 @@ class ConfigBuilder {
final config = parse0(parser, entry)
if( NF.getSyntaxParserVersion() == 'v1' )
validate(config, entry)
+ declaredParams.putAll(parser.getDeclaredParams())
result.merge(config)
}
@@ -723,8 +737,8 @@ class ConfigBuilder {
}
// -- add the command line parameters to the 'taskConfig' object
- if( cmdRun.hasParams() )
- config.params = mergeMaps( (Map)config.params, cmdRun.parsedParams(configVars()), NF.strictMode )
+ if( cliParams )
+ config.params = mergeMaps( (Map)config.params, cliParams, NF.strictMode )
if( cmdRun.withoutDocker && config.docker instanceof Map ) {
// disable docker execution
diff --git a/modules/nextflow/src/main/groovy/nextflow/config/ConfigParser.groovy b/modules/nextflow/src/main/groovy/nextflow/config/ConfigParser.groovy
index 9a51a02f81..78d86e01f8 100644
--- a/modules/nextflow/src/main/groovy/nextflow/config/ConfigParser.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/config/ConfigParser.groovy
@@ -67,6 +67,11 @@ interface ConfigParser {
*/
ConfigParser setParams(Map vars)
+ /**
+ * Set the profiles that should be applied.
+ */
+ ConfigParser setProfiles(List profiles)
+
/**
* Parse a config object from the given source.
*/
@@ -75,13 +80,13 @@ interface ConfigParser {
ConfigObject parse(Path path)
/**
- * Set the profiles that should be applied.
+ * Get the set of declared profiles.
*/
- ConfigParser setProfiles(List profiles)
+ Set getDeclaredProfiles()
/**
- * Get the set of available profiles.
+ * Get the map of declared params.
*/
- Set getProfiles()
+ Map getDeclaredParams()
}
diff --git a/modules/nextflow/src/main/groovy/nextflow/config/parser/v1/ConfigParserV1.groovy b/modules/nextflow/src/main/groovy/nextflow/config/parser/v1/ConfigParserV1.groovy
index a259185a96..22ba8159e9 100644
--- a/modules/nextflow/src/main/groovy/nextflow/config/parser/v1/ConfigParserV1.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/config/parser/v1/ConfigParserV1.groovy
@@ -123,10 +123,15 @@ class ConfigParserV1 implements ConfigParser {
}
@Override
- Set getProfiles() {
+ Set getDeclaredProfiles() {
Collections.unmodifiableSet(conditionalNames)
}
+ @Override
+ Map getDeclaredParams() {
+ [:]
+ }
+
private Grengine getGrengine() {
if( grengine ) {
return grengine
diff --git a/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigDsl.groovy b/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigDsl.groovy
index cf7e0ab835..a450ea7604 100644
--- a/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigDsl.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigDsl.groovy
@@ -48,7 +48,9 @@ class ConfigDsl extends Script {
private Map target = [:]
- private Set parsedProfiles = []
+ private Set declaredProfiles = []
+
+ private Map declaredParams = [:]
void setIgnoreIncludes(boolean value) {
this.ignoreIncludes = value
@@ -74,12 +76,20 @@ class ConfigDsl extends Script {
this.profiles = profiles
}
- void addParsedProfile(String profile) {
- parsedProfiles.add(profile)
+ void declareProfile(String profile) {
+ declaredProfiles.add(profile)
+ }
+
+ Set getDeclaredProfiles() {
+ return declaredProfiles
+ }
+
+ void declareParam(String name, Object value) {
+ declaredParams.put(name, value)
}
- Set getParsedProfiles() {
- return parsedProfiles
+ Map getDeclaredParams() {
+ return declaredParams
}
Map getTarget() {
@@ -104,8 +114,10 @@ class ConfigDsl extends Script {
}
}
- void assign(List names, Object right) {
- navigate(names.init()).put(names.last(), right)
+ void assign(List names, Object value) {
+ if( names.size() == 2 && names.first() == 'params' )
+ declareParam(names.last(), value)
+ navigate(names.init()).put(names.last(), value)
}
private Map navigate(List names) {
@@ -177,7 +189,8 @@ class ConfigDsl extends Script {
.setParams(target.params as Map)
.setProfiles(profiles)
final config = parser.parse(configText, includePath)
- parsedProfiles.addAll(parser.getProfiles())
+ declaredProfiles.addAll(parser.getDeclaredProfiles())
+ declaredParams.putAll(parser.getDeclaredParams())
final ctx = navigate(names)
ctx.putAll(Bolts.deepMerge(ctx, config))
@@ -280,7 +293,7 @@ class ConfigDsl extends Script {
@Override
void block(String name, Closure closure) {
blocks[name] = closure
- dsl.addParsedProfile(name)
+ dsl.declareProfile(name)
}
@Override
diff --git a/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigParserV2.groovy b/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigParserV2.groovy
index d6a7bc3e7d..7ff23c4b3a 100644
--- a/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigParserV2.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigParserV2.groovy
@@ -48,7 +48,9 @@ class ConfigParserV2 implements ConfigParser {
private List appliedProfiles
- private Set parsedProfiles
+ private Set declaredProfiles
+
+ private Map declaredParams
private GroovyShell groovyShell
@@ -58,11 +60,6 @@ class ConfigParserV2 implements ConfigParser {
return this
}
- @Override
- Set getProfiles() {
- return parsedProfiles
- }
-
@Override
ConfigParserV2 setIgnoreIncludes(boolean value) {
this.ignoreIncludes = value
@@ -101,6 +98,16 @@ class ConfigParserV2 implements ConfigParser {
return this
}
+ @Override
+ Set getDeclaredProfiles() {
+ return declaredProfiles
+ }
+
+ @Override
+ Map getDeclaredParams() {
+ return declaredParams
+ }
+
/**
* Parse the given script as a string and return the configuration object
*
@@ -126,7 +133,8 @@ class ConfigParserV2 implements ConfigParser {
script.run()
final target = script.getTarget()
- parsedProfiles = script.getParsedProfiles()
+ declaredProfiles = script.getDeclaredProfiles()
+ declaredParams = script.getDeclaredParams()
return Bolts.toConfigObject(target)
}
catch( CompilationFailedException e ) {
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy b/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy
index 67075cb1a3..f916cfd8bf 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy
@@ -98,6 +98,21 @@ abstract class BaseScript extends Script implements ExecutionContext {
binding.setVariable( 'secrets', SecretsLoader.secretContext() )
}
+ protected void params(Closure body) {
+ if( entryFlow )
+ throw new IllegalStateException("Workflow params definition must be defined before the entry workflow")
+ if( ExecutionStack.withinWorkflow() )
+ throw new IllegalStateException("Workflow params definition is not allowed within a workflow")
+
+ final dsl = new ParamsDsl()
+ final cl = (Closure)body.clone()
+ cl.setDelegate(dsl)
+ cl.setResolveStrategy(Closure.DELEGATE_FIRST)
+ cl.call()
+
+ dsl.apply(session)
+ }
+
protected process( String name, Closure body ) {
final process = new ProcessDef(this,body,name)
meta.addDefinition(process)
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ParamsDsl.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ParamsDsl.groovy
new file mode 100644
index 0000000000..d2c665938c
--- /dev/null
+++ b/modules/nextflow/src/main/groovy/nextflow/script/ParamsDsl.groovy
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2013-2024, 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
+
+import java.nio.file.Path
+
+import groovy.transform.Canonical
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+import nextflow.Session
+import nextflow.file.FileHelper
+import nextflow.exception.ScriptRuntimeException
+import nextflow.script.types.Types
+/**
+ * Implements the DSL for defining workflow params
+ *
+ * @author Ben Sherman
+ */
+@Slf4j
+@CompileStatic
+class ParamsDsl {
+
+ private Map declarations = [:]
+
+ void declare(String name, Class type) {
+ declarations[name] = new Param(name, type, Optional.empty())
+ }
+
+ void declare(String name, Class type, Object defaultValue) {
+ declarations[name] = new Param(name, type, Optional.of(defaultValue))
+ }
+
+ void apply(Session session) {
+ final cliParams = session.cliParams ?: [:]
+ final configParams = session.configParams ?: [:]
+
+ for( final name : cliParams.keySet() ) {
+ if( !declarations.containsKey(name) && !configParams.containsKey(name) )
+ throw new ScriptRuntimeException("Parameter `$name` was specified on the command line or params file but is not declared in the script or config")
+ }
+
+ final params = new HashMap()
+ for( final name : declarations.keySet() ) {
+ final decl = declarations[name]
+ if( cliParams.containsKey(name) )
+ params[name] = resolveFromCli(decl, cliParams[name])
+ else if( configParams.containsKey(name) )
+ params[name] = resolveFromCode(decl, configParams[name])
+ else if( decl.defaultValue.isPresent() )
+ params[name] = resolveFromCode(decl, decl.defaultValue.get())
+ else
+ throw new ScriptRuntimeException("Parameter `$name` is required but was not specified on the command line, params file, or config")
+
+ final actualType = params[name].getClass()
+ if( !decl.type.isAssignableFrom(actualType) )
+ throw new ScriptRuntimeException("Parameter `$name` with type ${Types.getName(decl.type)} cannot be assigned to ${params[name]} [${Types.getName(actualType)}]")
+ }
+
+ session.binding.setParams(params, true)
+ }
+
+ private Object resolveFromCli(Param decl, Object value) {
+ if( value == null )
+ return null
+
+ if( value !instanceof CharSequence )
+ return value
+
+ final str = value.toString()
+
+ if( decl.type == Boolean ) {
+ if( str.toLowerCase() == 'true' ) return Boolean.TRUE
+ if( str.toLowerCase() == 'false' ) return Boolean.FALSE
+ }
+
+ if( decl.type == Integer || decl.type == Float ) {
+ if( str.isInteger() ) return str.toInteger()
+ if( str.isLong() ) return str.toLong()
+ }
+
+ if( decl.type == Float ) {
+ if( str.isFloat() ) return str.toFloat()
+ if( str.isDouble() ) return str.toDouble()
+ }
+
+ if( decl.type == Path ) {
+ return FileHelper.asPath(str)
+ }
+
+ return value
+ }
+
+ private Object resolveFromCode(Param decl, Object value) {
+ if( value == null )
+ return null
+
+ if( decl.type == Path && value instanceof CharSequence )
+ return FileHelper.asPath(value.toString())
+
+ return value
+ }
+
+ @Canonical
+ private static class Param {
+ String name
+ Class type
+ Optional> defaultValue
+ }
+
+}
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy
index 9ef4999905..0ea8b267c9 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy
@@ -75,7 +75,7 @@ class ScriptBinding extends WorkflowBinding {
}
vars.put('args', args)
- // create and populate args
+ // create and populate params
params = new ParamsMap()
if( vars.params ) {
if( !(vars.params instanceof Map) ) throw new IllegalArgumentException("ScriptBinding 'params' must be a Map value")
@@ -132,10 +132,17 @@ class ScriptBinding extends WorkflowBinding {
* The map of the CLI named parameters
*
* @param values
+ * @param override
*/
- ScriptBinding setParams(Map values ) {
- if( values )
+ ScriptBinding setParams(Map values, boolean override=false) {
+ if( values ) {
+ if( override ) {
+ for( final key : values.keySet() )
+ params.remove(key)
+ }
params.putAll(values)
+ super.setVariable0('params', params)
+ }
return this
}
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ScriptRunner.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ScriptRunner.groovy
index 54bd1e1dac..4449ea27c5 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/ScriptRunner.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/ScriptRunner.groovy
@@ -113,21 +113,21 @@ class ScriptRunner {
/**
- * Execute a Nextflow script, it does the following:
- * parse the script
- * launch script execution
- * await for all tasks completion
+ * Execute a Nextflow script:
+ * 1. compile and load the script
+ * 2. execute the script
+ * 3. await for all tasks to complete
*
- * @param scriptFile The file containing the script to be executed
- * @param args The arguments to be passed to the script
- * @return The result as returned by the {@code #run} method
+ * @param args command-line positional arguments
+ * @param cliParams parameters specified on the command-line
+ * @param configParams parameters specified in the config
+ * @param entryName named workflow entrypoint
*/
-
- def execute( List args = null, String entryName=null ) {
+ def execute( List args=null, Map cliParams=null, Map configParams=null, String entryName=null ) {
assert scriptFile
// init session
- session.init(scriptFile, args)
+ session.init(scriptFile, args, cliParams, configParams)
// start session
session.start()
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/WorkflowBinding.groovy b/modules/nextflow/src/main/groovy/nextflow/script/WorkflowBinding.groovy
index 8b03bee573..bddaa0ed35 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/WorkflowBinding.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/WorkflowBinding.groovy
@@ -135,6 +135,10 @@ class WorkflowBinding extends Binding {
@Override
void setVariable(String name, Object value) {
lookupTable.put(value, name)
+ setVariable0(name, value)
+ }
+
+ protected void setVariable0(String name, Object value) {
super.setVariable(name, value)
}
diff --git a/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy
index fff17bbe92..65b616188b 100644
--- a/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy
@@ -17,7 +17,7 @@
package nextflow.config
import java.nio.file.Files
-import java.nio.file.Paths
+import java.nio.file.Path
import nextflow.SysEnv
import nextflow.cli.CliOptions
@@ -47,6 +47,24 @@ class ConfigBuilderTest extends Specification {
SysEnv.pop()
}
+ ConfigObject configWithParams(Path file, Map runOpts, Path baseDir=null) {
+ def run = new CmdRun(runOpts)
+ return new ConfigBuilder()
+ .setOptions(new CliOptions())
+ .setCmdRun(run)
+ .setCliParams(run.parsedParams(ConfigBuilder.getConfigVars(baseDir)))
+ .buildGivenFiles(file)
+ }
+
+ ConfigObject configWithParams(Map config, Map runOpts, Map cliOpts=[:]) {
+ def run = new CmdRun(runOpts)
+ return new ConfigBuilder(config)
+ .setOptions(new CliOptions(cliOpts))
+ .setCmdRun(run)
+ .setCliParams(run.parsedParams(ConfigBuilder.getConfigVars(null)))
+ .build()
+ }
+
def 'build config object' () {
setup:
@@ -143,7 +161,7 @@ class ConfigBuilderTest extends Specification {
setup:
def builder = [:] as ConfigBuilder
- builder.baseDir = Paths.get('/base/path')
+ builder.baseDir = Path.of('/base/path')
def text = '''
params.p = "$baseDir/1"
@@ -161,8 +179,8 @@ class ConfigBuilderTest extends Specification {
cfg.params.p == '/base/path/1'
cfg.params.q == '/base/path/2'
cfg.params.x == '/base/path/3'
- cfg.params.y == "${Paths.get('.').toRealPath()}/4"
- cfg.params.z == "${Paths.get('results').complete()}/5"
+ cfg.params.y == "${Path.of('.').toRealPath()}/4"
+ cfg.params.z == "${Path.of('results').complete()}/5"
}
@@ -185,9 +203,7 @@ class ConfigBuilderTest extends Specification {
}
'''
when:
- def opt = new CliOptions()
- def run = new CmdRun(params: [alpha: 'Hello', beta: 'World', omega: 'Last'])
- def result = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file)
+ def result = configWithParams(file, [params: [alpha: 'Hello', beta: 'World', omega: 'Last']])
then:
result.params.alpha == 'Hello' // <-- params defined as CLI options override the ones in the config file
@@ -218,9 +234,7 @@ class ConfigBuilderTest extends Specification {
}
'''
when:
- def opt = new CliOptions()
- def run = new CmdRun(params: [alpha: 'Hello', beta: 'World', omega: 'Last'])
- def result = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file)
+ def result = configWithParams(file, [params: [alpha: 'Hello', beta: 'World', omega: 'Last']])
then:
result.params.alpha == 'Hello' // <-- params defined as CLI options override the ones in the config file
@@ -270,9 +284,7 @@ class ConfigBuilderTest extends Specification {
'''
when:
- def opt = new CliOptions()
- def run = new CmdRun(params: [one: '1', two: 'dos', three: 'tres'])
- def config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(configMain.toPath())
+ def config = configWithParams(configMain.toPath(), [params: [one: '1', two: 'dos', three: 'tres']])
then:
config.params.one == 1
@@ -316,9 +328,7 @@ class ConfigBuilderTest extends Specification {
'''
when:
- def opt = new CliOptions()
- def run = new CmdRun(params: [igenomes_base: 'test'])
- def config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(configMain.toPath())
+ def config = configWithParams(configMain.toPath(), [params: [igenomes_base: 'test']])
then:
config.params.genomes.GRCh37 == [fasta:'test/genome.fa', bwa:'test/BWAIndex/genome.fa']
@@ -333,7 +343,6 @@ class ConfigBuilderTest extends Specification {
def folder = File.createTempDir()
def configMain = new File(folder,'my.config').absoluteFile
-
configMain.text = """
process.name = 'alpha'
params.one = 'a'
@@ -401,44 +410,34 @@ class ConfigBuilderTest extends Specification {
publishDir = [path: params.alpha]
}
}
-
}
-
'''
-
when:
- def opt = new CliOptions()
- def run = new CmdRun(params: [alpha: 'AAA', beta: 'BBB'])
- def config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file)
+ def config = configWithParams(file, [params: [alpha: 'AAA', beta: 'BBB']])
then:
config.params.alpha == 'AAA'
config.params.beta == 'BBB'
config.params.delta == 'Foo'
config.params.gamma == 'AAA'
- config.params.genomes.GRCh37.bed12 == '/data/genes.bed'
- config.params.genomes.GRCh37.bismark == '/data/BismarkIndex'
- config.params.genomes.GRCh37.bowtie == '/data/genome'
+ config.params.genomes.'GRCh37'.bed12 == '/data/genes.bed'
+ config.params.genomes.'GRCh37'.bismark == '/data/BismarkIndex'
+ config.params.genomes.'GRCh37'.bowtie == '/data/genome'
when:
- opt = new CliOptions()
- run = new CmdRun(params: [alpha: 'AAA', beta: 'BBB'], profile: 'first')
- config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file)
+ config = configWithParams(file, [params: [alpha: 'AAA', beta: 'BBB'], profile: 'first'])
then:
config.params.alpha == 'AAA'
config.params.beta == 'BBB'
config.params.delta == 'Foo'
config.params.gamma == 'First'
config.process.name == 'Bar'
- config.params.genomes.GRCh37.bed12 == '/data/genes.bed'
- config.params.genomes.GRCh37.bismark == '/data/BismarkIndex'
- config.params.genomes.GRCh37.bowtie == '/data/genome'
-
+ config.params.genomes.'GRCh37'.bed12 == '/data/genes.bed'
+ config.params.genomes.'GRCh37'.bismark == '/data/BismarkIndex'
+ config.params.genomes.'GRCh37'.bowtie == '/data/genome'
when:
- opt = new CliOptions()
- run = new CmdRun(params: [alpha: 'AAA', beta: 'BBB', genomes: 'xxx'], profile: 'second')
- config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file)
+ config = configWithParams(file, [params: [alpha: 'AAA', beta: 'BBB', genomes: 'xxx'], profile: 'second'])
then:
config.params.alpha == 'AAA'
config.params.beta == 'BBB'
@@ -453,10 +452,10 @@ class ConfigBuilderTest extends Specification {
def 'params-file should override params in the config file' () {
setup:
- def baseDir = Paths.get('/my/base/dir')
+ def baseDir = Path.of('/my/base/dir')
and:
- def params = Files.createTempFile('test', '.yml')
- params.text = '''
+ def paramsFile = Files.createTempFile('test', '.yml')
+ paramsFile.text = '''
alpha: "Hello"
beta: "World"
omega: "Last"
@@ -480,9 +479,7 @@ class ConfigBuilderTest extends Specification {
}
'''
when:
- def opt = new CliOptions()
- def run = new CmdRun(paramsFile: params)
- def result = new ConfigBuilder().setOptions(opt).setCmdRun(run).setBaseDir(baseDir).buildGivenFiles(file)
+ def result = configWithParams(file, [paramsFile: paramsFile], baseDir)
then:
result.params.alpha == 'Hello' // <-- params defined in the params-file overrides the ones in the config file
@@ -495,7 +492,7 @@ class ConfigBuilderTest extends Specification {
cleanup:
file?.delete()
- params?.delete()
+ paramsFile?.delete()
}
def 'params should override params-file and override params in the config file' () {
@@ -522,9 +519,7 @@ class ConfigBuilderTest extends Specification {
}
'''
when:
- def opt = new CliOptions()
- def run = new CmdRun(paramsFile: params, params: [alpha: 'Hola', beta: 'Mundo'])
- def result = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file)
+ def result = configWithParams(file, [paramsFile: params, params: [alpha: 'Hola', beta: 'Mundo']])
then:
result.params.alpha == 'Hola' // <-- this comes from the CLI
@@ -1629,56 +1624,56 @@ class ConfigBuilderTest extends Specification {
def config
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun()).build()
+ config = configWithParams([:], [:], [config: EMPTY])
then:
config.params == [:]
// get params for the CLI
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun(params: [foo:'one', bar:'two'])).build()
+ config = configWithParams([:], [params: [foo:'one', bar:'two']], [config: EMPTY])
then:
config.params == [foo:'one', bar:'two']
// get params from config file
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: [configFile])).setCmdRun(new CmdRun()).build()
+ config = configWithParams([:], [:], [config: [configFile]])
then:
config.params == [foo:1, bar:2, data: '/some/path']
// get params form JSON file
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun(paramsFile: jsonFile)).build()
+ config = configWithParams([:], [paramsFile: jsonFile], [config: EMPTY])
then:
config.params == [foo:10, bar:20]
// get params from YAML file
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun(paramsFile: yamlFile)).build()
+ config = configWithParams([:], [paramsFile: yamlFile], [config: EMPTY])
then:
config.params == [foo:100, bar:200]
// cli override config
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: [configFile])).setCmdRun(new CmdRun(params:[foo:'hello', baz:'world'])).build()
+ config = configWithParams([:], [params: [foo:'hello', baz:'world']], [config: [configFile]])
then:
config.params == [foo:'hello', bar:2, baz: 'world', data: '/some/path']
// CLI override JSON
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun(params:[foo:'hello', baz:'world'], paramsFile: jsonFile)).build()
+ config = configWithParams([:], [params: [foo:'hello', baz:'world'], paramsFile: jsonFile], [config: EMPTY])
then:
config.params == [foo:'hello', bar:20, baz: 'world']
// JSON override config
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: [configFile])).setCmdRun(new CmdRun(paramsFile: jsonFile)).build()
+ config = configWithParams([:], [paramsFile: jsonFile], [config: [configFile]])
then:
config.params == [foo:10, bar:20, data: '/some/path']
// CLI override JSON that override config
when:
- config = new ConfigBuilder().setOptions(new CliOptions(config: [configFile])).setCmdRun(new CmdRun(paramsFile: jsonFile, params: [foo:'Ciao'])).build()
+ config = configWithParams([:], [paramsFile: jsonFile, params: [foo:'Ciao']], [config: [configFile]])
then:
config.params == [foo:'Ciao', bar:20, data: '/some/path']
}
@@ -2257,9 +2252,9 @@ class ConfigBuilderTest extends Specification {
"""
when:
- def opt = new CliOptions()
- def run = new CmdRun(params: [bar: "world", 'baz.y': "mondo", 'baz.z.beta': "Welt"])
- def config = new ConfigBuilder(env: [NXF_CONFIG_FILE: configMain.toString()]).setOptions(opt).setCmdRun(run).build()
+ def config = configWithParams(
+ [env: [NXF_CONFIG_FILE: configMain.toString()]],
+ [params: [bar: "world", 'baz.y': "mondo", 'baz.z.beta': "Welt"]] )
then:
config.params.foo == 'Hello'
@@ -2342,10 +2337,7 @@ class ConfigBuilderTest extends Specification {
when:
- def cfg2 = new ConfigBuilder()
- .setOptions( new CliOptions(userConfig: [config.toString()]))
- .setCmdRun( new CmdRun(params: ['test.foo': 'CLI_FOO'] ))
- .build()
+ def cfg2 = configWithParams([:], [params: ['test.foo': 'CLI_FOO']], [userConfig: [config.toString()]])
then:
cfg2.params.test.foo == "CLI_FOO"
cfg2.params.test.bar == "bar_def"
@@ -2375,7 +2367,7 @@ class ConfigBuilderTest extends Specification {
'''.stripIndent()
when:
- def cfg1 = new ConfigBuilder().setCmdRun(new CmdRun(paramsFile: config.toString())).build()
+ def cfg1 = configWithParams([:], [paramsFile: config.toString()])
then:
cfg1.params.title == "something"
@@ -2403,7 +2395,7 @@ class ConfigBuilderTest extends Specification {
'''.stripIndent()
when:
- def cfg1 = new ConfigBuilder().setCmdRun(new CmdRun(paramsFile: config.toString())).build()
+ def cfg1 = configWithParams([:], [paramsFile: config.toString()])
then:
cfg1.params.title == "something"
diff --git a/modules/nextflow/src/test/groovy/nextflow/config/parser/v1/ConfigParserV1Test.groovy b/modules/nextflow/src/test/groovy/nextflow/config/parser/v1/ConfigParserV1Test.groovy
index 85d6f38a76..36f6cc51f9 100644
--- a/modules/nextflow/src/test/groovy/nextflow/config/parser/v1/ConfigParserV1Test.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/config/parser/v1/ConfigParserV1Test.groovy
@@ -399,7 +399,7 @@ class ConfigParserV1Test extends Specification {
}
- def 'should return the set of visited block names' () {
+ def 'should return the set of declared profiles' () {
given:
def text = '''
@@ -417,13 +417,13 @@ class ConfigParserV1Test extends Specification {
def slurper = new ConfigParserV1().setProfiles(['alpha'])
slurper.parse(text)
then:
- slurper.getProfiles() == ['alpha','beta'] as Set
+ slurper.getDeclaredProfiles() == ['alpha','beta'] as Set
when:
slurper = new ConfigParserV1().setProfiles(['omega'])
slurper.parse(text)
then:
- slurper.getProfiles() == ['alpha','beta'] as Set
+ slurper.getDeclaredProfiles() == ['alpha','beta'] as Set
}
def 'should disable includeConfig parsing' () {
diff --git a/modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy b/modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy
index 0943603030..98913ebf73 100644
--- a/modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy
@@ -348,7 +348,7 @@ class ConfigParserV2Test extends Specification {
}
- def 'should return the set of parsed profiles' () {
+ def 'should return the set of declared profiles' () {
given:
def text = '''
@@ -366,13 +366,42 @@ class ConfigParserV2Test extends Specification {
def slurper = new ConfigParserV2().setProfiles(['alpha'])
slurper.parse(text)
then:
- slurper.getProfiles() == ['alpha','beta'] as Set
+ slurper.getDeclaredProfiles() == ['alpha','beta'] as Set
when:
slurper = new ConfigParserV2().setProfiles(['omega'])
slurper.parse(text)
then:
- slurper.getProfiles() == ['alpha','beta'] as Set
+ slurper.getDeclaredProfiles() == ['alpha','beta'] as Set
+ }
+
+ def 'should return the map of declared params' () {
+
+ given:
+ def text = '''
+ params {
+ a = 1
+ b = 2
+ }
+
+ profiles {
+ alpha {
+ params.a = 3
+ }
+ }
+ '''
+
+ when:
+ def slurper = new ConfigParserV2().setParams([c: 4])
+ slurper.parse(text)
+ then:
+ slurper.getDeclaredParams() == [a: 1, b: 2]
+
+ when:
+ slurper = new ConfigParserV2().setParams([c: 4]).setProfiles(['alpha'])
+ slurper.parse(text)
+ then:
+ slurper.getDeclaredParams() == [a: 3, b: 2]
}
def 'should ignore config includes when specified' () {
diff --git a/modules/nextflow/src/test/groovy/nextflow/script/BaseScriptTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/BaseScriptTest.groovy
index cdd8dc9727..6db6f66aed 100644
--- a/modules/nextflow/src/test/groovy/nextflow/script/BaseScriptTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/script/BaseScriptTest.groovy
@@ -84,7 +84,6 @@ class BaseScriptTest extends Dsl2Spec {
def script = Files.createTempFile('test',null)
and:
def session = Mock(Session) {
- getPublishTargets() >> [:]
getConfig() >> [:]
}
def binding = new ScriptBinding([:])
@@ -119,7 +118,6 @@ class BaseScriptTest extends Dsl2Spec {
def script = folder.resolve('main.nf')
and:
def session = Mock(Session) {
- getPublishTargets() >> [:]
getConfig() >> [:]
}
def binding = new ScriptBinding([:])
diff --git a/modules/nextflow/src/test/groovy/nextflow/script/ParamsDslTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/ParamsDslTest.groovy
new file mode 100644
index 0000000000..51d75d39b7
--- /dev/null
+++ b/modules/nextflow/src/test/groovy/nextflow/script/ParamsDslTest.groovy
@@ -0,0 +1,83 @@
+package nextflow.script
+
+import java.nio.file.Path
+
+import nextflow.Session
+import nextflow.file.FileHelper
+import nextflow.exception.ScriptRuntimeException
+import spock.lang.Specification
+/**
+ *
+ * @author Ben Sherman
+ */
+class ParamsDslTest extends Specification {
+
+ def 'should declare workflow params with CLI overrides'() {
+ given:
+ def cliParams = [input: './data', chunk_size: '3']
+ def configParams = [outdir: 'results']
+ def session = new Session([params: configParams + cliParams])
+ session.init(null, null, cliParams, configParams)
+
+ when:
+ def dsl = new ParamsDsl()
+ dsl.declare('input', Path)
+ dsl.declare('chunk_size', Integer, 1)
+ dsl.declare('save_intermeds', Boolean, false)
+ dsl.apply(session)
+ then:
+ session.binding.getParams() == [input: FileHelper.asPath('./data'), chunk_size: 3, save_intermeds: false, outdir: 'results']
+ }
+
+ def 'should report error for missing required param'() {
+ given:
+ def cliParams = [:]
+ def configParams = [outdir: 'results']
+ def session = new Session()
+ session.init(null, null, cliParams, configParams)
+
+ when:
+ def dsl = new ParamsDsl()
+ dsl.declare('input', Path)
+ dsl.declare('save_intermeds', Boolean, false)
+ dsl.apply(session)
+ then:
+ def e = thrown(ScriptRuntimeException)
+ e.message == 'Parameter `input` is required but was not specified on the command line, params file, or config'
+ }
+
+ def 'should report error for invalid param'() {
+ given:
+ def cliParams = [inputs: './data']
+ def configParams = [outdir: 'results']
+ def session = new Session()
+ session.init(null, null, cliParams, configParams)
+
+ when:
+ def dsl = new ParamsDsl()
+ dsl.declare('input', Path)
+ dsl.declare('save_intermeds', Boolean, false)
+ dsl.apply(session)
+ then:
+ def e = thrown(ScriptRuntimeException)
+ e.message == 'Parameter `inputs` was specified on the command line or params file but is not declared in the script or config'
+ }
+
+ def 'should report error for invalid type'() {
+ given:
+ def cliParams = [input: './data', save_intermeds: 42]
+ def configParams = [:]
+ def session = new Session()
+ session.init(null, null, cliParams, configParams)
+
+ when:
+ def dsl = new ParamsDsl()
+ dsl.declare('input', Path)
+ dsl.declare('save_intermeds', Boolean, false)
+ dsl.apply(session)
+ then:
+ def e = thrown(ScriptRuntimeException)
+ e.message == 'Parameter `save_intermeds` with type Boolean cannot be assigned to 42 [Integer]'
+ }
+
+}
diff --git a/modules/nextflow/src/testFixtures/groovy/test/Dsl2Spec.groovy b/modules/nextflow/src/testFixtures/groovy/test/Dsl2Spec.groovy
index a30c57f916..63ef37765b 100644
--- a/modules/nextflow/src/testFixtures/groovy/test/Dsl2Spec.groovy
+++ b/modules/nextflow/src/testFixtures/groovy/test/Dsl2Spec.groovy
@@ -53,6 +53,6 @@ class Dsl2Spec extends BaseSpec {
def dsl_eval(String entry, String str) {
new MockScriptRunner()
- .setScript(str).execute(null, entry)
+ .setScript(str).execute(null, null, null, entry)
}
}
diff --git a/modules/nf-lang/src/main/antlr/ScriptParser.g4 b/modules/nf-lang/src/main/antlr/ScriptParser.g4
index 5bffbafc1a..ba5ed64d9a 100644
--- a/modules/nf-lang/src/main/antlr/ScriptParser.g4
+++ b/modules/nf-lang/src/main/antlr/ScriptParser.g4
@@ -110,7 +110,8 @@ scriptDeclaration
: featureFlagDeclaration #featureFlagDeclAlt
| includeDeclaration #includeDeclAlt
| importDeclaration #importDeclAlt
- | paramDeclaration #paramDeclAlt
+ | paramsDef #paramsDefAlt
+ | paramDeclarationV1 #paramDeclV1Alt
| enumDef #enumDefAlt
| processDef #processDefAlt
| workflowDef #workflowDefAlt
@@ -147,8 +148,24 @@ importDeclaration
: IMPORT qualifiedClassName
;
-// -- param declaration
+// -- 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
;
diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ParamBlockNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ParamBlockNode.java
new file mode 100644
index 0000000000..feabebd7f5
--- /dev/null
+++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ParamBlockNode.java
@@ -0,0 +1,32 @@
+/*
+ * 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 org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.Parameter;
+
+/**
+ * A workflow params definition.
+ *
+ * @author Ben Sherman
+ */
+public class ParamBlockNode extends ASTNode {
+ public final Parameter[] declarations;
+
+ public ParamBlockNode(Parameter[] declarations) {
+ this.declarations = declarations;
+ }
+}
diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ParamNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ParamNodeV1.java
similarity index 86%
rename from modules/nf-lang/src/main/java/nextflow/script/ast/ParamNode.java
rename to modules/nf-lang/src/main/java/nextflow/script/ast/ParamNodeV1.java
index 13a57e7a07..d331ddf16c 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/ast/ParamNode.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ParamNodeV1.java
@@ -19,15 +19,15 @@
import org.codehaus.groovy.ast.expr.Expression;
/**
- * A parameter declaration.
+ * A legacy parameter declaration.
*
* @author Ben Sherman
*/
-public class ParamNode extends ASTNode {
+public class ParamNodeV1 extends ASTNode {
public final Expression target;
public Expression value;
- public ParamNode(Expression target, Expression value) {
+ public ParamNodeV1(Expression target, Expression value) {
this.target = target;
this.value = value;
}
diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptNode.java
index 6abb50a6a3..230b51feef 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptNode.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptNode.java
@@ -32,7 +32,8 @@ public class ScriptNode extends ModuleNode {
private String shebang;
private List featureFlags = new ArrayList<>();
private List includes = new ArrayList<>();
- private List params = new ArrayList<>();
+ private ParamBlockNode params;
+ private List paramsV1 = new ArrayList<>();
private WorkflowNode entry;
private OutputBlockNode outputs;
private List workflows = new ArrayList<>();
@@ -54,7 +55,9 @@ public List getDeclarations() {
var declarations = new ArrayList();
declarations.addAll(featureFlags);
declarations.addAll(includes);
- declarations.addAll(params);
+ if( params != null )
+ declarations.add(params);
+ declarations.addAll(paramsV1);
if( entry != null )
declarations.add(entry);
if( outputs != null )
@@ -77,10 +80,14 @@ public List getIncludes() {
return includes;
}
- public List getParams() {
+ public ParamBlockNode getParams() {
return params;
}
+ public List getParamsV1() {
+ return paramsV1;
+ }
+
public WorkflowNode getEntry() {
return entry;
}
@@ -119,8 +126,12 @@ public void addInclude(IncludeNode includeNode) {
includes.add(includeNode);
}
- public void addParam(ParamNode paramNode) {
- params.add(paramNode);
+ public void setParams(ParamBlockNode params) {
+ this.params = params;
+ }
+
+ public void addParamV1(ParamNodeV1 paramNode) {
+ paramsV1.add(paramNode);
}
public void setEntry(WorkflowNode entry) {
diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitor.java
index 080c1dbed0..cc1ea845e4 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitor.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitor.java
@@ -16,6 +16,7 @@
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 {
@@ -26,7 +27,11 @@ public interface ScriptVisitor extends GroovyCodeVisitor {
void visitInclude(IncludeNode node);
- void visitParam(ParamNode node);
+ void visitParams(ParamBlockNode node);
+
+ void visitParam(Parameter node);
+
+ void visitParamV1(ParamNodeV1 node);
void visitWorkflow(WorkflowNode node);
diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java
index 4b5293850e..f1af301fb0 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java
@@ -17,6 +17,7 @@
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;
@@ -31,8 +32,10 @@ public void visit(ScriptNode script) {
visitFeatureFlag(featureFlag);
for( var includeNode : script.getIncludes() )
visitInclude(includeNode);
- for( var paramNode : script.getParams() )
- visitParam(paramNode);
+ 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() )
@@ -57,7 +60,17 @@ public void visitInclude(IncludeNode node) {
}
@Override
- public void visitParam(ParamNode node) {
+ 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);
}
diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java
index 090303f97b..6736cf2ace 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java
@@ -20,13 +20,14 @@
import nextflow.script.ast.FunctionNode;
import nextflow.script.ast.OutputNode;
-import nextflow.script.ast.ParamNode;
+import nextflow.script.ast.ParamNodeV1;
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.ClassNode;
import org.codehaus.groovy.ast.DynamicVariable;
+import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.SourceUnit;
@@ -62,8 +63,10 @@ public void visit() {
variableScopeVisitor.visit();
// resolve type names
- for( var paramNode : sn.getParams() )
- visitParam(paramNode);
+ 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() )
@@ -79,7 +82,13 @@ public void visit() {
}
@Override
- public void visitParam(ParamNode node) {
+ 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);
}
diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java
index 39e8d06138..5ee56e92e9 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java
@@ -15,6 +15,7 @@
*/
package nextflow.script.control;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -25,7 +26,8 @@
import nextflow.script.ast.FunctionNode;
import nextflow.script.ast.IncludeNode;
import nextflow.script.ast.OutputBlockNode;
-import nextflow.script.ast.ParamNode;
+import nextflow.script.ast.ParamBlockNode;
+import nextflow.script.ast.ParamNodeV1;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.ScriptVisitorSupport;
@@ -119,7 +121,24 @@ public void visitInclude(IncludeNode node) {
}
@Override
- public void visitParam(ParamNode node) {
+ public void visitParams(ParamBlockNode node) {
+ var statements = Arrays.stream(node.declarations)
+ .map((param) -> {
+ var name = constX(param.getName());
+ var type = classX(param.getType());
+ var arguments = param.hasInitialExpression()
+ ? args(name, type, param.getInitialExpression())
+ : args(name, type);
+ return stmt(callThisX("declare", arguments));
+ })
+ .toList();
+ var closure = closureX(block(new VariableScope(), statements));
+ var result = stmt(callThisX("params", args(closure)));
+ moduleNode.addStatement(result);
+ }
+
+ @Override
+ public void visitParamV1(ParamNodeV1 node) {
var result = stmt(assignX(node.target, node.value));
moduleNode.addStatement(result);
}
diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java
index 679166e458..68f2dee2bc 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java
@@ -25,6 +25,7 @@
import nextflow.script.ast.ImplicitClosureParameter;
import nextflow.script.ast.IncludeNode;
import nextflow.script.ast.OutputNode;
+import nextflow.script.ast.ParamBlockNode;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.ScriptVisitorSupport;
@@ -168,6 +169,22 @@ public void visitFeatureFlag(FeatureFlagNode node) {
}
}
+ @Override
+ public void visitParams(ParamBlockNode node) {
+ var declaredParams = new HashMap();
+ 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
@@ -348,7 +365,7 @@ public void visitFunction(FunctionNode node) {
for( var parameter : node.getParameters() ) {
if( parameter.hasInitialExpression() )
visit(parameter.getInitialExpression());
- vsc.declare(parameter, node);
+ vsc.declare(parameter, parameter);
}
visit(node.getCode());
diff --git a/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java b/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java
index c907c2bb72..4df64b9aa8 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java
@@ -24,6 +24,7 @@
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.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression;
import org.codehaus.groovy.ast.expr.CastExpression;
@@ -713,6 +714,10 @@ private static boolean hasTrailingComma(Expression node) {
return node.getNodeMetaData(ASTNodeMarker.TRAILING_COMMA) != null;
}
+ public static boolean hasType(Variable variable) {
+ return !variable.isDynamicTyped() || isLegacyType(variable.getType());
+ }
+
public static boolean isLegacyType(ClassNode cn) {
return cn.getNodeMetaData(ASTNodeMarker.LEGACY_TYPE) != null;
}
diff --git a/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java
index 72d988b5fe..06ee88118c 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java
@@ -15,6 +15,7 @@
*/
package nextflow.script.formatter;
+import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@@ -25,12 +26,14 @@
import nextflow.script.ast.IncludeNode;
import nextflow.script.ast.OutputBlockNode;
import nextflow.script.ast.OutputNode;
-import nextflow.script.ast.ParamNode;
+import nextflow.script.ast.ParamNodeV1;
+import nextflow.script.ast.ParamBlockNode;
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.ClassNode;
+import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
@@ -101,8 +104,8 @@ public void visit() {
.map(this::getIncludeWidth)
.max(Integer::compare).orElse(0);
- maxParamWidth = scriptNode.getParams().stream()
- .map(this::getParamWidth)
+ maxParamWidth = scriptNode.getParamsV1().stream()
+ .map(ScriptFormattingVisitor::parameterWidth)
.max(Integer::compare).orElse(0);
}
@@ -117,8 +120,10 @@ else if( decl instanceof IncludeNode in )
visitInclude(in);
else if( decl instanceof OutputBlockNode obn )
visitOutputs(obn);
- else if( decl instanceof ParamNode pn )
- visitParam(pn);
+ 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 WorkflowNode wn )
@@ -188,12 +193,49 @@ protected int getIncludeWidth(IncludeEntryNode entry) {
}
@Override
- public void visitParam(ParamNode node) {
+ public void visitParams(ParamBlockNode node) {
+ var alignmentWidth = options.harshilAlignment()
+ ? maxParameterWidth(node.declarations)
+ : 0;
+
+ fmt.appendLeadingComments(node);
+ fmt.append("params {\n");
+ fmt.incIndent();
+ for( var param : node.declarations ) {
+ fmt.appendLeadingComments(param);
+ fmt.appendIndent();
+ fmt.append(param.getName());
+ if( fmt.hasType(param) ) {
+ if( alignmentWidth > 0 ) {
+ var padding = alignmentWidth - param.getName().length() + 1;
+ fmt.append(" ".repeat(padding));
+ }
+ fmt.append(": ");
+ fmt.visitTypeAnnotation(param.getType());
+ }
+ if( param.hasInitialExpression() ) {
+ fmt.append(" = ");
+ fmt.visit(param.getInitialExpression());
+ }
+ fmt.appendNewLine();
+ }
+ fmt.decIndent();
+ fmt.append("}\n");
+ }
+
+ private static int maxParameterWidth(Parameter[] parameters) {
+ return Arrays.stream(parameters)
+ .map(param -> param.getName().length())
+ .max(Integer::compare).orElse(0);
+ }
+
+ @Override
+ public void visitParamV1(ParamNodeV1 node) {
fmt.appendLeadingComments(node);
fmt.appendIndent();
fmt.visit(node.target);
if( maxParamWidth > 0 ) {
- var padding = maxParamWidth - getParamWidth(node);
+ var padding = maxParamWidth - parameterWidth(node);
fmt.append(" ".repeat(padding));
}
fmt.append(" = ");
@@ -201,7 +243,7 @@ public void visitParam(ParamNode node) {
fmt.appendNewLine();
}
- protected int getParamWidth(ParamNode node) {
+ private static int parameterWidth(ParamNodeV1 node) {
var target = (PropertyExpression) node.target;
var name = target.getPropertyAsString();
return name != null ? name.length() : 0;
@@ -262,7 +304,7 @@ public void visitWorkflow(WorkflowNode node) {
protected void visitWorkflowTakes(List takes) {
var alignmentWidth = options.harshilAlignment()
- ? getMaxParameterWidth(takes)
+ ? maxParameterWidth(takes)
: 0;
for( var stmt : takes ) {
@@ -282,7 +324,7 @@ protected void visitWorkflowTakes(List takes) {
protected void visitWorkflowEmits(List emits) {
var alignmentWidth = options.harshilAlignment()
- ? getMaxParameterWidth(emits)
+ ? maxParameterWidth(emits)
: 0;
for( var stmt : emits ) {
@@ -319,27 +361,24 @@ else if( emit instanceof VariableExpression ve ) {
}
}
- protected int getMaxParameterWidth(List statements) {
+ private static int maxParameterWidth(List statements) {
if( statements.size() == 1 )
return 0;
- int maxWidth = 0;
- for( var stmt : statements ) {
- var stmtX = (ExpressionStatement)stmt;
- var emit = stmtX.getExpression();
- int width = 0;
- if( emit instanceof VariableExpression ve ) {
- width = ve.getName().length();
- }
- else if( emit instanceof AssignmentExpression assign ) {
- var target = (VariableExpression)assign.getLeftExpression();
- width = target.getName().length();
- }
-
- if( maxWidth < width )
- maxWidth = width;
- }
- return maxWidth;
+ return statements.stream()
+ .map((stmt) -> {
+ var stmtX = (ExpressionStatement)stmt;
+ var emit = stmtX.getExpression();
+ if( emit instanceof VariableExpression ve ) {
+ return ve.getName().length();
+ }
+ if( emit instanceof AssignmentExpression assign ) {
+ var target = (VariableExpression)assign.getLeftExpression();
+ return target.getName().length();
+ }
+ return 0;
+ })
+ .max(Integer::compare).orElse(0);
}
@Override
diff --git a/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java b/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java
index 9a5f766108..7475a23181 100644
--- a/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java
+++ b/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java
@@ -36,7 +36,8 @@
import nextflow.script.ast.InvalidDeclaration;
import nextflow.script.ast.OutputBlockNode;
import nextflow.script.ast.OutputNode;
-import nextflow.script.ast.ParamNode;
+import nextflow.script.ast.ParamNodeV1;
+import nextflow.script.ast.ParamBlockNode;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.WorkflowNode;
@@ -286,10 +287,22 @@ else if( ctx instanceof OutputDefAltContext odac ) {
moduleNode.setOutputs(node);
}
- else if( ctx instanceof ParamDeclAltContext pac ) {
- var node = paramDeclaration(pac.paramDeclaration());
+ else if( ctx instanceof ParamsDefAltContext pac ) {
+ var node = paramsDef(pac.paramsDef());
saveLeadingComments(node, ctx);
- moduleNode.addParam(node);
+ if( moduleNode.getParams() != null )
+ collectSyntaxError(new SyntaxException("Params block defined more than once", node));
+ if( !moduleNode.getParamsV1().isEmpty() )
+ collectSyntaxError(new SyntaxException("Params block cannot be mixed with legacy parameter declarations", node));
+ moduleNode.setParams(node);
+ }
+
+ else if( ctx instanceof ParamDeclV1AltContext pac ) {
+ var node = paramDeclarationV1(pac.paramDeclarationV1());
+ saveLeadingComments(node, ctx);
+ if( moduleNode.getParams() != null )
+ collectSyntaxError(new SyntaxException("Legacy parameter declarations cannot be mixed with the params block", node));
+ moduleNode.addParamV1(node);
}
else if( ctx instanceof ProcessDefAltContext pdac ) {
@@ -329,14 +342,43 @@ private FeatureFlagNode featureFlagDeclaration(FeatureFlagDeclarationContext ctx
return result;
}
- private ParamNode paramDeclaration(ParamDeclarationContext ctx) {
+ private ParamBlockNode paramsDef(ParamsDefContext ctx) {
+ var declarations = paramsBody(ctx.paramsBody());
+ return ast( new ParamBlockNode(declarations), ctx );
+ }
+
+ private Parameter[] paramsBody(ParamsBodyContext ctx) {
+ if( ctx == null )
+ return Parameter.EMPTY_ARRAY;
+ return ctx.paramDeclaration().stream()
+ .map(this::paramDeclaration)
+ .filter(param -> param != null)
+ .toArray(Parameter[]::new);
+ }
+
+ private Parameter paramDeclaration(ParamDeclarationContext ctx) {
+ if( ctx.statement() != null ) {
+ collectSyntaxError(new SyntaxException("Invalid parameter declaration", ast( new EmptyStatement(), ctx.statement() )));
+ return null;
+ }
+ var type = type(ctx.type());
+ var name = identifier(ctx.identifier());
+ var defaultValue = ctx.expression() != null ? expression(ctx.expression()) : null;
+ var result = ast( param(type, name, defaultValue), ctx );
+ checkInvalidVarName(name, result);
+ groovydocManager.handle(result, ctx);
+ saveLeadingComments(result, ctx);
+ return result;
+ }
+
+ private ParamNodeV1 paramDeclarationV1(ParamDeclarationV1Context ctx) {
Expression target = ast( varX("params"), ctx.PARAMS() );
for( var ident : ctx.identifier() ) {
var name = ast( constX(identifier(ident)), ident );
target = ast( propX(target, name), target, name );
}
var value = expression(ctx.expression());
- return ast( new ParamNode(target, value), ctx );
+ return ast( new ParamNodeV1(target, value), ctx );
}
private IncludeNode includeDeclaration(IncludeDeclarationContext ctx) {
@@ -491,7 +533,7 @@ private String processType(ProcessExecContext ctx) {
return "exec";
}
if( ctx.SHELL() != null ) {
- collectWarning("The `shell` block is deprecated, use `script` instead", ctx.SHELL().getText(), ast( new EmptyExpression(), ctx.SHELL() ));
+ collectWarning("The `shell` block is deprecated, use `script` instead", ctx.SHELL().getText(), ast( new EmptyStatement(), ctx.SHELL() ));
return "shell";
}
return "script";
diff --git a/tests-v1/checks/.PARSER-V1 b/tests-v1/checks/.PARSER-V1
index f48c838fa5..1e6b46ac74 100644
--- a/tests-v1/checks/.PARSER-V1
+++ b/tests-v1/checks/.PARSER-V1
@@ -1,4 +1,5 @@
# ADDITIONAL TESTS TO VERIFY THE V1 PARSER
+chunk.nf
complex-names.nf
env-out.nf
env2.nf
diff --git a/tests/checks/.IGNORE-PARSER-V2 b/tests/checks/.IGNORE-PARSER-V2
index c97215480e..157a392cbe 100644
--- a/tests/checks/.IGNORE-PARSER-V2
+++ b/tests/checks/.IGNORE-PARSER-V2
@@ -1,2 +1,4 @@
# TESTS THAT SHOULD ONLY BE RUN BY THE V2 PARSER
+chunk.nf
+params-dsl.nf
workflow-oncomplete-v2.nf
\ No newline at end of file
diff --git a/tests/checks/params-dsl.nf/.checks b/tests/checks/params-dsl.nf/.checks
new file mode 100644
index 0000000000..76af690189
--- /dev/null
+++ b/tests/checks/params-dsl.nf/.checks
@@ -0,0 +1,37 @@
+
+echo "Test successful run"
+echo
+$NXF_RUN --input ./data > stdout
+
+< stdout grep -F 'params.input = [./data]'
+< stdout grep -F 'params.save_intermeds = false'
+
+echo
+echo "Test missing required param"
+echo
+set +e
+$NXF_RUN &> stdout ; ret=$?
+set -e
+
+[[ $ret != 0 ]] || false
+
+< stdout grep -F 'Parameter `input` is required'
+
+echo
+echo "Test overwrite script param from config profile"
+echo
+$NXF_RUN -c ../../params-dsl.config -profile test > stdout
+
+< stdout grep -F 'params.input = [alpha, beta, delta]'
+< stdout grep -F 'params.save_intermeds = true'
+
+echo
+echo "Test invalid param"
+echo
+set +e
+$NXF_RUN --inputs ./data &> stdout ; ret=$?
+set -e
+
+[[ $ret != 0 ]] || false
+
+< stdout grep -F 'Parameter `inputs` was specified'
diff --git a/tests/chunk.nf b/tests/chunk.nf
index eabc02d6f7..4cc0ed9237 100644
--- a/tests/chunk.nf
+++ b/tests/chunk.nf
@@ -1,7 +1,9 @@
#!/usr/bin/env nextflow
-params.input = null
-params.chunkSize = 1
+params {
+ input: Path
+ chunkSize: Integer = 1
+}
process foo {
debug true
@@ -14,7 +16,7 @@ process foo {
}
workflow {
- channel.fromPath(params.input)
+ channel.of(params.input)
| splitFasta(by: params.chunkSize)
| foo
}
diff --git a/tests/params-dsl.config b/tests/params-dsl.config
new file mode 100644
index 0000000000..18ba6c6c01
--- /dev/null
+++ b/tests/params-dsl.config
@@ -0,0 +1,9 @@
+
+params.outdir = 'results'
+
+profiles {
+ test {
+ params.input = 'alpha,beta,delta'
+ params.save_intermeds = true
+ }
+}
diff --git a/tests/params-dsl.nf b/tests/params-dsl.nf
new file mode 100644
index 0000000000..f14088ebdb
--- /dev/null
+++ b/tests/params-dsl.nf
@@ -0,0 +1,30 @@
+#!/usr/bin/env nextflow
+/*
+ * Copyright 2013-2024, 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.
+ */
+
+params {
+ // List of IDs.
+ input: String
+
+ // Whether to save intermediate outputs.
+ save_intermeds: Boolean = false
+}
+
+workflow {
+ main:
+ println "params.input = ${params.input.tokenize(',')}"
+ println "params.save_intermeds = ${params.save_intermeds}"
+}