Skip to content

Add lib directory to classpath - #98

Merged
bentsherman merged 1 commit into
mainfrom
lib-dir-classpath
Sep 11, 2026
Merged

bentsherman merged 1 commit into
mainfrom
lib-dir-classpath

Conversation

@bentsherman

@bentsherman bentsherman commented Feb 21, 2025

Copy link
Copy Markdown
Member

Simplify lib directory support by using the same approach as Nextflow -- add the lib directory (and any JARs in it) to the compiler classpath. GroovyLibCache is removed; nf-lang's ResolveVisitor already compiles Groovy sources found on the classpath on demand, so the language server doesn't need to compile the lib directory itself.

Rebased on the current main (the original commit predates the compiler module moving to nf-lang).

The hang

The original attempt hung with no error output. The cause was ResolveVisitor.resolveFromClassResolver, which threw GroovyBugError when the class resolver returned a SourceUnit instead of a ClassNode -- exactly what happens when a .groovy file is found on the classpath. LanguageService.update() had no try/catch at the time, so the error propagated into the debounce executor, where a scheduled task's exception is captured in an unread Future and silently dropped. The update thread died on the first reference to a lib class, diagnostics were never published, and nothing was logged.

Both halves are fixed upstream: nf-lang now compiles the source unit via GroovyCompiler, and update0() catches Throwable.

Two rough edges handled here

  • GroovyCompiler.compile() lets CompilationFailedException escape, so a lib class with a syntax error -- normal while editing -- aborted the whole analysis pass and left the script with no diagnostics. Name resolution is now guarded per file. Arguably better fixed in nf-lang.
  • GroovyClassLoader fixes its classpath at construction, so a lib directory created after startup was never seen. Groovy sources are now resolved against the lib directory on each lookup via a GroovyResourceLoader. New JARs still require a restart.

ScriptLibDirTest covers resolution by simple and fully-qualified name, type annotations, live edits to lib classes, a lib class with a syntax error, a missing lib directory, and no workspace root.

Replace GroovyLibCache with the same approach used by Nextflow: add the
`lib` directory (and any JARs in it) to the compiler classpath. nf-lang
already compiles Groovy sources found on the classpath on demand, so the
language server no longer needs to compile the lib directory itself.

Groovy source files are resolved against the lib directory on every lookup
instead of through the classpath, so that new files (or the lib directory
itself) are picked up without restarting the server.

Name resolution is guarded against a Groovy class in the lib directory that
fails to compile -- nf-lang propagates the compilation error, which would
otherwise abort the analysis and leave the file with no diagnostics.

Signed-off-by: Ben Sherman <bentshermann@gmail.com>
@bentsherman

Copy link
Copy Markdown
Member Author

Two nf-lang fixes this approach wants, recorded here since they live in the nextflow repo rather than this one.

1. Hover (and go-to-definition, completion) on lib classes

nextflow/script/control/ResolveVisitor.resolveType() discards the node it just resolved:

if( resolveFromClassResolver(type.getName()) != null )
    return true;

The resolveFromLibImports branch that this PR removes called type.setRedirect(cn), so the script's ClassNode pointed at the compiled lib class and the providers could read its Groovydoc. Via the class resolver the type resolves -- so no error is reported -- but stays unlinked, and getDefinition() returns null.

var cn = resolveFromClassResolver(type.getName());
if( cn != null ) {
    type.setRedirect(cn);
    return true;
}

Verified locally against a patched nf-lang: hover output on a lib class and its methods matches main exactly. Note this branch is reached by every type that falls through to the class resolver, not just lib classes, so it deserves a proper look on the Nextflow side.

2. A lib class with a syntax error swallows the script's diagnostics

GroovyCompiler.compile() lets CompilationFailedException escape, so one broken file in lib aborts name resolution for the script that refers to it:

[error] Unexpected exception while resolving /path/to/test.nf: org.codehaus.groovy.control.CompilationFailedException: parsing failed

The script then reports nothing -- not even `Greeter` is not defined. This PR guards the ScriptResolveVisitor call so the failure is contained to one file instead of killing the whole pass, but the diagnostics for that file are still lost. Swallowing the failure in GroovyCompiler.compile() and returning an empty list would degrade correctly: the lib class simply doesn't resolve, and the rest of the script is checked as usual. That is what the old GroovyLibCache did (catch( CompilationFailedException e ) { // ignore }).

@bentsherman
bentsherman merged commit f1c0e57 into main Sep 11, 2026
2 checks passed
@bentsherman
bentsherman deleted the lib-dir-classpath branch September 11, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant