Skip to content
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

fix(export): Support typescript namespaces #1320

Merged
merged 1 commit into from
Apr 12, 2019
Merged
Show file tree
Hide file tree
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
86 changes: 68 additions & 18 deletions src/rules/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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) {
Expand All @@ -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))
}
}
},
Expand All @@ -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,
Expand All @@ -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, '')}'.`
)
}
}
}
}
},
Expand Down
166 changes: 147 additions & 19 deletions tests/src/rules/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
],
})
})
})