diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ProcessToGroovyVisitorV1.java b/modules/nf-lang/src/main/java/nextflow/script/control/ProcessToGroovyVisitorV1.java index 19988b1b34..e0263a824d 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ProcessToGroovyVisitorV1.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ProcessToGroovyVisitorV1.java @@ -68,7 +68,8 @@ public Statement transform(ProcessNodeV1 node) { args( closureX(null, node.exec), constX(sgh.getSourceText(node.exec)), - constX(node.type) + constX(node.type), + sgh.getVariableRefs(node.exec) ) )) ))); diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ProcessToGroovyVisitorV2.java b/modules/nf-lang/src/main/java/nextflow/script/control/ProcessToGroovyVisitorV2.java index 58b13c7a47..11b645fe41 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ProcessToGroovyVisitorV2.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ProcessToGroovyVisitorV2.java @@ -91,7 +91,8 @@ public Statement transform(ProcessNodeV2 node) { args( closureX(node.exec), constX(sgh.getSourceText(node.exec)), - constX(node.type) + constX(node.type), + sgh.getVariableRefs(node.exec) ) )) ))); diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyHelper.java b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyHelper.java index 467c9c8c04..e9f44c49e3 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyHelper.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyHelper.java @@ -15,6 +15,8 @@ */ package nextflow.script.control; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -22,10 +24,12 @@ import org.codehaus.groovy.ast.Variable; import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.PropertyExpression; import org.codehaus.groovy.ast.expr.VariableExpression; import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.control.SourceUnit; +import static nextflow.script.ast.ASTUtils.*; import static org.codehaus.groovy.ast.tools.GeneralUtils.*; /** @@ -41,6 +45,75 @@ public ScriptToGroovyHelper(SourceUnit sourceUnit) { this.sourceUnit = sourceUnit; } + /** + * Get the list of variable references in a statement. + * + * This method is used to collect references to task ext + * properties (e.g. `task.ext.args`) in the process body, so that + * they are included in the task hash. + * + * These properties are typically used like inputs, but are not + * explicitly declared, so they must be identified by their usage. + * + * The resulting list expression should be provided as the fourth + * argument of the BodyDef constructor. + * + * @param node + */ + public Expression getVariableRefs(Statement node) { + var refs = new VariableRefCollector().collect(node).stream() + .map(name -> createX("nextflow.script.TokenValRef", constX(name))) + .toList(); + + return listX(refs); + } + + private class VariableRefCollector extends CodeVisitorSupport { + + private Set variableRefs; + + public Set collect(Statement node) { + variableRefs = new HashSet<>(); + visit(node); + return variableRefs; + } + + @Override + public void visitPropertyExpression(PropertyExpression node) { + if( !isPropertyChain(node) ) { + super.visitPropertyExpression(node); + return; + } + + var name = asPropertyChain(node); + if( name.startsWith("task.ext.") ) + variableRefs.add(name); + } + + private static boolean isPropertyChain(PropertyExpression node) { + var target = node.getObjectExpression(); + while( target instanceof PropertyExpression pe ) + target = pe.getObjectExpression(); + return target instanceof VariableExpression; + } + + private static String asPropertyChain(PropertyExpression node) { + var list = new ArrayList(); + list.add(node.getPropertyAsString()); + + var target = node.getObjectExpression(); + while( target instanceof PropertyExpression pe ) { + list.add(pe.getPropertyAsString()); + target = pe.getObjectExpression(); + } + + list.add(target.getText()); + + Collections.reverse(list); + return String.join(".", list); + } + } + /** * Transform an expression into a lazy expression by * wrapping it in a closure if it references variables. diff --git a/tests/checks/task-ext.nf/.checks b/tests/checks/task-ext.nf/.checks new file mode 100644 index 0000000000..2e976fb1d2 --- /dev/null +++ b/tests/checks/task-ext.nf/.checks @@ -0,0 +1,30 @@ +set -e + +# +# initial run +# +$NXF_RUN -c ../../task-ext.config --value foo | tee .stdout + +[[ `grep INFO .nextflow.log | grep -c 'Submitted process'` == 1 ]] || false + +grep 'args = --some-param foo' .stdout + + +# +# resumed run (same value) +# +$NXF_RUN -c ../../task-ext.config --value foo -resume | tee .stdout + +[[ `grep INFO .nextflow.log | grep -c 'Cached process'` == 1 ]] || false + +grep 'args = --some-param foo' .stdout + + +# +# resumed run (new value) +# +$NXF_RUN -c ../../task-ext.config --value bar -resume | tee .stdout + +[[ `grep INFO .nextflow.log | grep -c 'Submitted process'` == 1 ]] || false + +grep 'args = --some-param bar' .stdout diff --git a/tests/task-ext.config b/tests/task-ext.config new file mode 100644 index 0000000000..e19aae3bb1 --- /dev/null +++ b/tests/task-ext.config @@ -0,0 +1,10 @@ + +params { + value = 'value' +} + +process { + withName: 'HELLO' { + ext.args = "--some-param ${params.value}" + } +} diff --git a/tests/task-ext.nf b/tests/task-ext.nf new file mode 100644 index 0000000000..ba2519e4b7 --- /dev/null +++ b/tests/task-ext.nf @@ -0,0 +1,13 @@ + +process HELLO { + debug true + + script: + """ + echo "args = ${task.ext.args}" + """ +} + +workflow { + HELLO() +}