123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- 'use strict';
- const {defaultsDeep} = require('lodash');
- const {getStringIfConstant} = require('@eslint-community/eslint-utils');
- const {callExpressionSelector} = require('./selectors/index.js');
- const MESSAGE_ID = 'importStyle';
- const messages = {
- [MESSAGE_ID]: 'Use {{allowedStyles}} import for module `{{moduleName}}`.',
- };
- const getActualImportDeclarationStyles = importDeclaration => {
- const {specifiers} = importDeclaration;
- if (specifiers.length === 0) {
- return ['unassigned'];
- }
- const styles = new Set();
- for (const specifier of specifiers) {
- if (specifier.type === 'ImportDefaultSpecifier') {
- styles.add('default');
- continue;
- }
- if (specifier.type === 'ImportNamespaceSpecifier') {
- styles.add('namespace');
- continue;
- }
- if (specifier.type === 'ImportSpecifier') {
- if (specifier.imported.type === 'Identifier' && specifier.imported.name === 'default') {
- styles.add('default');
- continue;
- }
- styles.add('named');
- continue;
- }
- }
- return [...styles];
- };
- const getActualExportDeclarationStyles = exportDeclaration => {
- const {specifiers} = exportDeclaration;
- if (specifiers.length === 0) {
- return ['unassigned'];
- }
- const styles = new Set();
- for (const specifier of specifiers) {
- if (specifier.type === 'ExportSpecifier') {
- if (specifier.exported.type === 'Identifier' && specifier.exported.name === 'default') {
- styles.add('default');
- continue;
- }
- styles.add('named');
- continue;
- }
- }
- return [...styles];
- };
- const getActualAssignmentTargetImportStyles = assignmentTarget => {
- if (assignmentTarget.type === 'Identifier' || assignmentTarget.type === 'ArrayPattern') {
- return ['namespace'];
- }
- if (assignmentTarget.type === 'ObjectPattern') {
- if (assignmentTarget.properties.length === 0) {
- return ['unassigned'];
- }
- const styles = new Set();
- for (const property of assignmentTarget.properties) {
- if (property.type === 'RestElement') {
- styles.add('named');
- continue;
- }
- if (property.key.type === 'Identifier') {
- if (property.key.name === 'default') {
- styles.add('default');
- } else {
- styles.add('named');
- }
- }
- }
- return [...styles];
- }
- // Next line is not test-coverable until unforceable changes to the language
- // like an addition of new AST node types usable in `const __HERE__ = foo;`.
- // An exotic custom parser or a bug in one could cover it too.
- /* c8 ignore next */
- return [];
- };
- const joinOr = words => words
- .map((word, index) => {
- if (index === words.length - 1) {
- return word;
- }
- if (index === words.length - 2) {
- return word + ' or';
- }
- return word + ',';
- })
- .join(' ');
- // Keep this alphabetically sorted for easier maintenance
- const defaultStyles = {
- chalk: {
- default: true,
- },
- path: {
- default: true,
- },
- util: {
- named: true,
- },
- };
- const assignedDynamicImportSelector = [
- 'VariableDeclarator',
- '[init.type="AwaitExpression"]',
- '[init.argument.type="ImportExpression"]',
- ].join('');
- const assignedRequireSelector = [
- 'VariableDeclarator',
- '[init.type="CallExpression"]',
- '[init.callee.type="Identifier"]',
- '[init.callee.name="require"]',
- ].join('');
- /** @param {import('eslint').Rule.RuleContext} context */
- const create = context => {
- let [
- {
- styles = {},
- extendDefaultStyles = true,
- checkImport = true,
- checkDynamicImport = true,
- checkExportFrom = false,
- checkRequire = true,
- } = {},
- ] = context.options;
- styles = extendDefaultStyles
- ? defaultsDeep({}, styles, defaultStyles)
- : styles;
- styles = new Map(
- Object.entries(styles).map(
- ([moduleName, styles]) =>
- [moduleName, new Set(Object.entries(styles).filter(([, isAllowed]) => isAllowed).map(([style]) => style))],
- ),
- );
- const report = (node, moduleName, actualImportStyles, allowedImportStyles, isRequire = false) => {
- if (!allowedImportStyles || allowedImportStyles.size === 0) {
- return;
- }
- let effectiveAllowedImportStyles = allowedImportStyles;
- // For `require`, `'default'` style allows both `x = require('x')` (`'namespace'` style) and
- // `{default: x} = require('x')` (`'default'` style) since we don't know in advance
- // whether `'x'` is a compiled ES6 module (with `default` key) or a CommonJS module and `require`
- // does not provide any automatic interop for this, so the user may have to use either of these.
- if (isRequire && allowedImportStyles.has('default') && !allowedImportStyles.has('namespace')) {
- effectiveAllowedImportStyles = new Set(allowedImportStyles);
- effectiveAllowedImportStyles.add('namespace');
- }
- if (actualImportStyles.every(style => effectiveAllowedImportStyles.has(style))) {
- return;
- }
- const data = {
- allowedStyles: joinOr([...allowedImportStyles.keys()]),
- moduleName,
- };
- context.report({
- node,
- messageId: MESSAGE_ID,
- data,
- });
- };
- let visitor = {};
- if (checkImport) {
- visitor = {
- ...visitor,
- ImportDeclaration(node) {
- const moduleName = getStringIfConstant(node.source, context.getScope());
- const allowedImportStyles = styles.get(moduleName);
- const actualImportStyles = getActualImportDeclarationStyles(node);
- report(node, moduleName, actualImportStyles, allowedImportStyles);
- },
- };
- }
- if (checkDynamicImport) {
- visitor = {
- ...visitor,
- 'ExpressionStatement > ImportExpression'(node) {
- const moduleName = getStringIfConstant(node.source, context.getScope());
- const allowedImportStyles = styles.get(moduleName);
- const actualImportStyles = ['unassigned'];
- report(node, moduleName, actualImportStyles, allowedImportStyles);
- },
- [assignedDynamicImportSelector](node) {
- const assignmentTargetNode = node.id;
- const moduleNameNode = node.init.argument.source;
- const moduleName = getStringIfConstant(moduleNameNode, context.getScope());
- if (!moduleName) {
- return;
- }
- const allowedImportStyles = styles.get(moduleName);
- const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
- report(node, moduleName, actualImportStyles, allowedImportStyles);
- },
- };
- }
- if (checkExportFrom) {
- visitor = {
- ...visitor,
- ExportAllDeclaration(node) {
- const moduleName = getStringIfConstant(node.source, context.getScope());
- const allowedImportStyles = styles.get(moduleName);
- const actualImportStyles = ['namespace'];
- report(node, moduleName, actualImportStyles, allowedImportStyles);
- },
- ExportNamedDeclaration(node) {
- const moduleName = getStringIfConstant(node.source, context.getScope());
- const allowedImportStyles = styles.get(moduleName);
- const actualImportStyles = getActualExportDeclarationStyles(node);
- report(node, moduleName, actualImportStyles, allowedImportStyles);
- },
- };
- }
- if (checkRequire) {
- visitor = {
- ...visitor,
- [`ExpressionStatement > ${callExpressionSelector({name: 'require', argumentsLength: 1})}.expression`](node) {
- const moduleName = getStringIfConstant(node.arguments[0], context.getScope());
- const allowedImportStyles = styles.get(moduleName);
- const actualImportStyles = ['unassigned'];
- report(node, moduleName, actualImportStyles, allowedImportStyles, true);
- },
- [assignedRequireSelector](node) {
- const assignmentTargetNode = node.id;
- const moduleNameNode = node.init.arguments[0];
- const moduleName = getStringIfConstant(moduleNameNode, context.getScope());
- if (!moduleName) {
- return;
- }
- const allowedImportStyles = styles.get(moduleName);
- const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
- report(node, moduleName, actualImportStyles, allowedImportStyles, true);
- },
- };
- }
- return visitor;
- };
- const schema = {
- type: 'array',
- additionalItems: false,
- items: [
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- checkImport: {
- type: 'boolean',
- },
- checkDynamicImport: {
- type: 'boolean',
- },
- checkExportFrom: {
- type: 'boolean',
- },
- checkRequire: {
- type: 'boolean',
- },
- extendDefaultStyles: {
- type: 'boolean',
- },
- styles: {
- $ref: '#/definitions/moduleStyles',
- },
- },
- },
- ],
- definitions: {
- moduleStyles: {
- type: 'object',
- additionalProperties: {
- $ref: '#/definitions/styles',
- },
- },
- styles: {
- anyOf: [
- {
- enum: [
- false,
- ],
- },
- {
- $ref: '#/definitions/booleanObject',
- },
- ],
- },
- booleanObject: {
- type: 'object',
- additionalProperties: {
- type: 'boolean',
- },
- },
- },
- };
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'problem',
- docs: {
- description: 'Enforce specific import styles per module.',
- },
- schema,
- messages,
- },
- };
|