Skip to content

Commit 01e1888

Browse files
authored
Fix GString equality checks with String (#6330)
--------- Signed-off-by: Ben Sherman <[email protected]>
1 parent d309cb0 commit 01e1888

File tree

11 files changed

+225
-55
lines changed

11 files changed

+225
-55
lines changed

docs/migrations/25-10.md

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,45 @@ 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>Configurable date and time formatting</h3>
32+
<h3>Improved handling of dynamic directives</h3>
3333

34-
:::{versionadded} 25.07.0-edge
35-
:::
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+
52+
<h3>Configurable date formatting</h3>
3653

37-
You can now customize the date and time format used in notifications, reports, and console output using the `NXF_DATE_FORMAT` environment variable. This addresses previous inconsistencies in timestamp representations:
54+
You can now customize the date and time format used in notifications, reports, and console output using the `NXF_DATE_FORMAT` environment variable:
3855

3956
```bash
57+
# Default format
58+
# e.g., 11-Aug-2016 09:40:20
59+
4060
# Use ISO format with timezone
41-
export NXF_DATE_FORMAT=iso
42-
nextflow run workflow.nf
43-
# Output: 2016-08-11T09:40:20+02:00
61+
export NXF_DATE_FORMAT="iso"
62+
# e.g., 2016-08-11T09:40:20+02:00
4463

45-
# Use custom format
64+
# Use custom format
4665
export NXF_DATE_FORMAT="yyyy-MM-dd HH:mm"
47-
nextflow run workflow.nf
48-
# Output: 2016-08-11 09:40
49-
50-
# Default format includes seconds
51-
nextflow run workflow.nf
52-
# Output: 11-Aug-2016 09:40:20
66+
# e.g., 2016-08-11 09:40
5367
```
5468

69+
This feature addresses previous inconsistencies in timestamp representations.
70+
5571
## Breaking changes
5672

5773
- 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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,15 +1276,16 @@ 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 can be specified without 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.
1287-
:::
1288+
Directives will be evaluated dynamically if they reference task inputs. Process configuration options must still be specified with a closure to be dynamic.
12881289

12891290
(dynamic-task-resources)=
12901291

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)