Skip to content

Generate LSP response types, redo nullable, delete aliases, statically type LSP handlers #1441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions internal/fourslash/baselineutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ type baselineFourslashLocationsOptions struct {
additionalSpan *lsproto.Location
}

func (f *FourslashTest) getBaselineForLocationsWithFileContents(spans []*lsproto.Location, options baselineFourslashLocationsOptions) string {
return f.getBaselineForGroupedLocationsWithFileContents(collections.GroupBy(spans, func(span *lsproto.Location) lsproto.DocumentUri { return span.Uri }), options)
func (f *FourslashTest) getBaselineForLocationsWithFileContents(spans []lsproto.Location, options baselineFourslashLocationsOptions) string {
return f.getBaselineForGroupedLocationsWithFileContents(collections.GroupBy(spans, func(span lsproto.Location) lsproto.DocumentUri { return span.Uri }), options)
}

func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedLocations *collections.MultiMap[lsproto.DocumentUri, *lsproto.Location], options baselineFourslashLocationsOptions) string {
func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedLocations *collections.MultiMap[lsproto.DocumentUri, lsproto.Location], options baselineFourslashLocationsOptions) string {
// We must always print the file containing the marker,
// but don't want to print it twice at the end if it already
// found in a file with ranges.
Expand Down Expand Up @@ -85,9 +85,9 @@ func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedLo
foundMarker = true
}

documentSpans := core.Map(locations, func(location *lsproto.Location) *documentSpan {
documentSpans := core.Map(locations, func(location lsproto.Location) *documentSpan {
return &documentSpan{
Location: *location,
Location: location,
}
})
baselineEntries = append(baselineEntries, f.getBaselineContentForFile(path, content, documentSpans, nil, options))
Expand Down
111 changes: 55 additions & 56 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ func (f *FourslashTest) initialize(t *testing.T, capabilities *lsproto.ClientCap
params := &lsproto.InitializeParams{}
params.Capabilities = getCapabilitiesWithDefaults(capabilities)
// !!! check for errors?
f.sendRequest(t, lsproto.MethodInitialize, params)
f.sendNotification(t, lsproto.MethodInitialized, &lsproto.InitializedParams{})
sendRequest(t, f, lsproto.InitializeInfo, params)
sendNotification(t, f, lsproto.InitializedInfo, &lsproto.InitializedParams{})
}

var (
Expand Down Expand Up @@ -267,20 +267,25 @@ func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lspr
return &capabilitiesWithDefaults
}

func (f *FourslashTest) sendRequest(t *testing.T, method lsproto.Method, params any) *lsproto.Message {
func sendRequest[Params, Resp any](t *testing.T, f *FourslashTest, info lsproto.RequestInfo[Params, Resp], params Params) (*lsproto.Message, Resp, bool) {
id := f.nextID()
req := lsproto.NewRequestMessage(
method,
info.Method,
lsproto.NewID(lsproto.IntegerOrString{Integer: &id}),
params,
)
f.writeMsg(t, req.Message())
return f.readMsg(t)
resp := f.readMsg(t)
if resp == nil {
return nil, *new(Resp), false
}
result, ok := resp.AsResponse().Result.(Resp)
return resp, result, ok
}

func (f *FourslashTest) sendNotification(t *testing.T, method lsproto.Method, params any) {
func sendNotification[Params any](t *testing.T, f *FourslashTest, info lsproto.NotificationInfo[Params], params Params) {
notification := lsproto.NewNotificationMessage(
method,
info.Method,
params,
)
f.writeMsg(t, notification.Message())
Expand Down Expand Up @@ -430,7 +435,7 @@ func (f *FourslashTest) openFile(t *testing.T, filename string) {
t.Fatalf("File %s not found in test data", filename)
}
f.activeFilename = filename
f.sendNotification(t, lsproto.MethodTextDocumentDidOpen, &lsproto.DidOpenTextDocumentParams{
sendNotification(t, f, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{
TextDocument: &lsproto.TextDocumentItem{
Uri: ls.FileNameToDocumentURI(filename),
LanguageId: getLanguageKind(filename),
Expand Down Expand Up @@ -544,17 +549,14 @@ func (f *FourslashTest) verifyCompletionsWorker(t *testing.T, expected *Completi
TextDocumentPositionParams: f.currentTextDocumentPositionParams(),
Context: &lsproto.CompletionContext{},
}
resMsg := f.sendRequest(t, lsproto.MethodTextDocumentCompletion, params)
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentCompletionInfo, params)
if resMsg == nil {
t.Fatalf(prefix+"Nil response received for completion request", f.lastKnownMarkerName)
}
result := resMsg.AsResponse().Result
switch result := result.(type) {
case *lsproto.CompletionList:
f.verifyCompletionsResult(t, f.currentCaretPosition, result, expected, prefix)
default:
t.Fatalf(prefix+"Unexpected response type for completion request: %v", result)
if !resultOk {
t.Fatalf(prefix+"Unexpected response type for completion request: %T", resMsg.AsResponse().Result)
}
Comment on lines 553 to 558
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be ideal if sendRequest (or a wrapper function) automatically errored based on resMsg and resultOk and used the passed Method for the errors.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to do this, but it means I have to also pass in prefix which felt strange...

f.verifyCompletionsResult(t, f.currentCaretPosition, result.List, expected, prefix)
}

func (f *FourslashTest) verifyCompletionsResult(
Expand Down Expand Up @@ -732,15 +734,14 @@ var completionIgnoreOpts = cmp.FilterPath(

func (f *FourslashTest) verifyCompletionItem(t *testing.T, prefix string, actual *lsproto.CompletionItem, expected *lsproto.CompletionItem) {
if expected.Detail != nil || expected.Documentation != nil {
response := f.sendRequest(t, lsproto.MethodCompletionItemResolve, actual)
if response == nil {
resMsg, result, resultOk := sendRequest(t, f, lsproto.CompletionItemResolveInfo, actual)
if resMsg == nil {
t.Fatal(prefix + "Expected non-nil response for completion item resolve, got nil")
}
resolvedItem, ok := response.AsResponse().Result.(*lsproto.CompletionItem)
if !ok {
t.Fatalf(prefix+"Expected response to be *lsproto.CompletionItem, got %T", response.AsResponse().Result)
if !resultOk {
t.Fatalf(prefix+"Unexpected response type for completion item resolve: %T", resMsg.AsResponse().Result)
}
actual = resolvedItem
actual = result
}
assertDeepEqual(t, actual, expected, prefix, completionIgnoreOpts)
if expected.Kind != nil {
Expand Down Expand Up @@ -799,28 +800,27 @@ func (f *FourslashTest) VerifyBaselineFindAllReferences(
TextDocumentPositionParams: f.currentTextDocumentPositionParams(),
Context: &lsproto.ReferenceContext{},
}
resMsg := f.sendRequest(t, lsproto.MethodTextDocumentReferences, params)
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentReferencesInfo, params)
if resMsg == nil {
if f.lastKnownMarkerName == nil {
t.Fatalf("Nil response received for references request at pos %v", f.currentCaretPosition)
} else {
t.Fatalf("Nil response received for references request at marker '%s'", *f.lastKnownMarkerName)
}
}

result := resMsg.AsResponse().Result
if resultAsLocation, ok := result.([]*lsproto.Location); ok {
f.baseline.addResult("findAllReferences", f.getBaselineForLocationsWithFileContents(resultAsLocation, baselineFourslashLocationsOptions{
marker: markerOrRange.GetMarker(),
markerName: "/*FIND ALL REFS*/",
}))
} else {
if !resultOk {
if f.lastKnownMarkerName == nil {
t.Fatalf("Unexpected references response type at pos %v: %T", f.currentCaretPosition, result)
t.Fatalf("Unexpected references response type at pos %v: %T", f.currentCaretPosition, resMsg.AsResponse().Result)
} else {
t.Fatalf("Unexpected references response type at marker '%s': %T", *f.lastKnownMarkerName, result)
t.Fatalf("Unexpected references response type at marker '%s': %T", *f.lastKnownMarkerName, resMsg.AsResponse().Result)
}
}

f.baseline.addResult("findAllReferences", f.getBaselineForLocationsWithFileContents(*result, baselineFourslashLocationsOptions{
marker: markerOrRange.GetMarker(),
markerName: "/*FIND ALL REFS*/",
}))

}

baseline.Run(t, f.baseline.getBaselineFileName(), f.baseline.content.String(), baseline.Options{})
Expand Down Expand Up @@ -854,39 +854,38 @@ func (f *FourslashTest) VerifyBaselineGoToDefinition(
params := &lsproto.DefinitionParams{
TextDocumentPositionParams: f.currentTextDocumentPositionParams(),
}
resMsg := f.sendRequest(t, lsproto.MethodTextDocumentDefinition, params)

resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentDefinitionInfo, params)
if resMsg == nil {
if f.lastKnownMarkerName == nil {
t.Fatalf("Nil response received for definition request at pos %v", f.currentCaretPosition)
} else {
t.Fatalf("Nil response received for definition request at marker '%s'", *f.lastKnownMarkerName)
}
}

result := resMsg.AsResponse().Result
if resultAsLocOrLocations, ok := result.(*lsproto.LocationOrLocations); ok {
var resultAsLocations []*lsproto.Location
if resultAsLocOrLocations != nil {
if resultAsLocOrLocations.Locations != nil {
resultAsLocations = core.Map(*resultAsLocOrLocations.Locations, func(loc lsproto.Location) *lsproto.Location {
return &loc
})
} else {
resultAsLocations = []*lsproto.Location{resultAsLocOrLocations.Location}
}
if !resultOk {
if f.lastKnownMarkerName == nil {
t.Fatalf("Unexpected definition response type at pos %v: %T", f.currentCaretPosition, resMsg.AsResponse().Result)
} else {
t.Fatalf("Unexpected definition response type at marker '%s': %T", *f.lastKnownMarkerName, resMsg.AsResponse().Result)
}
}

f.baseline.addResult("goToDefinition", f.getBaselineForLocationsWithFileContents(resultAsLocations, baselineFourslashLocationsOptions{
marker: markerOrRange.GetMarker(),
markerName: "/*GO TO DEFINITION*/",
}))
} else {
if f.lastKnownMarkerName == nil {
t.Fatalf("Unexpected definition response type at pos %v: %T", f.currentCaretPosition, result)
var resultAsLocations []lsproto.Location
if result != nil {
if result.Locations != nil {
resultAsLocations = *result.Locations
} else if result.Location != nil {
resultAsLocations = []lsproto.Location{*result.Location}
} else {
t.Fatalf("Unexpected definition response type at marker '%s': %T", *f.lastKnownMarkerName, result)
t.Fatalf("Unexpected definition response type at marker '%s': %T", *f.lastKnownMarkerName, result.DefinitionLinks)
}
}

f.baseline.addResult("goToDefinition", f.getBaselineForLocationsWithFileContents(resultAsLocations, baselineFourslashLocationsOptions{
marker: markerOrRange.GetMarker(),
markerName: "/*GO TO DEFINITION*/",
}))
}

baseline.Run(t, f.baseline.getBaselineFileName(), f.baseline.content.String(), baseline.Options{})
Expand Down Expand Up @@ -1060,16 +1059,16 @@ func (f *FourslashTest) editScript(t *testing.T, fileName string, start int, end
if err != nil {
panic(fmt.Sprintf("Failed to write file %s: %v", fileName, err))
}
f.sendNotification(t, lsproto.MethodTextDocumentDidChange, &lsproto.DidChangeTextDocumentParams{
sendNotification(t, f, lsproto.TextDocumentDidChangeInfo, &lsproto.DidChangeTextDocumentParams{
TextDocument: lsproto.VersionedTextDocumentIdentifier{
TextDocumentIdentifier: lsproto.TextDocumentIdentifier{
Uri: ls.FileNameToDocumentURI(fileName),
},
Version: script.version,
},
ContentChanges: []lsproto.TextDocumentContentChangeEvent{
ContentChanges: []lsproto.TextDocumentContentChangePartialOrWholeDocument{
{
TextDocumentContentChangePartial: &lsproto.TextDocumentContentChangePartial{
Partial: &lsproto.TextDocumentContentChangePartial{
Range: changeRange,
Text: newText,
},
Expand Down
12 changes: 6 additions & 6 deletions internal/ls/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/microsoft/typescript-go/internal/scanner"
)

func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (*lsproto.Definition, error) {
func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) {
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
if node.Kind == ast.KindSourceFile {
Expand Down Expand Up @@ -84,7 +84,7 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp
return nil, nil
}

func (l *LanguageService) ProvideTypeDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (*lsproto.Definition, error) {
func (l *LanguageService) ProvideTypeDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) {
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
if node.Kind == ast.KindSourceFile {
Expand Down Expand Up @@ -126,7 +126,7 @@ func getDeclarationNameForKeyword(node *ast.Node) *ast.Node {
return node
}

func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.Node) *lsproto.Definition {
func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.Node) lsproto.DefinitionResponse {
someHaveBody := core.Some(declarations, func(node *ast.Node) bool { return node.Body() != nil })
locations := make([]lsproto.Location, 0, len(declarations))
for _, decl := range declarations {
Expand All @@ -139,11 +139,11 @@ func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.No
})
}
}
return &lsproto.Definition{Locations: &locations}
return &lsproto.LocationOrLocationsOrDefinitionLinks{Locations: &locations}
}

func (l *LanguageService) createLocationFromFileAndRange(file *ast.SourceFile, textRange core.TextRange) *lsproto.Definition {
return &lsproto.Definition{
func (l *LanguageService) createLocationFromFileAndRange(file *ast.SourceFile, textRange core.TextRange) lsproto.DefinitionResponse {
return &lsproto.LocationOrLocationsOrDefinitionLinks{
Location: &lsproto.Location{
Uri: FileNameToDocumentURI(file.FileName()),
Range: *l.createLspRangeFromBounds(textRange.Pos(), textRange.End(), file),
Expand Down
8 changes: 4 additions & 4 deletions internal/ls/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ func TestDefinition(t *testing.T) {
testCases := []struct {
title string
input string
expected map[string]lsproto.Definition
expected map[string]lsproto.DefinitionResponse
}{
{
title: "localFunction",
input: `
// @filename: index.ts
function localFunction() { }
/*localFunction*/localFunction();`,
expected: map[string]lsproto.Definition{
expected: map[string]lsproto.DefinitionResponse{
"localFunction": {
Locations: &[]lsproto.Location{{
Uri: ls.FileNameToDocumentURI("/index.ts"),
Expand All @@ -49,7 +49,7 @@ function localFunction() { }
}
}

func runDefinitionTest(t *testing.T, input string, expected map[string]lsproto.Definition) {
func runDefinitionTest(t *testing.T, input string, expected map[string]lsproto.DefinitionResponse) {
testData := fourslash.ParseTestData(t, input, "/mainFile.ts")
file := testData.Files[0].FileName()
markerPositions := testData.MarkerPositions
Expand All @@ -69,6 +69,6 @@ func runDefinitionTest(t *testing.T, input string, expected map[string]lsproto.D
ls.FileNameToDocumentURI(file),
marker.LSPosition)
assert.NilError(t, err)
assert.DeepEqual(t, *locations, expectedResult)
assert.DeepEqual(t, locations, expectedResult)
}
}
8 changes: 4 additions & 4 deletions internal/ls/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
)

func (l *LanguageService) GetDocumentDiagnostics(ctx context.Context, documentURI lsproto.DocumentUri) (*lsproto.DocumentDiagnosticReport, error) {
program, file := l.getProgramAndFile(documentURI)
func (l *LanguageService) ProvideDiagnostics(ctx context.Context, uri lsproto.DocumentUri) (lsproto.DocumentDiagnosticResponse, error) {
program, file := l.getProgramAndFile(uri)

diagnostics := make([][]*ast.Diagnostic, 0, 3)
if syntaxDiagnostics := program.GetSyntacticDiagnostics(ctx, file); len(syntaxDiagnostics) != 0 {
Expand All @@ -26,8 +26,8 @@ func (l *LanguageService) GetDocumentDiagnostics(ctx context.Context, documentUR
}
}

return &lsproto.DocumentDiagnosticReport{
RelatedFullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
FullDocumentDiagnosticReport: lsproto.FullDocumentDiagnosticReport{
Items: toLSPDiagnostics(l.converters, diagnostics...),
},
Expand Down
12 changes: 6 additions & 6 deletions internal/ls/findallreferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ func getSymbolScope(symbol *ast.Symbol) *ast.Node {

// === functions on (*ls) ===

func (l *LanguageService) ProvideReferences(params *lsproto.ReferenceParams) []*lsproto.Location {
func (l *LanguageService) ProvideReferences(params *lsproto.ReferenceParams) []lsproto.Location {
// `findReferencedSymbols` except only computes the information needed to return reference locations
program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri)
position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position))
Expand All @@ -409,7 +409,7 @@ func (l *LanguageService) ProvideReferences(params *lsproto.ReferenceParams) []*
return core.FlatMap(symbolsAndEntries, l.convertSymbolAndEntriesToLocations)
}

func (l *LanguageService) ProvideImplementations(params *lsproto.ImplementationParams) []*lsproto.Location {
func (l *LanguageService) ProvideImplementations(params *lsproto.ImplementationParams) []lsproto.Location {
program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri)
position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position))
node := astnav.GetTouchingPropertyName(sourceFile, position)
Expand Down Expand Up @@ -437,19 +437,19 @@ func (l *LanguageService) getImplementationReferenceEntries(program *compiler.Pr
}

// == functions for conversions ==
func (l *LanguageService) convertSymbolAndEntriesToLocations(s *SymbolAndEntries) []*lsproto.Location {
func (l *LanguageService) convertSymbolAndEntriesToLocations(s *SymbolAndEntries) []lsproto.Location {
return l.convertEntriesToLocations(s.references)
}

func (l *LanguageService) convertEntriesToLocations(entries []*referenceEntry) []*lsproto.Location {
locations := make([]*lsproto.Location, len(entries))
func (l *LanguageService) convertEntriesToLocations(entries []*referenceEntry) []lsproto.Location {
locations := make([]lsproto.Location, len(entries))
for i, entry := range entries {
if entry.textRange == nil {
sourceFile := ast.GetSourceFileOfNode(entry.node)
entry.textRange = l.getRangeOfNode(entry.node, sourceFile, nil /*endNode*/)
entry.fileName = sourceFile.FileName()
}
locations[i] = &lsproto.Location{
locations[i] = lsproto.Location{
Uri: FileNameToDocumentURI(entry.fileName),
Range: *entry.textRange,
}
Expand Down
2 changes: 1 addition & 1 deletion internal/ls/findallreferences_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func runFindReferencesTest(t *testing.T, input string, expectedLocations map[str
libReference := 0

for _, loc := range referencesResult {
if name, ok := allExpectedLocations[*loc]; ok {
if name, ok := allExpectedLocations[loc]; ok {
// check if returned ref location is in this request's expected set
assert.Assert(t, expectedSet.Has(name), "Reference to '%s' not expected when find all references requested at %s", name, requestMarkerName)
} else if strings.Contains(string(loc.Uri), "//bundled") && strings.Contains(string(loc.Uri), "//libs") {
Expand Down
2 changes: 1 addition & 1 deletion internal/ls/findallreferencesexport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (l *LanguageService) GetExpectedReferenceFromMarker(fileName string, pos in
}
}

func (l *LanguageService) TestProvideReferences(fileName string, pos int) []*lsproto.Location {
func (l *LanguageService) TestProvideReferences(fileName string, pos int) []lsproto.Location {
_, sourceFile := l.tryGetProgramAndFile(fileName)
lsPos := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(pos))
return l.ProvideReferences(&lsproto.ReferenceParams{
Expand Down
Loading
Loading