1 | 'use strict';
|
2 | const {defaultsDeep} = require('lodash');
|
3 | const {getStringIfConstant} = require('eslint-utils');
|
4 | const eslintTemplateVisitor = require('eslint-template-visitor');
|
5 |
|
6 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
7 |
|
8 | const 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 |
|
42 | const 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 |
|
66 | const 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 |
|
92 |
|
93 |
|
94 |
|
95 | return [];
|
96 | };
|
97 |
|
98 | const 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 |
|
114 | const MESSAGE_ID = 'importStyle';
|
115 |
|
116 |
|
117 | const defaultStyles = {
|
118 | chalk: {
|
119 | default: true
|
120 | },
|
121 | path: {
|
122 | default: true
|
123 | },
|
124 | util: {
|
125 | named: true
|
126 | }
|
127 | };
|
128 |
|
129 | const templates = eslintTemplateVisitor({
|
130 | parserOptions: {
|
131 | sourceType: 'module',
|
132 | ecmaVersion: 2018
|
133 | }
|
134 | });
|
135 |
|
136 | const variableDeclarationVariable = templates.variableDeclarationVariable();
|
137 | const assignmentTargetVariable = templates.variable();
|
138 | const moduleNameVariable = templates.variable();
|
139 |
|
140 | const assignedDynamicImportTemplate = templates.template`async () => {
|
141 | ${variableDeclarationVariable} ${assignmentTargetVariable} = await import(${moduleNameVariable});
|
142 | }`.narrow('BlockStatement > :has(AwaitExpression)');
|
143 |
|
144 | const assignedRequireTemplate = templates.template`
|
145 | ${variableDeclarationVariable} ${assignmentTargetVariable} = require(${moduleNameVariable});
|
146 | `;
|
147 |
|
148 | const 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 |
|
180 |
|
181 |
|
182 |
|
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 |
|
307 | const 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 |
|
360 | module.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 | };
|