Skip to content

Push per-file diagnostics for clients without pull diagnostics support - #63921

Open
Christian Vuerings (christianvuerings) wants to merge 2 commits into
microsoft:mainfrom
christianvuerings:push-file-diagnostics
Open

Push per-file diagnostics for clients without pull diagnostics support#63921
Christian Vuerings (christianvuerings) wants to merge 2 commits into
microsoft:mainfrom
christianvuerings:push-file-diagnostics

Conversation

@christianvuerings

@christianvuerings Christian Vuerings (christianvuerings) commented Aug 20, 2026

Copy link
Copy Markdown

Fixes #63928.

Recreated from microsoft/typescript-go#4297, which was closed unmerged when development moved back to this repository (microsoft/typescript-go#4918). The change is ported onto the new tsc/ layout and rebased against current main. cc Jake Bailey (@jakebailey), who reviewed the original PR.

Problem

The LSP server only supports pull diagnostics (textDocument/diagnostic); push diagnostics are only used for config file errors. Clients that do not implement pull diagnostics get no file diagnostics at all. This affects eglot before Dec 2025, Nova, Claude Code, and other lighter LSP clients (see the discussion in microsoft/typescript-go#2362 and anthropics/claude-code#40282).

Change

When the client advertises textDocument.publishDiagnostics but not textDocument.diagnostic, the server now pushes per-file diagnostics for open files:

  • didOpen publishes initial diagnostics for the opened file
  • didChange schedules a debounced (500ms) snapshot update that requests open documents, so dirty programs are rebuilt eagerly, and republishes diagnostics for open files whose program updated
  • didClose clears diagnostics for the closed file
  • Triggers that would send workspace/diagnostic/refresh (watched file changes, config changes, ATA updates) schedule a snapshot update instead, since push-only clients cannot re-pull

Clients that support pull diagnostics are unaffected (verified: zero per-file pushes when textDocument.diagnostic is declared), and the existing disablePushDiagnostics initialization option also disables the new path. Diagnostics are converted with the existing DiagnosticToLSPPush path and include the document version.

Two adaptations were made while porting to current main:

  • ProvidePushDiagnostics honors the EnableValidation user preference, matching the pull path
  • In DidChangeFile, push-only clients return after scheduling the snapshot update, skipping the content-mapper workspace/diagnostic/refresh path since they cannot act on a refresh request

Validation

  • New unit tests in tsc/internal/project/project_test.go (open/change/close/version support/disabled)
  • go test ./tsc/internal/project/... ./tsc/internal/ls/... ./tsc/internal/lsp/... passes
  • The original branch was tested end to end over stdio against a large TypeScript monorepo (~50k files): diagnostics arrive ~1.2s after didOpen and ~500ms after each didChange, are cleared on didClose, and pull requests keep working alongside

Notes

The original review raised that push diagnostics get tricky around LS restarts and racy conditions, and would need testing in VS Code with pull diagnostics disabled; happy to help validate that or adjust the approach (for example gating behind an initialization option instead of capability detection).

Disclosure per CONTRIBUTING.md: this patch was authored with AI assistance (Claude Code). I have read and understand the change and will respond to review feedback myself.

Copilot AI balanced review requested due to automatic review settings August 20, 2026 16:22
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Aug 20, 2026
@typescript-automation

Copy link
Copy Markdown

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds push diagnostics for open files when clients lack pull-diagnostics support.

Changes:

  • Detects push-only clients and schedules debounced snapshot updates.
  • Publishes and clears version-aware per-file diagnostics.
  • Adds project-session coverage for open, change, close, version, and disabled behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tsc/internal/project/snapshot.go Logs new snapshot update reasons.
tsc/internal/project/session.go Schedules and publishes open-file diagnostics.
tsc/internal/project/project_test.go Tests push-diagnostic session behavior.
tsc/internal/lsp/server.go Enables pushes based on client capabilities.
tsc/internal/ls/diagnostics.go Computes push-formatted file diagnostics.

Comment thread tsc/internal/project/session.go Outdated
}
_ = s.updateContentMapperRegistrations(ctx, newSnapshot)
s.publishProgramDiagnostics(oldSnapshot, newSnapshot)
s.publishOpenFileDiagnostics(oldSnapshot, newSnapshot)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 2e7b1b3. Publishes are now serialized under a mutex and gated by snapshot ID: a pass whose snapshot is older than one already published is skipped, and clears are driven by a tracking map of published files rather than the previous snapshot's overlays, so a skipped intermediate pass cannot leave a closed file uncleared or let a stale pass publish after a close. Added a rapid didChange/didClose test, and the push diagnostics tests pass under -race with -count=3.

Comment thread tsc/internal/project/session.go Outdated
Comment on lines +2074 to +2077
_, wasOpen := oldSnapshot.fs.overlays[path]
if wasOpen && project.ProgramLastUpdate != newSnapshot.ID() {
continue
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 2e7b1b3. The publish pass now receives the SnapshotChange and forces a full republish when the change carries new user preferences or came from a diagnostics refresh, so toggling validate.enable or reportStyleChecksAsWarnings clears or republishes open-file diagnostics even though the program did not update. Covered by a new test toggling validate.enable off and back on.

Comment thread tsc/internal/ls/diagnostics.go Outdated
Comment on lines +67 to +69
lspDiagnostics := make([]*lsproto.Diagnostic, 0, len(diagnostics))
for _, diag := range diagnostics {
lspDiagnostics = append(lspDiagnostics, lsconv.DiagnosticToLSPPush(ctx, l.converters, diag))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 2e7b1b3. The normalization is now shared: toLSPDiagnosticsWith holds the style-checks-as-warnings and synthesized content-mapper aggregation logic, parameterized by the pull or push converter, and DiagnosticToLSPPush gained the reportStyleChecksAsWarnings option. Push and pull clients now receive equivalent file diagnostics.

Comment thread tsc/internal/project/session.go Outdated
return
}

ctx := s.backgroundContext()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 2e7b1b3. The push pass now runs its context with core.CheckerLifetimeDiagnostics, matching the pull handler, so it uses the dedicated diagnostics checker instead of consuming query checkers.

if capabilities := s.initializeParams.Capabilities; capabilities != nil && capabilities.TextDocument != nil {
clientSupportsPullDiagnostics = capabilities.TextDocument.Diagnostic != nil
}
pushFileDiagnostics := !disablePushDiagnostics && !clientSupportsPullDiagnostics

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added in 2e7b1b3: TestPushFileDiagnosticsGate in tsc/internal/lsp runs the real initialize handshake and covers all three cases: a push-only client receives per-file publishes, a pull-capable client receives none, and disablePushDiagnostics disables them.

ArshVermaGit

This comment was marked as spam.

@christianvuerings

Copy link
Copy Markdown
Author

Opened #63928 as the linked issue.

Arsh Verma (@ArshVermaGit) thanks for the review. 2e7b1b3 addresses the race concern: publishes are now serialized and gated by snapshot ID, so a slow pass for an older snapshot can no longer publish after a later change or close. New coverage: a rapid didChange sequence followed by an immediate didClose (asserting the final publish is the empty clear), a validation-preference toggle, and a server-level initialization gate test over the real LSP handshake; the push diagnostics tests also pass under -race with -count=3. On LSP restarts: the server keeps no cross-session state, so after a restart a reopened file gets fresh diagnostics on didOpen (covered by the open tests); diagnostics a client keeps showing for files it never reopens are client-side state that a server-side test cannot observe, and clearing those on server exit is the client's responsibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

LSP server sends no per-file diagnostics to clients without pull diagnostics support

3 participants