Skip to content

fix(export): Support typescript namespaces #1320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 12, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix(export): Support typescript namespaces
Fixes #1300
bradzacher committed Apr 12, 2019

Verified

This commit was signed with the committer’s verified signature.
euclio Andy Russell
commit 988e12b390c5d3d3b9bab4037bf6b47b10afe661
86 changes: 68 additions & 18 deletions src/rules/export.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,28 @@ import ExportMap, { recursivePatternCapture } from '../ExportMap'
import docsUrl from '../docsUrl'
import includes from 'array-includes'

/*
Notes on Typescript namespaces aka TSModuleDeclaration:
There are two forms:
- active namespaces: namespace Foo {} / module Foo {}
- ambient modules; declare module "eslint-plugin-import" {}
active namespaces:
- cannot contain a default export
- cannot contain an export all
- cannot contain a multi name export (export { a, b })
- can have active namespaces nested within them
ambient namespaces:
- can only be defined in .d.ts files
- cannot be nested within active namespaces
- have no other restrictions
*/

const rootProgram = 'root'
const tsTypePrefix = 'type:'

module.exports = {
meta: {
type: 'problem',
@@ -11,10 +33,15 @@ module.exports = {
},

create: function (context) {
const named = new Map()
const namespace = new Map([[rootProgram, new Map()]])

function addNamed(name, node, parent, isType) {
if (!namespace.has(parent)) {
namespace.set(parent, new Map())
}
const named = namespace.get(parent)

function addNamed(name, node, type) {
const key = type ? `${type}:${name}` : name
const key = isType ? `${tsTypePrefix}${name}` : name
let nodes = named.get(key)

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

function getParent(node) {
if (node.parent && node.parent.type === 'TSModuleBlock') {
return node.parent.parent
}

// just in case somehow a non-ts namespace export declaration isn't directly
// parented to the root Program node
return rootProgram
}

return {
'ExportDefaultDeclaration': (node) => addNamed('default', node),
'ExportDefaultDeclaration': (node) => addNamed('default', node, getParent(node)),

'ExportSpecifier': function (node) {
addNamed(node.exported.name, node.exported)
},
'ExportSpecifier': (node) => addNamed(node.exported.name, node.exported, getParent(node)),

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

const parent = getParent(node)
// support for old typescript versions
const isTypeVariableDecl = node.declaration.kind === 'type'

if (node.declaration.id != null) {
if (includes([
'TSTypeAliasDeclaration',
'TSInterfaceDeclaration',
], node.declaration.type)) {
addNamed(node.declaration.id.name, node.declaration.id, 'type')
addNamed(node.declaration.id.name, node.declaration.id, parent, true)
} else {
addNamed(node.declaration.id.name, node.declaration.id)
addNamed(node.declaration.id.name, node.declaration.id, parent, isTypeVariableDecl)
}
}

if (node.declaration.declarations != null) {
for (let declaration of node.declaration.declarations) {
recursivePatternCapture(declaration.id, v => addNamed(v.name, v))
recursivePatternCapture(declaration.id, v =>
addNamed(v.name, v, parent, isTypeVariableDecl))
}
}
},
@@ -63,11 +103,14 @@ module.exports = {
remoteExports.reportErrors(context, node)
return
}

const parent = getParent(node)

let any = false
remoteExports.forEach((v, name) =>
name !== 'default' &&
(any = true) && // poor man's filter
addNamed(name, node))
addNamed(name, node, parent))

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

'Program:exit': function () {
for (let [name, nodes] of named) {
if (nodes.size <= 1) continue

for (let node of nodes) {
if (name === 'default') {
context.report(node, 'Multiple default exports.')
} else context.report(node, `Multiple exports of name '${name}'.`)
for (let [, named] of namespace) {
for (let [name, nodes] of named) {
if (nodes.size <= 1) continue

for (let node of nodes) {
if (name === 'default') {
context.report(node, 'Multiple default exports.')
} else {
context.report(
node,
`Multiple exports of name '${name.replace(tsTypePrefix, '')}'.`
)
}
}
}
}
},
166 changes: 147 additions & 19 deletions tests/src/rules/export.js
Original file line number Diff line number Diff line change
@@ -126,26 +126,154 @@ context('Typescript', function () {
},
}

const isLT4 = process.env.ESLINT_VERSION === '3' || process.env.ESLINT_VERSION === '2';
const valid = [
test(Object.assign({
code: `
export const Foo = 1;
export interface Foo {}
`,
}, parserConfig)),
]
if (!isLT4) {
valid.unshift(test(Object.assign({
code: `
export const Foo = 1;
export type Foo = number;
`,
}, parserConfig)))
}
ruleTester.run('export', rule, {
valid: valid,
invalid: [],
valid: [
// type/value name clash
test(Object.assign({
code: `
export const Foo = 1;
export type Foo = number;
`,
}, parserConfig)),
test(Object.assign({
code: `
export const Foo = 1;
export interface Foo {}
`,
}, parserConfig)),

// namespace
test(Object.assign({
code: `
export const Bar = 1;
export namespace Foo {
export const Bar = 1;
}
`,
}, parserConfig)),
test(Object.assign({
code: `
export type Bar = string;
export namespace Foo {
export type Bar = string;
}
`,
}, parserConfig)),
test(Object.assign({
code: `
export const Bar = 1;
export type Bar = string;
export namespace Foo {
export const Bar = 1;
export type Bar = string;
}
`,
}, parserConfig)),
test(Object.assign({
code: `
export namespace Foo {
export const Foo = 1;
export namespace Bar {
export const Foo = 2;
}
export namespace Baz {
export const Foo = 3;
}
}
`,
}, parserConfig)),
],
invalid: [
// type/value name clash
test(Object.assign({
code: `
export type Foo = string;
export type Foo = number;
`,
errors: [
{
message: `Multiple exports of name 'Foo'.`,
line: 2,
},
{
message: `Multiple exports of name 'Foo'.`,
line: 3,
},
],
}, parserConfig)),

// namespace
test(Object.assign({
code: `
export const a = 1
export namespace Foo {
export const a = 2;
export const a = 3;
}
`,
errors: [
{
message: `Multiple exports of name 'a'.`,
line: 4,
},
{
message: `Multiple exports of name 'a'.`,
line: 5,
},
],
}, parserConfig)),
test(Object.assign({
code: `
declare module 'foo' {
const Foo = 1;
export default Foo;
export default Foo;
}
`,
errors: [
{
message: 'Multiple default exports.',
line: 4,
},
{
message: 'Multiple default exports.',
line: 5,
},
],
}, parserConfig)),
test(Object.assign({
code: `
export namespace Foo {
export namespace Bar {
export const Foo = 1;
export const Foo = 2;
}
export namespace Baz {
export const Bar = 3;
export const Bar = 4;
}
}
`,
errors: [
{
message: `Multiple exports of name 'Foo'.`,
line: 4,
},
{
message: `Multiple exports of name 'Foo'.`,
line: 5,
},
{
message: `Multiple exports of name 'Bar'.`,
line: 8,
},
{
message: `Multiple exports of name 'Bar'.`,
line: 9,
},
],
}, parserConfig)),
],
})
})
})