Skip to content

Create declaration emit diagnostic selectors on demand - #64723

Draft
Gavin Kline (gwkline) wants to merge 2 commits into
microsoft:mainfrom
gwkline:perf/declaration-diagnostic-contexts
Draft

Gavin Kline (gwkline) wants to merge 2 commits into
microsoft:mainfrom
gwkline:perf/declaration-diagnostic-contexts

Conversation

@gwkline

@gwkline Gavin Kline (gwkline) commented Oct 10, 2026 •

Copy link
Copy Markdown

Fixes #64720

Analysis

Every declaration the declaration transformer visits installs a diagnostic context. On main this costs two allocations per context:

  1. createGetSymbolAccessibilityDiagnosticForNode(input) allocates a selector closure. The selector picks the error message if a symbol used by the declaration turns out to be inaccessible.
  2. setupDiagnosticContext returns a second closure that restores the previous context.

The selector is only called from handleSymbolAccessibilityError, that is, when an accessibility error is actually reported, which almost never happens.

This runs for every emitted declaration file. Under noEmit with declaration enabled, it also runs for every file's declaration diagnostics.

Fix

  • SymbolTrackerSharedState holds a symbolAccessibilityDiagnosticContext{node, fn} instead of a function. The common path stores the declaration node, and get creates the selector only when an error is reported. Sites that need a custom selector still store a function:
    • default export assignments, including the CommonJS _default/_exported paths
    • class extends expressions
    • dynamic names (checkName)
    • the initial throwDiagnostic sentinel
  • setupDiagnosticContext returns the saved state as a struct, and restoreDiagnosticContext reinstates it. Setting up and restoring a context therefore no longer allocates.

createGetSymbolAccessibilityDiagnosticForNode only inspects the node's kind (and, for parameters, its parent) when called. canProduceDiagnostics accepts exactly the kinds it handles. So creating the selector later, from the same node, gives the same result.

Testing

New test declarationEmitInaccessibleNameContexts gives an inferred type that can't be named to:

  • variables and destructuring
  • function return types and parameters
  • class properties and static properties
  • constructor parameter properties
  • methods and static methods
  • getters and accessor pairs
  • namespace members
  • object literals
  • class extends expressions
  • expando properties on a function declaration and an arrow function
  • default exports

It reports 23 errors across 15 diagnostic codes. Its baselines were generated with the code on main and are unchanged by this PR. No existing baseline changes.

Results

M3 Pro, --extendedDiagnostics:

Program Allocations Check time
generator from the issue (1,557 files, noEmit + declaration) 14.5M → 8.7M (−40%) 0.300 s → 0.276 s
private monorepo package, --noEmit (8.6k files) 46.1M → 41.1M (−10.9%) 4.19 s → 4.09 s
same package, declaration build 35.4M → 32.5M (−8.0%) emitted output identical

The profiling, the patch and this description were produced with Claude Code (Claude Fable 5.1 and Claude Opus 5.5). I have read and understand the change and will handle review myself.

Copilot Checklist

I successfully ran the applicable command at the end of my session, and it completed without error:

  • npx hereby validate
  • npx hereby validate --api (for TypeScript API changes; no API changes here)

🤖 Generated with Claude Code

Every declaration visited by the declaration transformer installed a
closure that selects the diagnostic to report if one of its symbols turns
out to be inaccessible, and returned a second closure to restore the
previous context. Almost no visited declaration reports such an error, so
the closures were pure allocation overhead: about one in nine allocations
of a type check with declaration emit enabled.

The context now records the declaration node and builds the selector only
when an accessibility error is reported. The handful of sites that need a
custom selector keep passing a function. The saved context is a plain
struct restored by a method, so the deferred restore no longer allocates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Oct 10, 2026
Each exported declaration infers a type that declaration emit cannot name,
so every kind of declaration reports its own accessibility error. The
baselines were generated before diagnostic selectors were made lazy and
are unchanged by it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gwkline
Gavin Kline (gwkline) force-pushed the perf/declaration-diagnostic-contexts branch from 5da48e4 to 4ca431b Compare October 10, 2026 21:39

This branch has not been deployed

No deployments
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

None yet

Development

Successfully merging this pull request may close these issues.

Declaration diagnostics allocate a diagnostic selector closure for every declaration visited

1 participant