Skip to content
Open
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
15 changes: 15 additions & 0 deletions src/main/java/nextflow/lsp/ast/ASTNodeCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ public void clear() {
* @param fileCache
*/
public Set<URI> update(Set<URI> uris, FileCache fileCache) {
// expand the invalidation set to include dependents (e.g. files that
// include a changed module) so their cached analysis is recomputed
uris = expandInvalidation(uris);

// invalidate cache for each source file
for( var uri : uris ) {
var nodes = nodesByURI.remove(uri);
Expand Down Expand Up @@ -158,6 +162,17 @@ public Set<URI> update(Set<URI> uris, FileCache fileCache) {
return result;
}

/**
* Expand a set of changed files to include any cached files whose analysis
* depends on them (e.g. files that include a changed module). The default
* implementation returns the set unchanged.
*
* @param uris
*/
protected Set<URI> expandInvalidation(Set<URI> uris) {
return uris;
}

/**
* Perform additional AST analysis for a set of source files.
* Return the set of files whose errors have changed.
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/nextflow/lsp/services/script/ScriptAstCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -157,6 +159,40 @@ protected Set<URI> analyze(Set<URI> uris, FileCache fileCache) {
return changedUris;
}

/**
* Expand a set of changed files to include every cached file that
* (transitively) includes one of them. Those consumers must be re-parsed
* so that their cross-file inferred types (e.g. a process's record output)
* are recomputed from fresh AST nodes rather than stale cached metadata.
*
* @param uris
*/
@Override
protected Set<URI> expandInvalidation(Set<URI> uris) {
// reverse include graph: module uri -> files that include it
var dependents = new HashMap<URI, Set<URI>>();
for( var uri : getUris() ) {
for( var include : getIncludeNodes(uri) ) {
var depUri = localIncludeUri(uri, include.source.getText());
if( depUri != null )
dependents.computeIfAbsent(depUri, (k) -> new HashSet<>()).add(uri);
}
}
if( dependents.isEmpty() )
return uris;

var result = new HashSet<>(uris);
var queue = new ArrayDeque<>(uris);
while( !queue.isEmpty() ) {
var uri = queue.poll();
for( var consumer : dependents.getOrDefault(uri, Collections.emptySet()) ) {
if( result.add(consumer) )
queue.add(consumer);
}
}
return result;
}

/**
* Order the given URIs so that a file appears after every module it
* includes (dependencies first), via depth-first post-order traversal.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,79 @@ class ScriptDiagnosticsTest extends Specification {
diagnostics.findAll { it.message.contains('is not compatible with process input') } == []
}

def 'should re-check a consumer when an included module changes' () {
given:
def client = new TestLanguageClient()
def service = getScriptService(client)
def mainUri = getUri('workflow.nf')
def producerUri = getUri('producer.nf')
def consumerUri = getUri('consumer.nf')

def producer = { String outputExtra -> """\
nextflow.enable.types = true

process PRODUCER {
input:
record(id: String)

output:
record(id: id${outputExtra})

script:
\"\"\"
echo hello > data.txt
\"\"\"
}
""" }
def consumer = '''\
nextflow.enable.types = true

process CONSUMER {
input:
record(id: String, data: Path)

output:
record(id: id)

script:
"""
cat ${data}
"""
}
'''
def main = '''\
nextflow.enable.types = true

include { PRODUCER } from './producer.nf'
include { CONSUMER } from './consumer.nf'

workflow {
ch_produced = PRODUCER(channel.of(record(id: 'sample1')))
CONSUMER(ch_produced)
}
'''

when: 'producer emits a compatible record { id, data }'
open(service, producerUri, producer(", data: file('data.txt')"))
open(service, consumerUri, consumer)
open(service, mainUri, main)
service.updateNow()
then:
client.getDiagnostics(mainUri).findAll { it.message.contains('is not compatible with process input') } == []

when: 'only the producer module is edited to emit an incompatible record { id }'
open(service, producerUri, producer(''))
service.updateNow()
then: 'the consumer call is re-checked and now reports the mismatch'
client.getDiagnostics(mainUri).findAll { it.message.contains('is not compatible with process input') }.size() == 1

when: 'the producer module is restored'
open(service, producerUri, producer(", data: file('data.txt')"))
service.updateNow()
then: 'the stale warning is cleared'
client.getDiagnostics(mainUri).findAll { it.message.contains('is not compatible with process input') } == []
}

def 'should clear diagnostics when an error is fixed' () {
given:
def client = new TestLanguageClient()
Expand Down
Loading