Skip to content

Expose bind diags in LSP, show dead/deprecated code #955

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 3 commits into from
May 28, 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
16 changes: 11 additions & 5 deletions internal/ast/diagnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Diagnostic struct {
message string
messageChain []*Diagnostic
relatedInformation []*Diagnostic
reportsUnnecessary bool
reportsDeprecated bool
}

func (d *Diagnostic) File() *SourceFile { return d.file }
Expand All @@ -31,6 +33,8 @@ func (d *Diagnostic) Category() diagnostics.Category { return d.category }
func (d *Diagnostic) Message() string { return d.message }
func (d *Diagnostic) MessageChain() []*Diagnostic { return d.messageChain }
func (d *Diagnostic) RelatedInformation() []*Diagnostic { return d.relatedInformation }
func (d *Diagnostic) ReportsUnnecessary() bool { return d.reportsUnnecessary }
func (d *Diagnostic) ReportsDeprecated() bool { return d.reportsDeprecated }

func (d *Diagnostic) SetFile(file *SourceFile) { d.file = file }
func (d *Diagnostic) SetLocation(loc core.TextRange) { d.loc = loc }
Expand Down Expand Up @@ -67,11 +71,13 @@ func (d *Diagnostic) Clone() *Diagnostic {

func NewDiagnostic(file *SourceFile, loc core.TextRange, message *diagnostics.Message, args ...any) *Diagnostic {
return &Diagnostic{
file: file,
loc: loc,
code: message.Code(),
category: message.Category(),
message: message.Format(args...),
file: file,
loc: loc,
code: message.Code(),
category: message.Category(),
message: message.Format(args...),
reportsUnnecessary: message.ReportsUnnecessary(),
reportsDeprecated: message.ReportsDeprecated(),
}
}

Expand Down
12 changes: 10 additions & 2 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -13153,21 +13153,29 @@ func (c *Checker) getCannotFindNameDiagnosticForName(node *ast.Node) *diagnostic
}

func (c *Checker) GetDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
return c.getDiagnostics(ctx, sourceFile, &c.diagnostics)
}

func (c *Checker) GetSuggestionDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
return c.getDiagnostics(ctx, sourceFile, &c.suggestionDiagnostics)
}

func (c *Checker) getDiagnostics(ctx context.Context, sourceFile *ast.SourceFile, collection *ast.DiagnosticsCollection) []*ast.Diagnostic {
c.checkNotCanceled()
if sourceFile != nil {
c.CheckSourceFile(ctx, sourceFile)
if c.wasCanceled {
return nil
}
return c.diagnostics.GetDiagnosticsForFile(sourceFile.FileName())
return collection.GetDiagnosticsForFile(sourceFile.FileName())
}
for _, file := range c.files {
c.CheckSourceFile(ctx, file)
if c.wasCanceled {
return nil
}
}
return c.diagnostics.GetDiagnostics()
return collection.GetDiagnostics()
}

func (c *Checker) GetDiagnosticsWithoutCheck(sourceFile *ast.SourceFile) []*ast.Diagnostic {
Expand Down
37 changes: 37 additions & 0 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@ func (p *Program) GetSemanticDiagnostics(ctx context.Context, sourceFile *ast.So
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSemanticDiagnosticsForFile)
}

func (p *Program) GetSuggestionDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSuggestionDiagnosticsForFile)
}

func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic {
var globalDiagnostics []*ast.Diagnostic
checkers, done := p.checkerPool.GetAllCheckers(ctx)
Expand Down Expand Up @@ -452,6 +456,39 @@ func (p *Program) getSemanticDiagnosticsForFile(ctx context.Context, sourceFile
return filtered
}

func (p *Program) getSuggestionDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
Copy link
Member Author

Choose a reason for hiding this comment

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

This all is pretty duplicative but I'm not about to attempt to refactor it into some generic version, honestly.

if checker.SkipTypeChecking(sourceFile, p.compilerOptions) {
return nil
}

var fileChecker *checker.Checker
var done func()
if sourceFile != nil {
fileChecker, done = p.checkerPool.GetCheckerForFile(ctx, sourceFile)
defer done()
}

diags := slices.Clip(sourceFile.BindSuggestionDiagnostics)

checkers, closeCheckers := p.checkerPool.GetAllCheckers(ctx)
defer closeCheckers()

// Ask for diags from all checkers; checking one file may add diagnostics to other files.
// These are deduplicated later.
for _, checker := range checkers {
if sourceFile == nil || checker == fileChecker {
diags = append(diags, checker.GetSuggestionDiagnostics(ctx, sourceFile)...)
} else {
// !!! is there any case where suggestion diagnostics are produced in other checkers?
}
}
if ctx.Err() != nil {
return nil
}

return diags
}

func isCommentOrBlankLine(text string, pos int) bool {
for pos < len(text) && (text[pos] == ' ' || text[pos] == '\t') {
pos++
Expand Down
43 changes: 33 additions & 10 deletions internal/ls/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ func (l *LanguageService) GetDocumentDiagnostics(ctx context.Context, documentUR
syntaxDiagnostics := program.GetSyntacticDiagnostics(ctx, file)
var lspDiagnostics []*lsproto.Diagnostic
if len(syntaxDiagnostics) != 0 {
lspDiagnostics = make([]*lsproto.Diagnostic, len(syntaxDiagnostics))
for i, diag := range syntaxDiagnostics {
lspDiagnostics[i] = toLSPDiagnostic(diag, l.converters)
lspDiagnostics = make([]*lsproto.Diagnostic, 0, len(syntaxDiagnostics))
for _, diag := range syntaxDiagnostics {
lspDiagnostics = append(lspDiagnostics, toLSPDiagnostic(diag, l.converters))
}
} else {
checker, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
semanticDiagnostics := checker.GetDiagnostics(ctx, file)
lspDiagnostics = make([]*lsproto.Diagnostic, len(semanticDiagnostics))
for i, diag := range semanticDiagnostics {
lspDiagnostics[i] = toLSPDiagnostic(diag, l.converters)
diagnostics := program.GetSemanticDiagnostics(ctx, file)
suggestionDiagnostics := program.GetSuggestionDiagnostics(ctx, file)

lspDiagnostics = make([]*lsproto.Diagnostic, 0, len(diagnostics)+len(suggestionDiagnostics))
for _, diag := range diagnostics {
lspDiagnostics = append(lspDiagnostics, toLSPDiagnostic(diag, l.converters))
}
for _, diag := range suggestionDiagnostics {
// !!! user preference for suggestion diagnostics; keep only unnecessary/dprecated?
lspDiagnostics = append(lspDiagnostics, toLSPDiagnostic(diag, l.converters))
}
}
return &lsproto.DocumentDiagnosticReport{
Expand Down Expand Up @@ -60,6 +64,17 @@ func toLSPDiagnostic(diagnostic *ast.Diagnostic, converters *Converters) *lsprot
})
}

var tags []lsproto.DiagnosticTag
if diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated() {
tags = make([]lsproto.DiagnosticTag, 0, 2)
if diagnostic.ReportsUnnecessary() {
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
}
if diagnostic.ReportsDeprecated() {
tags = append(tags, lsproto.DiagnosticTagDeprecated)
}
}

return &lsproto.Diagnostic{
Range: converters.ToLSPRange(diagnostic.File(), diagnostic.Loc()),
Code: &lsproto.IntegerOrString{
Expand All @@ -68,6 +83,14 @@ func toLSPDiagnostic(diagnostic *ast.Diagnostic, converters *Converters) *lsprot
Severity: &severity,
Message: diagnostic.Message(),
Source: ptrTo("ts"),
RelatedInformation: &relatedInformation,
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
Tags: ptrToSliceIfNonEmpty(tags),
}
}

func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
if len(s) == 0 {
return nil
}
return &s
}