Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ tasks.withType(GroovyCompile).configureEach {
}

dependencies {
groovyCompiler 'org.apache.groovy:groovy:4.0.31'
groovyCompiler 'org.apache.groovy:groovy:4.0.33'

implementation 'io.nextflow:nf-lang:26.09.0-edge'
implementation 'org.apache.groovy:groovy:4.0.31'
implementation 'org.apache.groovy:groovy-json:4.0.31'
implementation 'org.apache.groovy:groovy-yaml:4.0.31'
implementation 'io.nextflow:nf-lang:26.09.1-edge'
implementation 'org.apache.groovy:groovy:4.0.33'
implementation 'org.apache.groovy:groovy-json:4.0.33'
implementation 'org.apache.groovy:groovy-yaml:4.0.33'
implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.0'
implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.23.0'

// runtime dependencies for Nextflow scripts
runtimeOnly 'org.apache.groovy:groovy-templates:4.0.31'
runtimeOnly 'org.apache.groovy:groovy-templates:4.0.33'
runtimeOnly 'org.yaml:snakeyaml:2.2'

// include Nextflow runtime at build-time to extract language definitions
Expand Down
52 changes: 51 additions & 1 deletion src/main/java/nextflow/lsp/ast/ASTNodeStringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package nextflow.lsp.ast;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -29,6 +30,7 @@
import nextflow.script.ast.ProcessNodeV1;
import nextflow.script.ast.ProcessNodeV2;
import nextflow.script.ast.RecordNode;
import nextflow.script.ast.ScriptNode;
import nextflow.script.ast.TupleParameter;
import nextflow.script.ast.WorkflowNode;
import nextflow.script.dsl.Constant;
Expand Down Expand Up @@ -113,7 +115,7 @@ private static String featureFlagToLabel(FeatureFlagNode node) {

private static String workflowToLabel(WorkflowNode node) {
if( node.isEntry() )
return "workflow <entry>";
return entryWorkflowToLabel(node, null);
var fmt = new Formatter(new FormattingOptions(2, true));
fmt.append("workflow ");
fmt.append(node.getName());
Expand Down Expand Up @@ -150,6 +152,54 @@ private static String workflowToLabel(WorkflowNode node) {
return fmt.toString();
}

/**
* An entry workflow is rendered with the params and output blocks
* of its script as its inputs and outputs, since that is how it is
* called when another pipeline includes it.
*
* @param node
* @param name the alias of an included pipeline, or null
*/
public static String entryWorkflowToLabel(WorkflowNode node, String name) {
var pipeline = ScriptNode.getPipeline(node);
var params = pipeline.getParams();
var outputs = pipeline.getOutputs();
var fmt = new Formatter(new FormattingOptions(2, true));
fmt.append("workflow ");
fmt.append(name != null ? name : "<entry>");
if( params == null && outputs == null )
return fmt.toString();

fmt.append(" {\n");
fmt.incIndent();
fmt.appendIndent();
fmt.append("params:\n");
if( params == null || params.declarations.length == 0 ) {
fmt.appendIndent();
fmt.append("<none>\n");
}
for( var param : params != null ? params.declarations : new Parameter[0] ) {
fmt.appendIndent();
typedInput(param, fmt);
fmt.appendNewLine();
}
fmt.appendNewLine();
fmt.appendIndent();
fmt.append("output:\n");
if( outputs == null || outputs.declarations.isEmpty() ) {
fmt.appendIndent();
fmt.append("<none>\n");
}
for( var output : outputs != null ? outputs.declarations : List.<Parameter>of() ) {
fmt.appendIndent();
typedInput(output, fmt);
fmt.appendNewLine();
}
fmt.decIndent();
fmt.append('}');
return fmt.toString();
}

private static void typedOutput(Expression output, Formatter fmt) {
if( output instanceof AssignmentExpression assign ) {
var target = assign.getLeftExpression();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/nextflow/lsp/ast/LanguageServerASTUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static ASTNode getDefinition(ASTNode node) {
return cce.getType().redirect();

if( node instanceof MapEntryExpression ) {
var namedParam = (Parameter) node.getNodeMetaData("_NAMED_PARAM");
var namedParam = (Parameter) node.getNodeMetaData(ASTNodeMarker.NAMED_PARAM);
if( namedParam != null )
return namedParam;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
import nextflow.lsp.services.HoverProvider;
import nextflow.lsp.util.Logger;
import nextflow.script.ast.FunctionNode;
import nextflow.script.ast.IncludeEntryNode;
import nextflow.script.ast.ProcessNode;
import nextflow.script.ast.WorkflowNode;
import nextflow.script.dsl.Types;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.Statement;
Expand Down Expand Up @@ -79,7 +81,9 @@ public Hover hover(TextDocumentIdentifier textDocument, Position position) {

var builder = new StringBuilder();

var label = ASTNodeStringUtils.getLabel(defNode);
var label = defNode instanceof WorkflowNode wn && wn.isEntry()
? ASTNodeStringUtils.entryWorkflowToLabel(wn, getAlias(offsetNode))
: ASTNodeStringUtils.getLabel(defNode);
if( label != null ) {
builder.append("```nextflow\n");
builder.append(label);
Expand Down Expand Up @@ -124,4 +128,19 @@ public Hover hover(TextDocumentIdentifier textDocument, Position position) {
return new Hover(new MarkupContent(MarkupKind.MARKDOWN, value));
}

/**
* Get the name by which an included pipeline is referenced.
*
* @param node
*/
private static String getAlias(ASTNode node) {
if( node instanceof MethodCallExpression mce )
return mce.getMethodAsString();
if( node instanceof VariableExpression ve )
return ve.getName();
if( node instanceof IncludeEntryNode entry )
return entry.getNameOrAlias();
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private List<? extends Object> children(WorkflowNode node) {
.filter(mn -> mn instanceof ProcessNode || mn instanceof WorkflowNode)
.distinct()
.map(mn -> Map.of(
"name", mn.getName(),
"name", mn.getName() != null ? mn.getName() : "<entry>",
"path", ast.getURI(mn).getPath()
))
.toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ private void visitPipeline(BinaryExpression node) {
if( rhs instanceof VariableExpression ve ) {
var defNode = asMethodVariable(ve.getAccessedVariable());
if( defNode instanceof WorkflowNode || defNode instanceof ProcessNode ) {
var label = defNode.getName();
var label = ve.getName();
var preds = visitWithPreds(lhs);
var dn = addNode(label, Node.Type.OPERATOR, defNode, preds);
vc.putSymbol(label, dn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
package nextflow.lsp.ast

import groovy.lang.groovydoc.Groovydoc
import nextflow.script.ast.ASTNodeMarker
import nextflow.script.ast.FeatureFlagNode
import nextflow.script.ast.FunctionNode
import nextflow.script.ast.ProcessNodeV1
import nextflow.script.ast.ScriptNode
import nextflow.script.ast.WorkflowNode
import nextflow.script.dsl.FeatureFlagDsl
import nextflow.script.dsl.ProcessDsl
Expand Down Expand Up @@ -81,6 +83,7 @@ class ASTNodeStringUtilsTest extends Specification {
when:
def entry = Mock(WorkflowNode) {
isEntry() >> true
getNodeMetaData(ASTNodeMarker.PIPELINE_SCRIPT) >> Mock(ScriptNode)
}
then:
ASTNodeStringUtils.getLabel(entry) == 'workflow <entry>'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2024-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.lsp.services.script

import nextflow.lsp.TestLanguageClient
import org.eclipse.lsp4j.HoverParams
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.TextDocumentIdentifier
import spock.lang.Specification

import static nextflow.lsp.TestUtils.*

/**
* Tests for including a pipeline -- the `params` / `workflow` / `output`
* trio of a script -- as a named workflow.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
class ScriptPipelineCompositionTest extends Specification {

static final String PIPELINE = '''\
nextflow.enable.types = true

params {
input: Path
aligner: String = 'star'
}

workflow {
main:
ch_bams = channel.of(params.input)

publish:
bams = ch_bams
}

output {
bams: Channel<Path> { path 'bams' }
}
'''

ScriptService pipelineService(TestLanguageClient client = new TestLanguageClient()) {
def service = getScriptService(client)
open(service, getUri('rnaseq.nf'), PIPELINE)
return service
}

def 'should not report diagnostics for a pipeline include' () {
given:
def client = new TestLanguageClient()
def service = pipelineService(client)
def uri = getUri('main.nf')

when:
open(service, uri, '''\
nextflow.enable.types = true

include {
params as RnaseqParams ;
workflow as RNASEQ
} from './rnaseq.nf'

params {
rnaseq: RnaseqParams
}

workflow {
main:
rnaseq = RNASEQ( params.rnaseq )

publish:
bams = rnaseq
}

output {
bams: Channel<Path> { path 'bams' }
}
''')
service.updateNow()
then:
client.getDiagnostics(uri).isEmpty()
}

def 'should get the hover of an included pipeline' () {
given:
def service = pipelineService()
def uri = getUri('main.nf')

when:
open(service, uri, '''\
nextflow.enable.types = true

include { workflow as RNASEQ } from './rnaseq.nf'

workflow {
main:
RNASEQ( record(input: file('sample.fq')) )
}
''')
service.updateNow()
def hover = service.hover(new HoverParams(new TextDocumentIdentifier(uri), new Position(6, 4)))
then:
def value = hover.getContents().getRight().getValue()
value.contains('workflow RNASEQ {')
value.contains('input: Path')
value.contains('bams: Channel<Path>')
}

}
Loading