UNPKG

9.04 kBJavaScriptView Raw
1'use strict';
2const {defaultsDeep} = require('lodash');
3const {getStringIfConstant} = require('eslint-utils');
4const eslintTemplateVisitor = require('eslint-template-visitor');
5
6const getDocumentationUrl = require('./utils/get-documentation-url');
7
8const getActualImportDeclarationStyles = importDeclaration => {
9 const {specifiers} = importDeclaration;
10
11 if (specifiers.length === 0) {
12 return ['unassigned'];
13 }
14
15 const styles = new Set();
16
17 for (const specifier of specifiers) {
18 if (specifier.type === 'ImportDefaultSpecifier') {
19 styles.add('default');
20 continue;
21 }
22
23 if (specifier.type === 'ImportNamespaceSpecifier') {
24 styles.add('namespace');
25 continue;
26 }
27
28 if (specifier.type === 'ImportSpecifier') {
29 if (specifier.imported.type === 'Identifier' && specifier.imported.name === 'default') {
30 styles.add('default');
31 continue;
32 }
33
34 styles.add('named');
35 continue;
36 }
37 }
38
39 return [...styles];
40};
41
42const getActualExportDeclarationStyles = exportDeclaration => {
43 const {specifiers} = exportDeclaration;
44
45 if (specifiers.length === 0) {
46 return ['unassigned'];
47 }
48
49 const styles = new Set();
50
51 for (const specifier of specifiers) {
52 if (specifier.type === 'ExportSpecifier') {
53 if (specifier.exported.type === 'Identifier' && specifier.exported.name === 'default') {
54 styles.add('default');
55 continue;
56 }
57
58 styles.add('named');
59 continue;
60 }
61 }
62
63 return [...styles];
64};
65
66const getActualAssignmentTargetImportStyles = assignmentTarget => {
67 if (assignmentTarget.type === 'Identifier') {
68 return ['namespace'];
69 }
70
71 if (assignmentTarget.type === 'ObjectPattern') {
72 if (assignmentTarget.properties.length === 0) {
73 return ['unassigned'];
74 }
75
76 const styles = new Set();
77
78 for (const property of assignmentTarget.properties) {
79 if (property.key.type === 'Identifier') {
80 if (property.key.name === 'default') {
81 styles.add('default');
82 } else {
83 styles.add('named');
84 }
85 }
86 }
87
88 return [...styles];
89 }
90
91 // Next line is not test-coverable until unforceable changes to the language
92 // like an addition of new AST node types usable in `const __HERE__ = foo;`.
93 // An exotic custom parser or a bug in one could cover it too.
94 /* istanbul ignore next */
95 return [];
96};
97
98const joinOr = words => {
99 return words
100 .map((word, index) => {
101 if (index === (words.length - 1)) {
102 return word;
103 }
104
105 if (index === (words.length - 2)) {
106 return word + ' or';
107 }
108
109 return word + ',';
110 })
111 .join(' ');
112};
113
114const MESSAGE_ID = 'importStyle';
115
116// Keep this alphabetically sorted for easier maintenance
117const defaultStyles = {
118 chalk: {
119 default: true
120 },
121 path: {
122 default: true
123 },
124 util: {
125 named: true
126 }
127};
128
129const templates = eslintTemplateVisitor({
130 parserOptions: {
131 sourceType: 'module',
132 ecmaVersion: 2018
133 }
134});
135
136const variableDeclarationVariable = templates.variableDeclarationVariable();
137const assignmentTargetVariable = templates.variable();
138const moduleNameVariable = templates.variable();
139
140const assignedDynamicImportTemplate = templates.template`async () => {
141 ${variableDeclarationVariable} ${assignmentTargetVariable} = await import(${moduleNameVariable});
142}`.narrow('BlockStatement > :has(AwaitExpression)');
143
144const assignedRequireTemplate = templates.template`
145 ${variableDeclarationVariable} ${assignmentTargetVariable} = require(${moduleNameVariable});
146`;
147
148const create = context => {
149 let [
150 {
151 styles = {},
152 extendDefaultStyles = true,
153 checkImport = true,
154 checkDynamicImport = true,
155 checkExportFrom = false,
156 checkRequire = true
157 } = {}
158 ] = context.options;
159
160 styles = extendDefaultStyles ?
161 defaultsDeep({}, styles, defaultStyles) :
162 styles;
163
164 styles = new Map(
165 Object.entries(styles).map(
166 ([moduleName, styles]) =>
167 [moduleName, new Map(Object.entries(styles))]
168 )
169 );
170
171 const report = (node, moduleName, actualImportStyles, allowedImportStyles, isRequire = false) => {
172 if (!allowedImportStyles || allowedImportStyles.size === 0) {
173 return;
174 }
175
176 let effectiveAllowedImportStyles = allowedImportStyles;
177
178 if (isRequire) {
179 // For `require`, `'default'` style allows both `x = require('x')` (`'namespace'` style) and
180 // `{default: x} = require('x')` (`'default'` style) since we don't know in advance
181 // whether `'x'` is a compiled ES6 module (with `default` key) or a CommonJS module and `require`
182 // does not provide any automatic interop for this, so the user may have to use either of theese.
183 if (allowedImportStyles.get('default') && !allowedImportStyles.get('namespace')) {
184 effectiveAllowedImportStyles = new Map(allowedImportStyles);
185 effectiveAllowedImportStyles.set('namespace', true);
186 }
187 }
188
189 if (actualImportStyles.every(style => effectiveAllowedImportStyles.get(style))) {
190 return;
191 }
192
193 const data = {
194 allowedStyles: joinOr([...allowedImportStyles.keys()]),
195 moduleName
196 };
197
198 context.report({
199 node,
200 messageId: MESSAGE_ID,
201 data
202 });
203 };
204
205 let visitor = {};
206
207 if (checkImport) {
208 visitor = {
209 ...visitor,
210
211 ImportDeclaration(node) {
212 const moduleName = getStringIfConstant(node.source, context.getScope());
213
214 const allowedImportStyles = styles.get(moduleName);
215 const actualImportStyles = getActualImportDeclarationStyles(node);
216
217 report(node, moduleName, actualImportStyles, allowedImportStyles);
218 }
219 };
220 }
221
222 if (checkDynamicImport) {
223 visitor = {
224 ...visitor,
225
226 'ExpressionStatement > ImportExpression'(node) {
227 const moduleName = getStringIfConstant(node.source, context.getScope());
228 const allowedImportStyles = styles.get(moduleName);
229 const actualImportStyles = ['unassigned'];
230
231 report(node, moduleName, actualImportStyles, allowedImportStyles);
232 },
233
234 [assignedDynamicImportTemplate](node) {
235 const assignmentTargetNode = assignedDynamicImportTemplate.context.getMatch(assignmentTargetVariable);
236 const moduleNameNode = assignedDynamicImportTemplate.context.getMatch(moduleNameVariable);
237 const moduleName = getStringIfConstant(moduleNameNode, context.getScope());
238
239 if (!moduleName) {
240 return;
241 }
242
243 const allowedImportStyles = styles.get(moduleName);
244 const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
245
246 report(node, moduleName, actualImportStyles, allowedImportStyles);
247 }
248 };
249 }
250
251 if (checkExportFrom) {
252 visitor = {
253 ...visitor,
254
255 ExportAllDeclaration(node) {
256 const moduleName = getStringIfConstant(node.source, context.getScope());
257
258 const allowedImportStyles = styles.get(moduleName);
259 const actualImportStyles = ['namespace'];
260
261 report(node, moduleName, actualImportStyles, allowedImportStyles);
262 },
263
264 ExportNamedDeclaration(node) {
265 const moduleName = getStringIfConstant(node.source, context.getScope());
266
267 const allowedImportStyles = styles.get(moduleName);
268 const actualImportStyles = getActualExportDeclarationStyles(node);
269
270 report(node, moduleName, actualImportStyles, allowedImportStyles);
271 }
272 };
273 }
274
275 if (checkRequire) {
276 visitor = {
277 ...visitor,
278
279 'ExpressionStatement > CallExpression[callee.name=\'require\'][arguments.length=1]'(node) {
280 const moduleName = getStringIfConstant(node.arguments[0], context.getScope());
281 const allowedImportStyles = styles.get(moduleName);
282 const actualImportStyles = ['unassigned'];
283
284 report(node, moduleName, actualImportStyles, allowedImportStyles, true);
285 },
286
287 [assignedRequireTemplate](node) {
288 const assignmentTargetNode = assignedRequireTemplate.context.getMatch(assignmentTargetVariable);
289 const moduleNameNode = assignedRequireTemplate.context.getMatch(moduleNameVariable);
290 const moduleName = getStringIfConstant(moduleNameNode, context.getScope());
291
292 if (!moduleName) {
293 return;
294 }
295
296 const allowedImportStyles = styles.get(moduleName);
297 const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
298
299 report(node, moduleName, actualImportStyles, allowedImportStyles, true);
300 }
301 };
302 }
303
304 return templates.visitor(visitor);
305};
306
307const schema = [
308 {
309 type: 'object',
310 properties: {
311 checkImport: {
312 type: 'boolean'
313 },
314 checkDynamicImport: {
315 type: 'boolean'
316 },
317 checkExportFrom: {
318 type: 'boolean'
319 },
320 checkRequire: {
321 type: 'boolean'
322 },
323 extendDefaultStyles: {
324 type: 'boolean'
325 },
326 styles: {
327 $ref: '#/items/0/definitions/moduleStyles'
328 }
329 },
330 additionalProperties: false,
331 definitions: {
332 moduleStyles: {
333 type: 'object',
334 additionalProperties: {
335 $ref: '#/items/0/definitions/styles'
336 }
337 },
338 styles: {
339 anyOf: [
340 {
341 enum: [
342 false
343 ]
344 },
345 {
346 $ref: '#/items/0/definitions/booleanObject'
347 }
348 ]
349 },
350 booleanObject: {
351 type: 'object',
352 additionalProperties: {
353 type: 'boolean'
354 }
355 }
356 }
357 }
358];
359
360module.exports = {
361 create,
362 meta: {
363 type: 'problem',
364 docs: {
365 url: getDocumentationUrl(__filename)
366 },
367 messages: {
368 [MESSAGE_ID]: 'Use {{allowedStyles}} import for module `{{moduleName}}`.'
369 },
370 schema
371 }
372};