Files
kjol/tools/tsgo/internal/lsp/lsproto/lsp_json_test.go
2026-07-09 16:50:43 -04:00

1037 lines
30 KiB
Go

package lsproto
import (
"reflect"
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/json"
"gotest.tools/v3/assert"
)
func TestUnmarshalRejectsNullForOptionalNonNullableFields(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
target any
errText string
}{
{
name: "InlayHint kind null",
input: `{"position": {"line": 0, "character": 0}, "label": "foo", "kind": null}`,
target: new(InlayHint),
errText: `null value is not allowed for field "kind"`,
},
{
name: "InlayHint textEdits null",
input: `{"position": {"line": 0, "character": 0}, "label": "foo", "textEdits": null}`,
target: new(InlayHint),
errText: `null value is not allowed for field "textEdits"`,
},
{
name: "InlayHint paddingLeft null",
input: `{"position": {"line": 0, "character": 0}, "label": "foo", "paddingLeft": null}`,
target: new(InlayHint),
errText: `null value is not allowed for field "paddingLeft"`,
},
{
name: "FoldingRange kind null",
input: `{"startLine": 0, "endLine": 10, "kind": null}`,
target: new(FoldingRange),
errText: `null value is not allowed for field "kind"`,
},
{
name: "FoldingRange startCharacter null",
input: `{"startLine": 0, "endLine": 10, "startCharacter": null}`,
target: new(FoldingRange),
errText: `null value is not allowed for field "startCharacter"`,
},
{
name: "CompletionItem insertTextFormat null",
input: `{"label": "test", "insertTextFormat": null}`,
target: new(CompletionItem),
errText: `null value is not allowed for field "insertTextFormat"`,
},
{
name: "Hover range null",
input: `{"contents": {"kind": "plaintext", "value": "hi"}, "range": null}`,
target: new(Hover),
errText: `null value is not allowed for field "range"`,
},
{
name: "WorkDoneProgressOptions workDoneProgress null",
input: `{"workDoneProgress": null}`,
target: new(WorkDoneProgressOptions),
errText: `null value is not allowed for field "workDoneProgress"`,
},
{
name: "CallHierarchyIncomingCallsParams item null",
input: `{"item": null}`,
target: new(CallHierarchyIncomingCallsParams),
errText: `null value is not allowed for field "item"`,
},
{
name: "CallHierarchyIncomingCall from null",
input: `{"from": null, "fromRanges": []}`,
target: new(CallHierarchyIncomingCall),
errText: `null value is not allowed for field "from"`,
},
{
name: "InitializeParams capabilities null",
input: `{"processId": null, "rootUri": null, "capabilities": null}`,
target: new(InitializeParams),
errText: `null value is not allowed for field "capabilities"`,
},
{
name: "InitializeResult capabilities null",
input: `{"capabilities": null}`,
target: new(InitializeResult),
errText: `null value is not allowed for field "capabilities"`,
},
{
name: "SemanticTokens data null (required slice)",
input: `{"data": null}`,
target: new(SemanticTokens),
errText: `null value is not allowed for field "data"`,
},
{
name: "TextDocumentEdit edits null (required slice)",
input: `{"textDocument": {"uri": "file:///a.ts", "version": 1}, "edits": null}`,
target: new(TextDocumentEdit),
errText: `null value is not allowed for field "edits"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := json.Unmarshal([]byte(tt.input), tt.target)
assert.ErrorContains(t, err, tt.errText)
})
}
}
func TestUnmarshalAcceptsNullForNullableFields(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
target any
}{
{
name: "InitializeParams rootUri null",
input: `{"processId": null, "rootUri": null, "capabilities": {}}`,
target: new(InitializeParams),
},
{
name: "InitializeParams workspaceFolders null",
input: `{"processId": null, "rootUri": null, "capabilities": {}, "workspaceFolders": null}`,
target: new(InitializeParams),
},
{
name: "InitializeParams processId null",
input: `{"processId": null, "rootUri": null, "capabilities": {}}`,
target: new(InitializeParams),
},
{
name: "InitializationOptions userPreferences null",
input: `{"userPreferences": null}`,
target: new(InitializationOptions),
},
{
name: "InitializeParams initializationOptions null",
input: `{"processId": null, "rootUri": null, "capabilities": {}, "initializationOptions": null}`,
target: new(InitializeParams),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := json.Unmarshal([]byte(tt.input), tt.target)
assert.NilError(t, err)
})
}
}
func TestUnmarshalAcceptsOmittedOptionalFields(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
target any
check func(t *testing.T, target any)
}{
{
name: "InlayHint with only required fields",
input: `{"position": {"line": 1, "character": 5}, "label": "test"}`,
target: new(InlayHint),
check: func(t *testing.T, target any) {
t.Helper()
hint := target.(*InlayHint)
assert.Assert(t, hint.Kind == nil)
assert.Assert(t, hint.TextEdits == nil)
assert.Assert(t, hint.Tooltip == nil)
assert.Assert(t, hint.PaddingLeft == nil)
assert.Assert(t, hint.PaddingRight == nil)
assert.Assert(t, hint.Data == nil)
assert.Equal(t, hint.Position.Line, uint32(1))
assert.Equal(t, hint.Position.Character, uint32(5))
},
},
{
name: "FoldingRange with only required fields",
input: `{"startLine": 5, "endLine": 10}`,
target: new(FoldingRange),
check: func(t *testing.T, target any) {
t.Helper()
fr := target.(*FoldingRange)
assert.Assert(t, fr.Kind == nil)
assert.Assert(t, fr.StartCharacter == nil)
assert.Assert(t, fr.EndCharacter == nil)
assert.Assert(t, fr.CollapsedText == nil)
assert.Equal(t, fr.StartLine, uint32(5))
assert.Equal(t, fr.EndLine, uint32(10))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := json.Unmarshal([]byte(tt.input), tt.target)
assert.NilError(t, err)
tt.check(t, tt.target)
})
}
}
func TestUnmarshalRejectsIncompleteObjects(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
target any
errText string
}{
{
name: "InlayHint missing position",
input: `{"label": "test"}`,
target: new(InlayHint),
errText: "missing required properties: position",
},
{
name: "InlayHint missing label",
input: `{"position": {"line": 0, "character": 0}}`,
target: new(InlayHint),
errText: "missing required properties: label",
},
{
name: "Location missing uri",
input: `{"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}}`,
target: new(Location),
errText: "missing required properties: uri",
},
{
name: "Location empty object",
input: `{}`,
target: new(Location),
errText: "missing required properties: uri, range",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := json.Unmarshal([]byte(tt.input), tt.target)
assert.ErrorContains(t, err, tt.errText)
})
}
}
func TestMarshalUnmarshalRoundTrip(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value any
}{
{
name: "InlayHint with kind",
value: &InlayHint{
Position: Position{Line: 1, Character: 5},
Label: StringOrInlayHintLabelParts{String: new("param")},
Kind: new(InlayHintKindParameter),
},
},
{
name: "InlayHint minimal",
value: &InlayHint{
Position: Position{Line: 0, Character: 0},
Label: StringOrInlayHintLabelParts{String: new("x")},
},
},
{
name: "FoldingRange with all fields",
value: &FoldingRange{
StartLine: 1,
StartCharacter: new(uint32(0)),
EndLine: 10,
EndCharacter: new(uint32(5)),
Kind: new(FoldingRangeKindRegion),
CollapsedText: new("..."),
},
},
{
name: "Location",
value: &Location{
Uri: "file:///test.ts",
Range: Range{
Start: Position{Line: 1, Character: 2},
End: Position{Line: 3, Character: 4},
},
},
},
{
name: "InitializeParams with null processId",
value: &InitializeParams{
ProcessId: IntegerOrNull{},
RootUri: DocumentUriOrNull{DocumentUri: new(DocumentUri("file:///workspace"))},
Capabilities: &ClientCapabilities{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
data, err := json.Marshal(tt.value)
assert.NilError(t, err)
// Unmarshal into a new value of the same type
switch v := tt.value.(type) {
case *InlayHint:
var result InlayHint
err = json.Unmarshal(data, &result)
assert.NilError(t, err)
assert.DeepEqual(t, *v, result)
case *FoldingRange:
var result FoldingRange
err = json.Unmarshal(data, &result)
assert.NilError(t, err)
assert.DeepEqual(t, *v, result)
case *Location:
var result Location
err = json.Unmarshal(data, &result)
assert.NilError(t, err)
assert.DeepEqual(t, *v, result)
case *InitializeParams:
var result InitializeParams
err = json.Unmarshal(data, &result)
assert.NilError(t, err)
assert.DeepEqual(t, *v, result)
default:
t.Fatalf("unhandled type %T", tt.value)
}
})
}
}
func TestUnmarshalUnionTypes(t *testing.T) {
t.Parallel()
t.Run("IntegerOrString with integer", func(t *testing.T) {
t.Parallel()
var v IntegerOrString
err := json.Unmarshal([]byte(`42`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Integer != nil)
assert.Equal(t, *v.Integer, int32(42))
assert.Assert(t, v.String == nil)
})
t.Run("IntegerOrString with string", func(t *testing.T) {
t.Parallel()
var v IntegerOrString
err := json.Unmarshal([]byte(`"hello"`), &v)
assert.NilError(t, err)
assert.Assert(t, v.String != nil)
assert.Equal(t, *v.String, "hello")
assert.Assert(t, v.Integer == nil)
})
t.Run("IntegerOrNull with integer", func(t *testing.T) {
t.Parallel()
var v IntegerOrNull
err := json.Unmarshal([]byte(`42`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Integer != nil)
assert.Equal(t, *v.Integer, int32(42))
})
t.Run("IntegerOrNull with null", func(t *testing.T) {
t.Parallel()
var v IntegerOrNull
err := json.Unmarshal([]byte(`null`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Integer == nil)
})
t.Run("DocumentUriOrNull with string", func(t *testing.T) {
t.Parallel()
var v DocumentUriOrNull
err := json.Unmarshal([]byte(`"file:///test.ts"`), &v)
assert.NilError(t, err)
assert.Assert(t, v.DocumentUri != nil)
assert.Equal(t, *v.DocumentUri, DocumentUri("file:///test.ts"))
})
t.Run("DocumentUriOrNull with null", func(t *testing.T) {
t.Parallel()
var v DocumentUriOrNull
err := json.Unmarshal([]byte(`null`), &v)
assert.NilError(t, err)
assert.Assert(t, v.DocumentUri == nil)
})
}
func TestMarshalUnionTypes(t *testing.T) {
t.Parallel()
t.Run("IntegerOrNull with value", func(t *testing.T) {
t.Parallel()
v := IntegerOrNull{Integer: new(int32(42))}
data, err := json.Marshal(&v)
assert.NilError(t, err)
assert.Equal(t, string(data), "42")
})
t.Run("IntegerOrNull with null", func(t *testing.T) {
t.Parallel()
v := IntegerOrNull{}
data, err := json.Marshal(&v)
assert.NilError(t, err)
assert.Equal(t, string(data), "null")
})
t.Run("IntegerOrString with integer", func(t *testing.T) {
t.Parallel()
v := IntegerOrString{Integer: new(int32(7))}
data, err := json.Marshal(&v)
assert.NilError(t, err)
assert.Equal(t, string(data), "7")
})
t.Run("IntegerOrString with string", func(t *testing.T) {
t.Parallel()
v := IntegerOrString{String: new("tok")}
data, err := json.Marshal(&v)
assert.NilError(t, err)
assert.Equal(t, string(data), `"tok"`)
})
}
func TestUnmarshalIgnoresUnknownFields(t *testing.T) {
t.Parallel()
t.Run("Location with extra fields", func(t *testing.T) {
t.Parallel()
var loc Location
err := json.Unmarshal([]byte(`{
"uri": "file:///test.ts",
"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 5}},
"someUnknownField": 42,
"anotherUnknown": {"nested": true}
}`), &loc)
assert.NilError(t, err)
assert.Equal(t, loc.Uri, DocumentUri("file:///test.ts"))
})
t.Run("InlayHint with extra fields", func(t *testing.T) {
t.Parallel()
var hint InlayHint
err := json.Unmarshal([]byte(`{
"position": {"line": 0, "character": 0},
"label": "x",
"futureField": [1, 2, 3]
}`), &hint)
assert.NilError(t, err)
})
}
func TestUnmarshalRejectsWrongTypes(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
target any
}{
{
name: "Location receives array",
input: `[]`,
target: new(Location),
},
{
name: "Location receives string",
input: `"not an object"`,
target: new(Location),
},
{
name: "Location receives number",
input: `42`,
target: new(Location),
},
{
name: "Location receives null",
input: `null`,
target: new(Location),
},
{
name: "FoldingRange receives boolean",
input: `true`,
target: new(FoldingRange),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := json.Unmarshal([]byte(tt.input), tt.target)
assert.Assert(t, err != nil, "expected error for input %s", tt.input)
})
}
}
func TestUnmarshalUnionTypeWrongKind(t *testing.T) {
t.Parallel()
t.Run("IntegerOrString rejects boolean", func(t *testing.T) {
t.Parallel()
var v IntegerOrString
err := json.Unmarshal([]byte(`true`), &v)
assert.Assert(t, err != nil)
})
t.Run("IntegerOrString rejects null", func(t *testing.T) {
t.Parallel()
var v IntegerOrString
err := json.Unmarshal([]byte(`null`), &v)
assert.Assert(t, err != nil)
})
t.Run("IntegerOrString rejects object", func(t *testing.T) {
t.Parallel()
var v IntegerOrString
err := json.Unmarshal([]byte(`{}`), &v)
assert.Assert(t, err != nil)
})
t.Run("IntegerOrString rejects array", func(t *testing.T) {
t.Parallel()
var v IntegerOrString
err := json.Unmarshal([]byte(`[]`), &v)
assert.Assert(t, err != nil)
})
t.Run("StringOrInlayHintLabelParts rejects number", func(t *testing.T) {
t.Parallel()
var v StringOrInlayHintLabelParts
err := json.Unmarshal([]byte(`42`), &v)
assert.Assert(t, err != nil)
})
t.Run("StringOrInlayHintLabelParts rejects boolean", func(t *testing.T) {
t.Parallel()
var v StringOrInlayHintLabelParts
err := json.Unmarshal([]byte(`true`), &v)
assert.Assert(t, err != nil)
})
}
func TestUnmarshalBooleanUnionTypes(t *testing.T) {
t.Parallel()
t.Run("BooleanOrHoverOptions with true", func(t *testing.T) {
t.Parallel()
var v BooleanOrHoverOptions
err := json.Unmarshal([]byte(`true`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Boolean != nil)
assert.Equal(t, *v.Boolean, true)
assert.Assert(t, v.HoverOptions == nil)
})
t.Run("BooleanOrHoverOptions with false", func(t *testing.T) {
t.Parallel()
var v BooleanOrHoverOptions
err := json.Unmarshal([]byte(`false`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Boolean != nil)
assert.Equal(t, *v.Boolean, false)
assert.Assert(t, v.HoverOptions == nil)
})
t.Run("BooleanOrHoverOptions with object", func(t *testing.T) {
t.Parallel()
var v BooleanOrHoverOptions
err := json.Unmarshal([]byte(`{}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Boolean == nil)
assert.Assert(t, v.HoverOptions != nil)
})
t.Run("BooleanOrHoverOptions rejects string", func(t *testing.T) {
t.Parallel()
var v BooleanOrHoverOptions
err := json.Unmarshal([]byte(`"nope"`), &v)
assert.Assert(t, err != nil)
})
}
func TestUnmarshalDiscriminatorUnion(t *testing.T) {
t.Parallel()
t.Run("WorkDoneProgressBegin", func(t *testing.T) {
t.Parallel()
var v WorkDoneProgressBeginOrReportOrEnd
err := json.Unmarshal([]byte(`{"kind": "begin", "title": "Indexing"}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Begin != nil)
assert.Assert(t, v.Report == nil)
assert.Assert(t, v.End == nil)
assert.Equal(t, v.Begin.Title, "Indexing")
})
t.Run("WorkDoneProgressReport", func(t *testing.T) {
t.Parallel()
var v WorkDoneProgressBeginOrReportOrEnd
err := json.Unmarshal([]byte(`{"kind": "report", "message": "50%"}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Begin == nil)
assert.Assert(t, v.Report != nil)
assert.Assert(t, v.End == nil)
assert.Assert(t, v.Report.Message != nil)
assert.Equal(t, *v.Report.Message, "50%")
})
t.Run("WorkDoneProgressEnd", func(t *testing.T) {
t.Parallel()
var v WorkDoneProgressBeginOrReportOrEnd
err := json.Unmarshal([]byte(`{"kind": "end"}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.Begin == nil)
assert.Assert(t, v.Report == nil)
assert.Assert(t, v.End != nil)
})
t.Run("invalid discriminator", func(t *testing.T) {
t.Parallel()
var v WorkDoneProgressBeginOrReportOrEnd
err := json.Unmarshal([]byte(`{"kind": "invalid"}`), &v)
assert.Assert(t, err != nil)
})
}
func TestUnmarshalPresenceDiscriminatorUnion(t *testing.T) {
t.Parallel()
t.Run("TextEdit via range field", func(t *testing.T) {
t.Parallel()
var v TextEditOrInsertReplaceEdit
err := json.Unmarshal([]byte(`{
"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}},
"newText": "x"
}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.TextEdit != nil)
assert.Assert(t, v.InsertReplaceEdit == nil)
assert.Equal(t, v.TextEdit.NewText, "x")
})
t.Run("InsertReplaceEdit via insert field", func(t *testing.T) {
t.Parallel()
var v TextEditOrInsertReplaceEdit
err := json.Unmarshal([]byte(`{
"insert": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}},
"replace": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 2}},
"newText": "y"
}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.TextEdit == nil)
assert.Assert(t, v.InsertReplaceEdit != nil)
assert.Equal(t, v.InsertReplaceEdit.NewText, "y")
})
}
func TestUnmarshalStringOrArrayUnion(t *testing.T) {
t.Parallel()
t.Run("StringOrInlayHintLabelParts with string", func(t *testing.T) {
t.Parallel()
var v StringOrInlayHintLabelParts
err := json.Unmarshal([]byte(`"hello"`), &v)
assert.NilError(t, err)
assert.Assert(t, v.String != nil)
assert.Equal(t, *v.String, "hello")
assert.Assert(t, v.InlayHintLabelParts == nil)
})
t.Run("StringOrInlayHintLabelParts with array", func(t *testing.T) {
t.Parallel()
var v StringOrInlayHintLabelParts
err := json.Unmarshal([]byte(`[{"value": "param"}, {"value": ": "}, {"value": "string"}]`), &v)
assert.NilError(t, err)
assert.Assert(t, v.String == nil)
assert.Assert(t, v.InlayHintLabelParts != nil)
assert.Equal(t, len(*v.InlayHintLabelParts), 3)
assert.Equal(t, (*v.InlayHintLabelParts)[0].Value, "param")
})
}
func TestUnmarshalDocumentEditUnion(t *testing.T) {
t.Parallel()
t.Run("TextDocumentEdit without kind", func(t *testing.T) {
t.Parallel()
var v TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
err := json.Unmarshal([]byte(`{
"textDocument": {"uri": "file:///a.ts", "version": 1},
"edits": [{"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}, "newText": "x"}]
}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.TextDocumentEdit != nil)
assert.Assert(t, v.CreateFile == nil)
assert.Assert(t, v.RenameFile == nil)
assert.Assert(t, v.DeleteFile == nil)
})
t.Run("CreateFile with kind create", func(t *testing.T) {
t.Parallel()
var v TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
err := json.Unmarshal([]byte(`{"kind": "create", "uri": "file:///new.ts"}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.TextDocumentEdit == nil)
assert.Assert(t, v.CreateFile != nil)
assert.Equal(t, v.CreateFile.Uri, DocumentUri("file:///new.ts"))
})
t.Run("RenameFile with kind rename", func(t *testing.T) {
t.Parallel()
var v TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
err := json.Unmarshal([]byte(`{"kind": "rename", "oldUri": "file:///old.ts", "newUri": "file:///new.ts"}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.RenameFile != nil)
assert.Equal(t, v.RenameFile.OldUri, DocumentUri("file:///old.ts"))
})
t.Run("DeleteFile with kind delete", func(t *testing.T) {
t.Parallel()
var v TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
err := json.Unmarshal([]byte(`{"kind": "delete", "uri": "file:///gone.ts"}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.DeleteFile != nil)
assert.Equal(t, v.DeleteFile.Uri, DocumentUri("file:///gone.ts"))
})
}
func TestUnmarshalFieldOrdering(t *testing.T) {
t.Parallel()
t.Run("Location with reversed field order", func(t *testing.T) {
t.Parallel()
var loc Location
err := json.Unmarshal([]byte(`{
"range": {"start": {"line": 1, "character": 2}, "end": {"line": 3, "character": 4}},
"uri": "file:///test.ts"
}`), &loc)
assert.NilError(t, err)
assert.Equal(t, loc.Uri, DocumentUri("file:///test.ts"))
assert.Equal(t, loc.Range.Start.Line, uint32(1))
})
t.Run("InlayHint with kind before label", func(t *testing.T) {
t.Parallel()
var hint InlayHint
err := json.Unmarshal([]byte(`{
"kind": 1,
"label": "x",
"position": {"line": 0, "character": 0}
}`), &hint)
assert.NilError(t, err)
assert.Assert(t, hint.Kind != nil)
assert.Equal(t, *hint.Kind, InlayHintKindType)
})
}
func TestUnmarshalEmptyObject(t *testing.T) {
t.Parallel()
t.Run("WorkDoneProgressOptions empty", func(t *testing.T) {
t.Parallel()
var v WorkDoneProgressOptions
err := json.Unmarshal([]byte(`{}`), &v)
assert.NilError(t, err)
assert.Assert(t, v.WorkDoneProgress == nil)
})
t.Run("InitializationOptions empty", func(t *testing.T) {
t.Parallel()
var v InitializationOptions
err := json.Unmarshal([]byte(`{}`), &v)
assert.NilError(t, err)
})
t.Run("ClientCapabilities empty", func(t *testing.T) {
t.Parallel()
var v ClientCapabilities
err := json.Unmarshal([]byte(`{}`), &v)
assert.NilError(t, err)
})
t.Run("ServerCapabilities empty", func(t *testing.T) {
t.Parallel()
var v ServerCapabilities
err := json.Unmarshal([]byte(`{}`), &v)
assert.NilError(t, err)
})
}
func TestMarshalOmitsZeroOptionalFields(t *testing.T) {
t.Parallel()
t.Run("InlayHint omits nil fields", func(t *testing.T) {
t.Parallel()
hint := InlayHint{
Position: Position{Line: 0, Character: 0},
Label: StringOrInlayHintLabelParts{String: new("x")},
}
data, err := json.Marshal(&hint)
assert.NilError(t, err)
s := string(data)
assert.Assert(t, !strings.Contains(s, "kind"), "should not contain 'kind', got: %s", s)
assert.Assert(t, !strings.Contains(s, "textEdits"), "should not contain 'textEdits', got: %s", s)
assert.Assert(t, !strings.Contains(s, "paddingLeft"), "should not contain 'paddingLeft', got: %s", s)
assert.Assert(t, strings.Contains(s, "position"), "should contain 'position', got: %s", s)
assert.Assert(t, strings.Contains(s, "label"), "should contain 'label', got: %s", s)
})
t.Run("FoldingRange omits nil optional fields", func(t *testing.T) {
t.Parallel()
fr := FoldingRange{StartLine: 1, EndLine: 10}
data, err := json.Marshal(&fr)
assert.NilError(t, err)
s := string(data)
assert.Assert(t, !strings.Contains(s, "kind"), "should not contain 'kind', got: %s", s)
assert.Assert(t, !strings.Contains(s, "startCharacter"), "should not contain 'startCharacter', got: %s", s)
assert.Assert(t, strings.Contains(s, "startLine"), "should contain 'startLine', got: %s", s)
assert.Assert(t, strings.Contains(s, "endLine"), "should contain 'endLine', got: %s", s)
})
}
func TestLiteralTypes(t *testing.T) {
t.Parallel()
t.Run("StringLiteralCreate marshal", func(t *testing.T) {
t.Parallel()
v := StringLiteralCreate{}
data, err := json.Marshal(v)
assert.NilError(t, err)
assert.Equal(t, string(data), `"create"`)
})
t.Run("StringLiteralCreate unmarshal", func(t *testing.T) {
t.Parallel()
var v StringLiteralCreate
err := json.Unmarshal([]byte(`"create"`), &v)
assert.NilError(t, err)
})
t.Run("StringLiteralCreate rejects wrong value", func(t *testing.T) {
t.Parallel()
var v StringLiteralCreate
err := json.Unmarshal([]byte(`"delete"`), &v)
assert.Assert(t, err != nil)
})
t.Run("StringLiteralCreate rejects wrong type", func(t *testing.T) {
t.Parallel()
var v StringLiteralCreate
err := json.Unmarshal([]byte(`42`), &v)
assert.Assert(t, err != nil)
})
}
func TestEnumStringValues(t *testing.T) {
t.Parallel()
t.Run("InlayHintKind values", func(t *testing.T) {
t.Parallel()
assert.Equal(t, InlayHintKindType.String(), "Type")
assert.Equal(t, InlayHintKindParameter.String(), "Parameter")
})
t.Run("SymbolKind values", func(t *testing.T) {
t.Parallel()
assert.Equal(t, SymbolKindFile.String(), "File")
assert.Equal(t, SymbolKindFunction.String(), "Function")
assert.Equal(t, SymbolKindVariable.String(), "Variable")
})
t.Run("unknown enum value", func(t *testing.T) {
t.Parallel()
v := InlayHintKind(999)
s := v.String()
assert.Assert(t, strings.Contains(s, "999"), "should contain the numeric value, got: %s", s)
})
}
// TestRoundTrip locks the generated codecs: every value must survive
// marshal -> unmarshal unchanged. This guards fidelity so codec changes
// (e.g. pruning or table-driving them) cannot silently corrupt the wire
// format. Cover a representative spread of shapes: required fields,
// nullable/non-nullable optionals, enums, slices, nested objects, and
// unions.
func TestRoundTrip(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value any
}{
{"Range", &Range{
Start: Position{Line: 1, Character: 2},
End: Position{Line: 3, Character: 4},
}},
{"TextEdit", &TextEdit{
Range: Range{Start: Position{Line: 1, Character: 2}, End: Position{Line: 3, Character: 4}},
NewText: "hello",
}},
{"MarkupContent", &MarkupContent{Kind: MarkupKindMarkdown, Value: "**x**"}},
{"DidChangeConfigurationParams object", &DidChangeConfigurationParams{
Settings: map[string]any{"js/ts": map[string]any{"x": float64(1)}},
}},
{"DidChangeConfigurationParams null", &DidChangeConfigurationParams{Settings: nil}},
{"CompletionItem", &CompletionItem{
Label: "pageXOffset",
Kind: new(CompletionItemKindField),
SortText: new("15"),
InsertTextFormat: new(InsertTextFormatPlainText),
}},
// StringOrTuple union (string arm and tuple arm).
{"ParameterInformation string label", &ParameterInformation{
Label: StringOrTuple{String: new("p: number")},
}},
{"ParameterInformation tuple label", &ParameterInformation{
Label: StringOrTuple{Tuple: &[2]uint32{0, 4}},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
data, err := json.Marshal(tt.value)
assert.NilError(t, err)
got := reflect.New(reflect.TypeOf(tt.value).Elem()).Interface()
assert.NilError(t, json.Unmarshal(data, got))
again, err := json.Marshal(got)
assert.NilError(t, err)
assert.Equal(t, string(data), string(again), "re-marshal differs")
})
}
}
// TestStrictnessMissingRequired confirms required fields are still enforced;
// default reflective decoding would silently accept these.
func TestStrictnessMissingRequired(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
target any
}{
{"TextEdit missing newText", `{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":1}}}`, new(TextEdit)},
{"Range missing end", `{"start":{"line":0,"character":0}}`, new(Range)},
{"Position missing character", `{"line":0}`, new(Position)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := json.Unmarshal([]byte(tt.input), tt.target)
assert.ErrorContains(t, err, "missing required properties")
})
}
}
// TestStrictnessNotObject confirms a non-object where an object is required
// is rejected rather than coerced.
func TestStrictnessNotObject(t *testing.T) {
t.Parallel()
err := json.Unmarshal([]byte(`"oops"`), new(TextEdit))
assert.Assert(t, err != nil)
assert.Assert(t, strings.Contains(err.Error(), "object") || strings.Contains(err.Error(), "cannot unmarshal"))
}
// TestUnmarshalParamsRequiresParams verifies that a NoParams method must be
// given no params while every other method must be given params, and that a
// mismatch (including a null value either way) is an InvalidParams error.
func TestUnmarshalParamsRequiresParams(t *testing.T) {
t.Parallel()
// NoParams: only truly-absent/empty params are accepted; null and any
// present value are rejected.
noParamsTests := []struct {
name string
params any
wantErr bool
}{
{"absent", nil, false},
{"empty", json.Value(``), false},
{"null", json.Value(`null`), true},
{"object", json.Value(`{}`), true},
}
for _, tt := range noParamsTests {
t.Run("NoParams/"+tt.name, func(t *testing.T) {
t.Parallel()
_, err := UnmarshalParams[NoParams](&RequestMessage{Params: tt.params})
if tt.wantErr {
assert.ErrorIs(t, err, ErrorCodeInvalidParams)
} else {
assert.NilError(t, err)
}
})
}
// Required-params method: only an object or array is accepted; absent,
// empty, null, and other scalars are rejected.
typedTests := []struct {
name string
params any
wantErr bool
}{
{"absent", nil, true},
{"empty", json.Value(``), true},
{"null", json.Value(`null`), true},
{"number", json.Value(`5`), true},
{"string", json.Value(`"x"`), true},
{"object", json.Value(`{"settings":{"x":1}}`), false},
}
for _, tt := range typedTests {
t.Run("typed/"+tt.name, func(t *testing.T) {
t.Parallel()
got, err := UnmarshalParams[*DidChangeConfigurationParams](&RequestMessage{Params: tt.params})
if tt.wantErr {
assert.ErrorIs(t, err, ErrorCodeInvalidParams)
} else {
assert.NilError(t, err)
assert.Assert(t, got != nil && got.Settings != nil)
}
})
}
}