[api-extractor] Optimize the analysis type-check pass - #5891
Conversation
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>
There was a problem hiding this comment.
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 passingskipDiagnostics: truewhen onlyhasGlobalNameis 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
| // 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)) { |
There was a problem hiding this comment.
API extractor only owns diagnostics relevant to files in the API report; files unrelated should be handled by the normal type checking pass.
| // 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.) |
There was a problem hiding this comment.
Can we tighten this a bit? It is repeating the exact same sentence from your other check above, reads like slop (trying to explain every exact nuance, no mental model of what the audience would reasonably already know or what questions they would care about)
| @@ -129,6 +129,25 @@ export class AstSymbolTable { | |||
| return this._exportAnalyzer.fetchAstModuleExportInfo(astModule); | |||
There was a problem hiding this comment.
Validation
- All api-extractor acceptance-test projects rebuilt clean (
api-extractor-scenariosself-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.jsonchanges.heft test: 89/89 pass.
Was there any validation that this optimization actually improved anything (e.g. runtime against a real repo)?
There was a problem hiding this comment.
I've vetted about 3 seconds of saving on an internal project, and the @rushstack/mcp-server project in the rushstack repo was an immediate beneficiary, as it pulls in a lot of unrelated .d.ts files.
Summary
API Extractor analyzes the compiler's
.d.tsoutputs and, by default, does not enableskipLibCheck. 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:Collector.analyzecalledprogram.getSemanticDiagnostics()with no argument (whole program).TypeScriptInternals.getGlobalVariableAnalyzercalledtypeChecker.getEmitResolver(), which forces a full-program diagnostic check before returning — even though onlyhasGlobalNameis needed.Changes
getEmitResolver(…, skipDiagnostics: true)— theglobalstable (ambient decls,jsGlobalAugmentations,declare global) is populated eagerly at checker creation, sohasGlobalNameworks without running diagnostics.getSemanticDiagnostics(sourceFile)— diagnostics are now reported only for source files reachable from the entry point: analyzed declaration files (newAstSymbolTable.collectAnalyzedSourceFiles()) plus the intermediate re-export files already collected asnonExternalSourceFiles.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 --cleanruns on@rushstack/mcp-server(3 runs each):getEmitResolverIn 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 reportedgetDiagnosticsWorkerdropping 14.1s → 11.3s.Validation
api-extractor-scenariosself-validates snapshots;test-01..05,lib1..5-test,api-documenter-*,d-cts/d-mts). Byte-identical API reports / rollups / doc models.node-core-library,ts-command-line,terminal,rush-lib,mcp-server) — no.api.md/.api.jsonchanges.heft test: 89/89 pass.A
minorchange file is included (the diagnostics-reporting behavior change).