[ 
https://issues.apache.org/jira/browse/GROOVY-12411?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18116759#comment-18116759
 ] 

Paul King commented on GROOVY-12411:
------------------------------------

Notes from reading the branch, kept off the PR thread to avoid lengthening it. 
Two parts: a trust question I would want closed before any release, and the 
tie-ins that make the case for the library seam discussed on the PR.

h2. 1. Trust: opening a folder can execute code from that folder

Read from the code, not reproduced.

{{CompilerSettings}}'s class javadoc says Grape and {{@ASTTest}} are off "so 
opening a workspace does not execute or download untrusted compile-time code". 
On my reading that guarantee does not currently hold:

* When the client supplies no classpath, {{WorkspaceLayout.classpathJars}} adds 
every {{lib/*.jar}} and {{libs/*.jar}} under the workspace folder to the 
compile classpath automatically.
* {{GroovyCompiler.loaderFor}} builds a {{GroovyClassLoader}} over that 
classpath and hands it to the {{CompilationUnit}}, where it becomes the 
transform loader.
* {{ASTTransformationVisitor}} enumerates 
{{META-INF/services/org.codehaus.groovy.transform.ASTTransformation}} from the 
transform loader and instantiates what it finds. Local transforms named by 
annotations in workspace sources load the same way.

So a repository containing {{lib/anything.jar}} with a global transform runs 
that transform inside the server process the moment a {{.groovy}} file is 
opened. The PR clearly thought about this class of problem -- javac is run with 
{{-proc:none}} precisely to stop Java annotation processors -- but the 
Groovy-side equivalent is open. It matters more here than for a typical 
language server because the positioning includes being a backend for AI agents, 
which routinely open repositories nobody has read.

Options, roughly in order of preference:

# Never infer a classpath from workspace contents: require the client to push 
it, as the design already does for everything else.
# Run global transforms only from the server's own classpath unless the client 
opts in.
# Honour a client-declared trust flag, defaulting to untrusted. VS Code's 
Workspace Trust is the natural client-side hook.

Whatever is chosen, the javadoc claim should be made to match it.

h2. 2. Minimum bar before a release

# Close the path above, and align the {{CompilerSettings}} javadoc with 
whatever is chosen.
# Stop disabling transforms with process-global system properties. 
{{GroovyCompiler.compile}} sets {{groovy.asttest.enable}} and 
{{groovy.grape.enable}} around each compile. That races anything else in the 
JVM -- {{ASTTestTransformation}} reads its flag on every visit -- and for Grape 
it is ineffective anyway, since {{Grape}} reads the property once into a static 
at class-init (the real protection is the disabled global transform, which the 
PR also sets). A per-{{CompilerConfiguration}} switch for {{@ASTTest}} would be 
the clean fix and is a small, separable core change. Theoretical in a dedicated 
server process; a real bug once the code is embedded in the Console, groovysh 
or a notebook kernel.
# Scenario tests for the above -- "a jar in {{lib/}} is not executed" is worth 
more than a lot of breadth coverage.
# Release hygiene: confirm the LSP4J licence election (it appears to be dual 
EPL-2.0 / EDL-1.0, and EDL is Category A, which is simpler for an ASF release), 
and whether Gson newly ships via the module's runtime classpath and so needs 
LICENSE/NOTICE entries. Also worth a second attempt at the PGP key newly added 
to the ignored list in {{verification-metadata.xml}}.

h2. 3. Follow-ons using tools we already have

Each of these is in-process and session-aware, and none of them wants to speak 
JSON-RPC to itself -- which is the case for the language-services library seam 
rather than a plug-in SPI.

|| Tool || What it gains || Depends on ||
| Groovy Console | Error underlining as you type with messages on hover, 
go-to-definition, outline, completion popup | Layer-1 diagnostics and 
navigation; Swing painter and popup UI |
| groovysh | Static completion through not-yet-evaluated expression chains, 
signature tips from groovydoc, pre-execution diagnostics | Session context; 
complements rather than replaces the existing runtime-reflective completion, 
which is the better oracle where a live object exists |
| groovy-jupyter | Chained completion (a known limitation of the kernel today); 
JupyterLab support nearly free, since jupyterlab-lsp feeds a concatenated 
virtual document and for Groovy that concatenation is a valid script | The same 
session context; plus a classpath hand-off for {{@Grab}}, since the kernel 
already knows what it resolved |
| groovy-typecheckers, groovy-verify | Checker results surface as editor 
diagnostics | Type-checking extensions being wired into the LSP's STC pass |
| Agents | Claude Code and OpenCode can consume a language server directly, so 
a {{groovylsp}} on the PATH is the integration; SDKMAN already puts it there | 
Diagnostic fidelity above all -- a false positive costs an agent more than a 
missing feature |
| IDEA plugin | Conformance oracle: run a shared corpus through the plugin's 
resolver and through the compiler-backed services and diff the results. Also 
{{ExternalAnnotator}}, which works in every edition including Android Studio 
and does not depend on the LSP API | Layer-1 library plus a test harness; no 
product change on either side |
| VS Code | A first-party extension would immediately be the best Groovy option 
available there | Separate deliverable; also needs a documented, scriptable way 
to produce a classpath from Gradle or Maven |

The shared hard part is session context: a REPL line, a console selection and a 
notebook cell are all fragments, not compilation units. Each needs a synthetic 
document -- accumulated imports, declarations for known variables typed from 
their runtime classes, previously defined methods and classes, then the 
fragment. groovysh, the Console and the Jupyter kernel need the same thing, so 
it should be built once.

h2. 4. Strategic directions

* *Type-checking extension results as editor diagnostics.* The LSP's STC pass 
constructs a {{StaticTypeCheckingVisitor}} directly and never calls 
{{addTypeCheckingExtensions}}, so {{NullChecker}}, {{RegexChecker}}, 
{{FormatStringChecker}}, {{PurityChecker}}, {{SqlInjectionChecker}} and 
groovy-verify are all invisible. This is the most differentiating thing a 
compiler-backed server can offer, since IDEA cannot model these without running 
them. groovy-verify's counterexamples map well onto LSP -- the obligation as 
the message, the counterexample as {{relatedInformation}}, and a "generate 
failing test" code action. For an agent, a concrete failing input is an 
unusually good repair signal. Gated on the trust decision above, since an 
extension is code, and it needs a slower lane than a 150ms debounce.
* *Parser error recovery.* The {{.}} that triggers completion leaves the buffer 
syntactically invalid, and Parrot builds no partial {{ModuleNode}} on a syntax 
error, so there is no AST at the moment it is most needed. A last-good-AST 
fallback per file or placeholder patching would unblock it now; real error 
recovery in the parser is the right long-term answer and would pay off across 
the Console, groovysh, our syntax diagnostics and every AST tool, well beyond 
the LSP.
* *Machine-readable transform descriptions produced from the compiler.* 
Transforms running at CANONICALIZATION and later -- {{@Delegate}}, 
{{@Immutable}}, {{@TupleConstructor}}, {{@Sortable}}, records, sealed 
completion -- are invisible to anything that stops earlier, which is the root 
cause of lag in the IDEA plugin, the community servers and our own tooling 
alike. A larger effort, but the one that attacks the cause rather than the 
symptom.

> Add a first-party Language Server Protocol implementation (groovy-lsp)
> ----------------------------------------------------------------------
>
>                 Key: GROOVY-12411
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12411
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> Ship an optional first-party language server ({{groovy-lsp}}) so editors and 
> AI tools can offer Groovy completion, navigation, diagnostics, and 
> conservative rename against the *in-tree* compiler.
> h2. Why
> Community servers lag language changes because they compile against a Groovy 
> snapshot, not this repository. {{groovy-lsp}} uses the same 
> {{CompilationUnit}} as the rest of Groovy, so parser and type-system work 
> appear in editors automatically.
> It is a Groovy *language core* for the Language Server Protocol, not a 
> workspace or IDE. The differentiator is the same compiler, a small surface, 
> and edits that refuse to guess, not more IDE features.
> h2. Intended use
> * *Editor / IDE:* start {{groovylsp}} and speak stdio JSON-RPC. The client 
> supplies {{groovy.classpath}} / {{groovy.sourcePaths}}.
> * *AI tools:* the same process is the semantic backend. A later 
> {{groovy-mcp}} module is expected to wrap it so a model can ask what an 
> identifier is without embedding the compiler.
> * *Other Apache-2.0 Groovy LSPs:* extend the compile and protocol loop 
> through the incubating {{GroovyLspExtension}} SPI instead of forking it. 
> Eclipse JDT / groovy-eclipse servers remain a parallel stack for a full Java 
> IDE; they cannot replace this compiler through the SPI.
> h2. Out of scope
> These are strengths of other implementations and stay out on purpose:
> * A full Java language server (Java completion, Java format, Java-only 
> rename). Joint *type resolution* is in scope (see below)
> * GDSL / Jenkins pipeline descriptors (a plugin may add them; this module 
> does not load GDSL)
> * Invoking Gradle or Maven (no Tooling API)
> * Debugger (DAP) and test runner
> * Identifier-text rename fallbacks (rename every {{foo}} in the file)
> * Extra name-span fields on {{AnnotatedNode}} (identifier selection is 
> {{Positions.toNameRange}} in groovy-lsp)
> h2. Joint Groovy and Java sources
> When the workspace contains {{.java}} files, groovy-lsp joint-compiles them 
> with Groovy through in-tree {{JavaAwareCompilationUnit}} ({{memStub}}): 
> Parrot to {{CONVERSION}}, in-memory Java stubs, {{javax.tools}} javac with 
> annotation processing off, then Groovy {{SEMANTIC_ANALYSIS}} (no Groovy class 
> generation). Open Java buffers overlay the filesystem. Javac diagnostics and 
> go-to-definition cross the language boundary via {{JavaSymbolIndex}} 
> ({{com.sun.source}} {{Trees}}, captured before {{generate()}}).
> That is type resolution and navigation, not a Java language server. Java 
> completion, Java formatting, and Java-only rename stay with a Java LS or 
> [groovy-language-server-edt|https://github.com/tomaszrup/groovy-language-server-edt].
> h2. Comparison with other Groovy language servers
> Four implementations, four jobs. *groovy-lsp* is the in-tree language core. 
> The others keep the IDE-shaped work this module refuses.
> * 
> [groovy-language-server|https://github.com/GroovyLanguageServer/groovy-language-server]
>  (Prominic): community server for Moonshine IDE. Apache 2.0 on a Groovy 
> 4.0.26 dependency.
> * [groovyls|https://github.com/trustytrojan/groovyls] (trustytrojan): Apache 
> 2.0 fork of Prominic. Jenkins GDSL, forced STC on every class, Aether 
> downloads. Those features are hard-wired, not a plugin.
> * 
> [groovy-language-server-edt|https://github.com/tomaszrup/groovy-language-server-edt]:
>  EPL 2.0, Equinox + JDT + groovy-eclipse. Full Java LS and joint IDE 
> features. Complementary stack; none of its source is copied.
> ||Aspect||groovy-lsp||Prominic||groovyls||edt||
> |License|Apache 2.0|Apache 2.0|Apache 2.0|EPL 2.0|
> |Compiler|in-tree {{CompilationUnit}}|Groovy 4.0.26 jar|Groovy 5.1.1 jar|JDT 
> + groovy-eclipse|
> |Phase|{{SEMANTIC_ANALYSIS}}, no Groovy 
> classgen|{{CANONICALIZATION}}|{{CANONICALIZATION}} + forced STC|JDT compile|
> |Classpath|client {{groovy.classpath}}|client {{groovy.classpath}}|client + 
> Aether downloads|Red Hat Java / Gradle wrapper|
> |GDSL / Jenkins|plugin, not core|no|yes|no|
> |Joint Groovy+Java|{{JavaAwareCompilationUnit}} + {{memStub}}; 
> def/hover|no|no|yes (JDT IDE)|
> |Rename|declarations and bound calls only|AST references + name regex|same as 
> Prominic|JDT; identifier-scan fallback|
> |Extension point|incubating {{GroovyLspExtension}}|none|none (baked in)|OSGi 
> / Equinox|
> h2. Extensibility as a reusable base
> {{groovy-lsp}} is meant to be the compile + protocol loop other *Apache-2.0* 
> Groovy language servers sit on, not a fork point. Plugins implement 
> incubating {{GroovyLspExtension}} ({{ServiceLoader}}; tests use 
> {{ExtensionHost}}). There is no second hover/completion SPI: inject members 
> in {{afterCompile}} so core hover, completion, navigation, and rename see 
> them.
> ||Hook||For||
> |{{configure(config, session)}}|extra classpath, extra {{groovy.*}} settings 
> (for example {{groovy.dependencies}})|
> |{{initialized}} / {{shutdown}}|load or release catalogs|
> |{{afterCompile(CompilationUnit)}}|GDSL injection, a forced STC pass; must 
> not generate class files|
> |{{extraDiagnostics}} / {{extraCodeActions}}|plugin diagnostics and actions 
> ({{ModuleNode}} is groovy-core AST)|
> |{{commands}} / {{executeCommand}}|extra workspace commands via 
> {{GroovyLspSession.applyEdit}}|
> {{GroovyLspSession}} is apply-edit, document text, folders, classpath, source 
> paths, extra settings, and position encoding. It is not a compile handle. A 
> throwing plugin is isolated and must not compile or publish on its own. The 
> built-in {{groovy.lsp.stc}} extension is the example: annotated 
> {{@TypeChecked}} / {{@CompileStatic}} only.
> * *groovyls* can sit on this SPI. GDSL inject and forced-on-all-classes STC 
> belong in {{afterCompile}}; Aether downloads belong in {{configure}} from 
> extra settings. Semantic tokens, conservative navigation, and joint type 
> resolution are already in core. That is a plugin, not a paste of groovyls 
> sources.
> * *Prominic groovy-language-server* has no unique language feature left. 
> Moonshine can run {{groovylsp}} and supply {{groovy.classpath}}. A 
> Moonshine-only command would be a tiny plugin.
> * *groovy-language-server-edt* cannot replace {{CompilationUnit}} with JDT 
> through the SPI. Joint *type resolution* is now in groovy-lsp core; edt 
> remains the complementary full Java IDE (library sources, file-move imports). 
> Its identifier-scan rename fallback is not adopted.
> The SPI is incubating: design may still settle. It is a seam for Apache-2.0 
> reuse, not a stability promise and not a slot for an Equinox compiler.
> h2. Rename and dynamically typed Groovy
> {{MethodCallExpression.methodTarget}} is often empty in dynamic Groovy. A 
> name-and-arity match is a heuristic: good enough for hover, not for rewriting 
> source.
> ||Feature||When {{methodTarget}} is set||When it is not (typical dynamic 
> Groovy)||
> |Hover|Bound signature|Unique arity match on a resolved receiver, labelled 
> {{_inferred_}}; otherwise {{name(...)}}|
> |Definition|That method|Every arity-matching candidate with a source 
> location; never a random first match|
> |Rename / prepareRename|Allowed|Refused. A heuristic must not rewrite the 
> wrong overload.|
> Declarations (class, method, field, parameter, bound local) rename by 
> identity (owner + name + descriptor), not the unqualified identifier. Class 
> rename also renames the file when the class name matches the file stem.
> h2. AST columns (not a breaking change)
> Compiler AST columns were already 1-based. The parser writes 
> {{setColumnNumber(token.getCharPositionInLine() + 1)}}; ANTLR's 
> {{getCharPositionInLine()}} is 0-based, Groovy's AST is not. Authored source 
> starts at column 1, the end column is exclusive, and missing positions are 
> {{-1}}.
> GROOVY-12085 changed the *unit* (Unicode code points instead of UTF-16 
> units). It did not change 0-based vs 1-based.
> The patch also corrects {{ASTNode}} javadoc that had said "0-indexed" (added 
> 2026-05-03, never shipped in a release). Getters, setters, and stored values 
> are unchanged. That is a documentation fix, not a runtime ABI change.
> LSP positions stay 0-based in the encoding negotiated at {{initialize}} 
> ({{utf-16}} by default; {{utf-8}} / {{utf-32}} optional). The server 
> converts; clients never see AST columns. Existing LSP clients do not need to 
> change how they send positions.
> h2. What lands
> * Optional subproject {{groovy-lsp}} ({{optionalModule()}}, not 
> {{groovy-all}})
> * {{groovylsp}} / {{groovylsp.bat}} on the binary distribution (stdio 
> JSON-RPC)
> * LSP 3.18 over Eclipse LSP4J 1.0; unknown 3.19 fields ignored; advertise 
> only implemented capabilities
> * LSP4J is EPL 2.0 Category B *binary*; its source is not mixed into Groovy
> * Compile open Groovy buffers through {{SEMANTIC_ANALYSIS}} (no Groovy class 
> generation)
> * Joint compile of workspace {{.java}} via {{JavaAwareCompilationUnit}} + 
> {{memStub}} and {{javax.tools}} javac
> * Incubating {{GroovyLspExtension}} ({{ServiceLoader}}). Built-in 
> {{groovy.lsp.stc}} runs STC only on {{@TypeChecked}} / {{@CompileStatic}} 
> members
> * {{LocatedMessage}}: {{toDiagnostic()}} / {{write()}} no longer NPE when the 
> CST context is null
> * Docs: {{groovy-lsp.adoc}}, {{subprojects/groovy-lsp/ARCHITECTURE.md}}
> h2. Language features
> File types: {{.groovy}}, {{.gvy}}, {{.gy}}, {{.gsh}}, or 
> {{languageId=groovy}}. Workspace {{.java}} participates in joint compile 
> (diagnostics, definition, hover), not as a Java language server.
> * *Editing:* incremental sync; completion (keywords, snippets, members, 
> workspace types with auto-import, GDK) and {{completionItem/resolve}}; format 
> / range format / on-type indent; organize imports; unused-import hints; 
> generate accessors / {{toString}} / {{equals}}/{{hashCode}} / constructor; 
> add import, package, {{@Override}}, missing class stub
> * *Navigation:* definition, type definition, implementation, declaration, 
> references (by identity); call and type hierarchy; document / workspace 
> symbols; folding; selection range; document highlight; import document links; 
> Groovy to Java and Java to Groovy definition on jointly compiled types
> * *Presentation:* push/pull diagnostics (compiler + unused import + STC on 
> annotated members + javac); semantic tokens; inlay hints; reference-count 
> code lenses
> * *Workspace:* folders, watched files, configuration, 
> {{window/workDoneProgress}}, {{workspace/willRenameFiles}} for Groovy sources
> * *Commands:* {{groovy.lsp.organizeImports}}, {{groovy.lsp.showReferences}}, 
> {{groovy.lsp.gotoSuperMethod}}
> h2. Safety and classpath
> * {{@Grab}} and {{@ASTTest}} off unless the client sets {{grapeEnabled}} / 
> {{astTestEnabled}}
> * Diagnostics scoped to the owning source unit (shared {{ErrorCollector}})
> * No Gradle or Maven Tooling API. If {{classpath}} / {{sourcePaths}} are 
> empty, fall back to {{src/main/groovy}}, {{src/main/java}} (and friends) and 
> jars under {{lib}} / {{libs}}
> * Unchanged inputs skip recompilation; {{GroovyClassLoader}} is reused and 
> closed on shutdown
> * Joint javac output is a per-compile directory under {{.groovy/lsp-javac}}, 
> owner-only where POSIX allows, deleted on the next compile
> * A throwing plugin must not abort compile or a request
> h2. How to run
> {code}
> groovylsp
> groovylsp --stdio
> groovylsp --socket <port>
> groovylsp --help
> groovylsp --version
> {code}
> Do not write to stdout from Groovy code running inside the server; the 
> launcher redirects {{System.out}} to {{System.err}} so stray prints cannot 
> break the protocol stream.
> h2. See also
> * GROOVY-12085 (code-point columns; not an origin change)



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to