diff --git a/src/main/java/nextflow/script/control/TypeCheckingVisitorEx.java b/src/main/java/nextflow/script/control/TypeCheckingVisitorEx.java index 7773154..6639eb9 100644 --- a/src/main/java/nextflow/script/control/TypeCheckingVisitorEx.java +++ b/src/main/java/nextflow/script/control/TypeCheckingVisitorEx.java @@ -39,12 +39,14 @@ import nextflow.script.types.Channel; import nextflow.script.types.ParamsMap; import nextflow.script.types.Record; +import nextflow.script.types.TaskConfig; import nextflow.script.types.Tuple; import nextflow.script.types.TypesEx; import nextflow.script.types.Value; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CodeVisitorSupport; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.GenericsType; import org.codehaus.groovy.ast.MethodNode; @@ -98,6 +100,7 @@ public class TypeCheckingVisitorEx extends ScriptVisitorSupport { private static final ClassNode PATH_TYPE = ClassHelper.makeCached(Path.class); private static final ClassNode PARAMS_TYPE = ClassHelper.makeCached(ParamsMap.class); private static final ClassNode RECORD_TYPE = ClassHelper.makeCached(Record.class); + private static final ClassNode TASK_CONFIG_TYPE = ClassHelper.makeCached(TaskConfig.class); private static final ClassNode TUPLE_TYPE = ClassHelper.makeCached(Tuple.class); private static final ClassNode VALUE_TYPE = ClassHelper.makeCached(Value.class); @@ -234,6 +237,9 @@ public void visitProcessV2(ProcessNodeV2 node) { visit(node.stub); visit(node.outputs); visitProcessTopics(node.topics); + if( !"exec".equals(node.type) ) + checkTaskScriptProperties(node.exec); + checkTaskScriptProperties(node.stub); } private void visitProcessDirectives(Statement block) { @@ -261,6 +267,37 @@ private void visitProcessTopics(Statement block) { } } + /** + * Report an error for each use of a restricted task property (e.g. + * `task.hash`, `task.workDir`) in the process script section. + * + * These properties depend on the task hash, which in turn is computed + * from the process script, so using them in the script creates a + * circular dependency. + * + * @param block + */ + private void checkTaskScriptProperties(Statement block) { + if( block != null ) + block.visit(new TaskScriptPropertyVisitor()); + } + + private class TaskScriptPropertyVisitor extends CodeVisitorSupport { + + private static final List RESTRICTED_TASK_PROPERTIES = List.of("hash", "workDir"); + + @Override + public void visitPropertyExpression(PropertyExpression node) { + super.visitPropertyExpression(node); + var property = node.getPropertyAsString(); + if( !RESTRICTED_TASK_PROPERTIES.contains(property) ) + return; + if( !TASK_CONFIG_TYPE.equals(getType(node.getObjectExpression())) ) + return; + addError("`task." + property + "` is not defined in the process `script:` section", node); + } + } + @Override public void visitFunction(FunctionNode node) { // visit parameters and code diff --git a/src/test/groovy/nextflow/script/types/TypeCheckingTest.groovy b/src/test/groovy/nextflow/script/types/TypeCheckingTest.groovy index 0a5ad9b..73e5818 100644 --- a/src/test/groovy/nextflow/script/types/TypeCheckingTest.groovy +++ b/src/test/groovy/nextflow/script/types/TypeCheckingTest.groovy @@ -993,6 +993,79 @@ class TypeCheckingTest extends Specification { ) } + def 'should report error for task.hash and task.workDir in the process script' () { + when: + def errors = getErrors( + '''\ + nextflow.enable.types = true + + process hello { + script: + "echo ${task.hash} > ${task.workDir}/out.txt" + } + ''' + ) + then: + errors.size() == 2 + errors[0].getStartLine() == 5 + errors[0].getOriginalMessage() == "`task.hash` is not defined in the process `script:` section" + errors[1].getStartLine() == 5 + errors[1].getOriginalMessage() == "`task.workDir` is not defined in the process `script:` section" + } + + def 'should report error for task.hash and task.workDir in the process stub' () { + when: + def errors = getErrors( + '''\ + nextflow.enable.types = true + + process hello { + script: + 'echo hello' + + stub: + "echo ${task.hash} > ${task.workDir}/out.txt" + } + ''' + ) + then: + errors.size() == 2 + errors[0].getStartLine() == 8 + errors[0].getOriginalMessage() == "`task.hash` is not defined in the process `script:` section" + errors[1].getStartLine() == 8 + errors[1].getOriginalMessage() == "`task.workDir` is not defined in the process `script:` section" + } + + def 'should allow other task properties in the process script' () { + expect: + check( + '''\ + nextflow.enable.types = true + + process hello { + script: + "echo ${task.cpus} ${task.process} ${task.attempt}" + } + ''', + null + ) + } + + def 'should allow task.hash and task.workDir in the process exec section' () { + expect: + check( + '''\ + nextflow.enable.types = true + + process hello { + exec: + println "${task.hash} ${task.workDir}" + } + ''', + null + ) + } + def 'should resolve record type' () { when: def exp = parseExpression(