Skip to content

Commit 4de2adf

Browse files
authored
Merge pull request #3584 from zelliott/refs
[api-extractor] Fix incorrect declaration references for symbols not exported from the package's entry point
2 parents e434009 + 7f2b01a commit 4de2adf

File tree

16 files changed

+346
-123
lines changed

16 files changed

+346
-123
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: 68 additions & 88 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,62 @@ 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-
}
94+
95+
// If it's global or from an external library, then use either Members or Exports. It's not possible for
96+
// global symbols or external library symbols to be Locals.
97+
const isGlobal: boolean = !!sourceFile && !ts.isExternalModule(sourceFile);
98+
const isFromExternalLibrary: boolean =
99+
!!sourceFile && this._collector.program.isSourceFileFromExternalLibrary(sourceFile);
100+
if (isGlobal || isFromExternalLibrary) {
112101
if (
102+
parent &&
113103
parent.members &&
114104
DeclarationReferenceGenerator._isSameSymbol(parent.members.get(symbol.escapedName), symbol)
115105
) {
116106
return Navigation.Members;
117107
}
118-
if (
119-
parent.globalExports &&
120-
DeclarationReferenceGenerator._isSameSymbol(parent.globalExports.get(symbol.escapedName), symbol)
121-
) {
122-
return 'global';
123-
}
108+
109+
return Navigation.Exports;
124110
}
125111

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;
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+
}
153125
}
154-
// declarations are exports if they have an `export` modifier.
155-
if (ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export) {
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+
}
137+
156138
return Navigation.Exports;
157139
}
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-
}
162140
}
163-
// all other declarations are locals
141+
142+
// Otherwise, we have a local symbol, so use a Locals navigation. These are either:
143+
//
144+
// 1. Symbols that are exported from a file module but not the package entry point.
145+
// 2. Symbols that are not exported from their parent module.
164146
return Navigation.Locals;
165147
}
166148

@@ -218,20 +200,21 @@ export class DeclarationReferenceGenerator {
218200
meaning: ts.SymbolFlags,
219201
includeModuleSymbols: boolean
220202
): DeclarationReference | undefined {
203+
const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
204+
const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile();
205+
221206
let followedSymbol: ts.Symbol = symbol;
222207
if (followedSymbol.flags & ts.SymbolFlags.ExportValue) {
223-
followedSymbol = this._typeChecker.getExportSymbolOfSymbol(followedSymbol);
208+
followedSymbol = this._collector.typeChecker.getExportSymbolOfSymbol(followedSymbol);
224209
}
225210
if (followedSymbol.flags & ts.SymbolFlags.Alias) {
226-
followedSymbol = this._typeChecker.getAliasedSymbol(followedSymbol);
211+
followedSymbol = this._collector.typeChecker.getAliasedSymbol(followedSymbol);
227212
}
228213

229214
if (DeclarationReferenceGenerator._isExternalModuleSymbol(followedSymbol)) {
230215
if (!includeModuleSymbols) {
231216
return undefined;
232217
}
233-
const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol);
234-
const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile();
235218
return new DeclarationReference(this._sourceFileToModuleSource(sourceFile));
236219
}
237220

@@ -270,13 +253,11 @@ export class DeclarationReferenceGenerator {
270253
}
271254
}
272255

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;
256+
const navigation: Navigation = this._getNavigationToSymbol(followedSymbol);
257+
258+
// If the symbol is a global, ensure the source is global.
259+
if (sourceFile && !ts.isExternalModule(sourceFile) && parentRef.source !== GlobalSource.instance) {
260+
parentRef = new DeclarationReference(GlobalSource.instance);
280261
}
281262

282263
return parentRef
@@ -313,7 +294,7 @@ export class DeclarationReferenceGenerator {
313294
if (grandParent && ts.isModuleDeclaration(grandParent)) {
314295
const grandParentSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(
315296
grandParent,
316-
this._typeChecker
297+
this._collector.typeChecker
317298
);
318299
if (grandParentSymbol) {
319300
return this._symbolToDeclarationReference(
@@ -334,28 +315,27 @@ export class DeclarationReferenceGenerator {
334315
}
335316

336317
private _getPackageName(sourceFile: ts.SourceFile): string {
337-
if (this._program.isSourceFileFromExternalLibrary(sourceFile)) {
338-
const packageJson: INodePackageJson | undefined = this._packageJsonLookup.tryLoadNodePackageJsonFor(
339-
sourceFile.fileName
340-
);
318+
if (this._collector.program.isSourceFileFromExternalLibrary(sourceFile)) {
319+
const packageJson: INodePackageJson | undefined =
320+
this._collector.packageJsonLookup.tryLoadNodePackageJsonFor(sourceFile.fileName);
341321

342322
if (packageJson && packageJson.name) {
343323
return packageJson.name;
344324
}
345325
return DeclarationReferenceGenerator.unknownReference;
346326
}
347-
return this._workingPackageName;
327+
return this._collector.workingPackage.name;
348328
}
349329

350330
private _sourceFileToModuleSource(sourceFile: ts.SourceFile | undefined): GlobalSource | ModuleSource {
351331
if (sourceFile && ts.isExternalModule(sourceFile)) {
352332
const packageName: string = this._getPackageName(sourceFile);
353333

354-
if (this._bundledPackageNames.has(packageName)) {
334+
if (this._collector.bundledPackageNames.has(packageName)) {
355335
// The api-extractor.json config file has a "bundledPackages" setting, which causes imports from
356336
// certain NPM packages to be treated as part of the working project. In this case, we need to
357337
// substitute the working package name.
358-
return new ModuleSource(this._workingPackageName);
338+
return new ModuleSource(this._collector.workingPackage.name);
359339
} else {
360340
return new ModuleSource(packageName);
361341
}

build-tests/api-extractor-lib2-test/etc/api-extractor-lib2-test.api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
// @public (undocumented)
88
class DefaultClass {
99
}
10-
1110
export default DefaultClass;
1211

1312
// @public (undocumented)
1413
export class Lib2Class {
14+
// (undocumented)
15+
prop: number;
1516
}
1617

1718
// @alpha (undocumented)
1819
export interface Lib2Interface {
1920
}
2021

21-
2222
```

build-tests/api-extractor-lib2-test/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
*/
1212

1313
/** @public */
14-
export class Lib2Class {}
14+
export class Lib2Class {
15+
prop: number;
16+
}
1517

1618
/** @alpha */
1719
export interface Lib2Interface {}

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",

0 commit comments

Comments
 (0)