Skip to content

Commit 5bec4c3

Browse files
jakebaileyCopilot
andauthored
Provide Program diagnostics as push diags in tsconfig.json (#2118)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7cf22f6 commit 5bec4c3

File tree

9 files changed

+407
-123
lines changed

9 files changed

+407
-123
lines changed

internal/fourslash/fourslash.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,14 @@ func (f *FourslashTest) nextID() int32 {
250250
}
251251

252252
func (f *FourslashTest) initialize(t *testing.T, capabilities *lsproto.ClientCapabilities) {
253+
initOptions := map[string]any{
254+
// Hack: disable push diagnostics entirely, since the fourslash runner does not
255+
// yet gracefully handle non-request messages.
256+
"disablePushDiagnostics": true,
257+
}
253258
params := &lsproto.InitializeParams{
254-
Locale: ptrTo("en-US"),
259+
Locale: ptrTo("en-US"),
260+
InitializationOptions: ptrTo[any](initOptions),
255261
}
256262
params.Capabilities = getCapabilitiesWithDefaults(capabilities)
257263
// !!! check for errors?

internal/ls/diagnostics.go

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@ package ls
22

33
import (
44
"context"
5-
"slices"
6-
"strings"
75

86
"github.com/microsoft/typescript-go/internal/ast"
9-
"github.com/microsoft/typescript-go/internal/diagnostics"
10-
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
117
"github.com/microsoft/typescript-go/internal/ls/lsconv"
128
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
139
)
@@ -41,76 +37,8 @@ func (l *LanguageService) toLSPDiagnostics(ctx context.Context, diagnostics ...[
4137
lspDiagnostics := make([]*lsproto.Diagnostic, 0, size)
4238
for _, diagSlice := range diagnostics {
4339
for _, diag := range diagSlice {
44-
lspDiagnostics = append(lspDiagnostics, l.toLSPDiagnostic(ctx, diag))
40+
lspDiagnostics = append(lspDiagnostics, lsconv.DiagnosticToLSPPull(ctx, l.converters, diag))
4541
}
4642
}
4743
return lspDiagnostics
4844
}
49-
50-
func (l *LanguageService) toLSPDiagnostic(ctx context.Context, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
51-
clientOptions := lsproto.GetClientCapabilities(ctx).TextDocument.Diagnostic
52-
var severity lsproto.DiagnosticSeverity
53-
switch diagnostic.Category() {
54-
case diagnostics.CategorySuggestion:
55-
severity = lsproto.DiagnosticSeverityHint
56-
case diagnostics.CategoryMessage:
57-
severity = lsproto.DiagnosticSeverityInformation
58-
case diagnostics.CategoryWarning:
59-
severity = lsproto.DiagnosticSeverityWarning
60-
default:
61-
severity = lsproto.DiagnosticSeverityError
62-
}
63-
64-
var relatedInformation []*lsproto.DiagnosticRelatedInformation
65-
if clientOptions.RelatedInformation {
66-
relatedInformation = make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation()))
67-
for _, related := range diagnostic.RelatedInformation() {
68-
relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{
69-
Location: lsproto.Location{
70-
Uri: lsconv.FileNameToDocumentURI(related.File().FileName()),
71-
Range: l.converters.ToLSPRange(related.File(), related.Loc()),
72-
},
73-
Message: related.Message(),
74-
})
75-
}
76-
}
77-
78-
var tags []lsproto.DiagnosticTag
79-
if len(clientOptions.TagSupport.ValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
80-
tags = make([]lsproto.DiagnosticTag, 0, 2)
81-
if diagnostic.ReportsUnnecessary() && slices.Contains(clientOptions.TagSupport.ValueSet, lsproto.DiagnosticTagUnnecessary) {
82-
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
83-
}
84-
if diagnostic.ReportsDeprecated() && slices.Contains(clientOptions.TagSupport.ValueSet, lsproto.DiagnosticTagDeprecated) {
85-
tags = append(tags, lsproto.DiagnosticTagDeprecated)
86-
}
87-
}
88-
89-
return &lsproto.Diagnostic{
90-
Range: l.converters.ToLSPRange(diagnostic.File(), diagnostic.Loc()),
91-
Code: &lsproto.IntegerOrString{
92-
Integer: ptrTo(diagnostic.Code()),
93-
},
94-
Severity: &severity,
95-
Message: messageChainToString(diagnostic),
96-
Source: ptrTo("ts"),
97-
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
98-
Tags: ptrToSliceIfNonEmpty(tags),
99-
}
100-
}
101-
102-
func messageChainToString(diagnostic *ast.Diagnostic) string {
103-
if len(diagnostic.MessageChain()) == 0 {
104-
return diagnostic.Message()
105-
}
106-
var b strings.Builder
107-
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n")
108-
return b.String()
109-
}
110-
111-
func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
112-
if len(s) == 0 {
113-
return nil
114-
}
115-
return &s
116-
}

internal/ls/lsconv/converters.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package lsconv
22

33
import (
4+
"context"
45
"fmt"
56
"net/url"
67
"slices"
78
"strings"
89
"unicode/utf16"
910
"unicode/utf8"
1011

12+
"github.com/microsoft/typescript-go/internal/ast"
1113
"github.com/microsoft/typescript-go/internal/core"
14+
"github.com/microsoft/typescript-go/internal/diagnostics"
15+
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
1216
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
1317
"github.com/microsoft/typescript-go/internal/tspath"
1418
)
@@ -199,3 +203,99 @@ func (c *Converters) PositionToLineAndCharacter(script Script, position core.Tex
199203
func ptrTo[T any](v T) *T {
200204
return &v
201205
}
206+
207+
type diagnosticCapabilities struct {
208+
relatedInformation bool
209+
tagValueSet []lsproto.DiagnosticTag
210+
}
211+
212+
// DiagnosticToLSPPull converts a diagnostic for pull diagnostics (textDocument/diagnostic)
213+
func DiagnosticToLSPPull(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
214+
clientCaps := lsproto.GetClientCapabilities(ctx).TextDocument.Diagnostic
215+
return diagnosticToLSP(converters, diagnostic, diagnosticCapabilities{
216+
relatedInformation: clientCaps.RelatedInformation,
217+
tagValueSet: clientCaps.TagSupport.ValueSet,
218+
})
219+
}
220+
221+
// DiagnosticToLSPPush converts a diagnostic for push diagnostics (textDocument/publishDiagnostics)
222+
func DiagnosticToLSPPush(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
223+
clientCaps := lsproto.GetClientCapabilities(ctx).TextDocument.PublishDiagnostics
224+
return diagnosticToLSP(converters, diagnostic, diagnosticCapabilities{
225+
relatedInformation: clientCaps.RelatedInformation,
226+
tagValueSet: clientCaps.TagSupport.ValueSet,
227+
})
228+
}
229+
230+
func diagnosticToLSP(converters *Converters, diagnostic *ast.Diagnostic, caps diagnosticCapabilities) *lsproto.Diagnostic {
231+
var severity lsproto.DiagnosticSeverity
232+
switch diagnostic.Category() {
233+
case diagnostics.CategorySuggestion:
234+
severity = lsproto.DiagnosticSeverityHint
235+
case diagnostics.CategoryMessage:
236+
severity = lsproto.DiagnosticSeverityInformation
237+
case diagnostics.CategoryWarning:
238+
severity = lsproto.DiagnosticSeverityWarning
239+
default:
240+
severity = lsproto.DiagnosticSeverityError
241+
}
242+
243+
var relatedInformation []*lsproto.DiagnosticRelatedInformation
244+
if caps.relatedInformation {
245+
relatedInformation = make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation()))
246+
for _, related := range diagnostic.RelatedInformation() {
247+
relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{
248+
Location: lsproto.Location{
249+
Uri: FileNameToDocumentURI(related.File().FileName()),
250+
Range: converters.ToLSPRange(related.File(), related.Loc()),
251+
},
252+
Message: related.Message(),
253+
})
254+
}
255+
}
256+
257+
var tags []lsproto.DiagnosticTag
258+
if len(caps.tagValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
259+
tags = make([]lsproto.DiagnosticTag, 0, 2)
260+
if diagnostic.ReportsUnnecessary() && slices.Contains(caps.tagValueSet, lsproto.DiagnosticTagUnnecessary) {
261+
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
262+
}
263+
if diagnostic.ReportsDeprecated() && slices.Contains(caps.tagValueSet, lsproto.DiagnosticTagDeprecated) {
264+
tags = append(tags, lsproto.DiagnosticTagDeprecated)
265+
}
266+
}
267+
268+
// For diagnostics without a file (e.g., program diagnostics), use a zero range
269+
var lspRange lsproto.Range
270+
if diagnostic.File() != nil {
271+
lspRange = converters.ToLSPRange(diagnostic.File(), diagnostic.Loc())
272+
}
273+
274+
return &lsproto.Diagnostic{
275+
Range: lspRange,
276+
Code: &lsproto.IntegerOrString{
277+
Integer: ptrTo(diagnostic.Code()),
278+
},
279+
Severity: &severity,
280+
Message: messageChainToString(diagnostic),
281+
Source: ptrTo("ts"),
282+
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
283+
Tags: ptrToSliceIfNonEmpty(tags),
284+
}
285+
}
286+
287+
func messageChainToString(diagnostic *ast.Diagnostic) string {
288+
if len(diagnostic.MessageChain()) == 0 {
289+
return diagnostic.Message()
290+
}
291+
var b strings.Builder
292+
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n")
293+
return b.String()
294+
}
295+
296+
func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
297+
if len(s) == 0 {
298+
return nil
299+
}
300+
return &s
301+
}

internal/lsp/server.go

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,13 @@ func (s *Server) RefreshDiagnostics(ctx context.Context) error {
219219
return nil
220220
}
221221

222+
// PublishDiagnostics implements project.Client.
223+
func (s *Server) PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error {
224+
notification := lsproto.TextDocumentPublishDiagnosticsInfo.NewNotificationMessage(params)
225+
s.outgoingQueue <- notification.Message()
226+
return nil
227+
}
228+
222229
func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferences, error) {
223230
caps := lsproto.GetClientCapabilities(ctx)
224231
if !caps.Workspace.Configuration {
@@ -716,15 +723,26 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali
716723
cwd = s.cwd
717724
}
718725

726+
var disablePushDiagnostics bool
727+
if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && *s.initializeParams.InitializationOptions != nil {
728+
// Check for disablePushDiagnostics option
729+
if initOpts, ok := (*s.initializeParams.InitializationOptions).(map[string]any); ok {
730+
if disable, ok := initOpts["disablePushDiagnostics"].(bool); ok {
731+
disablePushDiagnostics = disable
732+
}
733+
}
734+
}
735+
719736
s.session = project.NewSession(&project.SessionInit{
720737
Options: &project.SessionOptions{
721-
CurrentDirectory: cwd,
722-
DefaultLibraryPath: s.defaultLibraryPath,
723-
TypingsLocation: s.typingsLocation,
724-
PositionEncoding: s.positionEncoding,
725-
WatchEnabled: s.watchEnabled,
726-
LoggingEnabled: true,
727-
DebounceDelay: 500 * time.Millisecond,
738+
CurrentDirectory: cwd,
739+
DefaultLibraryPath: s.defaultLibraryPath,
740+
TypingsLocation: s.typingsLocation,
741+
PositionEncoding: s.positionEncoding,
742+
WatchEnabled: s.watchEnabled,
743+
LoggingEnabled: true,
744+
DebounceDelay: 500 * time.Millisecond,
745+
PushDiagnosticsEnabled: !disablePushDiagnostics,
728746
},
729747
FS: s.fs,
730748
Logger: s.logger,
@@ -733,36 +751,28 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali
733751
ParseCache: s.parseCache,
734752
})
735753

736-
if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && *s.initializeParams.InitializationOptions != nil {
737-
// handle userPreferences from initializationOptions
738-
userPreferences := s.session.NewUserPreferences()
739-
userPreferences.Parse(*s.initializeParams.InitializationOptions)
740-
s.session.InitializeWithConfig(userPreferences)
741-
} else {
742-
// request userPreferences if not provided at initialization
743-
userPreferences, err := s.RequestConfiguration(ctx)
744-
if err != nil {
745-
return err
746-
}
747-
s.session.InitializeWithConfig(userPreferences)
754+
userPreferences, err := s.RequestConfiguration(ctx)
755+
if err != nil {
756+
return err
757+
}
758+
s.session.InitializeWithConfig(userPreferences)
748759

749-
_, err = sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{
750-
Registrations: []*lsproto.Registration{
751-
{
752-
Id: "typescript-config-watch-id",
753-
Method: string(lsproto.MethodWorkspaceDidChangeConfiguration),
754-
RegisterOptions: ptrTo(any(lsproto.DidChangeConfigurationRegistrationOptions{
755-
Section: &lsproto.StringOrStrings{
756-
// !!! Both the 'javascript' and 'js/ts' scopes need to be watched for settings as well.
757-
Strings: &[]string{"typescript"},
758-
},
759-
})),
760-
},
760+
_, err = sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{
761+
Registrations: []*lsproto.Registration{
762+
{
763+
Id: "typescript-config-watch-id",
764+
Method: string(lsproto.MethodWorkspaceDidChangeConfiguration),
765+
RegisterOptions: ptrTo(any(lsproto.DidChangeConfigurationRegistrationOptions{
766+
Section: &lsproto.StringOrStrings{
767+
// !!! Both the 'javascript' and 'js/ts' scopes need to be watched for settings as well.
768+
Strings: &[]string{"typescript"},
769+
},
770+
})),
761771
},
762-
})
763-
if err != nil {
764-
return fmt.Errorf("failed to register configuration change watcher: %w", err)
765-
}
772+
},
773+
})
774+
if err != nil {
775+
return fmt.Errorf("failed to register configuration change watcher: %w", err)
766776
}
767777

768778
// !!! temporary.

internal/project/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ type Client interface {
1010
WatchFiles(ctx context.Context, id WatcherID, watchers []*lsproto.FileSystemWatcher) error
1111
UnwatchFiles(ctx context.Context, id WatcherID) error
1212
RefreshDiagnostics(ctx context.Context) error
13+
PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error
1314
}

0 commit comments

Comments
 (0)