@@ -13,6 +13,7 @@ import { ExportAnalyzer } from './ExportAnalyzer';
1313import { AstImport } from './AstImport' ;
1414import { MessageRouter } from '../collector/MessageRouter' ;
1515import { TypeScriptInternals } from './TypeScriptInternals' ;
16+ import { StringChecks } from './StringChecks' ;
1617
1718export type AstEntity = AstSymbol | AstImport ;
1819
@@ -210,6 +211,88 @@ export class AstSymbolTable {
210211 return this . _entitiesByIdentifierNode . get ( identifier ) ;
211212 }
212213
214+ /**
215+ * Builds an AstSymbol.localName for a given ts.Symbol. In the current implementation, the localName is
216+ * a TypeScript-like expression that may be a string literal or ECMAScript symbol expression.
217+ *
218+ * ```ts
219+ * class X {
220+ * // localName="identifier"
221+ * public identifier: number = 1;
222+ * // localName="\"identifier\""
223+ * public "quoted string!": number = 2;
224+ * // localName="[MyNamespace.MySymbol]"
225+ * public [MyNamespace.MySymbol]: number = 3;
226+ * }
227+ * ```
228+ */
229+ public static getLocalNameForSymbol ( symbol : ts . Symbol ) : string {
230+ const symbolName : string = symbol . name ;
231+
232+ // TypeScript binds well-known ECMAScript symbols like "[Symbol.iterator]" as "__@iterator".
233+ // Decode it back into "[Symbol.iterator]".
234+ const wellKnownSymbolName : string | undefined = TypeScriptHelpers . tryDecodeWellKnownSymbolName ( symbolName ) ;
235+ if ( wellKnownSymbolName ) {
236+ return wellKnownSymbolName ;
237+ }
238+
239+ const isUniqueSymbol : boolean = TypeScriptHelpers . isUniqueSymbolName ( symbolName ) ;
240+
241+ // We will try to obtain the name from a declaration; otherwise we'll fall back to the symbol name.
242+ let unquotedName : string = symbolName ;
243+
244+ for ( const declaration of symbol . declarations || [ ] ) {
245+ // Handle cases such as "export default class X { }" where the symbol name is "default"
246+ // but the local name is "X".
247+ const localSymbol : ts . Symbol | undefined = TypeScriptInternals . tryGetLocalSymbol ( declaration ) ;
248+ if ( localSymbol ) {
249+ unquotedName = localSymbol . name ;
250+ }
251+
252+ // If it is a non-well-known symbol, then return the late-bound name. For example, "X.Y.z" in this example:
253+ //
254+ // namespace X {
255+ // export namespace Y {
256+ // export const z: unique symbol = Symbol("z");
257+ // }
258+ // }
259+ //
260+ // class C {
261+ // public [X.Y.z](): void { }
262+ // }
263+ //
264+ if ( isUniqueSymbol ) {
265+ const declarationName : ts . DeclarationName | undefined = ts . getNameOfDeclaration ( declaration ) ;
266+ if ( declarationName && ts . isComputedPropertyName ( declarationName ) ) {
267+ const lateBoundName : string | undefined = TypeScriptHelpers . tryGetLateBoundName ( declarationName ) ;
268+ if ( lateBoundName ) {
269+ // Here the string may contain an expression such as "[X.Y.z]". Names starting with "[" are always
270+ // expressions. If a string literal contains those characters, the code below will JSON.stringify() it
271+ // to avoid a collision.
272+ return lateBoundName ;
273+ }
274+ }
275+ }
276+ }
277+
278+ // Otherwise that name may come from a quoted string or pseudonym like `__constructor`.
279+ // If the string is not a safe identifier, then we must add quotes.
280+ // Note that if it was quoted but did not need to be quoted, here we will remove the quotes.
281+ if ( ! StringChecks . isSafeUnquotedMemberIdentifier ( unquotedName ) ) {
282+ // For API Extractor's purposes, a canonical form is more appropriate than trying to reflect whatever
283+ // appeared in the source code. The code is not even guaranteed to be consistent, for example:
284+ //
285+ // class X {
286+ // public "f1"(x: string): void;
287+ // public f1(x: boolean): void;
288+ // public 'f1'(x: string | boolean): void { }
289+ // }
290+ return JSON . stringify ( unquotedName ) ;
291+ }
292+
293+ return unquotedName ;
294+ }
295+
213296 /**
214297 * Used by analyze to recursively analyze the entire child tree.
215298 */
@@ -425,35 +508,7 @@ export class AstSymbolTable {
425508 }
426509 }
427510
428- let localName : string | undefined = options . localName ;
429-
430- if ( localName === undefined ) {
431- // We will try to obtain the name from a declaration; otherwise we'll fall back to the symbol name
432- // This handles cases such as "export default class X { }" where the symbol name is "default"
433- // but the declaration name is "X".
434- localName = followedSymbol . name ;
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- }
454- }
455- }
456- }
511+ const localName : string | undefined = options . localName || AstSymbolTable . getLocalNameForSymbol ( followedSymbol ) ;
457512
458513 astSymbol = new AstSymbol ( {
459514 followedSymbol : followedSymbol ,
0 commit comments