Skip to content
Draft
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
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ configurations {
}

dependencies {
implementation 'io.nextflow:nf-lang:25.07.0-edge'
implementation 'io.nextflow:nf-lang:25.08.0-edge'
implementation 'org.apache.groovy:groovy:4.0.28'
implementation 'org.apache.groovy:groovy-json:4.0.28'
implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.0'
Expand All @@ -45,7 +45,7 @@ dependencies {
runtimeOnly 'org.yaml:snakeyaml:2.2'

// include Nextflow runtime at build-time to extract language definitions
nextflowRuntime 'io.nextflow:nextflow:25.07.0-edge'
nextflowRuntime 'io.nextflow:nextflow:25.08.0-edge'
nextflowRuntime 'io.nextflow:nf-amazon:3.1.0'
nextflowRuntime 'io.nextflow:nf-azure:1.19.0'
nextflowRuntime 'io.nextflow:nf-google:1.22.2'
Expand Down
53 changes: 38 additions & 15 deletions src/main/java/nextflow/lsp/services/config/ConfigSchemaFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@
import org.codehaus.groovy.runtime.IOGroovyMethods;

/**
* Load the config schema from the compiler as well as
* the index file.
* Load config scopes from various sources, including
* built-in definitions and index files.
*
* @author Ben Sherman <[email protected]>
*/
public class ConfigSchemaFactory {

/**
* Load config scopes from the compiler and index
* file, which defines config scopes for core plugins.
*/
public static SchemaNode.Scope load() {
var scope = SchemaNode.ROOT;
scope.children().putAll(fromIndex());
Expand All @@ -55,6 +59,25 @@ private static Map<String,SchemaNode> fromIndex() {
}
}

/**
* Load config scopes from a list of definitions (e.g. for
* third-party plugins).
*
* @param definitions
*/
public static Map<String,SchemaNode> fromDefinitions(List<Map> definitions) {
var entries = definitions.stream()
.filter(node -> "ConfigScope".equals(node.get("type")))
.map((node) -> {
var spec = (Map) node.get("spec");
var name = (String) spec.get("name");
var scope = fromScope(spec);
return Map.entry(name, scope);
})
.toArray(Map.Entry[]::new);
return Map.ofEntries(entries);
}

private static Map<String,SchemaNode> fromChildren(Map<String,?> children) {
var entries = children.entrySet().stream()
.map((entry) -> {
Expand All @@ -70,34 +93,34 @@ private static SchemaNode fromNode(Map<String,?> node) {
var type = (String) node.get("type");
var spec = (Map<String,?>) node.get("spec");

if( "Option".equals(type) )
if( "ConfigOption".equals(type) )
return fromOption(spec);

if( "Placeholder".equals(type) )
if( "ConfigPlaceholderScope".equals(type) )
return fromPlaceholder(spec);

if( "Scope".equals(type) )
if( "ConfigScope".equals(type) )
return fromScope(spec);

throw new IllegalStateException();
}

private static SchemaNode.Option fromOption(Map<String,?> node) {
var description = (String) node.get("description");
var type = fromType(node.get("type"));
private static SchemaNode.Option fromOption(Map<String,?> spec) {
var description = (String) spec.get("description");
var type = fromType(spec.get("type"));
return new SchemaNode.Option(description, type);
}

private static SchemaNode.Placeholder fromPlaceholder(Map<String,?> node) {
var description = (String) node.get("description");
var placeholderName = (String) node.get("placeholderName");
var scope = fromScope((Map<String,?>) node.get("scope"));
private static SchemaNode.Placeholder fromPlaceholder(Map<String,?> spec) {
var description = (String) spec.get("description");
var placeholderName = (String) spec.get("placeholderName");
var scope = fromScope((Map<String,?>) spec.get("scope"));
return new SchemaNode.Placeholder(description, placeholderName, scope);
}

private static SchemaNode.Scope fromScope(Map<String,?> node) {
var description = (String) node.get("description");
var children = fromChildren((Map<String,?>) node.get("children"));
private static SchemaNode.Scope fromScope(Map<String,?> spec) {
var description = (String) spec.get("description");
var children = fromChildren((Map<String,?>) spec.get("children"));
return new SchemaNode.Scope(description, children);
}

Expand Down
107 changes: 106 additions & 1 deletion src/main/java/nextflow/lsp/services/config/ConfigSchemaVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@
*/
package nextflow.lsp.services.config;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import groovy.json.JsonSlurper;
import nextflow.config.ast.ConfigApplyBlockNode;
import nextflow.config.ast.ConfigAssignNode;
import nextflow.config.ast.ConfigBlockNode;
import nextflow.config.ast.ConfigNode;
Expand All @@ -27,14 +36,23 @@
import nextflow.script.control.Phases;
import nextflow.script.types.TypesEx;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.control.messages.WarningMessage;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.StringGroovyMethods;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Token;

import static java.net.http.HttpResponse.BodyHandlers;
import static nextflow.script.ast.ASTUtils.*;

/**
* Validate config options against the config schema.
*
* Config scopes from third-party plugins are inferred
* from the `plugins` block, if specified.
*
* @author Ben Sherman <[email protected]>
*/
Expand All @@ -61,8 +79,95 @@ protected SourceUnit getSourceUnit() {

public void visit() {
var moduleNode = sourceUnit.getAST();
if( moduleNode instanceof ConfigNode cn )
if( moduleNode instanceof ConfigNode cn ) {
loadPluginScopes(cn);
super.visit(cn);
}
}

private void loadPluginScopes(ConfigNode cn) {
try {
var defaultScopes = schema.children();
var pluginScopes = pluginConfigScopes(cn);
var children = new HashMap<String, SchemaNode>();
children.putAll(defaultScopes);
children.putAll(pluginScopes);
this.schema = new SchemaNode.Scope(schema.description(), children);
}
catch( Exception e ) {
System.err.println("Failed to load plugin config scopes: " + e.toString());
}
}

private static final String PLUGIN_REGITRY_URL = "http://localhost:8080/api/";

private Map<String, SchemaNode> pluginConfigScopes(ConfigNode cn) {
var client = HttpClient.newBuilder().build();
var baseUri = URI.create(PLUGIN_REGITRY_URL);

var entries = cn.getConfigStatements().stream()

// extract plugin specs from `plugins` block
.map(stmt -> stmt instanceof ConfigApplyBlockNode node ? node : null)
.filter(node -> node != null && "plugins".equals(node.name))
.flatMap(node -> node.statements.stream())
.map((call) -> {
var arguments = asMethodCallArguments(call);
var firstArg = arguments.get(0);
return firstArg instanceof ConstantExpression ce ? ce.getText() : null;
})

// fetch plugin definitions from plugin registry
.filter(spec -> spec != null)
.map((spec) -> {
var tokens = StringGroovyMethods.tokenize(spec, "@");
var name = tokens.get(0);
var version = tokens.size() == 2 ? tokens.get(1) : null;
var path = version != null
? "v1/plugins/" + name + "/" + version
: "v1/plugins/" + name;
var request = HttpRequest.newBuilder()
.uri(baseUri.resolve(path))
.GET()
.header("Accept", "application/json")
.build();
try {
var response = client.send(request, BodyHandlers.ofString());
var json = new JsonSlurper().parseText(response.body());
return json instanceof Map m ? m : null;
}
catch( IOException | InterruptedException e ) {
return null;
}
})

// select latest plugin version if not specified
.filter(json -> json != null)
.map((json) -> {
if( json.containsKey("plugin") ) {
var plugin = (Map) json.get("plugin");
var releases = (List) plugin.get("releases");
return (Map) releases.get(0);
}
if( json.containsKey("pluginRelease") ) {
return (Map) json.get("pluginRelease");
}
return null;
})

// load config scopes from JSON data
.filter(release -> release != null)
.map((release) -> {
var text = (String) release.get("definitions");
var json = new JsonSlurper().parseText(text);
return ConfigSchemaFactory.fromDefinitions((List<Map>) json);
})
.toList();

var result = new HashMap<String, SchemaNode>();
for( var entry : entries )
result.putAll(entry);
return result;
}

@Override
Expand Down
Loading