Skip to content

Commit c5e13a0

Browse files
authored
Merge pull request microsoft#1305 from rbuckton/supportSymbolMembers
[api-extractor] Improved support for symbolic properties and methods
2 parents 05bc20e + b52b568 commit c5e13a0

10 files changed

Lines changed: 171 additions & 15 deletions

File tree

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

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { PackageMetadataManager } from './PackageMetadataManager';
1212
import { ExportAnalyzer } from './ExportAnalyzer';
1313
import { AstImport } from './AstImport';
1414
import { MessageRouter } from '../collector/MessageRouter';
15+
import { TypeScriptInternals } from './TypeScriptInternals';
1516

1617
export type AstEntity = AstSymbol | AstImport;
1718

@@ -302,7 +303,8 @@ export class AstSymbolTable {
302303
return undefined;
303304
}
304305

305-
const symbol: ts.Symbol | undefined = TypeScriptHelpers.getSymbolForDeclaration(node as ts.Declaration);
306+
const symbol: ts.Symbol | undefined = TypeScriptHelpers.getSymbolForDeclaration(node as ts.Declaration,
307+
this._typeChecker);
306308
if (!symbol) {
307309
throw new InternalError('Unable to find symbol for node');
308310
}
@@ -338,7 +340,8 @@ export class AstSymbolTable {
338340
const arbitraryDeclaration: ts.Declaration = followedSymbol.declarations[0];
339341

340342
// tslint:disable-next-line:no-bitwise
341-
if (followedSymbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient)) {
343+
if (followedSymbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient)
344+
&& !TypeScriptInternals.isLateBoundSymbol(followedSymbol)) {
342345
return undefined;
343346
}
344347

@@ -406,7 +409,8 @@ export class AstSymbolTable {
406409

407410
if (arbitraryParentDeclaration) {
408411
const parentSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(
409-
arbitraryParentDeclaration as ts.Declaration);
412+
arbitraryParentDeclaration as ts.Declaration,
413+
this._typeChecker);
410414

411415
parentAstSymbol = this._fetchAstSymbol({
412416
followedSymbol: parentSymbol,
@@ -428,11 +432,25 @@ export class AstSymbolTable {
428432
// This handles cases such as "export default class X { }" where the symbol name is "default"
429433
// but the declaration name is "X".
430434
localName = followedSymbol.name;
431-
for (const declaration of followedSymbol.declarations || []) {
432-
const declarationNameIdentifier: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration);
433-
if (declarationNameIdentifier && ts.isIdentifier(declarationNameIdentifier)) {
434-
localName = declarationNameIdentifier.getText().trim();
435-
break;
435+
if (TypeScriptHelpers.isWellKnownSymbolName(localName)) {
436+
// TypeScript binds well-known ECMAScript symbols like "Symbol.iterator" as "__@iterator".
437+
// This converts a string like "__@iterator" into the property name "[Symbol.iterator]".
438+
localName = `[Symbol.${localName.slice(3)}]`;
439+
} else {
440+
const isUniqueSymbol: boolean = TypeScriptHelpers.isUniqueSymbolName(localName);
441+
for (const declaration of followedSymbol.declarations || []) {
442+
const declarationName: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration);
443+
if (declarationName && ts.isIdentifier(declarationName)) {
444+
localName = declarationName.getText().trim();
445+
break;
446+
}
447+
if (isUniqueSymbol && declarationName && ts.isComputedPropertyName(declarationName)) {
448+
const lateBoundName: string | undefined = TypeScriptHelpers.tryGetLateBoundName(declarationName);
449+
if (lateBoundName) {
450+
localName = lateBoundName;
451+
break;
452+
}
453+
}
436454
}
437455
}
438456
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ export class ExportAnalyzer {
171171
private _getModuleSymbolFromSourceFile(sourceFile: ts.SourceFile,
172172
moduleReference: IAstModuleReference | undefined): ts.Symbol {
173173

174-
const moduleSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(sourceFile);
174+
const moduleSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(sourceFile,
175+
this._typeChecker);
175176
if (moduleSymbol !== undefined) {
176177
// This is the normal case. The SourceFile acts is a module and has a symbol.
177178
return moduleSymbol;

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ export class TypeScriptHelpers {
9696
* Same semantics as tryGetSymbolForDeclaration(), but throws an exception if the symbol
9797
* cannot be found.
9898
*/
99-
public static getSymbolForDeclaration(declaration: ts.Declaration): ts.Symbol {
100-
const symbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(declaration);
99+
public static getSymbolForDeclaration(declaration: ts.Declaration, checker: ts.TypeChecker): ts.Symbol {
100+
const symbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(declaration, checker);
101101
if (!symbol) {
102102
throw new Error(TypeScriptMessageFormatter.formatFileAndLineNumber(declaration) + ': '
103103
+ 'Unable to determine semantic information for this declaration');
@@ -209,4 +209,48 @@ export class TypeScriptHelpers {
209209

210210
return highest;
211211
}
212+
213+
// Matches TypeScript's encoded names for well-known ECMAScript symbols like
214+
// "__@iterator" or "__@toStringTag".
215+
private static readonly _wellKnownSymbolNameRegExp: RegExp = /^__@\w+$/;
216+
217+
/**
218+
* Returns whether the provided name was generated for a built-in ECMAScript symbol.
219+
*/
220+
public static isWellKnownSymbolName(name: string): boolean {
221+
return TypeScriptHelpers._wellKnownSymbolNameRegExp.test(name);
222+
}
223+
224+
// Matches TypeScript's encoded names for late-bound symbols derived from `unique symbol` declarations
225+
// which have the form of "__@<variableName>@<symbolId>", i.e. "__@someSymbol@12345".
226+
private static readonly _uniqueSymbolNameRegExp: RegExp = /^__@.*@\d+$/;
227+
228+
/**
229+
* Returns whether the provided name was generated for a TypeScript `unique symbol`.
230+
*/
231+
public static isUniqueSymbolName(name: string): boolean {
232+
return TypeScriptHelpers._uniqueSymbolNameRegExp.test(name);
233+
}
234+
235+
/**
236+
* Derives the string representation of a TypeScript late-bound symbol.
237+
*/
238+
public static tryGetLateBoundName(declarationName: ts.ComputedPropertyName): string | undefined {
239+
// Create a node printer that ignores comments and indentation that we can use to convert
240+
// declarationName to a string.
241+
const printer: ts.Printer = ts.createPrinter({ removeComments: true }, {
242+
onEmitNode(hint: ts.EmitHint, node: ts.Node | undefined,
243+
emit: (hint: ts.EmitHint, node: ts.Node | undefined) => void): void {
244+
if (node) {
245+
ts.setEmitFlags(declarationName, ts.EmitFlags.NoIndentation | ts.EmitFlags.SingleLine);
246+
}
247+
emit(hint, node);
248+
}
249+
});
250+
const sourceFile: ts.SourceFile = declarationName.getSourceFile();
251+
const text: string = printer.printNode(ts.EmitHint.Unspecified, declarationName, sourceFile);
252+
// clean up any emit flags we've set on any nodes in the tree.
253+
ts.disposeEmitNodes(sourceFile);
254+
return text;
255+
}
212256
}

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,29 @@ export class TypeScriptInternals {
2121
* @returns The associated Symbol. If there is no semantic information (e.g. if the
2222
* declaration is an extra semicolon somewhere), then "undefined" is returned.
2323
*/
24-
public static tryGetSymbolForDeclaration(declaration: ts.Declaration): ts.Symbol | undefined {
25-
const symbol: ts.Symbol = (declaration as any).symbol;
24+
public static tryGetSymbolForDeclaration(declaration: ts.Declaration, checker: ts.TypeChecker): ts.Symbol
25+
| undefined {
26+
let symbol: ts.Symbol | undefined = (declaration as any).symbol;
27+
if (symbol && symbol.escapedName === ts.InternalSymbolName.Computed) {
28+
const name: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration);
29+
symbol = name && checker.getSymbolAtLocation(name) || symbol;
30+
}
2631
return symbol;
2732
}
2833

34+
/**
35+
* Returns whether the provided Symbol is a TypeScript "late-bound" Symbol (i.e. was created by the Checker
36+
* for a computed property based on its type, rather than by the Binder).
37+
*/
38+
public static isLateBoundSymbol(symbol: ts.Symbol): boolean {
39+
// tslint:disable-next-line:no-bitwise
40+
if (symbol.flags & ts.SymbolFlags.Transient &&
41+
(symbol as any).checkFlags === (ts as any).CheckFlags.Late) {
42+
return true;
43+
}
44+
return false;
45+
}
46+
2947
/**
3048
* Retrieves the comment ranges associated with the specified node.
3149
*/

build-tests/api-extractor-test-01/dist/api-extractor-test-01-beta.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
/// <reference types="jest" />
13+
/// <reference lib="es2015.symbol.wellknown" />
1314
/// <reference lib="es2018.intl" />
1415
import Long from 'long';
1516
import { MAX_UNSIGNED_VALUE } from 'long';
@@ -67,6 +68,13 @@ export declare class AmbientConsumer {
6768
localTypings(): IAmbientInterfaceExample;
6869
}
6970

71+
/** @public */
72+
declare namespace ANamespace {
73+
const locallyExportedCustomSymbol: unique symbol;
74+
/** @public */
75+
const fullyExportedCustomSymbol: unique symbol;
76+
}
77+
7078
/**
7179
* Referenced by DefaultExportEdgeCaseReferencer.
7280
* @public
@@ -110,6 +118,9 @@ export declare class ClassWithSymbols {
110118
readonly [unexportedCustomSymbol]: number;
111119
readonly [locallyExportedCustomSymbol]: string;
112120
[fullyExportedCustomSymbol](): void;
121+
readonly [ANamespace.locallyExportedCustomSymbol]: string;
122+
[ANamespace.fullyExportedCustomSymbol](): void;
123+
readonly [Symbol.toStringTag]: string;
113124
}
114125

115126
/**

build-tests/api-extractor-test-01/dist/api-extractor-test-01-public.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
/// <reference types="jest" />
13+
/// <reference lib="es2015.symbol.wellknown" />
1314
/// <reference lib="es2018.intl" />
1415
import Long from 'long';
1516
import { MAX_UNSIGNED_VALUE } from 'long';
@@ -67,6 +68,13 @@ export declare class AmbientConsumer {
6768
localTypings(): IAmbientInterfaceExample;
6869
}
6970

71+
/** @public */
72+
declare namespace ANamespace {
73+
const locallyExportedCustomSymbol: unique symbol;
74+
/** @public */
75+
const fullyExportedCustomSymbol: unique symbol;
76+
}
77+
7078
/**
7179
* Referenced by DefaultExportEdgeCaseReferencer.
7280
* @public
@@ -110,6 +118,9 @@ export declare class ClassWithSymbols {
110118
readonly [unexportedCustomSymbol]: number;
111119
readonly [locallyExportedCustomSymbol]: string;
112120
[fullyExportedCustomSymbol](): void;
121+
readonly [ANamespace.locallyExportedCustomSymbol]: string;
122+
[ANamespace.fullyExportedCustomSymbol](): void;
123+
readonly [Symbol.toStringTag]: string;
113124
}
114125

115126
/**

build-tests/api-extractor-test-01/dist/api-extractor-test-01.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
/// <reference types="jest" />
13+
/// <reference lib="es2015.symbol.wellknown" />
1314
/// <reference lib="es2018.intl" />
1415
import Long from 'long';
1516
import { MAX_UNSIGNED_VALUE } from 'long';
@@ -67,6 +68,13 @@ export declare class AmbientConsumer {
6768
localTypings(): IAmbientInterfaceExample;
6869
}
6970

71+
/** @public */
72+
declare namespace ANamespace {
73+
const locallyExportedCustomSymbol: unique symbol;
74+
/** @public */
75+
const fullyExportedCustomSymbol: unique symbol;
76+
}
77+
7078
/**
7179
* Referenced by DefaultExportEdgeCaseReferencer.
7280
* @public
@@ -110,6 +118,9 @@ export declare class ClassWithSymbols {
110118
readonly [unexportedCustomSymbol]: number;
111119
readonly [locallyExportedCustomSymbol]: string;
112120
[fullyExportedCustomSymbol](): void;
121+
readonly [ANamespace.locallyExportedCustomSymbol]: string;
122+
[ANamespace.fullyExportedCustomSymbol](): void;
123+
readonly [Symbol.toStringTag]: string;
113124
}
114125

115126
/**

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,19 @@ export class ClassWithAccessModifiers {
4949
// @public (undocumented)
5050
export class ClassWithSymbols {
5151
// (undocumented)
52-
readonly [unexportedCustomSymbol]: number;
52+
[ANamespace.fullyExportedCustomSymbol](): void;
53+
// Warning: (ae-forgotten-export) The symbol "ANamespace" needs to be exported by the entry point index.d.ts
54+
//
5355
// (undocumented)
54-
readonly [locallyExportedCustomSymbol]: string;
56+
readonly [ANamespace.locallyExportedCustomSymbol]: string;
5557
// (undocumented)
5658
[fullyExportedCustomSymbol](): void;
59+
// (undocumented)
60+
readonly [locallyExportedCustomSymbol]: string;
61+
// (undocumented)
62+
readonly [Symbol.toStringTag]: string;
63+
// (undocumented)
64+
readonly [unexportedCustomSymbol]: number;
5765
}
5866

5967
// @public

build-tests/api-extractor-test-01/src/EcmaScriptSymbols.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4+
/// <reference lib="es2015.symbol.wellknown" />
5+
46
const unexportedCustomSymbol: unique symbol = Symbol('unexportedCustomSymbol');
57
export const locallyExportedCustomSymbol: unique symbol = Symbol('locallyExportedCustomSymbol');
68

79
/** @public */
810
export const fullyExportedCustomSymbol: unique symbol = Symbol('fullyExportedCustomSymbol');
911

12+
// NOTE: named 'ANamespace' so that it appears earlier in the rollup .d.ts file, due to
13+
// https://github.com/microsoft/TypeScript/issues/31746
14+
/** @public */
15+
export namespace ANamespace {
16+
export const locallyExportedCustomSymbol: unique symbol = Symbol('locallyExportedCustomSymbol');
17+
18+
/** @public */
19+
export const fullyExportedCustomSymbol: unique symbol = Symbol('fullyExportedCustomSymbol');
20+
}
21+
1022
/**
1123
* @public
1224
*/
@@ -19,4 +31,15 @@ export class ClassWithSymbols {
1931

2032
public [fullyExportedCustomSymbol](): void {
2133
}
34+
35+
public get [ANamespace.locallyExportedCustomSymbol](): string {
36+
return 'hello';
37+
}
38+
39+
public [ANamespace.fullyExportedCustomSymbol](): void {
40+
}
41+
42+
public get [Symbol.toStringTag](): string {
43+
return "ClassWithSymbols";
44+
}
2245
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/api-extractor",
5+
"comment": "Improve handling of symbolic property and method names.",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@microsoft/api-extractor",
10+
"email": "ron.buckton@microsoft.com"
11+
}

0 commit comments

Comments
 (0)