[api-extractor] Optimize the analysis type-check pass#5891
Open
dmichon-msft wants to merge 1 commit into
Open
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>
dmichon-msft
requested review from
apostolisms,
bmiddha,
iclanton,
jxanthony and
octogonz
as code owners
July 21, 2026 22:34
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
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)) { |
bmiddha
approved these changes
Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).