Skip to content

Commit 7c040c7

Browse files
committed
Fix GString equality check with String
Signed-off-by: Ben Sherman <[email protected]>
1 parent bfa67ca commit 7c040c7

File tree

11 files changed

+229
-48
lines changed

11 files changed

+229
-48
lines changed

docs/migrations/25-10.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,26 @@ workflow {
2929

3030
This syntax is simpler and easier to use with the {ref}`strict syntax <strict-syntax-page>`. See {ref}`workflow-handlers` for details.
3131

32+
<h3>Improved handling of dynamic directives</h3>
33+
34+
The {ref}`strict syntax <strict-syntax-page>` allows dynamic process directives to be specified without a closure:
35+
36+
```nextflow
37+
process hello {
38+
queue (entries > 100 ? 'long' : 'short')
39+
40+
input:
41+
tuple val(entries), path('data.txt')
42+
43+
script:
44+
"""
45+
your_command --here
46+
"""
47+
}
48+
```
49+
50+
See {ref}`dynamic-directives` for details.
51+
3252
## Breaking changes
3353

3454
- The AWS Java SDK used by Nextflow was upgraded from v1 to v2, which introduced some breaking changes to the `aws.client` config options. See {ref}`the guide <aws-java-sdk-v2-page>` for details.

docs/process.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,16 +1255,16 @@ To be defined dynamically, the directive's value needs to be expressed using a {
12551255

12561256
```nextflow
12571257
process hello {
1258-
executor 'sge'
1259-
queue { entries > 100 ? 'long' : 'short' }
1258+
executor 'sge'
1259+
queue { entries > 100 ? 'long' : 'short' }
12601260
1261-
input:
1262-
tuple val(entries), path('data.txt')
1261+
input:
1262+
tuple val(entries), path('data.txt')
12631263
1264-
script:
1265-
"""
1266-
your_command --here
1267-
"""
1264+
script:
1265+
"""
1266+
your_command --here
1267+
"""
12681268
}
12691269
```
12701270

@@ -1276,14 +1276,19 @@ All directives can be assigned a dynamic value except the following:
12761276
- {ref}`process-label`
12771277
- {ref}`process-maxforks`
12781278

1279-
:::{tip}
1280-
Assigning a string value with one or more variables is always resolved in a dynamic manner, and therefore is equivalent to the above syntax. For example, the above directive can also be written as:
1279+
:::{versionadded} 25.10
1280+
:::
1281+
1282+
Dynamic directives do not need to be wrapped in a closure when using the {ref}`strict syntax <strict-syntax-page>`:
12811283

12821284
```nextflow
1283-
queue "${ entries > 100 ? 'long' : 'short' }"
1285+
queue (entries > 100 ? 'long' : 'short')
12841286
```
12851287

1286-
Note, however, that the latter syntax can be used both for a directive's main argument (as in the above example) and for a directive's optional named attributes, whereas the closure syntax is only resolved dynamically for a directive's main argument.
1288+
Nextflow will evaluate this directive dynamically if it references task inputs. Directives that use an explicit closure are still resolved dynamically.
1289+
1290+
:::{note}
1291+
Process configuration options must still be specified with a closure in order to be dynamic.
12871292
:::
12881293

12891294
(dynamic-task-resources)=
@@ -1324,6 +1329,9 @@ disk 375.GB, type: 'local-ssd'
13241329
13251330
// dynamic request
13261331
disk { [request: 375.GB * task.attempt, type: 'local-ssd'] }
1332+
1333+
// dynamic request (25.10 or later, NXF_SYNTAX_PARSER=v2)
1334+
disk request: 375.GB * task.attempt, type: 'local-ssd'
13271335
```
13281336
:::
13291337

modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigCompiler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import nextflow.config.control.StripSecretsVisitor;
3232
import nextflow.config.parser.ConfigParserPluginFactory;
3333
import nextflow.script.control.Compiler;
34+
import nextflow.script.control.GStringToStringVisitor;
3435
import nextflow.script.control.PathCompareVisitor;
3536
import org.codehaus.groovy.ast.ASTNode;
3637
import org.codehaus.groovy.ast.ClassHelper;
@@ -179,6 +180,7 @@ private void analyze(SourceUnit source) {
179180
new StripSecretsVisitor(source).visitClass(cn);
180181
if( renderClosureAsString )
181182
new ClosureToStringVisitor(source).visitClass(cn);
183+
new GStringToStringVisitor(source).visitClass(cn);
182184
}
183185

184186
SourceUnit createSourceUnit(String source, Path path) {

modules/nextflow/src/main/groovy/nextflow/script/params/CmdEvalParam.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class CmdEvalParam extends BaseOutParam implements OptionalParam {
4545
}
4646

4747
BaseOutParam bind( def obj ) {
48-
if( obj !instanceof CharSequence )
48+
if( obj !instanceof CharSequence && obj !instanceof Closure )
4949
throw new IllegalArgumentException("Invalid argument for command output: $this")
5050
// the target value object
5151
target = obj
@@ -54,7 +54,9 @@ class CmdEvalParam extends BaseOutParam implements OptionalParam {
5454

5555
@Memoized
5656
String getTarget(Map<String,Object> context) {
57-
return target instanceof GString
57+
return target instanceof Closure
58+
? context.with(target)
59+
: target instanceof GString
5860
? target.cloneAsLazy(context).toString()
5961
: target.toString()
6062
}

modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptCompiler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import nextflow.script.ast.WorkflowNode;
3535
import nextflow.script.control.CallSiteCollector;
3636
import nextflow.script.control.Compiler;
37+
import nextflow.script.control.GStringToStringVisitor;
3738
import nextflow.script.control.ModuleResolver;
3839
import nextflow.script.control.OpCriteriaVisitor;
3940
import nextflow.script.control.PathCompareVisitor;
@@ -290,6 +291,7 @@ private void analyze(SourceUnit source) {
290291
new ScriptToGroovyVisitor(source).visit();
291292
new PathCompareVisitor(source).visitClass(cn);
292293
new OpCriteriaVisitor(source).visitClass(cn);
294+
new GStringToStringVisitor(source).visitClass(cn);
293295
}
294296

295297
SourceUnit createSourceUnit(URI uri) {

modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ class ConfigParserV2Test extends Specification {
428428
.parse(configText)
429429
then:
430430
config.params.str1 instanceof String
431-
config.params.str2 instanceof GString
431+
config.params.str2 instanceof String
432432
config.process.clusterOptions instanceof Closure
433433
config.process.ext.bar instanceof Closure
434434

@@ -438,7 +438,7 @@ class ConfigParserV2Test extends Specification {
438438
.parse(configText)
439439
then:
440440
config.params.str1 instanceof String
441-
config.params.str2 instanceof GString
441+
config.params.str2 instanceof String
442442
config.process.clusterOptions instanceof Closure
443443
config.process.ext.bar instanceof Closure
444444

modules/nextflow/src/test/groovy/nextflow/script/parser/v2/ScriptLoaderV2Test.groovy

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,59 @@ class ScriptLoaderV2Test extends Dsl2Spec {
177177
noExceptionThrown()
178178
}
179179

180+
def 'should eagerly evaluate GStrings' () {
181+
182+
given:
183+
def session = new Session()
184+
def parser = new ScriptLoaderV2(session)
185+
186+
def TEXT = '''
187+
workflow {
188+
assert "${'hello'}" == 'hello'
189+
assert "${'hello'}" in ['hello']
190+
}
191+
'''
192+
193+
when:
194+
parser.parse(TEXT)
195+
parser.runScript()
196+
197+
then:
198+
noExceptionThrown()
199+
}
200+
201+
def 'should lazily evaluate process inputs/outputs/directives' () {
202+
203+
given:
204+
def session = new Session()
205+
session.executorFactory = new MockExecutorFactory()
206+
def parser = new ScriptLoaderV2(session)
207+
208+
def TEXT = '''
209+
process HELLO {
210+
tag props.name
211+
212+
input:
213+
val props
214+
215+
output:
216+
val props.name
217+
218+
script:
219+
"echo 'Hello ${props.name}'"
220+
}
221+
222+
workflow {
223+
HELLO( [name: 'World'] )
224+
}
225+
'''
226+
227+
when:
228+
parser.parse(TEXT)
229+
parser.runScript()
230+
231+
then:
232+
noExceptionThrown()
233+
}
234+
180235
}

modules/nf-lang/src/main/java/nextflow/config/control/VariableScopeVisitor.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,12 @@ public void visitConfigAssign(ConfigAssignNode node) {
119119
var scopes = currentConfigScopes();
120120
inProcess = !scopes.isEmpty() && "process".equals(scopes.get(0));
121121
inClosure = node.value instanceof ClosureExpression;
122-
if( inClosure && !inProcess && !isWorkflowHandler(scopes, node) )
123-
vsc.addError("Dynamic config options are only allowed in the `process` scope", node);
122+
if( inClosure ) {
123+
if( isWorkflowHandler(scopes, node) )
124+
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);
125+
else if( !inProcess )
126+
vsc.addError("Dynamic config options are only allowed in the `process` scope", node);
127+
}
124128
if( inClosure ) {
125129
vsc.pushScope(ScriptDsl.class);
126130
if( inProcess )

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* from
3131
* "${foo} ${bar}"
3232
* to
33-
* "${->foo} ${->bar}
33+
* "${->foo} ${->bar}"
3434
*
3535
* @author Paolo Di Tommaso <[email protected]>
3636
*/
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.script.control;
18+
19+
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
20+
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
21+
import org.codehaus.groovy.ast.expr.ClosureExpression;
22+
import org.codehaus.groovy.ast.expr.Expression;
23+
import org.codehaus.groovy.ast.expr.GStringExpression;
24+
import org.codehaus.groovy.control.SourceUnit;
25+
26+
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
27+
28+
/**
29+
* Coerce a GString into a String.
30+
*
31+
* from
32+
* "${foo} ${bar}"
33+
* to
34+
* "${foo} ${bar}".toString()
35+
*
36+
* This enables equality checks between GStrings and Strings,
37+
* e.g. `"${'hello'}" == 'hello'`.
38+
*
39+
* @author Ben Sherman <[email protected]>
40+
*/
41+
public class GStringToStringVisitor extends ClassCodeExpressionTransformer {
42+
43+
private SourceUnit sourceUnit;
44+
45+
public GStringToStringVisitor(SourceUnit sourceUnit) {
46+
this.sourceUnit = sourceUnit;
47+
}
48+
49+
@Override
50+
protected SourceUnit getSourceUnit() {
51+
return sourceUnit;
52+
}
53+
54+
@Override
55+
public Expression transform(Expression node) {
56+
if( node instanceof ClosureExpression ce )
57+
super.visitClosureExpression(ce);
58+
59+
if( node instanceof GStringExpression gse )
60+
return transformToString(gse);
61+
62+
return super.transform(node);
63+
}
64+
65+
private Expression transformToString(GStringExpression node) {
66+
return callX(node, "toString", new ArgumentListExpression());
67+
}
68+
69+
}

0 commit comments

Comments
 (0)