Skip to content

Commit d61f74f

Browse files
committed
Improve DeclarationReferenceGenerator logic to handle more cases of unexported symbols
1 parent 8e270cf commit d61f74f

File tree

14 files changed

+139
-116
lines changed

14 files changed

+139
-116
lines changed

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,7 @@ export class ApiModelGenerator {
5454
public constructor(collector: Collector) {
5555
this._collector = collector;
5656
this._apiModel = new ApiModel();
57-
this._referenceGenerator = new DeclarationReferenceGenerator(
58-
collector.packageJsonLookup,
59-
collector.workingPackage.name,
60-
collector.program,
61-
collector.typeChecker,
62-
collector.bundledPackageNames
63-
);
57+
this._referenceGenerator = new DeclarationReferenceGenerator(collector);
6458
}
6559

6660
public get apiModel(): ApiModel {

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

Lines changed: 60 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,26 @@ import {
1010
Navigation,
1111
Meaning
1212
} from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference';
13-
import { PackageJsonLookup, INodePackageJson, InternalError } from '@rushstack/node-core-library';
13+
import { INodePackageJson, InternalError } from '@rushstack/node-core-library';
1414
import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers';
1515
import { TypeScriptInternals } from '../analyzer/TypeScriptInternals';
16+
import { Collector } from '../collector/Collector';
17+
import { CollectorEntity } from '../collector/CollectorEntity';
1618

1719
export class DeclarationReferenceGenerator {
1820
public static readonly unknownReference: string = '?';
1921

20-
private _packageJsonLookup: PackageJsonLookup;
21-
private _workingPackageName: string;
22-
private _program: ts.Program;
23-
private _typeChecker: ts.TypeChecker;
24-
private _bundledPackageNames: ReadonlySet<string>;
25-
26-
public constructor(
27-
packageJsonLookup: PackageJsonLookup,
28-
workingPackageName: string,
29-
program: ts.Program,
30-
typeChecker: ts.TypeChecker,
31-
bundledPackageNames: ReadonlySet<string>
32-
) {
33-
this._packageJsonLookup = packageJsonLookup;
34-
this._workingPackageName = workingPackageName;
35-
this._program = program;
36-
this._typeChecker = typeChecker;
37-
this._bundledPackageNames = bundledPackageNames;
22+
private _collector: Collector;
23+
24+
public constructor(collector: Collector) {
25+
this._collector = collector;
3826
}
3927

4028
/**
4129
* Gets the UID for a TypeScript Identifier that references a type.
4230
*/
4331
public getDeclarationReferenceForIdentifier(node: ts.Identifier): DeclarationReference | undefined {
44-
const symbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(node);
32+
const symbol: ts.Symbol | undefined = this._collector.typeChecker.getSymbolAtLocation(node);
4533
if (symbol !== undefined) {
4634
const isExpression: boolean = DeclarationReferenceGenerator._isInExpressionContext(node);
4735
return (
@@ -99,68 +87,48 @@ export class DeclarationReferenceGenerator {
9987
);
10088
}
10189

102-
private static _getNavigationToSymbol(symbol: ts.Symbol): Navigation | 'global' {
90+
private _getNavigationToSymbol(symbol: ts.Symbol): Navigation {
91+
const declaration: ts.Declaration | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
92+
const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile();
10393
const parent: ts.Symbol | undefined = TypeScriptInternals.getSymbolParent(symbol);
104-
// First, try to determine navigation to symbol via its parent.
105-
if (parent) {
106-
if (
107-
parent.exports &&
108-
DeclarationReferenceGenerator._isSameSymbol(parent.exports.get(symbol.escapedName), symbol)
109-
) {
110-
return Navigation.Exports;
111-
}
112-
if (
113-
parent.members &&
114-
DeclarationReferenceGenerator._isSameSymbol(parent.members.get(symbol.escapedName), symbol)
115-
) {
116-
return Navigation.Members;
117-
}
118-
if (
119-
parent.globalExports &&
120-
DeclarationReferenceGenerator._isSameSymbol(parent.globalExports.get(symbol.escapedName), symbol)
121-
) {
122-
return 'global';
123-
}
94+
95+
// If it's a global, then use an Exports navigation.
96+
if (sourceFile && !ts.isExternalModule(sourceFile)) {
97+
return Navigation.Exports;
12498
}
12599

126-
// Next, try determining navigation to symbol by its node
127-
if (symbol.valueDeclaration) {
128-
const declaration: ts.Declaration = ts.isBindingElement(symbol.valueDeclaration)
129-
? ts.walkUpBindingElementsAndPatterns(symbol.valueDeclaration)
130-
: symbol.valueDeclaration;
131-
if (ts.isClassElement(declaration) && ts.isClassLike(declaration.parent)) {
132-
// class members are an "export" if they have the static modifier.
133-
return ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Static
134-
? Navigation.Exports
135-
: Navigation.Members;
136-
}
137-
if (ts.isTypeElement(declaration) || ts.isObjectLiteralElement(declaration)) {
138-
// type and object literal element members are just members
139-
return Navigation.Members;
140-
}
141-
if (ts.isEnumMember(declaration)) {
142-
// enum members are exports
143-
return Navigation.Exports;
144-
}
145-
if (
146-
ts.isExportSpecifier(declaration) ||
147-
ts.isExportAssignment(declaration) ||
148-
ts.isExportSpecifier(declaration) ||
149-
ts.isExportDeclaration(declaration) ||
150-
ts.isNamedExports(declaration)
151-
) {
152-
return Navigation.Exports;
100+
// If it's from an external library, then use an Exports navigation.
101+
if (sourceFile && this._collector.program.isSourceFileFromExternalLibrary(sourceFile)) {
102+
return Navigation.Exports;
103+
}
104+
105+
// Otherwise, this symbol is from the current package.
106+
if (parent) {
107+
// If we've found an exported CollectorEntity, then it's exported from the package entry point, so
108+
// use an Exports navigation.
109+
const namedDeclaration: ts.DeclarationName | undefined = (
110+
declaration as ts.NamedDeclaration | undefined
111+
)?.name;
112+
if (namedDeclaration && ts.isIdentifier(namedDeclaration)) {
113+
const collectorEntity: CollectorEntity | undefined =
114+
this._collector.tryGetEntityForNode(namedDeclaration);
115+
if (collectorEntity && collectorEntity.exported) {
116+
return Navigation.Exports;
117+
}
153118
}
154-
// declarations are exports if they have an `export` modifier.
155-
if (ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export) {
119+
120+
// If its parent symbol is not a source file, then use an Exports navigation. If the parent symbol is
121+
// a source file, but it wasn't exported from the package entry point (in the check above), then the symbol
122+
// is a local, so fall through below.
123+
if (!DeclarationReferenceGenerator._isExternalModuleSymbol(parent)) {
156124
return Navigation.Exports;
157125
}
158-
if (ts.isSourceFile(declaration.parent) && !ts.isExternalModule(declaration.parent)) {
159-
// declarations in a source file are global if the source file is not a module.
160-
return 'global';
161-
}
162126
}
163-
// all other declarations are locals
127+
128+
// Otherwise, we have a local symbol, so use a Locals navigation. These are either:
129+
//
130+
// 1. Symbols that are exported from a file module but not the package entry point.
131+
// 2. Symbols that are not exported from their parent module.
164132
return Navigation.Locals;
165133
}
166134

@@ -218,20 +186,21 @@ export class DeclarationReferenceGenerator {
218186
meaning: ts.SymbolFlags,
219187
includeModuleSymbols: boolean
220188
): DeclarationReference | undefined {
189+
const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
190+
const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile();
191+
221192
let followedSymbol: ts.Symbol = symbol;
222193
if (followedSymbol.flags & ts.SymbolFlags.ExportValue) {
223-
followedSymbol = this._typeChecker.getExportSymbolOfSymbol(followedSymbol);
194+
followedSymbol = this._collector.typeChecker.getExportSymbolOfSymbol(followedSymbol);
224195
}
225196
if (followedSymbol.flags & ts.SymbolFlags.Alias) {
226-
followedSymbol = this._typeChecker.getAliasedSymbol(followedSymbol);
197+
followedSymbol = this._collector.typeChecker.getAliasedSymbol(followedSymbol);
227198
}
228199

229200
if (DeclarationReferenceGenerator._isExternalModuleSymbol(followedSymbol)) {
230201
if (!includeModuleSymbols) {
231202
return undefined;
232203
}
233-
const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
234-
const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile();
235204
return new DeclarationReference(this._sourceFileToModuleSource(sourceFile));
236205
}
237206

@@ -270,13 +239,11 @@ export class DeclarationReferenceGenerator {
270239
}
271240
}
272241

273-
let navigation: Navigation | 'global' =
274-
DeclarationReferenceGenerator._getNavigationToSymbol(followedSymbol);
275-
if (navigation === 'global') {
276-
if (parentRef.source !== GlobalSource.instance) {
277-
parentRef = new DeclarationReference(GlobalSource.instance);
278-
}
279-
navigation = Navigation.Exports;
242+
const navigation: Navigation = this._getNavigationToSymbol(followedSymbol);
243+
244+
// If the symbol is a global, ensure the source is global.
245+
if (sourceFile && !ts.isExternalModule(sourceFile) && parentRef.source !== GlobalSource.instance) {
246+
parentRef = new DeclarationReference(GlobalSource.instance);
280247
}
281248

282249
return parentRef
@@ -313,7 +280,7 @@ export class DeclarationReferenceGenerator {
313280
if (grandParent && ts.isModuleDeclaration(grandParent)) {
314281
const grandParentSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(
315282
grandParent,
316-
this._typeChecker
283+
this._collector.typeChecker
317284
);
318285
if (grandParentSymbol) {
319286
return this._symbolToDeclarationReference(
@@ -334,28 +301,27 @@ export class DeclarationReferenceGenerator {
334301
}
335302

336303
private _getPackageName(sourceFile: ts.SourceFile): string {
337-
if (this._program.isSourceFileFromExternalLibrary(sourceFile)) {
338-
const packageJson: INodePackageJson | undefined = this._packageJsonLookup.tryLoadNodePackageJsonFor(
339-
sourceFile.fileName
340-
);
304+
if (this._collector.program.isSourceFileFromExternalLibrary(sourceFile)) {
305+
const packageJson: INodePackageJson | undefined =
306+
this._collector.packageJsonLookup.tryLoadNodePackageJsonFor(sourceFile.fileName);
341307

342308
if (packageJson && packageJson.name) {
343309
return packageJson.name;
344310
}
345311
return DeclarationReferenceGenerator.unknownReference;
346312
}
347-
return this._workingPackageName;
313+
return this._collector.workingPackage.name;
348314
}
349315

350316
private _sourceFileToModuleSource(sourceFile: ts.SourceFile | undefined): GlobalSource | ModuleSource {
351317
if (sourceFile && ts.isExternalModule(sourceFile)) {
352318
const packageName: string = this._getPackageName(sourceFile);
353319

354-
if (this._bundledPackageNames.has(packageName)) {
320+
if (this._collector.bundledPackageNames.has(packageName)) {
355321
// The api-extractor.json config file has a "bundledPackages" setting, which causes imports from
356322
// certain NPM packages to be treated as part of the working project. In this case, we need to
357323
// substitute the working package name.
358-
return new ModuleSource(this._workingPackageName);
324+
return new ModuleSource(this._collector.workingPackage.name);
359325
} else {
360326
return new ModuleSource(packageName);
361327
}

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:class"
201201
},
202202
{
203203
"kind": "Content",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@
304304
{
305305
"kind": "Reference",
306306
"text": "Options",
307-
"canonicalReference": "api-extractor-scenarios!Options:interface"
307+
"canonicalReference": "api-extractor-scenarios!~Options:interface"
308308
},
309309
{
310310
"kind": "Content",

build-tests/api-extractor-scenarios/etc/exportImportStarAs2/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": "ForgottenClass",
200-
"canonicalReference": "api-extractor-scenarios!ForgottenClass:class"
200+
"canonicalReference": "api-extractor-scenarios!~ForgottenClass:class"
201201
},
202202
{
203203
"kind": "Content",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@
184184
{
185185
"kind": "Reference",
186186
"text": "Base",
187-
"canonicalReference": "api-extractor-lib2-test!~DefaultClass:class"
187+
"canonicalReference": "api-extractor-lib2-test!DefaultClass:class"
188188
},
189189
{
190190
"kind": "Content",

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@
202202
{
203203
"kind": "Reference",
204204
"text": "default",
205-
"canonicalReference": "api-extractor-lib2-test!~DefaultClass:class"
205+
"canonicalReference": "api-extractor-lib2-test!DefaultClass:class"
206206
},
207207
{
208208
"kind": "Content",
@@ -230,7 +230,7 @@
230230
{
231231
"kind": "Reference",
232232
"text": "DefaultClass_namedImport",
233-
"canonicalReference": "api-extractor-lib2-test!~DefaultClass:class"
233+
"canonicalReference": "api-extractor-lib2-test!DefaultClass:class"
234234
},
235235
{
236236
"kind": "Content",
@@ -258,7 +258,7 @@
258258
{
259259
"kind": "Reference",
260260
"text": "DefaultClass_reExport",
261-
"canonicalReference": "api-extractor-lib2-test!~DefaultClass:class"
261+
"canonicalReference": "api-extractor-lib2-test!DefaultClass:class"
262262
},
263263
{
264264
"kind": "Content",

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,35 @@
454454
},
455455
"implementsTokenRanges": []
456456
},
457+
{
458+
"kind": "Class",
459+
"canonicalReference": "api-extractor-scenarios!SomeClass4:class",
460+
"docComment": "/**\n * Reference to a symbol exported from another file, but not exported from the package.\n *\n * @public\n */\n",
461+
"excerptTokens": [
462+
{
463+
"kind": "Content",
464+
"text": "export declare class SomeClass4 extends "
465+
},
466+
{
467+
"kind": "Reference",
468+
"text": "SomeClass5",
469+
"canonicalReference": "api-extractor-scenarios!~SomeClass5:class"
470+
},
471+
{
472+
"kind": "Content",
473+
"text": " "
474+
}
475+
],
476+
"releaseTag": "Public",
477+
"name": "SomeClass4",
478+
"preserveMemberOrder": false,
479+
"members": [],
480+
"extendsTokenRange": {
481+
"startIndex": 1,
482+
"endIndex": 2
483+
},
484+
"implementsTokenRanges": []
485+
},
457486
{
458487
"kind": "Enum",
459488
"canonicalReference": "api-extractor-scenarios!SomeEnum:enum",

build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export class SomeClass1 {
4242
export class SomeClass3 extends SomeClass2 {
4343
}
4444

45+
// Warning: (ae-forgotten-export) The symbol "SomeClass5" needs to be exported by the entry point index.d.ts
46+
//
47+
// @public
48+
export class SomeClass4 extends SomeClass5 {
49+
}
50+
4551
// @public (undocumented)
4652
export enum SomeEnum {
4753
// (undocumented)

build-tests/api-extractor-scenarios/etc/referenceTokens/rollup.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ declare class SomeClass2 {
3434
export declare class SomeClass3 extends SomeClass2 {
3535
}
3636

37+
/**
38+
* Reference to a symbol exported from another file, but not exported from the package.
39+
* @public
40+
*/
41+
export declare class SomeClass4 extends SomeClass5 {
42+
}
43+
44+
declare class SomeClass5 {
45+
}
46+
3747
/** @public */
3848
export declare enum SomeEnum {
3949
A = "A",

0 commit comments

Comments
 (0)