diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 1bfa982b57..b5f47c751a 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -129,6 +129,25 @@ export class AstSymbolTable { return this._exportAnalyzer.fetchAstModuleExportInfo(astModule); } + /** + * Returns the set of source files that contain a declaration that was analyzed, i.e. that is + * reachable from the entry point. + * + * @remarks + * API Extractor analyzes the compiler's `.d.ts` outputs, and by default does not use + * `skipLibCheck`, so type-checking the entire program would bind-and-check every transitively + * reachable declaration file -- including deep dependencies that are not part of the API surface. + * This set allows compiler diagnostics to be scoped to just the files that contribute + * declarations to the analyzed API. + */ + public collectAnalyzedSourceFiles(): Set { + const sourceFiles: Set = new Set(); + for (const declaration of this._astDeclarationsByDeclaration.keys()) { + sourceFiles.add(declaration.getSourceFile()); + } + return sourceFiles; + } + /** * Attempts to retrieve an export by name from the specified `AstModule`. * Returns undefined if no match was found. diff --git a/apps/api-extractor/src/analyzer/TypeScriptInternals.ts b/apps/api-extractor/src/analyzer/TypeScriptInternals.ts index 88059f26e8..07fe3d5547 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptInternals.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptInternals.ts @@ -139,7 +139,24 @@ export class TypeScriptInternals { if (!typeChecker.getEmitResolver) { throw new InternalError('Missing TypeChecker.getEmitResolver'); } - const resolver: any = typeChecker.getEmitResolver(); + + // We only use `EmitResolver.hasGlobalName`, which simply queries the type checker's `globals` + // symbol table. That table (including ambient declarations, `jsGlobalAugmentations`, and + // `declare global` augmentations) is populated synchronously when the checker is created, so it + // does not require type checking to have run. + // + // By default, `getEmitResolver()` forces a full-program diagnostic type-check before returning + // the resolver. Since API Extractor otherwise only type-checks the source files that are + // reachable from the entry point (see `Collector.analyze`), that full check would dominate the + // analysis cost. Passing `skipDiagnostics: true` avoids it while still returning a resolver + // whose `hasGlobalName` behaves identically. The parameter is a compiler internal; older + // TypeScript versions that lack it simply ignore the extra argument (falling back to the + // previous, slower behavior). + const resolver: any = typeChecker.getEmitResolver( + /*sourceFile*/ undefined, + /*cancellationToken*/ undefined, + /*skipDiagnostics*/ true + ); if (!resolver.hasGlobalName) { throw new InternalError('Missing EmitResolver.hasGlobalName'); } diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 2e6a374f11..44b0ef3cc9 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -200,13 +200,6 @@ export class Collector { throw new Error('DtsRollupGenerator.analyze() was already called'); } - // This runs a full type analysis, and then augments the Abstract Syntax Tree (i.e. declarations) - // with semantic information (i.e. symbols). The "diagnostics" are a subset of the everyday - // compile errors that would result from a full compilation. - for (const diagnostic of this._program.getSemanticDiagnostics()) { - this.messageRouter.addCompilerDiagnostic(diagnostic); - } - const sourceFiles: readonly ts.SourceFile[] = this.program.getSourceFiles(); if (this.messageRouter.showDiagnostics) { @@ -294,6 +287,25 @@ export class Collector { } } + // 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 = this.astSymbolTable.collectAnalyzedSourceFiles(); + for (const sourceFile of nonExternalSourceFiles) { + diagnosticSourceFiles.add(sourceFile); + } + for (const sourceFile of diagnosticSourceFiles) { + for (const diagnostic of this._program.getSemanticDiagnostics(sourceFile)) { + this.messageRouter.addCompilerDiagnostic(diagnostic); + } + } + // Here, we're collecting reference directives from all non-external source files // that were encountered while looking for exports, but only those references that // were explicitly written by the developer and marked with the `preserve="true"` diff --git a/common/changes/@microsoft/api-extractor/optimize-typecheck_2026-07-21-22-06-48.json b/common/changes/@microsoft/api-extractor/optimize-typecheck_2026-07-21-22-06-48.json new file mode 100644 index 0000000000..57a0061195 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/optimize-typecheck_2026-07-21-22-06-48.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Improve analysis performance by scoping compiler diagnostics to the source files that are reachable from the entry point, rather than type-checking the entire program. As a result, compiler errors are now only reported for declarations that contribute to the analyzed API surface.", + "type": "minor", + "packageName": "@microsoft/api-extractor" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "dmichon-msft@users.noreply.github.com" +}