Skip to content

Commit cd8f956

Browse files
committed
Add name checking for config files
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
1 parent fdbd247 commit cd8f956

File tree

14 files changed

+937
-472
lines changed

14 files changed

+937
-472
lines changed

modules/compiler/src/main/java/config/ast/ConfigAssignNode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*/
2626
public class ConfigAssignNode extends ConfigStatement {
2727
public final List<String> names;
28-
public final Expression value;
28+
public Expression value;
2929

3030
public ConfigAssignNode(List<String> names, Expression value) {
3131
this.names = names;

modules/compiler/src/main/java/config/ast/ConfigIncludeNode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* @author Ben Sherman <bentshermann@gmail.com>
2323
*/
2424
public class ConfigIncludeNode extends ConfigStatement {
25-
public final Expression source;
25+
public Expression source;
2626

2727
public ConfigIncludeNode(Expression source) {
2828
this.source = source;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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.config.control;
17+
18+
import java.util.Collections;
19+
20+
import nextflow.config.ast.ConfigAssignNode;
21+
import nextflow.config.ast.ConfigIncludeNode;
22+
import nextflow.config.ast.ConfigNode;
23+
import nextflow.config.ast.ConfigVisitorSupport;
24+
import nextflow.script.control.ResolveVisitor;
25+
import org.codehaus.groovy.ast.DynamicVariable;
26+
import org.codehaus.groovy.ast.expr.Expression;
27+
import org.codehaus.groovy.ast.expr.VariableExpression;
28+
import org.codehaus.groovy.control.CompilationUnit;
29+
import org.codehaus.groovy.control.SourceUnit;
30+
31+
/**
32+
*
33+
* @author Ben Sherman <bentshermann@gmail.com>
34+
*/
35+
public class ConfigResolveVisitor extends ConfigVisitorSupport {
36+
37+
private SourceUnit sourceUnit;
38+
39+
private ResolveVisitor resolver;
40+
41+
public ConfigResolveVisitor(SourceUnit sourceUnit, CompilationUnit compilationUnit) {
42+
this.sourceUnit = sourceUnit;
43+
this.resolver = new ResolveVisitor(sourceUnit, compilationUnit, Collections.emptyList(), Collections.emptyList());
44+
}
45+
46+
@Override
47+
protected SourceUnit getSourceUnit() {
48+
return sourceUnit;
49+
}
50+
51+
public void visit() {
52+
var moduleNode = sourceUnit.getAST();
53+
if( moduleNode instanceof ConfigNode cn ) {
54+
// initialize variable scopes
55+
new VariableScopeVisitor(sourceUnit).visit();
56+
57+
// resolve type names
58+
super.visit(cn);
59+
60+
// report errors for any unresolved variable references
61+
new DynamicVariablesVisitor().visit(cn);
62+
}
63+
}
64+
65+
@Override
66+
public void visitConfigAssign(ConfigAssignNode node) {
67+
node.value = resolver.transform(node.value);
68+
}
69+
70+
@Override
71+
public void visitConfigInclude(ConfigIncludeNode node) {
72+
node.source = resolver.transform(node.source);
73+
}
74+
75+
private class DynamicVariablesVisitor extends ConfigVisitorSupport {
76+
77+
@Override
78+
protected SourceUnit getSourceUnit() {
79+
return sourceUnit;
80+
}
81+
82+
@Override
83+
public void visitVariableExpression(VariableExpression node) {
84+
var variable = node.getAccessedVariable();
85+
if( variable instanceof DynamicVariable )
86+
resolver.addError("`" + node.getName() + "` is not defined", node);
87+
}
88+
}
89+
90+
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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.config.control;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
import nextflow.config.ast.ConfigAssignNode;
22+
import nextflow.config.ast.ConfigBlockNode;
23+
import nextflow.config.ast.ConfigIncludeNode;
24+
import nextflow.config.ast.ConfigNode;
25+
import nextflow.config.ast.ConfigVisitorSupport;
26+
import nextflow.config.dsl.ConfigDsl;
27+
import nextflow.script.control.VariableScopeChecker;
28+
import nextflow.script.dsl.ProcessDsl;
29+
import nextflow.script.dsl.ScriptDsl;
30+
import org.codehaus.groovy.ast.ClassNode;
31+
import org.codehaus.groovy.ast.DynamicVariable;
32+
import org.codehaus.groovy.ast.Variable;
33+
import org.codehaus.groovy.ast.VariableScope;
34+
import org.codehaus.groovy.ast.expr.BinaryExpression;
35+
import org.codehaus.groovy.ast.expr.ClosureExpression;
36+
import org.codehaus.groovy.ast.expr.ConstantExpression;
37+
import org.codehaus.groovy.ast.expr.DeclarationExpression;
38+
import org.codehaus.groovy.ast.expr.Expression;
39+
import org.codehaus.groovy.ast.expr.MethodCallExpression;
40+
import org.codehaus.groovy.ast.expr.TupleExpression;
41+
import org.codehaus.groovy.ast.expr.VariableExpression;
42+
import org.codehaus.groovy.ast.stmt.BlockStatement;
43+
import org.codehaus.groovy.ast.stmt.CatchStatement;
44+
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
45+
import org.codehaus.groovy.control.SourceUnit;
46+
import org.codehaus.groovy.syntax.Types;
47+
48+
/**
49+
* Initialize the variable scopes for an AST.
50+
*
51+
* See: org.codehaus.groovy.classgen.VariableScopeVisitor
52+
*
53+
* @author Ben Sherman <bentshermann@gmail.com>
54+
*/
55+
class VariableScopeVisitor extends ConfigVisitorSupport {
56+
57+
private SourceUnit sourceUnit;
58+
59+
private VariableScopeChecker vsc;
60+
61+
private List<String> configScopes = new ArrayList<>();
62+
63+
public VariableScopeVisitor(SourceUnit sourceUnit) {
64+
this.sourceUnit = sourceUnit;
65+
this.vsc = new VariableScopeChecker(sourceUnit, new ClassNode(ConfigDsl.class));
66+
}
67+
68+
@Override
69+
protected SourceUnit getSourceUnit() {
70+
return sourceUnit;
71+
}
72+
73+
public void visit() {
74+
var moduleNode = sourceUnit.getAST();
75+
if( moduleNode instanceof ConfigNode cn ) {
76+
super.visit(cn);
77+
vsc.checkUnusedVariables();
78+
}
79+
}
80+
81+
@Override
82+
public void visitConfigBlock(ConfigBlockNode node) {
83+
var newScope = node.kind == null;
84+
if( newScope )
85+
configScopes.add(node.name);
86+
super.visitConfigBlock(node);
87+
if( newScope )
88+
configScopes.remove(configScopes.size() - 1);
89+
}
90+
91+
@Override
92+
public void visitConfigInclude(ConfigIncludeNode node) {
93+
checkConfigInclude(node);
94+
visit(node.source);
95+
}
96+
97+
private void checkConfigInclude(ConfigIncludeNode node) {
98+
if( configScopes.isEmpty() )
99+
return;
100+
if( configScopes.size() == 2 && "profiles".equals(configScopes.get(0)) )
101+
return;
102+
vsc.addError("Config includes are only allowed at the top-level or in a profile.", node);
103+
}
104+
105+
// statements
106+
107+
@Override
108+
public void visitBlockStatement(BlockStatement node) {
109+
var newScope = node.getVariableScope() != null;
110+
if( newScope ) vsc.pushScope();
111+
node.setVariableScope(currentScope());
112+
super.visitBlockStatement(node);
113+
if( newScope ) vsc.popScope();
114+
}
115+
116+
@Override
117+
public void visitCatchStatement(CatchStatement node) {
118+
vsc.pushScope();
119+
vsc.declare(node.getVariable(), node);
120+
super.visitCatchStatement(node);
121+
vsc.popScope();
122+
}
123+
124+
@Override
125+
public void visitExpressionStatement(ExpressionStatement node) {
126+
var exp = node.getExpression();
127+
if( exp instanceof BinaryExpression be && Types.isAssignment(be.getOperation().getType()) ) {
128+
var source = be.getRightExpression();
129+
var target = be.getLeftExpression();
130+
visit(source);
131+
if( !checkImplicitDeclaration(target) ) {
132+
visit(target);
133+
}
134+
return;
135+
}
136+
super.visitExpressionStatement(node);
137+
}
138+
139+
private boolean checkImplicitDeclaration(Expression node) {
140+
if( node instanceof TupleExpression te ) {
141+
var result = false;
142+
for( var el : te.getExpressions() )
143+
result |= declareAssignedVariable((VariableExpression) el);
144+
return result;
145+
}
146+
else if( node instanceof VariableExpression ve ) {
147+
return declareAssignedVariable(ve);
148+
}
149+
return false;
150+
}
151+
152+
private boolean declareAssignedVariable(VariableExpression ve) {
153+
var variable = vsc.findVariableDeclaration(ve.getName(), ve);
154+
if( variable != null ) {
155+
ve.setAccessedVariable(variable);
156+
return false;
157+
}
158+
else {
159+
vsc.addError("`" + ve.getName() + "` was assigned but not declared", ve);
160+
return true;
161+
}
162+
}
163+
164+
// expressions
165+
166+
private static final List<String> KEYWORDS = List.of(
167+
"case",
168+
"for",
169+
"switch",
170+
"while"
171+
);
172+
173+
@Override
174+
public void visitMethodCallExpression(MethodCallExpression node) {
175+
if( node.isImplicitThis() && node.getMethod() instanceof ConstantExpression ) {
176+
var name = node.getMethodAsString();
177+
var variable = vsc.findVariableDeclaration(name, node);
178+
if( variable == null ) {
179+
if( !KEYWORDS.contains(name) )
180+
vsc.addError("`" + name + "` is not defined", node.getMethod());
181+
}
182+
}
183+
super.visitMethodCallExpression(node);
184+
}
185+
186+
@Override
187+
public void visitDeclarationExpression(DeclarationExpression node) {
188+
visit(node.getRightExpression());
189+
190+
if( node.isMultipleAssignmentDeclaration() ) {
191+
for( var el : node.getTupleExpression() )
192+
vsc.declare((VariableExpression) el);
193+
}
194+
else {
195+
vsc.declare(node.getVariableExpression());
196+
}
197+
}
198+
199+
private boolean inClosure;
200+
201+
@Override
202+
public void visitClosureExpression(ClosureExpression node) {
203+
var ic = inClosure;
204+
inClosure = true;
205+
vsc.pushScope(ic ? null : ScriptDsl.class);
206+
var inProcess = "process".equals(configScopes.get(configScopes.size() - 1));
207+
if( !ic && inProcess )
208+
vsc.pushScope(ProcessDsl.class);
209+
node.setVariableScope(currentScope());
210+
if( node.getParameters() != null ) {
211+
for( var parameter : node.getParameters() ) {
212+
vsc.declare(parameter, parameter);
213+
if( parameter.hasInitialExpression() )
214+
visit(parameter.getInitialExpression());
215+
}
216+
}
217+
super.visitClosureExpression(node);
218+
for( var it = currentScope().getReferencedLocalVariablesIterator(); it.hasNext(); ) {
219+
var variable = it.next();
220+
variable.setClosureSharedVariable(true);
221+
}
222+
if( !ic && inProcess )
223+
vsc.popScope();
224+
vsc.popScope();
225+
inClosure = ic;
226+
}
227+
228+
@Override
229+
public void visitVariableExpression(VariableExpression node) {
230+
var name = node.getName();
231+
Variable variable = vsc.findVariableDeclaration(name, node);
232+
if( variable == null ) {
233+
if( "it".equals(name) ) {
234+
vsc.addFutureWarning("Implicit closure parameter `it` will not be supported in a future version", node);
235+
}
236+
else {
237+
variable = new DynamicVariable(name, false);
238+
}
239+
}
240+
if( variable != null ) {
241+
node.setAccessedVariable(variable);
242+
}
243+
}
244+
245+
// helpers
246+
247+
private VariableScope currentScope() {
248+
return vsc.getCurrentScope();
249+
}
250+
251+
}

0 commit comments

Comments
 (0)