diff --git a/append_encoders.gen.go b/append_encoders.gen.go index d43dded..4a2ebb6 100644 --- a/append_encoders.gen.go +++ b/append_encoders.gen.go @@ -6823,6 +6823,17 @@ func (x *InitializeParams) appendLSP(dst []byte) ([]byte, error) { return nil, err } } + if !x.WorkspaceFolders.IsZero() { + dst = appendObjectName(dst, &first, `workspaceFolders`) + if x.WorkspaceFolders.IsNull() { + dst = append(dst, nullLiteral...) + } else { + nv, _ := x.WorkspaceFolders.Get() + if dst, err = appendSliceWorkspaceFolderJSON(dst, nv); err != nil { + return nil, err + } + } + } dst = appendObjectName(dst, &first, `processId`) if x.ProcessID == nil { dst = append(dst, nullLiteral...) @@ -6872,17 +6883,6 @@ func (x *InitializeParams) appendLSP(dst []byte) ([]byte, error) { dst = appendObjectName(dst, &first, `trace`) dst = appendJSONString(dst, string(x.Trace)) } - if !x.WorkspaceFolders.IsZero() { - dst = appendObjectName(dst, &first, `workspaceFolders`) - if x.WorkspaceFolders.IsNull() { - dst = append(dst, nullLiteral...) - } else { - nv, _ := x.WorkspaceFolders.Get() - if dst, err = appendSliceWorkspaceFolderJSON(dst, nv); err != nil { - return nil, err - } - } - } return append(dst, '}'), nil } @@ -14789,82 +14789,6 @@ func (x *WorkspaceUnchangedDocumentDiagnosticReport) appendLSPJSON(dst []byte) ( return x.appendLSP(dst) } -func (x *_InitializeParams) appendLSP(dst []byte) ([]byte, error) { - if x == nil { - return append(dst, nullLiteral...), nil - } - var err error - _ = err - dst = append(dst, '{') - first := true - _ = first - if x.WorkDoneToken != nil { - dst = appendObjectName(dst, &first, `workDoneToken`) - if dst, err = appendUnionProgressTokenJSON(dst, x.WorkDoneToken); err != nil { - return nil, err - } - } - dst = appendObjectName(dst, &first, `processId`) - if x.ProcessID == nil { - dst = append(dst, nullLiteral...) - } else { - dst = appendInt32JSON(dst, int32(*x.ProcessID)) - } - if !isZeroGeneratedClientInfo(x.ClientInfo) { - dst = appendObjectName(dst, &first, `clientInfo`) - if dst, err = x.ClientInfo.appendLSP(dst); err != nil { - return nil, err - } - } - if x.Locale != nil { - dst = appendObjectName(dst, &first, `locale`) - if x.Locale == nil { - dst = append(dst, nullLiteral...) - } else { - dst = appendJSONString(dst, string(*x.Locale)) - } - } - if !x.RootPath.IsZero() { - dst = appendObjectName(dst, &first, `rootPath`) - if x.RootPath.IsNull() { - dst = append(dst, nullLiteral...) - } else { - nv, _ := x.RootPath.Get() - dst = appendJSONString(dst, string(nv)) - } - } - dst = appendObjectName(dst, &first, `rootUri`) - if x.RootURI == nil { - dst = append(dst, nullLiteral...) - } else { - dst = appendJSONString(dst, string(*x.RootURI)) - } - dst = appendObjectName(dst, &first, `capabilities`) - if dst, err = x.Capabilities.appendLSP(dst); err != nil { - return nil, err - } - if len(x.InitializationOptions) > 0 { - dst = appendObjectName(dst, &first, `initializationOptions`) - if dst, err = appendRawJSONValue(dst, x.InitializationOptions); err != nil { - return nil, err - } - } - if x.Trace != "" { - dst = appendObjectName(dst, &first, `trace`) - dst = appendJSONString(dst, string(x.Trace)) - } - return append(dst, '}'), nil -} - -// appendLSPJSON implements appendMarshaler with a pre-sized buffer. -func (x *_InitializeParams) appendLSPJSON(dst []byte) ([]byte, error) { - if x == nil { - return append(dst, nullLiteral...), nil - } - dst = slices.Grow(dst, 2048) - return x.appendLSP(dst) -} - func appendSliceCodeActionKindDocumentationJSON(dst []byte, x []CodeActionKindDocumentation) ([]byte, error) { dst = slices.Grow(dst, 2+len(x)*96) dst = append(dst, '[') diff --git a/decoders.gen.go b/decoders.gen.go index 33284ef..49f2751 100644 --- a/decoders.gen.go +++ b/decoders.gen.go @@ -14879,6 +14879,22 @@ func (x *InitializeParams) unmarshalLSP(raw []byte, i int) (int, error) { return i, err } i = n + case keyEquals(key, `workspaceFolders`): + if n, ok := dvNull(raw, i); ok { + x.WorkspaceFolders = Nullable[[]WorkspaceFolder]{set: true, null: true} + i = n + } else { + val, n, err := dvValue(raw, i) + if err != nil { + return n, err + } + var v []WorkspaceFolder + if err := decodeWith(val, &v); err != nil { + return i, err + } + x.WorkspaceFolders = Nullable[[]WorkspaceFolder]{set: true, value: v} + i = n + } case keyEquals(key, `processId`): if n, ok := dvNull(raw, i); ok { x.ProcessID = nil @@ -14962,22 +14978,6 @@ func (x *InitializeParams) unmarshalLSP(raw []byte, i int) (int, error) { } x.Trace = TraceValue(v) i = n - case keyEquals(key, `workspaceFolders`): - if n, ok := dvNull(raw, i); ok { - x.WorkspaceFolders = Nullable[[]WorkspaceFolder]{set: true, null: true} - i = n - } else { - val, n, err := dvValue(raw, i) - if err != nil { - return n, err - } - var v []WorkspaceFolder - if err := decodeWith(val, &v); err != nil { - return i, err - } - x.WorkspaceFolders = Nullable[[]WorkspaceFolder]{set: true, value: v} - i = n - } default: _, n, err := dvValue(raw, i) if err != nil { @@ -32196,153 +32196,6 @@ func (x *WorkspaceUnchangedDocumentDiagnosticReport) UnmarshalJSONFrom(dec *json return x.unmarshalLSPValue(slices.Clone(raw)) } -func (x *_InitializeParams) unmarshalLSP(raw []byte, i int) (int, error) { - if n, ok := dvNull(raw, i); ok { - *x = _InitializeParams{} - return n, nil - } - if i >= len(raw) || raw[i] != '{' { - return i, dvSyntaxError(i, "object") - } - i = skipSpace(raw, i+1) - if i < len(raw) && raw[i] == '}' { - return i + 1, nil - } - for { - key, n, err := dvMemberKey(raw, i) - if err != nil { - return n, err - } - i = n - _ = key - switch { - case keyEquals(key, `workDoneToken`): - val, n, err := dvValue(raw, i) - if err != nil { - return n, err - } - if err := unmarshalProgressTokenValue(val, &x.WorkDoneToken); err != nil { - return i, err - } - i = n - case keyEquals(key, `processId`): - if n, ok := dvNull(raw, i); ok { - x.ProcessID = nil - i = n - } else { - v, n, err := dvInt32(raw, i) - if err != nil { - return n, err - } - if x.ProcessID == nil { - x.ProcessID = new(int32) - } - *x.ProcessID = v - i = n - } - case keyEquals(key, `clientInfo`): - n, err := x.ClientInfo.unmarshalLSP(raw, i) - if err != nil { - return n, err - } - i = n - case keyEquals(key, `locale`): - if n, ok := dvNull(raw, i); ok { - x.Locale = nil - i = n - } else { - v, n, err := dvString(raw, i) - if err != nil { - return n, err - } - if x.Locale == nil { - x.Locale = new(string) - } - *x.Locale = v - i = n - } - case keyEquals(key, `rootPath`): - if n, ok := dvNull(raw, i); ok { - x.RootPath = Nullable[string]{set: true, null: true} - i = n - } else { - v, n, err := dvString(raw, i) - if err != nil { - return n, err - } - x.RootPath = Nullable[string]{set: true, value: v} - i = n - } - case keyEquals(key, `rootUri`): - if n, ok := dvNull(raw, i); ok { - x.RootURI = nil - i = n - } else { - v, n, err := dvURI(raw, i) - if err != nil { - return n, err - } - if x.RootURI == nil { - x.RootURI = new(uri.URI) - } - *x.RootURI = v - i = n - } - case keyEquals(key, `capabilities`): - n, err := x.Capabilities.unmarshalLSP(raw, i) - if err != nil { - return n, err - } - i = n - case keyEquals(key, `initializationOptions`): - val, n, err := dvValue(raw, i) - if err != nil { - return n, err - } - x.InitializationOptions = jsontext.Value(val) - i = n - case keyEquals(key, `trace`): - v, n, err := dvString(raw, i) - if err != nil { - return n, err - } - x.Trace = TraceValue(v) - i = n - default: - _, n, err := dvValue(raw, i) - if err != nil { - return n, err - } - i = n - } - var done bool - i, done, err = dvObjectNext(raw, i) - if err != nil { - return i, err - } - if done { - return i, nil - } - } -} - -func (x *_InitializeParams) unmarshalLSPValue(raw jsontext.Value) error { - i, err := x.unmarshalLSP(raw, skipSpace(raw, 0)) - if err != nil { - return err - } - return dvEnd(raw, i) -} - -// UnmarshalJSONFrom implements the v2 UnmarshalerFrom interface via the byte walker. -func (x *_InitializeParams) UnmarshalJSONFrom(dec *jsontext.Decoder) error { - raw, err := dec.ReadValue() - if err != nil { - return err - } - return x.unmarshalLSPValue(slices.Clone(raw)) -} - func (x *Diagnostic) unmarshalLSPWithScalarBoxes(raw []byte, i int, capN int, scalarBoxes *[]String, markupBoxes *[]MarkupContent) (int, error) { if n, ok := dvNull(raw, i); ok { *x = Diagnostic{} diff --git a/encoders.gen.go b/encoders.gen.go index adc6f18..9f9ffae 100644 --- a/encoders.gen.go +++ b/encoders.gen.go @@ -4878,6 +4878,14 @@ func (x InitializeParams) MarshalJSONTo(enc *jsontext.Encoder) error { return err } } + if !x.WorkspaceFolders.IsZero() { + if err := enc.WriteToken(jsontext.String(`workspaceFolders`)); err != nil { + return err + } + if err := json.MarshalEncode(enc, x.WorkspaceFolders); err != nil { + return err + } + } if err := enc.WriteToken(jsontext.String(`processId`)); err != nil { return err } @@ -4936,14 +4944,6 @@ func (x InitializeParams) MarshalJSONTo(enc *jsontext.Encoder) error { return err } } - if !x.WorkspaceFolders.IsZero() { - if err := enc.WriteToken(jsontext.String(`workspaceFolders`)); err != nil { - return err - } - if err := json.MarshalEncode(enc, x.WorkspaceFolders); err != nil { - return err - } - } return enc.WriteToken(jsontext.EndObject) } @@ -10611,79 +10611,6 @@ func (x WorkspaceUnchangedDocumentDiagnosticReport) MarshalJSONTo(enc *jsontext. return enc.WriteToken(jsontext.EndObject) } -func (x _InitializeParams) MarshalJSONTo(enc *jsontext.Encoder) error { - if err := enc.WriteToken(jsontext.BeginObject); err != nil { - return err - } - if x.WorkDoneToken != nil { - if err := enc.WriteToken(jsontext.String(`workDoneToken`)); err != nil { - return err - } - if err := encodeProgressTokenTo(enc, x.WorkDoneToken); err != nil { - return err - } - } - if err := enc.WriteToken(jsontext.String(`processId`)); err != nil { - return err - } - if err := json.MarshalEncode(enc, x.ProcessID); err != nil { - return err - } - if !isZeroGeneratedClientInfo(x.ClientInfo) { - if err := enc.WriteToken(jsontext.String(`clientInfo`)); err != nil { - return err - } - if err := x.ClientInfo.MarshalJSONTo(enc); err != nil { - return err - } - } - if x.Locale != nil { - if err := enc.WriteToken(jsontext.String(`locale`)); err != nil { - return err - } - if err := json.MarshalEncode(enc, x.Locale); err != nil { - return err - } - } - if !x.RootPath.IsZero() { - if err := enc.WriteToken(jsontext.String(`rootPath`)); err != nil { - return err - } - if err := json.MarshalEncode(enc, x.RootPath); err != nil { - return err - } - } - if err := enc.WriteToken(jsontext.String(`rootUri`)); err != nil { - return err - } - if err := json.MarshalEncode(enc, x.RootURI); err != nil { - return err - } - if err := enc.WriteToken(jsontext.String(`capabilities`)); err != nil { - return err - } - if err := x.Capabilities.MarshalJSONTo(enc); err != nil { - return err - } - if len(x.InitializationOptions) > 0 { - if err := enc.WriteToken(jsontext.String(`initializationOptions`)); err != nil { - return err - } - if err := enc.WriteValue(x.InitializationOptions); err != nil { - return err - } - } - if x.Trace != "" { - if err := enc.WriteToken(jsontext.String(`trace`)); err != nil { - return err - } - if err := json.MarshalEncode(enc, x.Trace); err != nil { - return err - } - } - return enc.WriteToken(jsontext.EndObject) -} - func (x CompletionItemSlice) MarshalJSONTo(enc *jsontext.Encoder) error { if err := enc.WriteToken(jsontext.BeginArray); err != nil { return err diff --git a/integration_test.go b/integration_test.go index df1ce5d..1e54507 100644 --- a/integration_test.go +++ b/integration_test.go @@ -84,11 +84,6 @@ func (testClient) ApplyEdit(context.Context, *ApplyWorkspaceEditParams) (*ApplyW // 2. a client->server notification reaches the server (textDocument/didOpen); // 3. a server->client request issued from within a handler round-trips // (workspace/applyEdit). -// -// TestIntegrationRoundTrip drives the production Handlers() chain through -// NewServer/NewClient over an in-memory pipe: a client->server request, a -// notification, and a server->client callback. It is the regression guard for -// the reply-clobbering defect once present in protocol.CancelHandler. func TestIntegrationRoundTrip(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() @@ -158,3 +153,76 @@ func TestIntegrationRoundTrip(t *testing.T) { t.Errorf("didOpen URI = %q, want %q", gotURI, wantURI) } } + +// notifyingServer pushes a server->client window/logMessage notification from +// inside its Hover handler. It models the common case of a server that emits +// log notifications early in a session, which is what exposed the connection +// teardown bug fixed in this change. +type notifyingServer struct { + UnimplementedServer + + mu sync.Mutex + client Client +} + +var _ Server = (*notifyingServer)(nil) + +func (s *notifyingServer) Hover(ctx context.Context, _ *HoverParams) (*Hover, error) { + s.mu.Lock() + client := s.client + s.mu.Unlock() + + // A server->client notification: with the bug present, the client's + // un-overridden UnimplementedClient.LogMessage returned errNotImplemented, + // which the dispatcher escalated to conn.fail and tore the connection down. + if err := client.LogMessage(ctx, &LogMessageParams{Type: MessageTypeInfo, Message: "hello"}); err != nil { + return nil, err + } + + return &Hover{Contents: String("ok")}, nil +} + +// TestIntegrationUnimplementedClientSurvivesNotification is the regression guard +// for the bug where an un-overridden UnimplementedClient notification method +// returned errNotImplemented, causing the jsonrpc2 dispatcher to fail the whole +// connection. A bare UnimplementedClient (overriding nothing) must absorb a +// server-sent window/logMessage notification and the connection must remain +// usable for a subsequent request. +func TestIntegrationUnimplementedClientSurvivesNotification(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + + a, b := net.Pipe() + + ns := ¬ifyingServer{} + + _, connA, clientDispatcher := NewServer(ctx, ns, jsonrpc2.NewStream(a)) + defer func() { _ = connA.Close() }() + + ns.mu.Lock() + ns.client = clientDispatcher + ns.mu.Unlock() + + // The client overrides nothing: every notification falls through to + // UnimplementedClient, the exact configuration that previously killed the + // connection on the first server notification. + _, connB, serverDispatcher := NewClient(ctx, UnimplementedClient{}, jsonrpc2.NewStream(b)) + defer func() { _ = connB.Close() }() + + // The handler pushes window/logMessage to the client mid-request. If the + // notification tore the connection down, this Hover call would fail (or the + // LogMessage inside the handler would error and surface here). + hover, err := serverDispatcher.Hover(ctx, &HoverParams{}) + if err != nil { + t.Fatalf("hover call after server notification: %v", err) + } + if contents, ok := hover.Contents.(String); !ok || contents != "ok" { + t.Fatalf("hover contents = %#v, want String(%q)", hover.Contents, "ok") + } + + // Prove the connection is still alive after the notification by issuing a + // second request that must also round-trip. + if _, err := serverDispatcher.Hover(ctx, &HoverParams{}); err != nil { + t.Fatalf("second hover call: connection did not survive the notification: %v", err) + } +} diff --git a/internal/genlsp/emit.go b/internal/genlsp/emit.go index b31aff6..9060011 100644 --- a/internal/genlsp/emit.go +++ b/internal/genlsp/emit.go @@ -216,13 +216,39 @@ func detectImports(body string) []string { func (g *Generator) analyzeStructures() []*renderedStruct { out := make([]*renderedStruct, 0, len(g.model.Structures)) for _, s := range g.model.Structures { + if strings.HasPrefix(s.Name, "_") { + // Underscore-prefixed structures are private bases that exist only to + // be flattened into their public referrers; do not emit them as their + // own type. They remain in g.structures for lookup during flattening. + continue + } rs := &renderedStruct{ Name: s.Name, Doc: g.docComment(s.Name, s.Documentation, s.Since, s.Deprecated, s.Proposed), } - for _, ref := range append(append([]*Type{}, s.Extends...), s.Mixins...) { - if ref.Kind == KindReference { - rs.Embeds = append(rs.Embeds, ref.Name) + seenEmbeds := make(map[string]bool) + seenFields := make(map[string]bool) + for _, p := range s.Properties { + seenFields[p.Name] = true + } + for _, refs := range [...][]*Type{s.Extends, s.Mixins} { + for _, ref := range refs { + if ref.Kind != KindReference { + continue + } + if base, ok := g.structures[ref.Name]; ok && strings.HasPrefix(ref.Name, "_") { + // Merge the private base directly into this struct so the embed of + // the underscore type disappears while its contributed embeds and + // fields come along, rendered against the public owner. + embeds, fields := g.inlineContribution(s.Name, base, seenEmbeds, seenFields) + rs.Embeds = append(rs.Embeds, embeds...) + rs.Fields = append(rs.Fields, fields...) + continue + } + if !seenEmbeds[ref.Name] { + seenEmbeds[ref.Name] = true + rs.Embeds = append(rs.Embeds, ref.Name) + } } } for _, p := range s.Properties { @@ -236,6 +262,50 @@ func (g *Generator) analyzeStructures() []*renderedStruct { return out } +// inlineContribution returns the embeds and fields a structure contributes when +// flattened into a referrer, recursively flattening any further +// underscore-prefixed references. owner is the public struct the fields are +// rendered against, so doc hints and hot-field overrides match the parent. +func (g *Generator) inlineContribution( + owner string, + s *Structure, + seenEmbeds, seenFields map[string]bool, +) (embeds []string, fields []renderedField) { + visited := make(map[string]bool) + var flatten func(structure *Structure) + flatten = func(structure *Structure) { + if visited[structure.Name] { + return + } + visited[structure.Name] = true + + for _, refs := range [...][]*Type{structure.Extends, structure.Mixins} { + for _, ref := range refs { + if ref.Kind != KindReference { + continue + } + if base, ok := g.structures[ref.Name]; ok && strings.HasPrefix(ref.Name, "_") { + flatten(base) + continue + } + if !seenEmbeds[ref.Name] { + seenEmbeds[ref.Name] = true + embeds = append(embeds, ref.Name) + } + } + } + for _, p := range structure.Properties { + if !seenFields[p.Name] { + seenFields[p.Name] = true + fields = append(fields, g.renderField(owner, p)) + } + } + } + + flatten(s) + return embeds, fields +} + func (g *Generator) renderField(owner string, p *Property) renderedField { fieldName := exportName(p.Name) hint := owner + fieldName @@ -298,6 +368,10 @@ func (g *Generator) renderStructures(structs []*renderedStruct) string { var b strings.Builder for _, s := range structs { b.WriteString(s.Doc) + if len(s.Embeds) == 0 && len(s.Fields) == 0 { + fmt.Fprintf(&b, "type %s struct{}\n\n", s.Name) + continue + } fmt.Fprintf(&b, "type %s struct {\n", s.Name) for _, e := range s.Embeds { fmt.Fprintf(&b, "\t%s\n", e) @@ -558,6 +632,10 @@ func (g *Generator) renderScalarWrappers(b *strings.Builder) { func (g *Generator) renderLiteralStruct(b *strings.Builder, d *literalDecl) { fmt.Fprintf(b, "// %s is a generated inline object literal type.\n", d.Name) + if len(d.Lit.Properties) == 0 { + fmt.Fprintf(b, "type %s struct{}\n\n", d.Name) + return + } fmt.Fprintf(b, "type %s struct {\n", d.Name) for i, p := range d.Lit.Properties { f := g.renderField(d.Name, p) diff --git a/internal/genlsp/emit_test.go b/internal/genlsp/emit_test.go index 2426c25..03b66a0 100644 --- a/internal/genlsp/emit_test.go +++ b/internal/genlsp/emit_test.go @@ -102,6 +102,89 @@ func TestGeneratedFileName(t *testing.T) { } } +func TestAnalyzeStructuresDeduplicatesFlattenedPrivateBases(t *testing.T) { + ref := func(name string) *Type { return &Type{Kind: KindReference, Name: name} } + prop := func(name string) *Property { + return &Property{Name: name, Type: &Type{Kind: KindBase, Name: string(BaseString)}} + } + m := &MetaModel{ + Structures: []*Structure{ + {Name: "SharedEmbed"}, + { + Name: "_BaseA", + Extends: []*Type{ref("SharedEmbed")}, + Properties: []*Property{prop("shadow"), prop("baseAOnly")}, + }, + { + Name: "_BaseB", + Extends: []*Type{ref("SharedEmbed")}, + Properties: []*Property{prop("shadow"), prop("baseBOnly")}, + }, + { + Name: "Public", + Extends: []*Type{ref("_BaseA"), ref("_BaseB"), ref("SharedEmbed")}, + Properties: []*Property{prop("shadow"), prop("own")}, + }, + { + Name: "_CycleA", + Extends: []*Type{ref("_CycleB")}, + Properties: []*Property{prop("cycleA")}, + }, + { + Name: "_CycleB", + Extends: []*Type{ref("_CycleA")}, + Properties: []*Property{prop("cycleB")}, + }, + { + Name: "PublicCycle", + Extends: []*Type{ref("_CycleA")}, + Properties: []*Property{prop("cycleOwn")}, + }, + }, + } + g := NewGenerator(m, "protocol") + + var public, publicCycle *renderedStruct + for _, s := range g.analyzeStructures() { + switch s.Name { + case "Public": + public = s + case "PublicCycle": + publicCycle = s + } + if public != nil && publicCycle != nil { + break + } + } + if public == nil { + t.Fatal("Public structure not rendered") + } + if want := []string{"SharedEmbed"}; !slices.Equal(public.Embeds, want) { + t.Fatalf("Public embeds = %v, want %v", public.Embeds, want) + } + + var gotFields []string + for _, f := range public.Fields { + gotFields = append(gotFields, f.Name) + } + wantFields := []string{"BaseAOnly", "BaseBOnly", "Shadow", "Own"} + if !slices.Equal(gotFields, wantFields) { + t.Fatalf("Public fields = %v, want %v", gotFields, wantFields) + } + + if publicCycle == nil { + t.Fatal("PublicCycle structure not rendered") + } + gotFields = gotFields[:0] + for _, f := range publicCycle.Fields { + gotFields = append(gotFields, f.Name) + } + wantFields = []string{"CycleB", "CycleA", "CycleOwn"} + if !slices.Equal(gotFields, wantFields) { + t.Fatalf("PublicCycle fields = %v, want %v", gotFields, wantFields) + } +} + func TestHotOptionalField(t *testing.T) { tests := map[string]struct { owner string diff --git a/lifecycle.gen.go b/lifecycle.gen.go index c402d15..8159bb2 100644 --- a/lifecycle.gen.go +++ b/lifecycle.gen.go @@ -11,38 +11,8 @@ import ( // InitializeParams is defined by the LSP specification. type InitializeParams struct { - _InitializeParams - WorkspaceFoldersInitializeParams -} - -// InitializeResult The result returned from an initialize request. -type InitializeResult struct { - // Capabilities The capabilities the language server provides. - Capabilities ServerCapabilities `json:"capabilities"` - - // ServerInfo Information about the server. - // - // Since: 3.15.0 - ServerInfo ServerInfo `json:"serverInfo,omitzero"` -} - -// InitializeError The data type of the ResponseError if the -// initialize request fails. -type InitializeError struct { - // Retry Indicates whether the client execute the following retry logic: - // (1) show the message provided by the ResponseError to the user - // (2) user selects retry or cancel - // (3) if user selected retry the initialize method is sent again. - Retry bool `json:"retry"` -} - -// InitializedParams is defined by the LSP specification. -type InitializedParams struct { -} - -// _InitializeParams The initialize parameters -type _InitializeParams struct { WorkDoneProgressParams + WorkspaceFoldersInitializeParams // ProcessID The process Id of the parent process that started // the server. @@ -89,6 +59,30 @@ type _InitializeParams struct { Trace TraceValue `json:"trace,omitzero"` } +// InitializeResult The result returned from an initialize request. +type InitializeResult struct { + // Capabilities The capabilities the language server provides. + Capabilities ServerCapabilities `json:"capabilities"` + + // ServerInfo Information about the server. + // + // Since: 3.15.0 + ServerInfo ServerInfo `json:"serverInfo,omitzero"` +} + +// InitializeError The data type of the ResponseError if the +// initialize request fails. +type InitializeError struct { + // Retry Indicates whether the client execute the following retry logic: + // (1) show the message provided by the ResponseError to the user + // (2) user selects retry or cancel + // (3) if user selected retry the initialize method is sent again. + Retry bool `json:"retry"` +} + +// InitializedParams is defined by the LSP specification. +type InitializedParams struct{} + // ServerCapabilities Defines the capabilities provided by a language // server. type ServerCapabilities struct { diff --git a/nullable.go b/nullable.go index edf43a7..c5d9223 100644 --- a/nullable.go +++ b/nullable.go @@ -25,6 +25,14 @@ type Nullable[T any] struct { null bool } +// NewNullable returns a Nullable holding the present value v (set=true, null=false). +func NewNullable[T any](v T) Nullable[T] { return Nullable[T]{set: true, value: v} } + +// NullNullable returns a Nullable that represents an explicit JSON null +// (set=true, null=true). This is the second non-zero tri-state; the third +// (absent) needs no constructor because it is the zero Nullable[T]{}. +func NullNullable[T any]() Nullable[T] { return Nullable[T]{set: true, null: true} } + // IsZero reports whether the value is absent. It drives the ",omitzero" tag so // an unset Nullable is omitted entirely. func (n Nullable[T]) IsZero() bool { return !n.set } diff --git a/nullable_test.go b/nullable_test.go index 696a9fc..d490a72 100644 --- a/nullable_test.go +++ b/nullable_test.go @@ -3,7 +3,105 @@ package protocol -import "testing" +import ( + "testing" + + gocmp "github.com/google/go-cmp/cmp" +) + +// TestNewNullable verifies the three constructor paths for Nullable[T]: +// NewNullable (value state), NullNullable (null state), and the zero value +// (absent state). It also checks JSON round-trips through a real struct field. +func TestNewNullable(t *testing.T) { + t.Run("state predicates", func(t *testing.T) { + wf := WorkspaceFolder{URI: "file:///a", Name: "a"} + var zeroWF WorkspaceFolder + + tests := map[string]struct { + n Nullable[WorkspaceFolder] + wantIsZero bool + wantIsNull bool + wantVal WorkspaceFolder + wantOK bool + }{ + "success: NewNullable sets value state": { + n: NewNullable(wf), + wantIsZero: false, + wantIsNull: false, + wantVal: wf, + wantOK: true, + }, + "success: NullNullable sets null state": { + n: NullNullable[WorkspaceFolder](), + wantIsZero: false, + wantIsNull: true, + wantVal: zeroWF, + wantOK: false, + }, + "success: zero Nullable is absent state": { + n: Nullable[WorkspaceFolder]{}, + wantIsZero: true, + wantIsNull: false, + wantVal: zeroWF, + wantOK: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + if got := tt.n.IsZero(); got != tt.wantIsZero { + t.Errorf("IsZero = %v, want %v", got, tt.wantIsZero) + } + if got := tt.n.IsNull(); got != tt.wantIsNull { + t.Errorf("IsNull = %v, want %v", got, tt.wantIsNull) + } + got, ok := tt.n.Get() + if ok != tt.wantOK { + t.Errorf("Get() ok = %v, want %v", ok, tt.wantOK) + } + if diff := gocmp.Diff(tt.wantVal, got); diff != "" { + t.Errorf("Get() value mismatch (-want +got):\n%s", diff) + } + }) + } + }) + + t.Run("JSON round-trip via WorkspaceFoldersInitializeParams", func(t *testing.T) { + wf := []WorkspaceFolder{{URI: "file:///w", Name: "w"}} + + tests := map[string]struct { + build func() WorkspaceFoldersInitializeParams + wantJSON string + }{ + "success: NewNullable marshals to value": { + build: func() WorkspaceFoldersInitializeParams { + return WorkspaceFoldersInitializeParams{WorkspaceFolders: NewNullable(wf)} + }, + wantJSON: `{"workspaceFolders":[{"uri":"file:///w","name":"w"}]}`, + }, + "success: NullNullable marshals to null": { + build: func() WorkspaceFoldersInitializeParams { + return WorkspaceFoldersInitializeParams{WorkspaceFolders: NullNullable[[]WorkspaceFolder]()} + }, + wantJSON: `{"workspaceFolders":null}`, + }, + "success: zero Nullable is omitted": { + build: func() WorkspaceFoldersInitializeParams { return WorkspaceFoldersInitializeParams{} }, + wantJSON: `{}`, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + out, err := Marshal(tt.build()) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if got, want := canon(t, out), canon(t, []byte(tt.wantJSON)); got != want { + t.Errorf("JSON mismatch: got=%s want=%s", got, want) + } + }) + } + }) +} // TestNullableTriState verifies that an optional-AND-nullable field // distinguishes absent, explicit null, and a value across a JSON round-trip. @@ -14,9 +112,24 @@ func TestNullableTriState(t *testing.T) { wantNull bool wantLen int }{ - "success: absent": {`{}`, true, false, 0}, - "success: null": {`{"workspaceFolders":null}`, false, true, 0}, - "success: value": {`{"workspaceFolders":[{"uri":"file:///w","name":"w"}]}`, false, false, 1}, + "success: absent": { + json: `{}`, + wantAbsent: true, + wantNull: false, + wantLen: 0, + }, + "success: null": { + json: `{"workspaceFolders":null}`, + wantAbsent: false, + wantNull: true, + wantLen: 0, + }, + "success: value": { + json: `{"workspaceFolders":[{"uri":"file:///w","name":"w"}]}`, + wantAbsent: false, + wantNull: false, + wantLen: 1, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { diff --git a/protocol.go b/protocol.go index e3429a7..f5139a2 100644 --- a/protocol.go +++ b/protocol.go @@ -27,6 +27,8 @@ func NewServer(ctx context.Context, server Server, stream jsonrpc2.Stream) (cont // NewClient returns the context in which the [Client] is embedded, the jsonrpc2 // connection, and the [Server] dispatcher. The connection serves the supplied // [Client] and is wired with the union-aware [lspCodec]. +// +//nolint:unparam // returned context mirrors NewServer and is part of the stable symmetric API; callers may embed and reuse it func NewClient(ctx context.Context, client Client, stream jsonrpc2.Stream) (context.Context, jsonrpc2.Conn, Server) { ctx = WithClient(ctx, client) diff --git a/types_unions.gen.go b/types_unions.gen.go index f2563ee..1ad4d37 100644 --- a/types_unions.gen.go +++ b/types_unions.gen.go @@ -56,8 +56,7 @@ type MarkedStringSlice []MarkedString type ParameterInformationLabelTuple [2]uint32 // SemanticTokensOptionsRange is a generated inline object literal type. -type SemanticTokensOptionsRange struct { -} +type SemanticTokensOptionsRange struct{} // Definition The definition of a symbol represented as one or many [Location]. // For most programming languages there is only one location at which a symbol is diff --git a/unimplemented.go b/unimplemented.go index a6c2875..f0809ba 100644 --- a/unimplemented.go +++ b/unimplemented.go @@ -9,15 +9,25 @@ import ( "go.lsp.dev/jsonrpc2" ) -// errNotImplemented is the sentinel returned by every [UnimplementedServer] and -// [UnimplementedClient] method. It carries the JSON-RPC "method not found" code -// so peers observe a well-formed, classified error for un-overridden methods. +// errNotImplemented is the sentinel returned by every un-overridden request +// method of [UnimplementedServer] and [UnimplementedClient]. It carries the +// JSON-RPC "method not found" code so peers observe a well-formed, classified +// error response for un-overridden requests. +// +// Notification methods do NOT return this sentinel: a notification has no +// response, so the jsonrpc2 dispatcher treats a non-nil error from a +// notification handler as a connection-level failure and tears the connection +// down. An un-overridden notification therefore returns nil (the notification +// is silently ignored), which is also what the LSP specification prescribes for +// notifications a peer does not handle. var errNotImplemented = jsonrpc2.NewError(jsonrpc2.ErrMethodNotFound.Code, "not implemented") // UnimplementedServer is an embeddable default implementation of the [Server] -// interface. Every method returns [errNotImplemented] together with the zero -// value of its result, so consumers can embed it and override only the methods -// they support. +// interface. Each un-overridden request method returns [errNotImplemented] +// together with the zero value of its result, and each un-overridden +// notification method returns nil (ignoring the notification), so consumers can +// embed it and override only the methods they support without an un-overridden +// notification tearing down the connection. type UnimplementedServer struct{} // compile-time assertion that UnimplementedServer satisfies Server. @@ -28,35 +38,37 @@ func (UnimplementedServer) Initialize(context.Context, *InitializeParams) (*Init } func (UnimplementedServer) Initialized(context.Context, *InitializedParams) error { - return errNotImplemented + return nil // initialized is a notification; see errNotImplemented. } func (UnimplementedServer) Shutdown(context.Context) error { return errNotImplemented } -func (UnimplementedServer) Exit(context.Context) error { return errNotImplemented } +func (UnimplementedServer) Exit(context.Context) error { + return nil // exit is a notification; see errNotImplemented. +} func (UnimplementedServer) SetTrace(context.Context, *SetTraceParams) error { - return errNotImplemented + return nil // $/setTrace is a notification; see errNotImplemented. } func (UnimplementedServer) Progress(context.Context, *ProgressParams) error { - return errNotImplemented + return nil // $/progress is a notification; see errNotImplemented. } func (UnimplementedServer) WorkDoneProgressCancel(context.Context, *WorkDoneProgressCancelParams) error { - return errNotImplemented + return nil // window/workDoneProgress/cancel is a notification; see errNotImplemented. } func (UnimplementedServer) DidOpen(context.Context, *DidOpenTextDocumentParams) error { - return errNotImplemented + return nil // textDocument/didOpen is a notification; see errNotImplemented. } func (UnimplementedServer) DidChange(context.Context, *DidChangeTextDocumentParams) error { - return errNotImplemented + return nil // textDocument/didChange is a notification; see errNotImplemented. } func (UnimplementedServer) WillSave(context.Context, *WillSaveTextDocumentParams) error { - return errNotImplemented + return nil // textDocument/willSave is a notification; see errNotImplemented. } func (UnimplementedServer) WillSaveWaitUntil(context.Context, *WillSaveTextDocumentParams) ([]TextEdit, error) { @@ -64,27 +76,27 @@ func (UnimplementedServer) WillSaveWaitUntil(context.Context, *WillSaveTextDocum } func (UnimplementedServer) DidSave(context.Context, *DidSaveTextDocumentParams) error { - return errNotImplemented + return nil // textDocument/didSave is a notification; see errNotImplemented. } func (UnimplementedServer) DidClose(context.Context, *DidCloseTextDocumentParams) error { - return errNotImplemented + return nil // textDocument/didClose is a notification; see errNotImplemented. } func (UnimplementedServer) DidOpenNotebookDocument(context.Context, *DidOpenNotebookDocumentParams) error { - return errNotImplemented + return nil // notebookDocument/didOpen is a notification; see errNotImplemented. } func (UnimplementedServer) DidChangeNotebookDocument(context.Context, *DidChangeNotebookDocumentParams) error { - return errNotImplemented + return nil // notebookDocument/didChange is a notification; see errNotImplemented. } func (UnimplementedServer) DidSaveNotebookDocument(context.Context, *DidSaveNotebookDocumentParams) error { - return errNotImplemented + return nil // notebookDocument/didSave is a notification; see errNotImplemented. } func (UnimplementedServer) DidCloseNotebookDocument(context.Context, *DidCloseNotebookDocumentParams) error { - return errNotImplemented + return nil // notebookDocument/didClose is a notification; see errNotImplemented. } func (UnimplementedServer) Declaration(context.Context, *DeclarationParams) (DeclarationResult, error) { @@ -272,11 +284,11 @@ func (UnimplementedServer) WorkspaceSymbolResolve(context.Context, *WorkspaceSym } func (UnimplementedServer) DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error { - return errNotImplemented + return nil // workspace/didChangeConfiguration is a notification; see errNotImplemented. } func (UnimplementedServer) DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error { - return errNotImplemented + return nil // workspace/didChangeWorkspaceFolders is a notification; see errNotImplemented. } func (UnimplementedServer) WillCreateFiles(context.Context, *CreateFilesParams) (*WorkspaceEdit, error) { @@ -292,19 +304,19 @@ func (UnimplementedServer) WillDeleteFiles(context.Context, *DeleteFilesParams) } func (UnimplementedServer) DidCreateFiles(context.Context, *CreateFilesParams) error { - return errNotImplemented + return nil // workspace/didCreateFiles is a notification; see errNotImplemented. } func (UnimplementedServer) DidRenameFiles(context.Context, *RenameFilesParams) error { - return errNotImplemented + return nil // workspace/didRenameFiles is a notification; see errNotImplemented. } func (UnimplementedServer) DidDeleteFiles(context.Context, *DeleteFilesParams) error { - return errNotImplemented + return nil // workspace/didDeleteFiles is a notification; see errNotImplemented. } func (UnimplementedServer) DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error { - return errNotImplemented + return nil // workspace/didChangeWatchedFiles is a notification; see errNotImplemented. } func (UnimplementedServer) ExecuteCommand(context.Context, *ExecuteCommandParams) (LSPAny, error) { @@ -320,20 +332,22 @@ func (UnimplementedServer) Request(context.Context, string, any) (any, error) { } // UnimplementedClient is an embeddable default implementation of the [Client] -// interface. Every method returns [errNotImplemented] together with the zero -// value of its result, so consumers can embed it and override only the methods -// they support. +// interface. Each un-overridden request method returns [errNotImplemented] +// together with the zero value of its result, and each un-overridden +// notification method returns nil (ignoring the notification), so consumers can +// embed it and override only the methods they support without an un-overridden +// notification tearing down the connection. type UnimplementedClient struct{} // compile-time assertion that UnimplementedClient satisfies Client. var _ Client = UnimplementedClient{} func (UnimplementedClient) Progress(context.Context, *ProgressParams) error { - return errNotImplemented + return nil // $/progress is a notification; see errNotImplemented. } func (UnimplementedClient) LogTrace(context.Context, *LogTraceParams) error { - return errNotImplemented + return nil // $/logTrace is a notification; see errNotImplemented. } func (UnimplementedClient) RegisterCapability(context.Context, *RegistrationParams) error { @@ -345,7 +359,7 @@ func (UnimplementedClient) UnregisterCapability(context.Context, *Unregistration } func (UnimplementedClient) ShowMessage(context.Context, *ShowMessageParams) error { - return errNotImplemented + return nil // window/showMessage is a notification; see errNotImplemented. } func (UnimplementedClient) ShowMessageRequest(context.Context, *ShowMessageRequestParams) (*MessageActionItem, error) { @@ -353,7 +367,7 @@ func (UnimplementedClient) ShowMessageRequest(context.Context, *ShowMessageReque } func (UnimplementedClient) LogMessage(context.Context, *LogMessageParams) error { - return errNotImplemented + return nil // window/logMessage is a notification; see errNotImplemented. } func (UnimplementedClient) ShowDocument(context.Context, *ShowDocumentParams) (*ShowDocumentResult, error) { @@ -365,11 +379,11 @@ func (UnimplementedClient) WorkDoneProgressCreate(context.Context, *WorkDoneProg } func (UnimplementedClient) Telemetry(context.Context, LSPAny) error { - return errNotImplemented + return nil // telemetry/event is a notification; see errNotImplemented. } func (UnimplementedClient) PublishDiagnostics(context.Context, *PublishDiagnosticsParams) error { - return errNotImplemented + return nil // textDocument/publishDiagnostics is a notification; see errNotImplemented. } func (UnimplementedClient) Configuration(context.Context, *ConfigurationParams) ([]LSPAny, error) {