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) } }) } }