Skip to content

Commit

Permalink
[api-extractor] Fix 'export * as __' part of #3593
Browse files Browse the repository at this point in the history
"Incorrect canonical reference to aliased class in .api.json"
#3593

This fixes the other aliasing, namely through (nested)
namespaces.
Added a test scenario "docReferencesNamespaceAlias", which
consists of nested namespaces that have their own index.d.ts,
re-exporting the default export of each sibling file and
their respective sub-namespace (top-level -> 'renamed'
-> 'sub').
The *.api.json canonicalReferences now correctly use the
nested namespace "fully-qualified name".

Again, some other test result changed unexpectedly, maybe
revealing an existing bug / wrong expected result.
The scenarios are "exportImportStarAs2", "importEquals"
and "includeForgottenExports". The pattern is that
before, namespace and class were separate tokens, but
are now aggregated to one dot-separated expression.
I hope that these are improvements, but I am not sure,
so this should be reviewed.

Implementation:
---------------
When DeclarationReferenceGenerator#_getParentReference()
looks for a parent symbol, it must also resolve
namespace parent symbols the Collector derived from
'import * as ___'.
To that end, Collector now has a method
getExportingNamespace() that pulls namespace imports from
the CollectorEntities of a namespace symbol (if present).
For that, namespace imports are now also registered in the
_entitiesBySymbol Map.
In DeclarationReferenceGenerator, the handling of
namespaces had to be improved. _getNavigationToSymbol()
now detects namespaces, even behind an alias, and
then always returns "." as separator. To find the
parent, it also considers namespaces now.
_symbolToDeclarationReference() takes care not to
follow an alias that leads to a *-imported namespace,
because the target is the imported source file that
no longer allows to retrieve the namespace name.
fwienber committed Sep 2, 2022
1 parent b11cc24 commit ef387a7
Showing 16 changed files with 574 additions and 19 deletions.
21 changes: 21 additions & 0 deletions apps/api-extractor/src/collector/Collector.ts
Original file line number Diff line number Diff line change
@@ -314,6 +314,25 @@ export class Collector {
return collectorEntity?.nameForEmit ?? symbol.name;
}

/**
* Returns (the symbol of) the namespace that exports the given `symbol`, if applicable.
* @param symbol the symbol representing the namespace that exports the given `symbol`,
* or undefined if there is no such namespace
*/
public getExportingNamespace(symbol: ts.Symbol): ts.Symbol | undefined {
const collectorEntity: CollectorEntity | undefined = this._entitiesBySymbol.get(symbol);
if (collectorEntity) {
const exportingNamespace: AstNamespaceImport | undefined = collectorEntity.getExportingNamespace();
if (exportingNamespace) {
return TypeScriptInternals.tryGetSymbolForDeclaration(
exportingNamespace.declaration,
this.typeChecker
);
}
}
return undefined;
}

public fetchSymbolMetadata(astSymbol: AstSymbol): SymbolMetadata {
if (astSymbol.symbolMetadata === undefined) {
this._fetchSymbolMetadata(astSymbol);
@@ -435,6 +454,8 @@ export class Collector {
this._entitiesByAstEntity.set(astEntity, entity);
if (astEntity instanceof AstSymbol) {
this._entitiesBySymbol.set(astEntity.followedSymbol, entity);
} else if (astEntity instanceof AstNamespaceImport) {
this._entitiesBySymbol.set((astEntity.declaration as unknown as ts.Type).symbol, entity);
}
this._entities.push(entity);
this._collectReferenceDirectives(astEntity);
13 changes: 13 additions & 0 deletions apps/api-extractor/src/collector/CollectorEntity.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import { AstSymbol } from '../analyzer/AstSymbol';
import { Collector } from './Collector';
import { Sort } from '@rushstack/node-core-library';
import { AstEntity } from '../analyzer/AstEntity';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport';

/**
* This is a data structure used by the Collector to track an AstEntity that may be emitted in the *.d.ts file.
@@ -189,6 +190,18 @@ export class CollectorEntity {
return this._localExportNamesByParent.size > 0;
}

/**
* Return the first namespace that exports this entity.
*/
public getExportingNamespace(): AstNamespaceImport | undefined {
for (const [parent, localExportNames] of this._localExportNamesByParent) {
if (parent.consumable && localExportNames.size > 0 && parent.astEntity instanceof AstNamespaceImport) {
return parent.astEntity;
}
}
return undefined;
}

/**
* Adds a new export name to the entity.
*/
26 changes: 22 additions & 4 deletions apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts
Original file line number Diff line number Diff line change
@@ -88,9 +88,18 @@ export class DeclarationReferenceGenerator {
}

private _getNavigationToSymbol(symbol: ts.Symbol): Navigation {
// resolve alias first:
if (symbol.flags & ts.SymbolFlags.Alias) {
symbol = this._collector.typeChecker.getAliasedSymbol(symbol);
}
// namespace always uses ".":
if (symbol.flags & ts.SymbolFlags.Namespace) {
return Navigation.Exports;
}
const declaration: ts.Declaration | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile();
const parent: ts.Symbol | undefined = TypeScriptInternals.getSymbolParent(symbol);
const parent: ts.Symbol | undefined =
TypeScriptInternals.getSymbolParent(symbol) ?? this._collector.getExportingNamespace(symbol);

// If it's global or from an external library, then use either Members or Exports. It's not possible for
// global symbols or external library symbols to be Locals.
@@ -208,7 +217,11 @@ export class DeclarationReferenceGenerator {
followedSymbol = this._collector.typeChecker.getExportSymbolOfSymbol(followedSymbol);
}
if (followedSymbol.flags & ts.SymbolFlags.Alias) {
followedSymbol = this._collector.typeChecker.getAliasedSymbol(followedSymbol);
const nextFollowedSymbol: ts.Symbol = this._collector.typeChecker.getAliasedSymbol(followedSymbol);
// do not follow alias to namespace, as it results in a source file that no longer knows its short name:
if (!(nextFollowedSymbol.flags & ts.SymbolFlags.Namespace)) {
followedSymbol = nextFollowedSymbol;
}
}

if (DeclarationReferenceGenerator._isExternalModuleSymbol(followedSymbol)) {
@@ -266,9 +279,13 @@ export class DeclarationReferenceGenerator {
}

private _getParentReference(symbol: ts.Symbol): DeclarationReference | undefined {
const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
// First case: the symbol is exported via a namespace
const exportingNamespace: ts.Symbol | undefined = this._collector.getExportingNamespace(symbol);
if (exportingNamespace) {
return this._symbolToDeclarationReference(exportingNamespace, exportingNamespace.flags, true);
}

// First, try to find a parent symbol via the symbol tree.
// Next, try to find a parent symbol via the symbol tree.
const parentSymbol: ts.Symbol | undefined = TypeScriptInternals.getSymbolParent(symbol);
if (parentSymbol) {
return this._symbolToDeclarationReference(
@@ -290,6 +307,7 @@ export class DeclarationReferenceGenerator {
//
// In the example above, `SomeType` doesn't have a parent symbol per the TS internal API above,
// but its reference still needs to be qualified with the parent reference for `n`.
const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
const grandParent: ts.Node | undefined = declaration?.parent?.parent;
if (grandParent && ts.isModuleDeclaration(grandParent)) {
const grandParentSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
"docReferences2",
"docReferences3",
"docReferencesAlias",
"docReferencesNamespaceAlias",
"dynamicImportType",
"dynamicImportType2",
"dynamicImportType3",
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
{
"metadata": {
"toolPackage": "@microsoft/api-extractor",
"toolVersion": "[test mode]",
"schemaVersion": 1009,
"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": "@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": "Class",
"canonicalReference": "api-extractor-scenarios!Item:class",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export default class Item "
}
],
"releaseTag": "Public",
"name": "Item",
"preserveMemberOrder": false,
"members": [
{
"kind": "Property",
"canonicalReference": "api-extractor-scenarios!Item#options:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "options: "
},
{
"kind": "Reference",
"text": "Options",
"canonicalReference": "api-extractor-scenarios!renamed.Options:interface"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "options",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isStatic": false,
"isProtected": false
}
],
"implementsTokenRanges": []
},
{
"kind": "Namespace",
"canonicalReference": "api-extractor-scenarios!renamed:namespace",
"docComment": "",
"excerptTokens": [],
"releaseTag": "None",
"name": "renamed",
"preserveMemberOrder": false,
"members": [
{
"kind": "Interface",
"canonicalReference": "api-extractor-scenarios!renamed.Options:interface",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export default interface Options "
}
],
"releaseTag": "Public",
"name": "Options",
"preserveMemberOrder": false,
"members": [
{
"kind": "PropertySignature",
"canonicalReference": "api-extractor-scenarios!renamed.Options#color:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "color: "
},
{
"kind": "Content",
"text": "'red' | 'blue'"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "color",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
},
{
"kind": "PropertySignature",
"canonicalReference": "api-extractor-scenarios!renamed.Options#name:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "name: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "name",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
},
{
"kind": "PropertySignature",
"canonicalReference": "api-extractor-scenarios!renamed.Options#subOptions:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "subOptions: "
},
{
"kind": "Reference",
"text": "SubOptions",
"canonicalReference": "api-extractor-scenarios!renamed.sub.SubOptions:interface"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "subOptions",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
}
],
"extendsTokenRanges": []
},
{
"kind": "Namespace",
"canonicalReference": "api-extractor-scenarios!renamed.sub:namespace",
"docComment": "",
"excerptTokens": [],
"releaseTag": "None",
"name": "sub",
"preserveMemberOrder": false,
"members": [
{
"kind": "Interface",
"canonicalReference": "api-extractor-scenarios!renamed.sub.SubOptions:interface",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export default interface SubOptions "
}
],
"releaseTag": "Public",
"name": "SubOptions",
"preserveMemberOrder": false,
"members": [
{
"kind": "PropertySignature",
"canonicalReference": "api-extractor-scenarios!renamed.sub.SubOptions#count:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "count: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "count",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
}
],
"extendsTokenRanges": []
}
]
}
]
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## 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 class Item {
// (undocumented)
options: Options;
}

// @public (undocumented)
interface Options {
// (undocumented)
color: 'red' | 'blue';
// (undocumented)
name: string;
// (undocumented)
subOptions: SubOptions;
}

declare namespace renamed {
export {
sub,
Options
}
}
export { renamed }

declare namespace sub {
export {
SubOptions
}
}

// @public (undocumented)
interface SubOptions {
// (undocumented)
count: number;
}

// (No @packageDocumentation comment for this package)

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** @public */
export declare class Item {
options: Options;
}

/** @public */
declare interface Options {
name: string;
color: 'red' | 'blue';
subOptions: SubOptions;
}

declare namespace renamed {
export {
sub,
Options
}
}
export { renamed }

declare namespace sub {
export {
SubOptions
}
}

/** @public */
declare interface SubOptions {
count: number;
}

export { }
Original file line number Diff line number Diff line change
@@ -190,13 +190,9 @@
"kind": "Content",
"text": "export declare function exportedApi(): "
},
{
"kind": "Content",
"text": "forgottenNs."
},
{
"kind": "Reference",
"text": "ForgottenClass",
"text": "forgottenNs.ForgottenClass",
"canonicalReference": "api-extractor-scenarios!ForgottenClass:class"
},
{
@@ -206,7 +202,7 @@
],
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
"endIndex": 2
},
"releaseTag": "Public",
"overloadIndex": 1,
Original file line number Diff line number Diff line change
@@ -183,11 +183,11 @@
},
{
"kind": "Content",
"text": "typeof colors."
"text": "typeof "
},
{
"kind": "Reference",
"text": "zebra",
"text": "colors.zebra",
"canonicalReference": "colors!zebra:var"
},
{
Original file line number Diff line number Diff line change
@@ -468,7 +468,7 @@
{
"kind": "Reference",
"text": "ForgottenExport4.ForgottenExport5",
"canonicalReference": "api-extractor-scenarios!~ForgottenExport4.ForgottenExport5:class"
"canonicalReference": "api-extractor-scenarios!ForgottenExport4.ForgottenExport5:class"
},
{
"kind": "Content",
@@ -493,13 +493,9 @@
"kind": "Content",
"text": "export declare function someFunction5(): "
},
{
"kind": "Content",
"text": "internal2."
},
{
"kind": "Reference",
"text": "ForgottenExport6",
"text": "internal2.ForgottenExport6",
"canonicalReference": "api-extractor-scenarios!ForgottenExport6:class"
},
{
@@ -509,7 +505,7 @@
],
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
"endIndex": 2
},
"releaseTag": "Public",
"overloadIndex": 1,
Original file line number Diff line number Diff line change
@@ -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 Options from './renamed/Options';

/** @public */
export default class Item {
options: Options;
}
Original file line number Diff line number Diff line change
@@ -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.

import * as renamed from './renamed';
export { renamed };
export { default as Item } from './Item';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import SubOptions from './sub/SubOptions';

/** @public */
export default interface Options {
name: string;
color: 'red' | 'blue';
subOptions: SubOptions;
}
Original file line number Diff line number Diff line change
@@ -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.

import * as sub from './sub';
export { sub };
export { default as Options } from './Options';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

/** @public */
export default interface SubOptions {
count: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

export { default as SubOptions } from './SubOptions';

0 comments on commit ef387a7

Please sign in to comment.