Skip to content

[api-extractor] Optimize the analysis type-check pass#5891

Open
dmichon-msft wants to merge 1 commit into
microsoft:mainfrom
dmichon-msft:api-extractor-optimize-typecheck
Open

[api-extractor] Optimize the analysis type-check pass#5891
dmichon-msft wants to merge 1 commit into
microsoft:mainfrom
dmichon-msft:api-extractor-optimize-typecheck

Conversation

@dmichon-msft

Copy link
Copy Markdown
Contributor

Summary

API Extractor analyzes the compiler's .d.ts outputs and, by default, does not enable skipLibCheck. Two code paths therefore forced a full-program semantic type-check over the entire transitively-reachable declaration closure — including deep dependencies that are not part of the analyzed API surface:

  1. Collector.analyze called program.getSemanticDiagnostics() with no argument (whole program).
  2. TypeScriptInternals.getGlobalVariableAnalyzer called typeChecker.getEmitResolver(), which forces a full-program diagnostic check before returning — even though only hasGlobalName is needed.

Changes

  • getEmitResolver(…, skipDiagnostics: true) — the globals table (ambient decls, jsGlobalAugmentations, declare global) is populated eagerly at checker creation, so hasGlobalName works without running diagnostics.
  • Scoped getSemanticDiagnostics(sourceFile) — diagnostics are now reported only for source files reachable from the entry point: analyzed declaration files (new AstSymbolTable.collectAnalyzedSourceFiles()) plus the intermediate re-export files already collected as nonExternalSourceFiles.

Binding, which the analysis relies on, still happens eagerly for all files when the checker is created, so the analysis itself is unaffected. The net behavior change is that compiler errors are now only reported for declarations that contribute to the analyzed API surface (errors in unreachable dependency files are no longer surfaced).

Performance

Verified through real heft build --clean runs on @rushstack/mcp-server (3 runs each):

Baseline Optimized
api-extractor step (subtree) ~4789ms ~942ms
getEmitResolver ~4035ms ~0ms
full build wall clock ~14.4s ~9.8s

In baseline, the expensive whole-program check actually fires inside getEmitResolver() during collector construction, which is why both changes are required. A sweep across all ~70 in-repo api-extractor packages showed a consistent ~1s saving on most packages, up to ~4.3s on the heaviest (mcp-server); packages with a cheap external type closure (e.g. rush-lib) are unaffected. An external production repo reported getDiagnosticsWorker dropping 14.1s → 11.3s.

Validation

  • All api-extractor acceptance-test projects rebuilt clean (api-extractor-scenarios self-validates snapshots; test-01..05, lib1..5-test, api-documenter-*, d-cts/d-mts). Byte-identical API reports / rollups / doc models.
  • Additionally cross-checked several library consumers (node-core-library, ts-command-line, terminal, rush-lib, mcp-server) — no .api.md/.api.json changes.
  • heft test: 89/89 pass.

A minor change file is included (the diagnostics-reporting behavior change).

Previously, `Collector.analyze` type-checked the entire program via `getSemanticDiagnostics()`, and `getGlobalVariableAnalyzer` forced a full-program check inside `getEmitResolver()`. Because API Extractor analyzes the compiler's .d.ts outputs with `skipLibCheck` disabled by default, this bind-and-checked every transitively reachable declaration file, including deep dependencies outside the API surface.

This change:
- Passes `skipDiagnostics: true` to `getEmitResolver()`, since only `hasGlobalName` is needed and the globals table is populated eagerly at checker creation.
- Scopes `getSemanticDiagnostics()` to the source files reachable from the entry point (analyzed declarations plus intermediate re-export files) via the new `AstSymbolTable.collectAnalyzedSourceFiles()`.

Binding, which the analysis relies on, still happens eagerly for all files, so the analysis itself is unchanged. Compiler errors are now only reported for declarations that contribute to the analyzed API. Verified on @rushstack/mcp-server: the api-extractor step dropped from ~4.8s to ~0.9s with byte-identical output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Optimizes API Extractor’s analysis-time TypeScript type-checking by avoiding whole-program semantic diagnostics and avoiding an expensive internal getEmitResolver() diagnostic pass, while keeping analysis results (API reports/rollups/models) unchanged.

Changes:

  • Scope semantic diagnostics to source files reachable from the entry point (analyzed declarations + intermediate re-export files).
  • Avoid full-program diagnostics triggered by TypeChecker.getEmitResolver() by passing skipDiagnostics: true when only hasGlobalName is needed.
  • Add AstSymbolTable.collectAnalyzedSourceFiles() to identify the analyzed declaration source-file set used for diagnostics scoping.
Show a summary per file
File Description
common/changes/@microsoft/api-extractor/optimize-typecheck_2026-07-21-22-06-48.json Change file documenting the (minor) behavior change in diagnostics reporting scope.
apps/api-extractor/src/collector/Collector.ts Replaces whole-program getSemanticDiagnostics() with diagnostics scoped to reachable/analyzed source files.
apps/api-extractor/src/analyzer/TypeScriptInternals.ts Calls getEmitResolver(..., skipDiagnostics: true) to avoid triggering a full diagnostic pass when only hasGlobalName is needed.
apps/api-extractor/src/analyzer/AstSymbolTable.ts Adds helper to collect source files containing declarations encountered/analyzed during symbol table construction.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment on lines +290 to +304
// Report compiler diagnostics, but only for the source files that are reachable from the entry
// point: the files that contribute an analyzed declaration, plus the intermediate re-export
// files that were visited while resolving exports. API Extractor analyzes the compiler's `.d.ts`
// outputs and (by default) does not enable `skipLibCheck`, so running `getSemanticDiagnostics()`
// across the entire program would bind-and-check every transitively reachable declaration file,
// including deep dependencies that are not part of the API surface. Scoping the diagnostics to
// the analyzed files avoids that cost while still surfacing every error that pertains to the
// exported API. (Binding, which the analysis relies on, happens eagerly for all files when the
// type checker is created, so this does not affect the analysis itself.)
const diagnosticSourceFiles: Set<ts.SourceFile> = this.astSymbolTable.collectAnalyzedSourceFiles();
for (const sourceFile of nonExternalSourceFiles) {
diagnosticSourceFiles.add(sourceFile);
}
for (const sourceFile of diagnosticSourceFiles) {
for (const diagnostic of this._program.getSemanticDiagnostics(sourceFile)) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs triage

Development

Successfully merging this pull request may close these issues.

3 participants