Skip to content
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
4 changes: 2 additions & 2 deletions pkg/checker/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -2405,7 +2405,7 @@ func (c *Checker) typeMaybeAssignableTo(source *Type, target *Type) bool {
func (c *Checker) getTypePredicateArgument(predicate *TypePredicate, callExpression *ast.Node) *ast.Node {
if predicate.kind == TypePredicateKindIdentifier || predicate.kind == TypePredicateKindAssertsIdentifier {
arguments := callExpression.Arguments()
if int(predicate.parameterIndex) < len(arguments) {
if predicate.parameterIndex >= 0 && int(predicate.parameterIndex) < len(arguments) {
return arguments[predicate.parameterIndex]
}
} else {
Expand Down Expand Up @@ -2496,7 +2496,7 @@ func (c *Checker) isReachableFlowNodeWorker(f *FlowState, flow *ast.FlowNode, no
case flags&ast.FlowFlagsCall != 0:
if signature := c.getEffectsSignature(flow.Node); signature != nil {
if predicate := c.getTypePredicateOfSignature(signature); predicate != nil && predicate.kind == TypePredicateKindAssertsIdentifier && predicate.t == nil {
if arguments := flow.Node.Arguments(); int(predicate.parameterIndex) < len(arguments) && c.isFalseExpression(arguments[predicate.parameterIndex]) {
if arguments := flow.Node.Arguments(); predicate.parameterIndex >= 0 && int(predicate.parameterIndex) < len(arguments) && c.isFalseExpression(arguments[predicate.parameterIndex]) {
return false
}
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,14 @@ func (f *FourslashTest) nextID() int32 {
}

func (f *FourslashTest) initialize(t *testing.T, capabilities *lsproto.ClientCapabilities) {
initOptions := map[string]any{
// Hack: disable push diagnostics entirely, since the fourslash runner does not
// yet gracefully handle non-request messages.
"disablePushDiagnostics": true,
}
params := &lsproto.InitializeParams{
Locale: ptrTo("en-US"),
Locale: ptrTo("en-US"),
InitializationOptions: ptrTo[any](initOptions),
}
params.Capabilities = getCapabilitiesWithDefaults(capabilities)
// !!! check for errors?
Expand Down
74 changes: 1 addition & 73 deletions pkg/ls/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ package ls

import (
"context"
"slices"
"strings"

"github.com/buke/typescript-go-internal/pkg/ast"
"github.com/buke/typescript-go-internal/pkg/diagnostics"
"github.com/buke/typescript-go-internal/pkg/diagnosticwriter"
"github.com/buke/typescript-go-internal/pkg/ls/lsconv"
"github.com/buke/typescript-go-internal/pkg/lsp/lsproto"
)
Expand Down Expand Up @@ -41,76 +37,8 @@ func (l *LanguageService) toLSPDiagnostics(ctx context.Context, diagnostics ...[
lspDiagnostics := make([]*lsproto.Diagnostic, 0, size)
for _, diagSlice := range diagnostics {
for _, diag := range diagSlice {
lspDiagnostics = append(lspDiagnostics, l.toLSPDiagnostic(ctx, diag))
lspDiagnostics = append(lspDiagnostics, lsconv.DiagnosticToLSPPull(ctx, l.converters, diag))
}
}
return lspDiagnostics
}

func (l *LanguageService) toLSPDiagnostic(ctx context.Context, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
clientOptions := lsproto.GetClientCapabilities(ctx).TextDocument.Diagnostic
var severity lsproto.DiagnosticSeverity
switch diagnostic.Category() {
case diagnostics.CategorySuggestion:
severity = lsproto.DiagnosticSeverityHint
case diagnostics.CategoryMessage:
severity = lsproto.DiagnosticSeverityInformation
case diagnostics.CategoryWarning:
severity = lsproto.DiagnosticSeverityWarning
default:
severity = lsproto.DiagnosticSeverityError
}

var relatedInformation []*lsproto.DiagnosticRelatedInformation
if clientOptions.RelatedInformation {
relatedInformation = make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation()))
for _, related := range diagnostic.RelatedInformation() {
relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{
Location: lsproto.Location{
Uri: lsconv.FileNameToDocumentURI(related.File().FileName()),
Range: l.converters.ToLSPRange(related.File(), related.Loc()),
},
Message: related.Message(),
})
}
}

var tags []lsproto.DiagnosticTag
if len(clientOptions.TagSupport.ValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
tags = make([]lsproto.DiagnosticTag, 0, 2)
if diagnostic.ReportsUnnecessary() && slices.Contains(clientOptions.TagSupport.ValueSet, lsproto.DiagnosticTagUnnecessary) {
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
}
if diagnostic.ReportsDeprecated() && slices.Contains(clientOptions.TagSupport.ValueSet, lsproto.DiagnosticTagDeprecated) {
tags = append(tags, lsproto.DiagnosticTagDeprecated)
}
}

return &lsproto.Diagnostic{
Range: l.converters.ToLSPRange(diagnostic.File(), diagnostic.Loc()),
Code: &lsproto.IntegerOrString{
Integer: ptrTo(diagnostic.Code()),
},
Severity: &severity,
Message: messageChainToString(diagnostic),
Source: ptrTo("ts"),
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
Tags: ptrToSliceIfNonEmpty(tags),
}
}

func messageChainToString(diagnostic *ast.Diagnostic) string {
if len(diagnostic.MessageChain()) == 0 {
return diagnostic.Message()
}
var b strings.Builder
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n")
return b.String()
}

func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
if len(s) == 0 {
return nil
}
return &s
}
100 changes: 100 additions & 0 deletions pkg/ls/lsconv/converters.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package lsconv

import (
"context"
"fmt"
"net/url"
"slices"
"strings"
"unicode/utf16"
"unicode/utf8"

"github.com/buke/typescript-go-internal/pkg/ast"
"github.com/buke/typescript-go-internal/pkg/core"
"github.com/buke/typescript-go-internal/pkg/diagnostics"
"github.com/buke/typescript-go-internal/pkg/diagnosticwriter"
"github.com/buke/typescript-go-internal/pkg/lsp/lsproto"
"github.com/buke/typescript-go-internal/pkg/tspath"
)
Expand Down Expand Up @@ -199,3 +203,99 @@ func (c *Converters) PositionToLineAndCharacter(script Script, position core.Tex
func ptrTo[T any](v T) *T {
return &v
}

type diagnosticCapabilities struct {
relatedInformation bool
tagValueSet []lsproto.DiagnosticTag
}

// DiagnosticToLSPPull converts a diagnostic for pull diagnostics (textDocument/diagnostic)
func DiagnosticToLSPPull(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
clientCaps := lsproto.GetClientCapabilities(ctx).TextDocument.Diagnostic
return diagnosticToLSP(converters, diagnostic, diagnosticCapabilities{
relatedInformation: clientCaps.RelatedInformation,
tagValueSet: clientCaps.TagSupport.ValueSet,
})
}

// DiagnosticToLSPPush converts a diagnostic for push diagnostics (textDocument/publishDiagnostics)
func DiagnosticToLSPPush(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
clientCaps := lsproto.GetClientCapabilities(ctx).TextDocument.PublishDiagnostics
return diagnosticToLSP(converters, diagnostic, diagnosticCapabilities{
relatedInformation: clientCaps.RelatedInformation,
tagValueSet: clientCaps.TagSupport.ValueSet,
})
}

func diagnosticToLSP(converters *Converters, diagnostic *ast.Diagnostic, caps diagnosticCapabilities) *lsproto.Diagnostic {
var severity lsproto.DiagnosticSeverity
switch diagnostic.Category() {
case diagnostics.CategorySuggestion:
severity = lsproto.DiagnosticSeverityHint
case diagnostics.CategoryMessage:
severity = lsproto.DiagnosticSeverityInformation
case diagnostics.CategoryWarning:
severity = lsproto.DiagnosticSeverityWarning
default:
severity = lsproto.DiagnosticSeverityError
}

var relatedInformation []*lsproto.DiagnosticRelatedInformation
if caps.relatedInformation {
relatedInformation = make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation()))
for _, related := range diagnostic.RelatedInformation() {
relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{
Location: lsproto.Location{
Uri: FileNameToDocumentURI(related.File().FileName()),
Range: converters.ToLSPRange(related.File(), related.Loc()),
},
Message: related.Message(),
})
}
}

var tags []lsproto.DiagnosticTag
if len(caps.tagValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
tags = make([]lsproto.DiagnosticTag, 0, 2)
if diagnostic.ReportsUnnecessary() && slices.Contains(caps.tagValueSet, lsproto.DiagnosticTagUnnecessary) {
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
}
if diagnostic.ReportsDeprecated() && slices.Contains(caps.tagValueSet, lsproto.DiagnosticTagDeprecated) {
tags = append(tags, lsproto.DiagnosticTagDeprecated)
}
}

// For diagnostics without a file (e.g., program diagnostics), use a zero range
var lspRange lsproto.Range
if diagnostic.File() != nil {
lspRange = converters.ToLSPRange(diagnostic.File(), diagnostic.Loc())
}

return &lsproto.Diagnostic{
Range: lspRange,
Code: &lsproto.IntegerOrString{
Integer: ptrTo(diagnostic.Code()),
},
Severity: &severity,
Message: messageChainToString(diagnostic),
Source: ptrTo("ts"),
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
Tags: ptrToSliceIfNonEmpty(tags),
}
}

func messageChainToString(diagnostic *ast.Diagnostic) string {
if len(diagnostic.MessageChain()) == 0 {
return diagnostic.Message()
}
var b strings.Builder
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n")
return b.String()
}

func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
if len(s) == 0 {
return nil
}
return &s
}
80 changes: 45 additions & 35 deletions pkg/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ func (s *Server) RefreshDiagnostics(ctx context.Context) error {
return nil
}

// PublishDiagnostics implements project.Client.
func (s *Server) PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error {
notification := lsproto.TextDocumentPublishDiagnosticsInfo.NewNotificationMessage(params)
s.outgoingQueue <- notification.Message()
return nil
}

func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferences, error) {
caps := lsproto.GetClientCapabilities(ctx)
if !caps.Workspace.Configuration {
Expand Down Expand Up @@ -716,15 +723,26 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali
cwd = s.cwd
}

var disablePushDiagnostics bool
if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && *s.initializeParams.InitializationOptions != nil {
// Check for disablePushDiagnostics option
if initOpts, ok := (*s.initializeParams.InitializationOptions).(map[string]any); ok {
if disable, ok := initOpts["disablePushDiagnostics"].(bool); ok {
disablePushDiagnostics = disable
}
}
}

s.session = project.NewSession(&project.SessionInit{
Options: &project.SessionOptions{
CurrentDirectory: cwd,
DefaultLibraryPath: s.defaultLibraryPath,
TypingsLocation: s.typingsLocation,
PositionEncoding: s.positionEncoding,
WatchEnabled: s.watchEnabled,
LoggingEnabled: true,
DebounceDelay: 500 * time.Millisecond,
CurrentDirectory: cwd,
DefaultLibraryPath: s.defaultLibraryPath,
TypingsLocation: s.typingsLocation,
PositionEncoding: s.positionEncoding,
WatchEnabled: s.watchEnabled,
LoggingEnabled: true,
DebounceDelay: 500 * time.Millisecond,
PushDiagnosticsEnabled: !disablePushDiagnostics,
},
FS: s.fs,
Logger: s.logger,
Expand All @@ -733,36 +751,28 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali
ParseCache: s.parseCache,
})

if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && *s.initializeParams.InitializationOptions != nil {
// handle userPreferences from initializationOptions
userPreferences := s.session.NewUserPreferences()
userPreferences.Parse(*s.initializeParams.InitializationOptions)
s.session.InitializeWithConfig(userPreferences)
} else {
// request userPreferences if not provided at initialization
userPreferences, err := s.RequestConfiguration(ctx)
if err != nil {
return err
}
s.session.InitializeWithConfig(userPreferences)
userPreferences, err := s.RequestConfiguration(ctx)
if err != nil {
return err
}
s.session.InitializeWithConfig(userPreferences)

_, err = sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{
Registrations: []*lsproto.Registration{
{
Id: "typescript-config-watch-id",
Method: string(lsproto.MethodWorkspaceDidChangeConfiguration),
RegisterOptions: ptrTo(any(lsproto.DidChangeConfigurationRegistrationOptions{
Section: &lsproto.StringOrStrings{
// !!! Both the 'javascript' and 'js/ts' scopes need to be watched for settings as well.
Strings: &[]string{"typescript"},
},
})),
},
_, err = sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{
Registrations: []*lsproto.Registration{
{
Id: "typescript-config-watch-id",
Method: string(lsproto.MethodWorkspaceDidChangeConfiguration),
RegisterOptions: ptrTo(any(lsproto.DidChangeConfigurationRegistrationOptions{
Section: &lsproto.StringOrStrings{
// !!! Both the 'javascript' and 'js/ts' scopes need to be watched for settings as well.
Strings: &[]string{"typescript"},
},
})),
},
})
if err != nil {
return fmt.Errorf("failed to register configuration change watcher: %w", err)
}
},
})
if err != nil {
return fmt.Errorf("failed to register configuration change watcher: %w", err)
}

// !!! temporary.
Expand Down
6 changes: 3 additions & 3 deletions pkg/module/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1957,11 +1957,11 @@ func matchesPatternWithTrailer(target string, name string) bool {
if strings.HasSuffix(target, "*") {
return false
}
starPos := strings.Index(target, "*")
if starPos == -1 {
before, after, ok := strings.Cut(target, "*")
if !ok {
return false
}
return strings.HasPrefix(name, target[:starPos]) && strings.HasSuffix(name, target[starPos+1:])
return strings.HasPrefix(name, before) && strings.HasSuffix(name, after)
}

/** True if `extension` is one of the supported `extensions`. */
Expand Down
4 changes: 1 addition & 3 deletions pkg/modulespecifiers/specifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1193,9 +1193,7 @@ func tryGetModuleNameFromExportsOrImports(
return tspath.CombinePaths(packageName, fragmentWithJsExtension)
}
case MatchingModePattern:
starPos := strings.Index(pathOrPattern, "*")
leadingSlice := pathOrPattern[0:starPos]
trailingSlice := pathOrPattern[starPos+1:]
leadingSlice, trailingSlice, _ := strings.Cut(pathOrPattern, "*")
caseSensitive := host.UseCaseSensitiveFileNames()
if canTryTsExtension && stringutil.HasPrefix(targetFilePath, leadingSlice, caseSensitive) && stringutil.HasSuffix(targetFilePath, trailingSlice, caseSensitive) {
starReplacement := targetFilePath[len(leadingSlice) : len(targetFilePath)-len(trailingSlice)]
Expand Down
1 change: 1 addition & 0 deletions pkg/project/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ type Client interface {
WatchFiles(ctx context.Context, id WatcherID, watchers []*lsproto.FileSystemWatcher) error
UnwatchFiles(ctx context.Context, id WatcherID) error
RefreshDiagnostics(ctx context.Context) error
PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error
}
Loading