Skip to content

Commit 9a70cdb

Browse files
authored
Register fully-qualified process names at compile-time (#6312)
Signed-off-by: Ben Sherman <[email protected]>
1 parent cbfa232 commit 9a70cdb

File tree

8 files changed

+229
-21
lines changed

8 files changed

+229
-21
lines changed

docs/config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,15 @@ process {
243243
cpus = 4
244244
withLabel: hello { cpus = 8 }
245245
withName: bye { cpus = 16 }
246-
withName: 'mysub:bye' { cpus = 32 }
246+
withName: 'aloha:bye' { cpus = 32 }
247247
}
248248
```
249249

250250
With the above configuration:
251251
- All processes will use 4 cpus (unless otherwise specified in their process definition).
252252
- Processes annotated with the `hello` label will use 8 cpus.
253253
- Any process named `bye` (or imported as `bye`) will use 16 cpus.
254-
- Any process named `bye` (or imported as `bye`) invoked by a workflow named `mysub` will use 32 cpus.
254+
- Any process named `bye` (or imported as `bye`) invoked by a workflow named `aloha` will use 32 cpus.
255255

256256
(config-profiles)=
257257

docs/reference/feature-flags.md

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,23 @@
22

33
# Feature flags
44

5-
Feature flags are used to introduce experimental or other opt-in features. They can be specified in the pipeline script or the configuration file.
5+
Feature flags are used to introduce experimental or other opt-in features. They must be specified in the pipeline script.
66

77
`nextflow.enable.configProcessNamesValidation`
8+
: :::{deprecated} 25.10.0
9+
Use the {ref}`strict syntax <strict-syntax-page>` instead. It validates process selectors without producing false warnings.
10+
:::
811
: When `true`, prints a warning for every `withName:` process selector that doesn't match a process in the pipeline (default: `true`).
912

1013
`nextflow.enable.dsl`
11-
: Defines the DSL version to use (`1` or `2`).
12-
: :::{versionchanged} 22.03.0-edge
13-
DSL2 was made the default DSL version.
14-
:::
15-
: :::{versionchanged} 22.12.0-edge
16-
DSL1 was removed.
14+
: :::{deprecated} 25.04.0
1715
:::
16+
: Defines the DSL version to use (`1` or `2`).
1817

1918
`nextflow.enable.moduleBinaries`
2019
: When `true`, enables the use of modules with binary scripts. See {ref}`module-binaries` for more information.
2120

2221
`nextflow.enable.strict`
23-
: :::{versionadded} 22.05.0-edge
24-
:::
2522
: When `true`, the pipeline is executed in "strict" mode, which introduces the following rules:
2623

2724
- When reading a params file, Nextflow will fail if a dynamic param value references an undefined variable
@@ -53,15 +50,13 @@ Feature flags are used to introduce experimental or other opt-in features. They
5350
: When `true`, enables the use of the {ref}`workflow output definition <workflow-output-def>`.
5451

5552
`nextflow.preview.recursion`
56-
: :::{versionadded} 21.11.0-edge
57-
:::
5853
: *Experimental: may change in a future release.*
5954
: When `true`, enables {ref}`process and workflow recursion <workflow-recursion>`.
6055

6156
`nextflow.preview.topic`
62-
: :::{versionadded} 23.11.0-edge
57+
: :::{versionadded} 24.04.0
6358
:::
64-
: :::{versionchanged} 25.04.0
59+
: :::{deprecated} 25.04.0
6560
This feature flag is no longer required to use topic channels.
6661
:::
6762
: When `true`, enables {ref}`topic channels <channel-topic>` feature.

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,21 @@
2323
import java.util.Collection;
2424
import java.util.Collections;
2525
import java.util.HashMap;
26+
import java.util.IdentityHashMap;
2627
import java.util.List;
2728
import java.util.Map;
2829
import java.util.Set;
2930

3031
import groovy.lang.GroovyClassLoader;
3132
import groovy.lang.GroovyCodeSource;
3233
import com.google.common.hash.Hashing;
34+
import nextflow.script.ast.WorkflowNode;
35+
import nextflow.script.control.CallSiteCollector;
3336
import nextflow.script.control.Compiler;
3437
import nextflow.script.control.ModuleResolver;
3538
import nextflow.script.control.OpCriteriaVisitor;
3639
import nextflow.script.control.PathCompareVisitor;
40+
import nextflow.script.control.ProcessNameResolver;
3741
import nextflow.script.control.ResolveIncludeVisitor;
3842
import nextflow.script.control.ScriptResolveVisitor;
3943
import nextflow.script.control.ScriptToGroovyVisitor;
@@ -42,6 +46,7 @@
4246
import org.codehaus.groovy.ast.ASTNode;
4347
import org.codehaus.groovy.ast.ClassHelper;
4448
import org.codehaus.groovy.ast.ClassNode;
49+
import org.codehaus.groovy.ast.MethodNode;
4550
import org.codehaus.groovy.control.CompilationFailedException;
4651
import org.codehaus.groovy.control.CompilationUnit;
4752
import org.codehaus.groovy.control.CompilerConfiguration;
@@ -162,24 +167,31 @@ private CompileResult compile0(GroovyCodeSource codeSource) throws IOException {
162167
.findFirst()
163168
.get();
164169

170+
var modules = collectModules(unit, classes);
171+
var processNames = new ProcessNameResolver(unit.getCallSites()).resolve(su);
172+
return new CompileResult(main, modules, processNames);
173+
}
174+
175+
private Map<Path,Class> collectModules(ScriptCompilationUnit unit, List<Class> classes) {
165176
// match each module script class to the source path
166177
// using the class name
167-
var modules = new HashMap<Path,Class>();
178+
var result = new HashMap<Path,Class>();
168179
for( var c : classes ) {
169180
for( var source : unit.getModules() ) {
170181
if( source.getName().equals(c.getSimpleName()) ) {
171182
var path = Path.of(source.getSource().getURI());
172-
modules.put(path, c);
183+
result.put(path, c);
173184
break;
174185
}
175186
}
176187
}
177-
return new CompileResult(main, modules);
188+
return result;
178189
}
179190

180191
public static record CompileResult(
181192
Class main,
182-
Map<Path,Class> modules
193+
Map<Path,Class> modules,
194+
Set<String> processNames
183195
) {}
184196

185197
private static class ScriptClassLoader extends GroovyClassLoader {
@@ -207,6 +219,8 @@ private static List<ClassNode> defaultImports() {
207219

208220
private Set<SourceUnit> modules;
209221

222+
private Map<WorkflowNode, Map<String, MethodNode>> callSites = new IdentityHashMap<>();
223+
210224
ScriptCompilationUnit(CompilerConfiguration configuration, GroovyClassLoader loader) {
211225
super(configuration, null, loader);
212226
super.addPhaseOperation(source -> analyze(source), Phases.CONVERSION);
@@ -216,6 +230,10 @@ Set<SourceUnit> getModules() {
216230
return modules;
217231
}
218232

233+
Map<WorkflowNode, Map<String, MethodNode>> getCallSites() {
234+
return callSites;
235+
}
236+
219237
@Override
220238
public void addPhaseOperation(ISourceUnitOperation op, int phase) {
221239
super.addPhaseOperation((source) -> {
@@ -264,6 +282,9 @@ private void analyze(SourceUnit source) {
264282
if( source.getErrorCollector().hasErrors() )
265283
return;
266284

285+
// collect call sites for each workflow in the script
286+
callSites.putAll(new CallSiteCollector().apply(source));
287+
267288
// convert to Groovy
268289
new ScriptToGroovyVisitor(source).visit();
269290
new PathCompareVisitor(source).visitClass(cn);

modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptLoaderV2.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ class ScriptLoaderV2 implements ScriptLoader {
113113
result.modules().forEach((path, clazz) -> {
114114
createScript(clazz, new ScriptBinding(), path, true)
115115
})
116+
117+
for( final name : result.processNames() )
118+
ScriptMeta.addResolvedName(name)
116119
}
117120
catch( CompilationFailedException e ) {
118121
if( scriptPath )

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import nextflow.exception.ScriptCompilationException
77
import nextflow.script.BaseScript
88
import nextflow.script.ScriptMeta
99
import nextflow.script.WorkflowDef
10-
import spock.lang.Specification
10+
import test.Dsl2Spec
11+
import test.MockExecutorFactory
1112
/**
1213
*
1314
* @author Ben Sherman <[email protected]>
1415
*/
15-
class ScriptLoaderV2Test extends Specification {
16+
class ScriptLoaderV2Test extends Dsl2Spec {
1617

1718
def 'should run a file script' () {
1819

@@ -119,6 +120,43 @@ class ScriptLoaderV2Test extends Specification {
119120
meta.getWorkflow('hello').declaredOutputs == ['result']
120121
}
121122

123+
def 'should register fully-qualified process names' () {
124+
125+
given:
126+
def session = new Session()
127+
session.executorFactory = new MockExecutorFactory()
128+
def parser = new ScriptLoaderV2(session)
129+
130+
def TEXT = '''
131+
132+
process hello {
133+
'echo hello'
134+
}
135+
136+
process bye {
137+
'echo bye'
138+
}
139+
140+
workflow aloha {
141+
if( params.mode == 'hello' )
142+
hello()
143+
if( params.mode == 'bye' )
144+
bye()
145+
}
146+
147+
workflow {
148+
aloha()
149+
}
150+
'''
151+
152+
when:
153+
parser.parse(TEXT)
154+
parser.runScript()
155+
156+
then:
157+
ScriptMeta.allProcessNames() == [ 'hello', 'bye', 'aloha:hello', 'aloha:bye' ] as Set
158+
}
159+
122160
def 'should allow explicit `it` closure parameter' () {
123161

124162
given:
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2024-2025, 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+
package nextflow.script.control;
17+
18+
import java.util.IdentityHashMap;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import nextflow.script.ast.ASTNodeMarker;
23+
import nextflow.script.ast.ProcessNode;
24+
import nextflow.script.ast.ScriptNode;
25+
import nextflow.script.ast.WorkflowNode;
26+
import org.codehaus.groovy.ast.CodeVisitorSupport;
27+
import org.codehaus.groovy.ast.MethodNode;
28+
import org.codehaus.groovy.ast.expr.MethodCallExpression;
29+
import org.codehaus.groovy.control.SourceUnit;
30+
31+
/**
32+
* Collect call sites for each workflow in a script.
33+
*
34+
* @author Ben Sherman <[email protected]>
35+
*/
36+
public class CallSiteCollector {
37+
38+
public Map<WorkflowNode, Map<String, MethodNode>> apply(SourceUnit source) {
39+
var callSites = new IdentityHashMap<WorkflowNode, Map<String, MethodNode>>();
40+
if( source.getAST() instanceof ScriptNode sn ) {
41+
for ( var wn : sn.getWorkflows() )
42+
callSites.put(wn, new WorkflowVisitor().apply(wn));
43+
}
44+
return callSites;
45+
}
46+
47+
public class WorkflowVisitor extends CodeVisitorSupport {
48+
49+
private Map<String, MethodNode> calls;
50+
51+
public Map<String, MethodNode> apply(WorkflowNode node) {
52+
calls = new HashMap<>();
53+
visit(node.main);
54+
visit(node.emits);
55+
visit(node.publishers);
56+
return calls;
57+
}
58+
59+
@Override
60+
public void visitMethodCallExpression(MethodCallExpression node) {
61+
visit(node.getObjectExpression());
62+
visit(node.getArguments());
63+
64+
if( node.isImplicitThis() ) {
65+
var mn = (MethodNode) node.getNodeMetaData(ASTNodeMarker.METHOD_TARGET);
66+
if( mn instanceof WorkflowNode || mn instanceof ProcessNode )
67+
calls.put(node.getMethodAsString(), mn);
68+
}
69+
}
70+
}
71+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2024-2025, 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+
package nextflow.script.control;
17+
18+
import java.util.HashSet;
19+
import java.util.LinkedList;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import nextflow.script.ast.ProcessNode;
25+
import nextflow.script.ast.ScriptNode;
26+
import nextflow.script.ast.WorkflowNode;
27+
import org.codehaus.groovy.ast.MethodNode;
28+
import org.codehaus.groovy.control.SourceUnit;
29+
30+
/**
31+
* Resolve all fully-qualified process names invoked
32+
* (directly or indirectly) by an entry workflow.
33+
*
34+
* @author Ben Sherman <[email protected]>
35+
*/
36+
public class ProcessNameResolver {
37+
38+
private Map<WorkflowNode, Map<String, MethodNode>> callSites;
39+
40+
public ProcessNameResolver(Map<WorkflowNode, Map<String, MethodNode>> callSites) {
41+
this.callSites = callSites;
42+
}
43+
44+
public Set<String> resolve(SourceUnit main) {
45+
var result = new HashSet<String>();
46+
var queue = new LinkedList<CallScope>();
47+
48+
if( main.getAST() instanceof ScriptNode sn && sn.getEntry() != null )
49+
queue.add(new CallScope("", sn.getEntry()));
50+
51+
while( !queue.isEmpty() ) {
52+
var scope = queue.remove();
53+
var calls = callSites.get(scope.node());
54+
calls.forEach((name, mn) -> {
55+
if( mn instanceof WorkflowNode wn ) {
56+
var workflowName = fullyQualifiedName(scope.name(), name);
57+
queue.add(new CallScope(workflowName, wn));
58+
}
59+
else if( mn instanceof ProcessNode pn ) {
60+
var processName = fullyQualifiedName(scope.name(), name);
61+
result.add(processName);
62+
}
63+
});
64+
}
65+
66+
return result;
67+
}
68+
69+
private String fullyQualifiedName(String scope, String name) {
70+
return scope.isEmpty()
71+
? name
72+
: scope + ":" + name;
73+
}
74+
75+
private static record CallScope(
76+
String name,
77+
WorkflowNode node
78+
) {}
79+
}

modules/nf-lang/src/main/java/nextflow/script/dsl/FeatureFlagDsl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
public class FeatureFlagDsl {
1919

20+
@Deprecated
2021
@FeatureFlag("nextflow.enable.configProcessNamesValidation")
2122
@Description("""
2223
When `true`, prints a warning for every `withName:` process selector that doesn't match a process in the pipeline (default: `true`).

0 commit comments

Comments
 (0)