Skip to content

Commit 9cd8303

Browse files
committed
Resolve plugin includes in script
1 parent 7c99338 commit 9cd8303

12 files changed

+311
-40
lines changed

src/main/java/nextflow/lsp/NextflowLanguageServer.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ public static void main(String[] args) {
120120
private LanguageClient client = null;
121121

122122
private Map<String, String> workspaceRoots = new HashMap<>();
123-
private Map<String, LanguageService> scriptServices = new HashMap<>();
124-
private Map<String, LanguageService> configServices = new HashMap<>();
123+
private Map<String, ConfigService> configServices = new HashMap<>();
124+
private Map<String, ScriptService> scriptServices = new HashMap<>();
125125

126126
private LanguageServerConfiguration configuration = LanguageServerConfiguration.defaults();
127127

@@ -498,8 +498,8 @@ private void initializeWorkspaces() {
498498
progress.update(progressMessage, count * 100 / total);
499499
count++;
500500

501-
scriptServices.get(name).initialize(configuration);
502501
configServices.get(name).initialize(configuration);
502+
scriptServices.get(name).initialize(configuration, configServices.get(name).getPluginSpecCache());
503503
}
504504

505505
progress.end();
@@ -517,16 +517,16 @@ public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) {
517517
var name = workspaceFolder.getName();
518518
log.debug("workspace/didChangeWorkspaceFolders remove " + name);
519519
workspaceRoots.remove(name);
520-
scriptServices.remove(name).clearDiagnostics();
521520
configServices.remove(name).clearDiagnostics();
521+
scriptServices.remove(name).clearDiagnostics();
522522
}
523523
for( var workspaceFolder : event.getAdded() ) {
524524
var name = workspaceFolder.getName();
525525
var uri = workspaceFolder.getUri();
526526
log.debug("workspace/didChangeWorkspaceFolders add " + name + " " + uri);
527527
addWorkspaceFolder(name, uri);
528-
scriptServices.get(name).initialize(configuration);
529528
configServices.get(name).initialize(configuration);
529+
scriptServices.get(name).initialize(configuration, configServices.get(name).getPluginSpecCache());
530530
}
531531
}
532532

@@ -586,13 +586,13 @@ public CompletableFuture<Either<List<? extends SymbolInformation>, List<? extend
586586
private void addWorkspaceFolder(String name, String uri) {
587587
workspaceRoots.put(name, uri);
588588

589-
var scriptService = new ScriptService(uri);
590-
scriptService.connect(client);
591-
scriptServices.put(name, scriptService);
592-
593589
var configService = new ConfigService(uri);
594590
configService.connect(client);
595591
configServices.put(name, configService);
592+
593+
var scriptService = new ScriptService(uri);
594+
scriptService.connect(client);
595+
scriptServices.put(name, scriptService);
596596
}
597597

598598
private String relativePath(String uri) {
@@ -620,12 +620,12 @@ private LanguageService getLanguageService(String uri) {
620620
return service;
621621
}
622622

623-
private LanguageService getLanguageService0(String uri, Map<String, LanguageService> services) {
623+
private LanguageService getLanguageService0(String uri, Map<String, ? extends LanguageService> services) {
624624
var service = workspaceRoots.entrySet().stream()
625625
.filter((entry) -> entry.getValue() != null && uri.startsWith(entry.getValue()))
626626
.findFirst()
627-
.map((entry) -> services.get(entry.getKey()))
628-
.orElse(services.get(DEFAULT_WORKSPACE_FOLDER_NAME));
627+
.map((entry) -> (LanguageService) services.get(entry.getKey()))
628+
.orElse((LanguageService) services.get(DEFAULT_WORKSPACE_FOLDER_NAME));
629629
if( service == null || !service.matchesFile(uri) )
630630
return null;
631631
return service;

src/main/java/nextflow/lsp/services/config/ConfigAstCache.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import nextflow.lsp.compiler.LanguageServerErrorCollector;
3232
import nextflow.lsp.file.FileCache;
3333
import nextflow.lsp.services.LanguageServerConfiguration;
34+
import nextflow.lsp.spec.PluginSpecCache;
3435
import nextflow.script.control.PhaseAware;
3536
import nextflow.script.control.Phases;
3637
import nextflow.script.types.Types;
@@ -66,9 +67,9 @@ private static CompilerConfiguration createConfiguration() {
6667
return config;
6768
}
6869

69-
public void initialize(LanguageServerConfiguration configuration) {
70+
public void initialize(LanguageServerConfiguration configuration, PluginSpecCache pluginSpecCache) {
7071
this.configuration = configuration;
71-
this.pluginSpecCache = new PluginSpecCache(configuration.pluginRegistryUrl());
72+
this.pluginSpecCache = pluginSpecCache;
7273
}
7374

7475
@Override

src/main/java/nextflow/lsp/services/config/ConfigCompletionProvider.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@
6161
*/
6262
public class ConfigCompletionProvider implements CompletionProvider {
6363

64-
private static final List<CompletionItem> TOPLEVEL_ITEMS = topLevelItems();
65-
6664
private static Logger log = Logger.getInstance();
6765

6866
private ConfigAstCache ast;
@@ -85,17 +83,18 @@ public Either<List<CompletionItem>, CompletionList> completion(TextDocumentIdent
8583
return Either.forLeft(Collections.emptyList());
8684

8785
var nodeStack = ast.getNodesAtPosition(uri, position);
86+
var schema = ast.getConfigNode(uri).getSchema();
8887
if( nodeStack.isEmpty() )
89-
return Either.forLeft(TOPLEVEL_ITEMS);
88+
return Either.forLeft(topLevelItems(schema));
9089

9190
if( isConfigExpression(nodeStack) ) {
9291
addCompletionItems(nodeStack);
9392
}
9493
else {
9594
var names = currentConfigScope(nodeStack);
9695
if( names.isEmpty() )
97-
return Either.forLeft(TOPLEVEL_ITEMS);
98-
addConfigOptions(names, ast.getConfigNode(uri).getSchema());
96+
return Either.forLeft(topLevelItems(schema));
97+
addConfigOptions(names, schema);
9998
}
10099

101100
return ch.isIncomplete()
@@ -190,10 +189,9 @@ private void addConfigOptions(List<String> names, SchemaNode.Scope schema) {
190189
});
191190
}
192191

193-
private static List<CompletionItem> topLevelItems() {
194-
var defaultScopes = ConfigSchemaFactory.defaultScopes();
192+
private static List<CompletionItem> topLevelItems(SchemaNode.Scope schema) {
195193
var result = new ArrayList<CompletionItem>();
196-
defaultScopes.forEach((name, child) -> {
194+
schema.children().forEach((name, child) -> {
197195
if( child instanceof SchemaNode.Option option ) {
198196
result.add(configOption(name, option.description(), option.type()));
199197
}

src/main/java/nextflow/lsp/services/config/ConfigSchemaVisitor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import nextflow.config.ast.ConfigNode;
2828
import nextflow.config.ast.ConfigVisitorSupport;
2929
import nextflow.config.schema.SchemaNode;
30+
import nextflow.lsp.spec.ConfigSpecFactory;
31+
import nextflow.lsp.spec.PluginSpecCache;
3032
import nextflow.script.control.PhaseAware;
3133
import nextflow.script.control.Phases;
3234
import nextflow.script.types.TypesEx;
@@ -83,7 +85,7 @@ public void visit() {
8385
}
8486

8587
private SchemaNode.Scope getPluginScopes(ConfigNode cn) {
86-
var defaultScopes = ConfigSchemaFactory.defaultScopes();
88+
var defaultScopes = ConfigSpecFactory.defaultScopes();
8789
var pluginScopes = pluginConfigScopes(cn);
8890
var children = new HashMap<String, SchemaNode>();
8991
children.putAll(defaultScopes);
@@ -113,6 +115,7 @@ private Map<String, SchemaNode> pluginConfigScopes(ConfigNode cn) {
113115
return pluginSpecCache.get(name, version);
114116
})
115117
.filter(spec -> spec != null)
118+
.map(spec -> spec.configScopes())
116119
.toList();
117120

118121
var result = new HashMap<String, SchemaNode>();

src/main/java/nextflow/lsp/services/config/ConfigService.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import nextflow.lsp.services.LanguageService;
2424
import nextflow.lsp.services.LinkProvider;
2525
import nextflow.lsp.services.SemanticTokensProvider;
26+
import nextflow.lsp.spec.PluginSpecCache;
2627

2728
/**
2829
* Implementation of language services for Nextflow config files.
@@ -31,6 +32,8 @@
3132
*/
3233
public class ConfigService extends LanguageService {
3334

35+
private PluginSpecCache pluginSpecCache;
36+
3437
private ConfigAstCache astCache;
3538

3639
public ConfigService(String rootUri) {
@@ -46,11 +49,16 @@ public boolean matchesFile(String uri) {
4649
@Override
4750
public void initialize(LanguageServerConfiguration configuration) {
4851
synchronized (this) {
49-
astCache.initialize(configuration);
52+
pluginSpecCache = new PluginSpecCache(configuration.pluginRegistryUrl());
53+
astCache.initialize(configuration, pluginSpecCache);
5054
}
5155
super.initialize(configuration);
5256
}
5357

58+
public PluginSpecCache getPluginSpecCache() {
59+
return pluginSpecCache;
60+
}
61+
5462
@Override
5563
protected ASTNodeCache getAstCache() {
5664
return astCache;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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.lsp.services.script;
17+
18+
import java.util.List;
19+
20+
import nextflow.lsp.spec.PluginSpecCache;
21+
import nextflow.script.ast.IncludeNode;
22+
import nextflow.script.ast.ScriptNode;
23+
import nextflow.script.ast.ScriptVisitorSupport;
24+
import nextflow.script.control.PhaseAware;
25+
import nextflow.script.control.Phases;
26+
import org.codehaus.groovy.ast.ASTNode;
27+
import org.codehaus.groovy.ast.MethodNode;
28+
import org.codehaus.groovy.control.SourceUnit;
29+
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
30+
import org.codehaus.groovy.syntax.SyntaxException;
31+
32+
/**
33+
* Resolve plugin includes against plugin specs.
34+
*
35+
* @author Ben Sherman <[email protected]>
36+
*/
37+
public class ResolvePluginIncludeVisitor extends ScriptVisitorSupport {
38+
39+
private SourceUnit sourceUnit;
40+
41+
private PluginSpecCache pluginSpecCache;
42+
43+
public ResolvePluginIncludeVisitor(SourceUnit sourceUnit, PluginSpecCache pluginSpecCache) {
44+
this.sourceUnit = sourceUnit;
45+
this.pluginSpecCache = pluginSpecCache;
46+
}
47+
48+
@Override
49+
protected SourceUnit getSourceUnit() {
50+
return sourceUnit;
51+
}
52+
53+
public void visit() {
54+
var moduleNode = sourceUnit.getAST();
55+
if( moduleNode instanceof ScriptNode sn )
56+
super.visit(sn);
57+
}
58+
59+
@Override
60+
public void visitInclude(IncludeNode node) {
61+
var source = node.source.getText();
62+
if( !source.startsWith("plugin/") )
63+
return;
64+
var pluginName = source.split("/")[1];
65+
var spec = pluginSpecCache.get(pluginName, null);
66+
if( spec == null ) {
67+
addError("Plugin '" + pluginName + "' does not exist or is not specified in the configuration file", node);
68+
return;
69+
}
70+
for( var entry : node.entries ) {
71+
var entryName = entry.name;
72+
var mn = findMethod(spec.functions(), entryName);
73+
if( mn != null ) {
74+
entry.setTarget(mn);
75+
continue;
76+
}
77+
if( findMethod(spec.factories(), entryName) != null )
78+
continue;
79+
if( findMethod(spec.operators(), entryName) != null )
80+
continue;
81+
addError("Included name '" + entryName + "' is not defined in plugin '" + pluginName + "'", node);
82+
}
83+
}
84+
85+
private static MethodNode findMethod(List<MethodNode> methods, String name) {
86+
return methods.stream()
87+
.filter(mn -> mn.getName().equals(name))
88+
.findFirst().orElse(null);
89+
}
90+
91+
@Override
92+
public void addError(String message, ASTNode node) {
93+
var cause = new ResolveIncludeError(message, node);
94+
var errorMessage = new SyntaxErrorMessage(cause, sourceUnit);
95+
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
96+
}
97+
98+
private class ResolveIncludeError extends SyntaxException implements PhaseAware {
99+
100+
public ResolveIncludeError(String message, ASTNode node) {
101+
super(message, node);
102+
}
103+
104+
@Override
105+
public int getPhase() {
106+
return Phases.INCLUDE_RESOLUTION;
107+
}
108+
}
109+
}

src/main/java/nextflow/lsp/services/script/ScriptAstCache.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import nextflow.lsp.compiler.LanguageServerErrorCollector;
3131
import nextflow.lsp.file.FileCache;
3232
import nextflow.lsp.services.LanguageServerConfiguration;
33+
import nextflow.lsp.spec.PluginSpecCache;
3334
import nextflow.script.ast.FunctionNode;
3435
import nextflow.script.ast.IncludeNode;
3536
import nextflow.script.ast.ProcessNode;
@@ -60,6 +61,8 @@ public class ScriptAstCache extends ASTNodeCache {
6061

6162
private LanguageServerConfiguration configuration;
6263

64+
private PluginSpecCache pluginSpecCache;
65+
6366
public ScriptAstCache(String rootUri) {
6467
super(createCompiler());
6568
this.libCache = createLibCache(rootUri);
@@ -89,8 +92,9 @@ private static CompilerConfiguration createConfiguration() {
8992
return config;
9093
}
9194

92-
public void initialize(LanguageServerConfiguration configuration) {
95+
public void initialize(LanguageServerConfiguration configuration, PluginSpecCache pluginSpecCache) {
9396
this.configuration = configuration;
97+
this.pluginSpecCache = pluginSpecCache;
9498
}
9599

96100
@Override
@@ -111,6 +115,8 @@ protected Set<URI> analyze(Set<URI> uris, FileCache fileCache) {
111115
var visitor = new ResolveIncludeVisitor(sourceUnit, compiler(), uris);
112116
visitor.visit();
113117

118+
new ResolvePluginIncludeVisitor(sourceUnit, pluginSpecCache).visit();
119+
114120
var uri = sourceUnit.getSource().getURI();
115121
if( visitor.isChanged() ) {
116122
var errorCollector = (LanguageServerErrorCollector) sourceUnit.getErrorCollector();

src/main/java/nextflow/lsp/services/script/ScriptService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import nextflow.lsp.services.RenameProvider;
3333
import nextflow.lsp.services.SemanticTokensProvider;
3434
import nextflow.lsp.services.SymbolProvider;
35+
import nextflow.lsp.spec.PluginSpecCache;
3536

3637
/**
3738
* Implementation of language services for Nextflow scripts.
@@ -52,10 +53,9 @@ public boolean matchesFile(String uri) {
5253
return uri.endsWith(".nf");
5354
}
5455

55-
@Override
56-
public void initialize(LanguageServerConfiguration configuration) {
56+
public void initialize(LanguageServerConfiguration configuration, PluginSpecCache pluginSpecCache) {
5757
synchronized (this) {
58-
astCache.initialize(configuration);
58+
astCache.initialize(configuration, pluginSpecCache);
5959
}
6060
super.initialize(configuration);
6161
}

0 commit comments

Comments
 (0)