From abf7573d72cb278f0b0416a8a1257002f975d983 Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Mon, 19 Oct 2026 16:01:54 +0530 Subject: [PATCH] [api-extractor] Fix handling of exported var declarations fetchReferencedAstEntity() skipped every symbol with the FunctionScopedVariable flag, so that "typeof x" on a function parameter is not followed. TypeScript also sets that flag on "var" declarations, so an exported var lost its name in the .d.ts rollup, or failed with "Unable to analyze the export". Now only symbols that are not var declarations are skipped. Add an exportVar scenario that covers both cases. --- .../src/analyzer/ExportAnalyzer.ts | 9 +- .../api-extractor-scenarios.api.json | 240 ++++++++++++++++++ .../exportVar/api-extractor-scenarios.api.md | 17 ++ .../etc/exportVar/rollup.d.ts | 9 + .../src/exportVar/index.ts | 9 + .../src/exportVar/var.ts | 6 + ...actor-var-exports_2026-10-19-10-30-50.json | 11 + 7 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 build-tests/api-extractor-scenarios/etc/exportVar/api-extractor-scenarios.api.json create mode 100644 build-tests/api-extractor-scenarios/etc/exportVar/api-extractor-scenarios.api.md create mode 100644 build-tests/api-extractor-scenarios/etc/exportVar/rollup.d.ts create mode 100644 build-tests/api-extractor-scenarios/src/exportVar/index.ts create mode 100644 build-tests/api-extractor-scenarios/src/exportVar/var.ts create mode 100644 common/changes/@microsoft/api-extractor/api-extractor-var-exports_2026-10-19-10-30-50.json diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index ee5866578af..36c3bc2dfbf 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -376,14 +376,19 @@ export class ExportAnalyzer { symbol: ts.Symbol, referringModuleIsExternal: boolean ): AstEntity | undefined { - // eslint-disable-next-line no-bitwise - if ((symbol.flags & ts.SymbolFlags.FunctionScopedVariable) !== 0) { + if ( + // eslint-disable-next-line no-bitwise + (symbol.flags & ts.SymbolFlags.FunctionScopedVariable) !== 0 && + symbol.valueDeclaration?.kind !== ts.SyntaxKind.VariableDeclaration + ) { // If a symbol refers back to part of its own definition, don't follow that rabbit hole // Example: // // function f(x: number): typeof x { // return 123; // } + // + // TypeScript also uses this flag for "var" declarations, which are followed normally. return undefined; } diff --git a/build-tests/api-extractor-scenarios/etc/exportVar/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/exportVar/api-extractor-scenarios.api.json new file mode 100644 index 00000000000..9c4146ad6a3 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/exportVar/api-extractor-scenarios.api.json @@ -0,0 +1,240 @@ +{ + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "toolVersion": "[test mode]", + "schemaVersion": 1011, + "oldestForwardsCompatibleVersion": 1001, + "tsdocConfig": { + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "noStandardTags": true, + "tagDefinitions": [ + { + "tagName": "@alpha", + "syntaxKind": "modifier" + }, + { + "tagName": "@beta", + "syntaxKind": "modifier" + }, + { + "tagName": "@defaultValue", + "syntaxKind": "block" + }, + { + "tagName": "@decorator", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@deprecated", + "syntaxKind": "block" + }, + { + "tagName": "@eventProperty", + "syntaxKind": "modifier" + }, + { + "tagName": "@example", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@experimental", + "syntaxKind": "modifier" + }, + { + "tagName": "@inheritDoc", + "syntaxKind": "inline" + }, + { + "tagName": "@internal", + "syntaxKind": "modifier" + }, + { + "tagName": "@label", + "syntaxKind": "inline" + }, + { + "tagName": "@link", + "syntaxKind": "inline", + "allowMultiple": true + }, + { + "tagName": "@override", + "syntaxKind": "modifier" + }, + { + "tagName": "@packageDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@param", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@privateRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@public", + "syntaxKind": "modifier" + }, + { + "tagName": "@readonly", + "syntaxKind": "modifier" + }, + { + "tagName": "@remarks", + "syntaxKind": "block" + }, + { + "tagName": "@returns", + "syntaxKind": "block" + }, + { + "tagName": "@sealed", + "syntaxKind": "modifier" + }, + { + "tagName": "@see", + "syntaxKind": "block" + }, + { + "tagName": "@throws", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@typeParam", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@virtual", + "syntaxKind": "modifier" + }, + { + "tagName": "@jsx", + "syntaxKind": "block" + }, + { + "tagName": "@jsxRuntime", + "syntaxKind": "block" + }, + { + "tagName": "@jsxFrag", + "syntaxKind": "block" + }, + { + "tagName": "@jsxImportSource", + "syntaxKind": "block" + }, + { + "tagName": "@betaDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@internalRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@preapproved", + "syntaxKind": "modifier" + } + ], + "supportForTags": { + "@alpha": true, + "@beta": true, + "@defaultValue": true, + "@decorator": true, + "@deprecated": true, + "@eventProperty": true, + "@example": true, + "@experimental": true, + "@inheritDoc": true, + "@internal": true, + "@label": true, + "@link": true, + "@override": true, + "@packageDocumentation": true, + "@param": true, + "@privateRemarks": true, + "@public": true, + "@readonly": true, + "@remarks": true, + "@returns": true, + "@sealed": true, + "@see": true, + "@throws": true, + "@typeParam": true, + "@virtual": true, + "@betaDocumentation": true, + "@internalRemarks": true, + "@preapproved": true + }, + "reportUnsupportedHtmlElements": false + } + }, + "kind": "Package", + "canonicalReference": "api-extractor-scenarios!", + "docComment": "", + "name": "api-extractor-scenarios", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EntryPoint", + "canonicalReference": "api-extractor-scenarios!", + "name": "", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Variable", + "canonicalReference": "api-extractor-scenarios!Var:var", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "_default: " + }, + { + "kind": "Content", + "text": "{\n a: string;\n}" + } + ], + "fileUrlPath": "src/exportVar/var.ts", + "isReadonly": false, + "releaseTag": "Public", + "name": "Var", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Variable", + "canonicalReference": "api-extractor-scenarios!zero:var", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "zero: " + }, + { + "kind": "Content", + "text": "number" + } + ], + "fileUrlPath": "src/exportVar/index.ts", + "isReadonly": false, + "releaseTag": "Public", + "name": "zero", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ] + } + ] +} diff --git a/build-tests/api-extractor-scenarios/etc/exportVar/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/exportVar/api-extractor-scenarios.api.md new file mode 100644 index 00000000000..547e35b87d4 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/exportVar/api-extractor-scenarios.api.md @@ -0,0 +1,17 @@ +## API Report File for "api-extractor-scenarios" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export var Var: { + a: string; +}; + +// @public (undocumented) +export var zero: number; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build-tests/api-extractor-scenarios/etc/exportVar/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/exportVar/rollup.d.ts new file mode 100644 index 00000000000..15ab8e696aa --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/exportVar/rollup.d.ts @@ -0,0 +1,9 @@ +/** @public */ +export declare var Var: { + a: string; +}; + +/** @public */ +export declare var zero: number; + +export { } diff --git a/build-tests/api-extractor-scenarios/src/exportVar/index.ts b/build-tests/api-extractor-scenarios/src/exportVar/index.ts new file mode 100644 index 00000000000..305b93e5f76 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportVar/index.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import Var from './var'; + +export { Var }; + +/** @public */ +export declare var zero: number; diff --git a/build-tests/api-extractor-scenarios/src/exportVar/var.ts b/build-tests/api-extractor-scenarios/src/exportVar/var.ts new file mode 100644 index 00000000000..09987bec901 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportVar/var.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** @public */ +declare var _default: { a: string }; +export default _default; diff --git a/common/changes/@microsoft/api-extractor/api-extractor-var-exports_2026-10-19-10-30-50.json b/common/changes/@microsoft/api-extractor/api-extractor-var-exports_2026-10-19-10-30-50.json new file mode 100644 index 00000000000..9a03f80a4ef --- /dev/null +++ b/common/changes/@microsoft/api-extractor/api-extractor-var-exports_2026-10-19-10-30-50.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Fix an issue where an exported `var` declaration lost its name in the .d.ts rollup or caused an \"Unable to analyze the export\" error", + "type": "patch", + "packageName": "@microsoft/api-extractor" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "yashvt9404@gmail.com" +} \ No newline at end of file