Skip to content

Commit 988e12b

Browse files
committed
fix(export): Support typescript namespaces
Fixes #1300
1 parent 70c3679 commit 988e12b

File tree

2 files changed

+215
-37
lines changed

2 files changed

+215
-37
lines changed

Diff for: src/rules/export.js

+68-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@ import ExportMap, { recursivePatternCapture } from '../ExportMap'
22
import docsUrl from '../docsUrl'
33
import includes from 'array-includes'
44

5+
/*
6+
Notes on Typescript namespaces aka TSModuleDeclaration:
7+
8+
There are two forms:
9+
- active namespaces: namespace Foo {} / module Foo {}
10+
- ambient modules; declare module "eslint-plugin-import" {}
11+
12+
active namespaces:
13+
- cannot contain a default export
14+
- cannot contain an export all
15+
- cannot contain a multi name export (export { a, b })
16+
- can have active namespaces nested within them
17+
18+
ambient namespaces:
19+
- can only be defined in .d.ts files
20+
- cannot be nested within active namespaces
21+
- have no other restrictions
22+
*/
23+
24+
const rootProgram = 'root'
25+
const tsTypePrefix = 'type:'
26+
527
module.exports = {
628
meta: {
729
type: 'problem',
@@ -11,10 +33,15 @@ module.exports = {
1133
},
1234

1335
create: function (context) {
14-
const named = new Map()
36+
const namespace = new Map([[rootProgram, new Map()]])
37+
38+
function addNamed(name, node, parent, isType) {
39+
if (!namespace.has(parent)) {
40+
namespace.set(parent, new Map())
41+
}
42+
const named = namespace.get(parent)
1543

16-
function addNamed(name, node, type) {
17-
const key = type ? `${type}:${name}` : name
44+
const key = isType ? `${tsTypePrefix}${name}` : name
1845
let nodes = named.get(key)
1946

2047
if (nodes == null) {
@@ -25,30 +52,43 @@ module.exports = {
2552
nodes.add(node)
2653
}
2754

55+
function getParent(node) {
56+
if (node.parent && node.parent.type === 'TSModuleBlock') {
57+
return node.parent.parent
58+
}
59+
60+
// just in case somehow a non-ts namespace export declaration isn't directly
61+
// parented to the root Program node
62+
return rootProgram
63+
}
64+
2865
return {
29-
'ExportDefaultDeclaration': (node) => addNamed('default', node),
66+
'ExportDefaultDeclaration': (node) => addNamed('default', node, getParent(node)),
3067

31-
'ExportSpecifier': function (node) {
32-
addNamed(node.exported.name, node.exported)
33-
},
68+
'ExportSpecifier': (node) => addNamed(node.exported.name, node.exported, getParent(node)),
3469

3570
'ExportNamedDeclaration': function (node) {
3671
if (node.declaration == null) return
3772

73+
const parent = getParent(node)
74+
// support for old typescript versions
75+
const isTypeVariableDecl = node.declaration.kind === 'type'
76+
3877
if (node.declaration.id != null) {
3978
if (includes([
4079
'TSTypeAliasDeclaration',
4180
'TSInterfaceDeclaration',
4281
], node.declaration.type)) {
43-
addNamed(node.declaration.id.name, node.declaration.id, 'type')
82+
addNamed(node.declaration.id.name, node.declaration.id, parent, true)
4483
} else {
45-
addNamed(node.declaration.id.name, node.declaration.id)
84+
addNamed(node.declaration.id.name, node.declaration.id, parent, isTypeVariableDecl)
4685
}
4786
}
4887

4988
if (node.declaration.declarations != null) {
5089
for (let declaration of node.declaration.declarations) {
51-
recursivePatternCapture(declaration.id, v => addNamed(v.name, v))
90+
recursivePatternCapture(declaration.id, v =>
91+
addNamed(v.name, v, parent, isTypeVariableDecl))
5292
}
5393
}
5494
},
@@ -63,11 +103,14 @@ module.exports = {
63103
remoteExports.reportErrors(context, node)
64104
return
65105
}
106+
107+
const parent = getParent(node)
108+
66109
let any = false
67110
remoteExports.forEach((v, name) =>
68111
name !== 'default' &&
69112
(any = true) && // poor man's filter
70-
addNamed(name, node))
113+
addNamed(name, node, parent))
71114

72115
if (!any) {
73116
context.report(node.source,
@@ -76,13 +119,20 @@ module.exports = {
76119
},
77120

78121
'Program:exit': function () {
79-
for (let [name, nodes] of named) {
80-
if (nodes.size <= 1) continue
81-
82-
for (let node of nodes) {
83-
if (name === 'default') {
84-
context.report(node, 'Multiple default exports.')
85-
} else context.report(node, `Multiple exports of name '${name}'.`)
122+
for (let [, named] of namespace) {
123+
for (let [name, nodes] of named) {
124+
if (nodes.size <= 1) continue
125+
126+
for (let node of nodes) {
127+
if (name === 'default') {
128+
context.report(node, 'Multiple default exports.')
129+
} else {
130+
context.report(
131+
node,
132+
`Multiple exports of name '${name.replace(tsTypePrefix, '')}'.`
133+
)
134+
}
135+
}
86136
}
87137
}
88138
},

Diff for: tests/src/rules/export.js

+147-19
Original file line numberDiff line numberDiff line change
@@ -126,26 +126,154 @@ context('Typescript', function () {
126126
},
127127
}
128128

129-
const isLT4 = process.env.ESLINT_VERSION === '3' || process.env.ESLINT_VERSION === '2';
130-
const valid = [
131-
test(Object.assign({
132-
code: `
133-
export const Foo = 1;
134-
export interface Foo {}
135-
`,
136-
}, parserConfig)),
137-
]
138-
if (!isLT4) {
139-
valid.unshift(test(Object.assign({
140-
code: `
141-
export const Foo = 1;
142-
export type Foo = number;
143-
`,
144-
}, parserConfig)))
145-
}
146129
ruleTester.run('export', rule, {
147-
valid: valid,
148-
invalid: [],
130+
valid: [
131+
// type/value name clash
132+
test(Object.assign({
133+
code: `
134+
export const Foo = 1;
135+
export type Foo = number;
136+
`,
137+
}, parserConfig)),
138+
test(Object.assign({
139+
code: `
140+
export const Foo = 1;
141+
export interface Foo {}
142+
`,
143+
}, parserConfig)),
144+
145+
// namespace
146+
test(Object.assign({
147+
code: `
148+
export const Bar = 1;
149+
export namespace Foo {
150+
export const Bar = 1;
151+
}
152+
`,
153+
}, parserConfig)),
154+
test(Object.assign({
155+
code: `
156+
export type Bar = string;
157+
export namespace Foo {
158+
export type Bar = string;
159+
}
160+
`,
161+
}, parserConfig)),
162+
test(Object.assign({
163+
code: `
164+
export const Bar = 1;
165+
export type Bar = string;
166+
export namespace Foo {
167+
export const Bar = 1;
168+
export type Bar = string;
169+
}
170+
`,
171+
}, parserConfig)),
172+
test(Object.assign({
173+
code: `
174+
export namespace Foo {
175+
export const Foo = 1;
176+
export namespace Bar {
177+
export const Foo = 2;
178+
}
179+
export namespace Baz {
180+
export const Foo = 3;
181+
}
182+
}
183+
`,
184+
}, parserConfig)),
185+
],
186+
invalid: [
187+
// type/value name clash
188+
test(Object.assign({
189+
code: `
190+
export type Foo = string;
191+
export type Foo = number;
192+
`,
193+
errors: [
194+
{
195+
message: `Multiple exports of name 'Foo'.`,
196+
line: 2,
197+
},
198+
{
199+
message: `Multiple exports of name 'Foo'.`,
200+
line: 3,
201+
},
202+
],
203+
}, parserConfig)),
204+
205+
// namespace
206+
test(Object.assign({
207+
code: `
208+
export const a = 1
209+
export namespace Foo {
210+
export const a = 2;
211+
export const a = 3;
212+
}
213+
`,
214+
errors: [
215+
{
216+
message: `Multiple exports of name 'a'.`,
217+
line: 4,
218+
},
219+
{
220+
message: `Multiple exports of name 'a'.`,
221+
line: 5,
222+
},
223+
],
224+
}, parserConfig)),
225+
test(Object.assign({
226+
code: `
227+
declare module 'foo' {
228+
const Foo = 1;
229+
export default Foo;
230+
export default Foo;
231+
}
232+
`,
233+
errors: [
234+
{
235+
message: 'Multiple default exports.',
236+
line: 4,
237+
},
238+
{
239+
message: 'Multiple default exports.',
240+
line: 5,
241+
},
242+
],
243+
}, parserConfig)),
244+
test(Object.assign({
245+
code: `
246+
export namespace Foo {
247+
export namespace Bar {
248+
export const Foo = 1;
249+
export const Foo = 2;
250+
}
251+
export namespace Baz {
252+
export const Bar = 3;
253+
export const Bar = 4;
254+
}
255+
}
256+
`,
257+
errors: [
258+
{
259+
message: `Multiple exports of name 'Foo'.`,
260+
line: 4,
261+
},
262+
{
263+
message: `Multiple exports of name 'Foo'.`,
264+
line: 5,
265+
},
266+
{
267+
message: `Multiple exports of name 'Bar'.`,
268+
line: 8,
269+
},
270+
{
271+
message: `Multiple exports of name 'Bar'.`,
272+
line: 9,
273+
},
274+
],
275+
}, parserConfig)),
276+
],
149277
})
150278
})
151279
})

0 commit comments

Comments
 (0)