keep unimplemented notifications from closing connections#60
Conversation
…ection alive A non-nil error from a notification handler has no response slot, so the jsonrpc2 dispatcher escalates it to conn.fail and tears the connection down. UnimplementedServer/UnimplementedClient returned errNotImplemented uniformly, so an embedder that did not override every notification lost the connection the instant a peer sent one (e.g. gopls front-loading window/logMessage right after initialize, before initialized/didOpen). Return nil from the 26 notification stubs (ignoring the notification, as the LSP spec prescribes) and keep errNotImplemented only on request stubs, where it correctly becomes a method-not-found response. Doc comments now spell out the request-vs-notification distinction so the stubs are not "consistency-fixed" back.
The LSP metaModel splits InitializeParams into a private
_InitializeParams base plus a WorkspaceFoldersInitializeParams mixin.
Embedding the private base leaked an underscore-named type into the
public API and forced the parent to reach its own fields through an
extra indirection on the hot initialize decode path.
Flatten any extends/mixins reference whose target name begins with
"_" directly into the referrer: inline the base's embeds and rendered
fields (rendered against the public owner so doc hints and hot-field
overrides match), and stop emitting underscore structs as standalone
types. InitializeParams now carries WorkDoneProgressParams and the
eight former base fields itself, and _InitializeParams is gone.
Also collapse zero-field, zero-embed structs to the canonical
struct{} form, since gofmt keeps the multi-line empty body as-is.
Regenerated output is included; the net reduction comes from the
parent no longer delegating codec methods through the dropped embed.
Nullable[T] is tri-state (absent / explicit null / value), so the
single NewOptional-style constructor only covers the value arm.
Callers had to spell struct literals like Nullable[T]{set: true,
null: true} to build the null state, mirroring internal generated
code.
Add NewNullable(v) for the present-value state and NullNullable[T]()
for the explicit-null state. Absent needs no constructor since it is
the zero Nullable[T]{}. Cover all three states and their JSON
round-trip via a real Nullable field.
When recursively flattening private base structures (those prefixed with an underscore), diamond inheritance or overlapping mixins could previously produce duplicate struct fields or embedded types. This would lead to Go compile errors. This change introduces deduplication tracking in inlineContribution so each embedded type and property is emitted at most once per public structure.
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #60 +/- ##
=====================================
Coverage 85.1% 85.2%
=====================================
Files 27 27
Lines 6077 6135 +58
=====================================
+ Hits 5177 5230 +53
- Misses 900 905 +5
|
Preserve the behavior-only cleanup as a narrow review aid. Keep generator traversal allocation-free with unchanged output. Document the stable NewClient return shape instead of breaking API.
ff20a21 to
19a4c8b
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request flattens underscore-prefixed private base structures (such as _InitializeParams) directly into their public referrers during LSP code generation, eliminating the need to emit them as separate types. It also updates UnimplementedServer and UnimplementedClient notification methods to return nil instead of errNotImplemented to prevent connection teardown when notifications are unhandled, supported by a new integration test. Additionally, helper constructors NewNullable and NullNullable are introduced. The review feedback suggests tracking seenEmbeds and seenFields at the public structure level and passing them into inlineContribution to prevent duplicate fields or embeds when a public structure embeds multiple private bases or defines overlapping fields.
Share embed and field dedupe state across each public structure. Let public fields override inherited private-base fields before render. Cover duplicate private bases and direct public overrides in tests.
4b5fd2e to
dadabcc
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request flattens private base structures (prefixed with an underscore) directly into their public referrers within the LSP generator, simplifying the generated Go structs and removing unnecessary private types. Additionally, it updates un-overridden notification methods in UnimplementedServer and UnimplementedClient to return nil instead of errNotImplemented, preventing connection teardowns when unhandled notifications are received. A review comment correctly identifies a potential infinite recursion risk in the recursive flatten function of the generator and suggests adding cycle detection.
| var flatten func(structure *Structure) | ||
| flatten = func(structure *Structure) { |
There was a problem hiding this comment.
To prevent potential infinite recursion and stack overflow in case of cyclic dependencies in the meta-model structures (e.g., if a private base structure directly or indirectly references itself), it is safer to implement cycle detection using a visited map within the recursive flatten function.
visited := make(map[string]bool)
var flatten func(structure *Structure)
flatten = func(structure *Structure) {
if visited[structure.Name] {
return
}
visited[structure.Name] = trueStop recursive private-base flattening when a structure repeats. Cover cyclic private bases so generation cannot recurse forever. Keep generated output unchanged for the current acyclic meta-model.
a8c26c4 to
dbfd534
Compare
Summary
This PR fixes the default unimplemented LSP stubs so unhandled notifications are ignored instead of failing the JSON-RPC connection, while preserving
method not foundresponses for unhandled requests.It also cleans up the generated protocol surface around
InitializeParamsby flattening private_-prefixed meta-model bases into their public referrers, adds explicit constructors forNullable[T], and tightens the structure-flattening generator so duplicate inherited fields/embeds are emitted only once.Why
A JSON-RPC notification has no response slot. Returning a non-nil error from an unimplemented notification handler causes the dispatcher to treat that error as a connection-level failure. That made embedders of
UnimplementedServerorUnimplementedClientlose the connection when a peer sent a notification they did not override, such as earlywindow/logMessagetraffic.The LSP metaModel also exposes private helper structures such as
_InitializeParams. Emitting those as public Go types leaks implementation detail into the API and adds avoidable indirection on initialize encoding/decoding.What changed
nilfrom unimplemented notification stubs so notifications are ignored per LSP/JSON-RPC semantics.errNotImplementedfor request stubs so peers still receive classifiedmethod not foundresponses._-prefixed meta-model structures into public referrers and emit empty structs asstruct{}.NewNullable(v)andNullNullable[T]()constructors for the value and explicit-null states ofNullable[T].Impact
Consumers can embed
UnimplementedServerorUnimplementedClientand override only the methods they support without unhandled notifications tearing down the connection. Generated initialize types are also cleaner and avoid exposing private meta-model helper types.Validation
go test ./...go vet ./...go tool golangci-lint run ./...(0 issues)