Skip to content

Commit fb400b6

Browse files
authored
Merge pull request #3602 from fwienber/fwienber/api-extractor-fix-3593-reference-alias
[api-extractor] Fix #3593 Incorrect canonical reference to aliased class in .api.json
2 parents b6bd043 + 1ba1066 commit fb400b6

File tree

29 files changed

+990
-56
lines changed

29 files changed

+990
-56
lines changed

apps/api-extractor/src/analyzer/AstNamespaceImport.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface IAstNamespaceImportOptions {
1111
readonly astModule: AstModule;
1212
readonly namespaceName: string;
1313
readonly declaration: ts.Declaration;
14+
readonly symbol: ts.Symbol;
1415
}
1516

1617
/**
@@ -67,11 +68,17 @@ export class AstNamespaceImport extends AstSyntheticEntity {
6768
*/
6869
public readonly declaration: ts.Declaration;
6970

71+
/**
72+
* The original `ts.SymbolFlags.Namespace` symbol.
73+
*/
74+
public readonly symbol: ts.Symbol;
75+
7076
public constructor(options: IAstNamespaceImportOptions) {
7177
super();
7278
this.astModule = options.astModule;
7379
this.namespaceName = options.namespaceName;
7480
this.declaration = options.declaration;
81+
this.symbol = options.symbol;
7582
}
7683

7784
/** {@inheritdoc} */

apps/api-extractor/src/analyzer/ExportAnalyzer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,8 @@ export class ExportAnalyzer {
626626
namespaceImport = new AstNamespaceImport({
627627
namespaceName: declarationSymbol.name,
628628
astModule: astModule,
629-
declaration: declaration
629+
declaration: declaration,
630+
symbol: declarationSymbol
630631
});
631632
this._astNamespaceImportByModule.set(astModule, namespaceImport);
632633
}

apps/api-extractor/src/collector/Collector.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export class Collector {
8282
AstEntity,
8383
CollectorEntity
8484
>();
85+
private readonly _entitiesBySymbol: Map<ts.Symbol, CollectorEntity> = new Map<ts.Symbol, CollectorEntity>();
8586

8687
private readonly _starExportedExternalModulePaths: string[] = [];
8788

@@ -294,6 +295,14 @@ export class Collector {
294295
return undefined;
295296
}
296297

298+
/**
299+
* For a given analyzed ts.Symbol, return the CollectorEntity that it refers to. Returns undefined if it
300+
* doesn't refer to anything interesting.
301+
*/
302+
public tryGetEntityForSymbol(symbol: ts.Symbol): CollectorEntity | undefined {
303+
return this._entitiesBySymbol.get(symbol);
304+
}
305+
297306
/**
298307
* Returns the associated `CollectorEntity` for the given `astEntity`, if one was created during analysis.
299308
*/
@@ -420,6 +429,11 @@ export class Collector {
420429
entity = new CollectorEntity(astEntity);
421430

422431
this._entitiesByAstEntity.set(astEntity, entity);
432+
if (astEntity instanceof AstSymbol) {
433+
this._entitiesBySymbol.set(astEntity.followedSymbol, entity);
434+
} else if (astEntity instanceof AstNamespaceImport) {
435+
this._entitiesBySymbol.set(astEntity.symbol, entity);
436+
}
423437
this._entities.push(entity);
424438
this._collectReferenceDirectives(astEntity);
425439
}

apps/api-extractor/src/collector/CollectorEntity.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@ export class CollectorEntity {
175175
return false;
176176
}
177177

178+
/**
179+
* Return the first consumable parent that exports this entity. If there is none, returns
180+
* `undefined`.
181+
*/
182+
public getFirstExportingConsumableParent(): CollectorEntity | undefined {
183+
for (const [parent, localExportNames] of this._localExportNamesByParent) {
184+
if (parent.consumable && localExportNames.size > 0) {
185+
return parent;
186+
}
187+
}
188+
return undefined;
189+
}
190+
178191
/**
179192
* Adds a new export name to the entity.
180193
*/

apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers';
1515
import { TypeScriptInternals } from '../analyzer/TypeScriptInternals';
1616
import { Collector } from '../collector/Collector';
1717
import { CollectorEntity } from '../collector/CollectorEntity';
18+
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport';
1819

1920
export class DeclarationReferenceGenerator {
2021
public static readonly unknownReference: string = '?';
@@ -109,34 +110,27 @@ export class DeclarationReferenceGenerator {
109110
return Navigation.Exports;
110111
}
111112

112-
// Otherwise, this symbol is from the current package.
113-
if (parent) {
114-
// If we've found an exported CollectorEntity, then it's exported from the package entry point, so
115-
// use Exports.
116-
const namedDeclaration: ts.DeclarationName | undefined = (
117-
declaration as ts.NamedDeclaration | undefined
118-
)?.name;
119-
if (namedDeclaration && ts.isIdentifier(namedDeclaration)) {
120-
const collectorEntity: CollectorEntity | undefined =
121-
this._collector.tryGetEntityForNode(namedDeclaration);
122-
if (collectorEntity && collectorEntity.exported) {
123-
return Navigation.Exports;
124-
}
125-
}
126-
127-
// If its parent symbol is not a source file, then use either Exports or Members. If the parent symbol
128-
// is a source file, but it wasn't exported from the package entry point (in the check above), then the
129-
// symbol is a local, so fall through below.
130-
if (!DeclarationReferenceGenerator._isExternalModuleSymbol(parent)) {
131-
if (
132-
parent.members &&
133-
DeclarationReferenceGenerator._isSameSymbol(parent.members.get(symbol.escapedName), symbol)
134-
) {
135-
return Navigation.Members;
136-
}
113+
// Otherwise, this symbol is from the current package. If we've found an associated consumable
114+
// `CollectorEntity`, then use Exports. We use `consumable` here instead of `exported` because
115+
// if the symbol is exported from a non-consumable `AstNamespaceImport`, we don't want to use
116+
// Exports. We should use Locals instead.
117+
const entity: CollectorEntity | undefined = this._collector.tryGetEntityForSymbol(symbol);
118+
if (entity?.consumable) {
119+
return Navigation.Exports;
120+
}
137121

138-
return Navigation.Exports;
122+
// If its parent symbol is not a source file, then use either Exports or Members. If the parent symbol
123+
// is a source file, but it wasn't exported from the package entry point (in the check above), then the
124+
// symbol is a local, so fall through below.
125+
if (parent && !DeclarationReferenceGenerator._isExternalModuleSymbol(parent)) {
126+
if (
127+
parent.members &&
128+
DeclarationReferenceGenerator._isSameSymbol(parent.members.get(symbol.escapedName), symbol)
129+
) {
130+
return Navigation.Members;
139131
}
132+
133+
return Navigation.Exports;
140134
}
141135

142136
// Otherwise, we have a local symbol, so use a Locals navigation. These are either:
@@ -209,6 +203,12 @@ export class DeclarationReferenceGenerator {
209203
}
210204
if (followedSymbol.flags & ts.SymbolFlags.Alias) {
211205
followedSymbol = this._collector.typeChecker.getAliasedSymbol(followedSymbol);
206+
207+
// Without this logic, we end up following the symbol `ns` in `import * as ns from './file'` to
208+
// the actual file `file.ts`. We don't want to do this, so revert to the original symbol.
209+
if (followedSymbol.flags & ts.SymbolFlags.ValueModule) {
210+
followedSymbol = symbol;
211+
}
212212
}
213213

214214
if (DeclarationReferenceGenerator._isExternalModuleSymbol(followedSymbol)) {
@@ -229,6 +229,11 @@ export class DeclarationReferenceGenerator {
229229
}
230230

231231
let localName: string = followedSymbol.name;
232+
const entity: CollectorEntity | undefined = this._collector.tryGetEntityForSymbol(followedSymbol);
233+
if (entity?.nameForEmit) {
234+
localName = entity.nameForEmit;
235+
}
236+
232237
if (followedSymbol.escapedName === ts.InternalSymbolName.Constructor) {
233238
localName = 'constructor';
234239
} else {
@@ -267,8 +272,38 @@ export class DeclarationReferenceGenerator {
267272

268273
private _getParentReference(symbol: ts.Symbol): DeclarationReference | undefined {
269274
const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
275+
const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile();
276+
277+
// Note that it's possible for a symbol to be exported from an entry point as well as one or more
278+
// namespaces. In that case, it's not clear what to choose as its parent. Today's logic is neither
279+
// perfect nor particularly stable to API items being renamed and shuffled around.
280+
const entity: CollectorEntity | undefined = this._collector.tryGetEntityForSymbol(symbol);
281+
if (entity) {
282+
if (entity.exportedFromEntryPoint) {
283+
return new DeclarationReference(this._sourceFileToModuleSource(sourceFile));
284+
}
285+
286+
const firstExportingConsumableParent: CollectorEntity | undefined =
287+
entity.getFirstExportingConsumableParent();
288+
if (
289+
firstExportingConsumableParent &&
290+
firstExportingConsumableParent.astEntity instanceof AstNamespaceImport
291+
) {
292+
const parentSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(
293+
firstExportingConsumableParent.astEntity.declaration,
294+
this._collector.typeChecker
295+
);
296+
if (parentSymbol) {
297+
return this._symbolToDeclarationReference(
298+
parentSymbol,
299+
parentSymbol.flags,
300+
/*includeModuleSymbols*/ true
301+
);
302+
}
303+
}
304+
}
270305

271-
// First, try to find a parent symbol via the symbol tree.
306+
// Next, try to find a parent symbol via the symbol tree.
272307
const parentSymbol: ts.Symbol | undefined = TypeScriptInternals.getSymbolParent(symbol);
273308
if (parentSymbol) {
274309
return this._symbolToDeclarationReference(
@@ -306,7 +341,6 @@ export class DeclarationReferenceGenerator {
306341
}
307342

308343
// At this point, we have a local symbol in a module.
309-
const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile();
310344
if (sourceFile && ts.isExternalModule(sourceFile)) {
311345
return new DeclarationReference(this._sourceFileToModuleSource(sourceFile));
312346
} else {

build-tests/api-extractor-scenarios/config/build-config.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"docReferences",
1515
"docReferences2",
1616
"docReferences3",
17+
"docReferencesAlias",
18+
"docReferencesNamespaceAlias",
1719
"dynamicImportType",
1820
"dynamicImportType2",
1921
"dynamicImportType3",

build-tests/api-extractor-scenarios/etc/ambientNameConflict/api-extractor-scenarios.api.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
{
198198
"kind": "Reference",
199199
"text": "MyPromise",
200-
"canonicalReference": "api-extractor-scenarios!~Promise:class"
200+
"canonicalReference": "api-extractor-scenarios!~Promise_2:class"
201201
},
202202
{
203203
"kind": "Content",

0 commit comments

Comments
 (0)