Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
epidemian
GitHub Repository: epidemian/eslint-plugin-import
Path: blob/main/src/rules/export.js
829 views
1
import ExportMap, { recursivePatternCapture } from '../ExportMap';
2
import docsUrl from '../docsUrl';
3
import includes from 'array-includes';
4
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
27
/**
28
* Detect function overloads like:
29
* ```ts
30
* export function foo(a: number);
31
* export function foo(a: string);
32
* export function foo(a: number|string) { return a; }
33
* ```
34
* @param {Set<Object>} nodes
35
* @returns {boolean}
36
*/
37
function isTypescriptFunctionOverloads(nodes) {
38
const types = new Set(Array.from(nodes, node => node.parent.type));
39
return (
40
types.has('TSDeclareFunction') &&
41
(
42
types.size === 1 ||
43
(types.size === 2 && types.has('FunctionDeclaration'))
44
)
45
);
46
}
47
48
module.exports = {
49
meta: {
50
type: 'problem',
51
docs: {
52
url: docsUrl('export'),
53
},
54
schema: [],
55
},
56
57
create(context) {
58
const namespace = new Map([[rootProgram, new Map()]]);
59
60
function addNamed(name, node, parent, isType) {
61
if (!namespace.has(parent)) {
62
namespace.set(parent, new Map());
63
}
64
const named = namespace.get(parent);
65
66
const key = isType ? `${tsTypePrefix}${name}` : name;
67
let nodes = named.get(key);
68
69
if (nodes == null) {
70
nodes = new Set();
71
named.set(key, nodes);
72
}
73
74
nodes.add(node);
75
}
76
77
function getParent(node) {
78
if (node.parent && node.parent.type === 'TSModuleBlock') {
79
return node.parent.parent;
80
}
81
82
// just in case somehow a non-ts namespace export declaration isn't directly
83
// parented to the root Program node
84
return rootProgram;
85
}
86
87
return {
88
'ExportDefaultDeclaration': (node) => addNamed('default', node, getParent(node)),
89
90
'ExportSpecifier': (node) => addNamed(
91
node.exported.name,
92
node.exported,
93
getParent(node.parent),
94
),
95
96
'ExportNamedDeclaration': function (node) {
97
if (node.declaration == null) return;
98
99
const parent = getParent(node);
100
// support for old TypeScript versions
101
const isTypeVariableDecl = node.declaration.kind === 'type';
102
103
if (node.declaration.id != null) {
104
if (includes([
105
'TSTypeAliasDeclaration',
106
'TSInterfaceDeclaration',
107
], node.declaration.type)) {
108
addNamed(node.declaration.id.name, node.declaration.id, parent, true);
109
} else {
110
addNamed(node.declaration.id.name, node.declaration.id, parent, isTypeVariableDecl);
111
}
112
}
113
114
if (node.declaration.declarations != null) {
115
for (const declaration of node.declaration.declarations) {
116
recursivePatternCapture(declaration.id, v =>
117
addNamed(v.name, v, parent, isTypeVariableDecl));
118
}
119
}
120
},
121
122
'ExportAllDeclaration': function (node) {
123
if (node.source == null) return; // not sure if this is ever true
124
125
// `export * as X from 'path'` does not conflict
126
if (node.exported && node.exported.name) return;
127
128
const remoteExports = ExportMap.get(node.source.value, context);
129
if (remoteExports == null) return;
130
131
if (remoteExports.errors.length) {
132
remoteExports.reportErrors(context, node);
133
return;
134
}
135
136
const parent = getParent(node);
137
138
let any = false;
139
remoteExports.forEach((v, name) => {
140
if (name !== 'default') {
141
any = true; // poor man's filter
142
addNamed(name, node, parent);
143
}
144
});
145
146
if (!any) {
147
context.report(
148
node.source,
149
`No named exports found in module '${node.source.value}'.`,
150
);
151
}
152
},
153
154
'Program:exit': function () {
155
for (const [, named] of namespace) {
156
for (const [name, nodes] of named) {
157
if (nodes.size <= 1) continue;
158
159
if (isTypescriptFunctionOverloads(nodes)) continue;
160
161
for (const node of nodes) {
162
if (name === 'default') {
163
context.report(node, 'Multiple default exports.');
164
} else {
165
context.report(
166
node,
167
`Multiple exports of name '${name.replace(tsTypePrefix, '')}'.`,
168
);
169
}
170
}
171
}
172
}
173
},
174
};
175
},
176
};
177
178