Skip to content

Commit 146d82e

Browse files
authored
Merge pull request microsoft#1400 from microsoft/octogonz/ae-issue-1350
[api-extractor] Add support for an exported name that conflicts with a global name
2 parents 05a5610 + 27b48d5 commit 146d82e

10 files changed

Lines changed: 196 additions & 17 deletions

File tree

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

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -352,41 +352,78 @@ export class Collector {
352352
* Ensures a unique name for each item in the package typings file.
353353
*/
354354
private _makeUniqueNames(): void {
355+
// The following examples illustrate the nameForEmit heuristics:
356+
//
357+
// Example 1:
358+
// class X { } <--- nameForEmit should be "A" to simplify things and reduce possibility of conflicts
359+
// export { X as A };
360+
//
361+
// Example 2:
362+
// class X { } <--- nameForEmit should be "X" because choosing A or B would be nondeterministic
363+
// export { X as A };
364+
// export { X as B };
365+
//
366+
// Example 3:
367+
// class X { } <--- nameForEmit should be "X_1" because Y has a stronger claim to the name
368+
// export { X as A };
369+
// export { X as B };
370+
// class Y { } <--- nameForEmit should be "X"
371+
// export { Y as X };
372+
373+
// Set of names that should NOT be used when generating a unique nameForEmit
355374
const usedNames: Set<string> = new Set<string>();
356-
this._collectGlobalNames(usedNames);
357375

358-
// First collect the explicit package exports (named)
376+
// First collect the names of explicit package exports, and perform a sanity check.
359377
for (const entity of this._entities) {
360378
for (const exportName of entity.exportNames) {
361379
if (usedNames.has(exportName)) {
362380
// This should be impossible
363381
throw new InternalError(`A package cannot have two exports with the name "${exportName}"`);
364382
}
365-
366383
usedNames.add(exportName);
367384
}
368385
}
369386

370-
// Next generate unique names for the non-exports that will be emitted (and the default export)
387+
// Next, add in the global names
388+
const globalNames: Set<string> = new Set<string>();
389+
this._collectGlobalNames(globalNames);
390+
391+
for (const globalName of globalNames) {
392+
// Note that globalName may conflict with an exported name.
393+
// We'll check for this conflict below.
394+
usedNames.add(globalName);
395+
}
396+
397+
// Ensure that each entity has a unique nameForEmit
371398
for (const entity of this._entities) {
372399

373-
// If this entity is exported exactly once, then emit the exported name
400+
// What name would we ideally want to emit it as?
401+
let idealNameForEmit: string;
402+
403+
// If this entity is exported exactly once, then we prefer the exported name
374404
if (entity.singleExportName !== undefined && entity.singleExportName !== ts.InternalSymbolName.Default) {
375-
entity.nameForEmit = entity.singleExportName;
376-
continue;
405+
idealNameForEmit = entity.singleExportName;
406+
} else {
407+
// otherwise use the local name
408+
idealNameForEmit = entity.astEntity.localName;
377409
}
378410

379-
// If the localName happens to be the same as one of the exports, then emit that name
380-
if (entity.exportNames.has(entity.astEntity.localName)) {
381-
entity.nameForEmit = entity.astEntity.localName;
382-
continue;
411+
// If the idealNameForEmit happens to be the same as one of the exports, then we're safe to use that...
412+
if (entity.exportNames.has(idealNameForEmit)) {
413+
// ...except that if it conflicts with a global name, then the global name wins
414+
if (!globalNames.has(idealNameForEmit)) {
415+
entity.nameForEmit = idealNameForEmit;
416+
continue;
417+
}
383418
}
384419

385-
// In all other cases, generate a unique name based on the localName
420+
// Generate a unique name based on idealNameForEmit
386421
let suffix: number = 1;
387-
let nameForEmit: string = entity.astEntity.localName;
422+
let nameForEmit: string = idealNameForEmit;
423+
424+
// Choose a name that doesn't conflict with usedNames
388425
while (usedNames.has(nameForEmit)) {
389-
nameForEmit = `${entity.astEntity.localName}_${++suffix}`;
426+
nameForEmit = `${idealNameForEmit}_${++suffix}`;
390427
}
391428
entity.nameForEmit = nameForEmit;
392429
usedNames.add(nameForEmit);

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,17 @@ export class CollectorEntity {
8181
* such as "export class X { }" instead of "export { X }".
8282
*/
8383
public get shouldInlineExport(): boolean {
84-
return this._singleExportName !== undefined
85-
&& this._singleExportName !== ts.InternalSymbolName.Default
86-
&& this.astEntity instanceof AstSymbol;
84+
// We don't inline an AstImport
85+
if (this.astEntity instanceof AstSymbol) {
86+
// We don't inline a symbol with more than one exported name
87+
if (this._singleExportName !== undefined && this._singleExportName !== ts.InternalSymbolName.Default) {
88+
// We can't inline a symbol whose emitted name is different from the export name
89+
if (this._nameForEmit === undefined || this._nameForEmit === this._singleExportName) {
90+
return true;
91+
}
92+
}
93+
}
94+
return false;
8795
}
8896

8997
/**

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"scenarioFolderNames": [
33
"ambientNameConflict",
4+
"ambientNameConflict2",
45
"apiItemKinds",
56
"circularImport",
67
"circularImport2",
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"metadata": {
3+
"toolPackage": "@microsoft/api-extractor",
4+
"toolVersion": "[test mode]",
5+
"schemaVersion": 1003,
6+
"oldestForwardsCompatibleVersion": 1001
7+
},
8+
"kind": "Package",
9+
"canonicalReference": "api-extractor-scenarios!",
10+
"docComment": "",
11+
"name": "api-extractor-scenarios",
12+
"members": [
13+
{
14+
"kind": "EntryPoint",
15+
"canonicalReference": "api-extractor-scenarios!",
16+
"name": "",
17+
"members": [
18+
{
19+
"kind": "Class",
20+
"canonicalReference": "api-extractor-scenarios!Date_2:class",
21+
"docComment": "/**\n * A local class declaration whose name is the same as the system `Date` global symbol.\n *\n * @public\n */\n",
22+
"excerptTokens": [
23+
{
24+
"kind": "Content",
25+
"text": "export declare class Date "
26+
}
27+
],
28+
"releaseTag": "Public",
29+
"name": "Date_2",
30+
"members": [],
31+
"implementsTokenRanges": []
32+
},
33+
{
34+
"kind": "Function",
35+
"canonicalReference": "api-extractor-scenarios!getDate:function(1)",
36+
"docComment": "/**\n * An API that references the system `Date` global symbol.\n *\n * @public\n */\n",
37+
"excerptTokens": [
38+
{
39+
"kind": "Content",
40+
"text": "export declare function getDate(): "
41+
},
42+
{
43+
"kind": "Reference",
44+
"text": "Date",
45+
"canonicalReference": "!Date:interface"
46+
},
47+
{
48+
"kind": "Content",
49+
"text": ";"
50+
}
51+
],
52+
"returnTypeTokenRange": {
53+
"startIndex": 1,
54+
"endIndex": 2
55+
},
56+
"releaseTag": "Public",
57+
"overloadIndex": 1,
58+
"parameters": [],
59+
"name": "getDate"
60+
}
61+
]
62+
}
63+
]
64+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## API Report File for "api-extractor-scenarios"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
// @public
8+
class Date_2 {
9+
}
10+
11+
export { Date_2 as Date }
12+
13+
// @public
14+
export function getDate(): Date;
15+
16+
17+
// (No @packageDocumentation comment for this package)
18+
19+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
/**
3+
* A local class declaration whose name is the same as the system `Date` global symbol.
4+
* @public
5+
*/
6+
declare class Date_2 {
7+
}
8+
export { Date_2 as Date }
9+
10+
/**
11+
* An API that references the system `Date` global symbol.
12+
* @public
13+
*/
14+
export declare function getDate(): Date;
15+
16+
export { }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
/**
5+
* A local class declaration whose name is the same as the system `Date` global symbol.
6+
* @public
7+
*/
8+
export class Date { }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
/**
5+
* An API that references the system `Date` global symbol.
6+
* @public
7+
*/
8+
export function getDate(): Date {
9+
return new Date();
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
export { getDate } from './getDate';
5+
export { Date } from './Date';
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": "Add support for an exported name that conflicts with a global name (GitHub #1350)",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@microsoft/api-extractor",
10+
"email": "4673363+octogonz@users.noreply.github.com"
11+
}

0 commit comments

Comments
 (0)