Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
epidemian
GitHub Repository: epidemian/eslint-plugin-import
Path: blob/main/src/rules/namespace.js
829 views
1
import declaredScope from 'eslint-module-utils/declaredScope';
2
import Exports from '../ExportMap';
3
import importDeclaration from '../importDeclaration';
4
import docsUrl from '../docsUrl';
5
6
module.exports = {
7
meta: {
8
type: 'problem',
9
docs: {
10
url: docsUrl('namespace'),
11
},
12
13
schema: [
14
{
15
type: 'object',
16
properties: {
17
allowComputed: {
18
description: 'If `false`, will report computed (and thus, un-lintable) references to namespace members.',
19
type: 'boolean',
20
default: false,
21
},
22
},
23
additionalProperties: false,
24
},
25
],
26
},
27
28
create: function namespaceRule(context) {
29
30
// read options
31
const {
32
allowComputed = false,
33
} = context.options[0] || {};
34
35
const namespaces = new Map();
36
37
function makeMessage(last, namepath) {
38
return `'${last.name}' not found in ${namepath.length > 1 ? 'deeply ' : ''}imported namespace '${namepath.join('.')}'.`;
39
}
40
41
return {
42
// pick up all imports at body entry time, to properly respect hoisting
43
Program({ body }) {
44
function processBodyStatement(declaration) {
45
if (declaration.type !== 'ImportDeclaration') return;
46
47
if (declaration.specifiers.length === 0) return;
48
49
const imports = Exports.get(declaration.source.value, context);
50
if (imports == null) return null;
51
52
if (imports.errors.length) {
53
imports.reportErrors(context, declaration);
54
return;
55
}
56
57
for (const specifier of declaration.specifiers) {
58
switch (specifier.type) {
59
case 'ImportNamespaceSpecifier':
60
if (!imports.size) {
61
context.report(
62
specifier,
63
`No exported names found in module '${declaration.source.value}'.`,
64
);
65
}
66
namespaces.set(specifier.local.name, imports);
67
break;
68
case 'ImportDefaultSpecifier':
69
case 'ImportSpecifier': {
70
const meta = imports.get(
71
// default to 'default' for default https://i.imgur.com/nj6qAWy.jpg
72
specifier.imported ? specifier.imported.name : 'default',
73
);
74
if (!meta || !meta.namespace) { break; }
75
namespaces.set(specifier.local.name, meta.namespace);
76
break;
77
}
78
}
79
}
80
}
81
body.forEach(processBodyStatement);
82
},
83
84
// same as above, but does not add names to local map
85
ExportNamespaceSpecifier(namespace) {
86
const declaration = importDeclaration(context);
87
88
const imports = Exports.get(declaration.source.value, context);
89
if (imports == null) return null;
90
91
if (imports.errors.length) {
92
imports.reportErrors(context, declaration);
93
return;
94
}
95
96
if (!imports.size) {
97
context.report(
98
namespace,
99
`No exported names found in module '${declaration.source.value}'.`,
100
);
101
}
102
},
103
104
// todo: check for possible redefinition
105
106
MemberExpression(dereference) {
107
if (dereference.object.type !== 'Identifier') return;
108
if (!namespaces.has(dereference.object.name)) return;
109
if (declaredScope(context, dereference.object.name) !== 'module') return;
110
111
if (dereference.parent.type === 'AssignmentExpression' && dereference.parent.left === dereference) {
112
context.report(
113
dereference.parent,
114
`Assignment to member of namespace '${dereference.object.name}'.`,
115
);
116
}
117
118
// go deep
119
let namespace = namespaces.get(dereference.object.name);
120
const namepath = [dereference.object.name];
121
// while property is namespace and parent is member expression, keep validating
122
while (namespace instanceof Exports && dereference.type === 'MemberExpression') {
123
124
if (dereference.computed) {
125
if (!allowComputed) {
126
context.report(
127
dereference.property,
128
`Unable to validate computed reference to imported namespace '${dereference.object.name}'.`,
129
);
130
}
131
return;
132
}
133
134
if (!namespace.has(dereference.property.name)) {
135
context.report(
136
dereference.property,
137
makeMessage(dereference.property, namepath),
138
);
139
break;
140
}
141
142
const exported = namespace.get(dereference.property.name);
143
if (exported == null) return;
144
145
// stash and pop
146
namepath.push(dereference.property.name);
147
namespace = exported.namespace;
148
dereference = dereference.parent;
149
}
150
151
},
152
153
VariableDeclarator({ id, init }) {
154
if (init == null) return;
155
if (init.type !== 'Identifier') return;
156
if (!namespaces.has(init.name)) return;
157
158
// check for redefinition in intermediate scopes
159
if (declaredScope(context, init.name) !== 'module') return;
160
161
// DFS traverse child namespaces
162
function testKey(pattern, namespace, path = [init.name]) {
163
if (!(namespace instanceof Exports)) return;
164
165
if (pattern.type !== 'ObjectPattern') return;
166
167
for (const property of pattern.properties) {
168
if (
169
property.type === 'ExperimentalRestProperty'
170
|| property.type === 'RestElement'
171
|| !property.key
172
) {
173
continue;
174
}
175
176
if (property.key.type !== 'Identifier') {
177
context.report({
178
node: property,
179
message: 'Only destructure top-level names.',
180
});
181
continue;
182
}
183
184
if (!namespace.has(property.key.name)) {
185
context.report({
186
node: property,
187
message: makeMessage(property.key, path),
188
});
189
continue;
190
}
191
192
path.push(property.key.name);
193
const dependencyExportMap = namespace.get(property.key.name);
194
// could be null when ignored or ambiguous
195
if (dependencyExportMap !== null) {
196
testKey(property.value, dependencyExportMap.namespace, path);
197
}
198
path.pop();
199
}
200
}
201
202
testKey(id, namespaces.get(init.name));
203
},
204
205
JSXMemberExpression({ object, property }) {
206
if (!namespaces.has(object.name)) return;
207
const namespace = namespaces.get(object.name);
208
if (!namespace.has(property.name)) {
209
context.report({
210
node: property,
211
message: makeMessage(property, [object.name]),
212
});
213
}
214
},
215
};
216
},
217
};
218
219