securityos/node_modules/eslint-plugin-unicorn/rules/prefer-module.js

335 lines
8.9 KiB
JavaScript

'use strict';
const {isOpeningParenToken} = require('@eslint-community/eslint-utils');
const isShadowed = require('./utils/is-shadowed.js');
const assertToken = require('./utils/assert-token.js');
const {referenceIdentifierSelector} = require('./selectors/index.js');
const {isStaticRequire} = require('./ast/index.js');
const {
removeParentheses,
replaceReferenceIdentifier,
removeSpacesAfter,
} = require('./fix/index.js');
const ERROR_USE_STRICT_DIRECTIVE = 'error/use-strict-directive';
const ERROR_GLOBAL_RETURN = 'error/global-return';
const ERROR_IDENTIFIER = 'error/identifier';
const SUGGESTION_USE_STRICT_DIRECTIVE = 'suggestion/use-strict-directive';
const SUGGESTION_DIRNAME = 'suggestion/dirname';
const SUGGESTION_FILENAME = 'suggestion/filename';
const SUGGESTION_IMPORT = 'suggestion/import';
const SUGGESTION_EXPORT = 'suggestion/export';
const messages = {
[ERROR_USE_STRICT_DIRECTIVE]: 'Do not use "use strict" directive.',
[ERROR_GLOBAL_RETURN]: '"return" should be used inside a function.',
[ERROR_IDENTIFIER]: 'Do not use "{{name}}".',
[SUGGESTION_USE_STRICT_DIRECTIVE]: 'Remove "use strict" directive.',
[SUGGESTION_DIRNAME]: 'Replace "__dirname" with `"…(import.meta.url)"`.',
[SUGGESTION_FILENAME]: 'Replace "__filename" with `"…(import.meta.url)"`.',
[SUGGESTION_IMPORT]: 'Switch to `import`.',
[SUGGESTION_EXPORT]: 'Switch to `export`.',
};
const identifierSelector = referenceIdentifierSelector([
'exports',
'require',
'module',
'__filename',
'__dirname',
]);
function fixRequireCall(node, sourceCode) {
if (!isStaticRequire(node.parent) || node.parent.callee !== node) {
return;
}
const requireCall = node.parent;
const {
parent,
callee,
arguments: [source],
} = requireCall;
// `require("foo")`
if (parent.type === 'ExpressionStatement' && parent.parent.type === 'Program') {
return function * (fixer) {
yield fixer.replaceText(callee, 'import');
const openingParenthesisToken = sourceCode.getTokenAfter(
callee,
isOpeningParenToken,
);
yield fixer.replaceText(openingParenthesisToken, ' ');
const closingParenthesisToken = sourceCode.getLastToken(requireCall);
yield fixer.remove(closingParenthesisToken);
for (const node of [callee, requireCall, source]) {
yield * removeParentheses(node, fixer, sourceCode);
}
};
}
// `const foo = require("foo")`
// `const {foo} = require("foo")`
if (
parent.type === 'VariableDeclarator'
&& parent.init === requireCall
&& (
parent.id.type === 'Identifier'
|| (
parent.id.type === 'ObjectPattern'
&& parent.id.properties.every(
({type, key, value, computed}) =>
type === 'Property'
&& !computed
&& value.type === 'Identifier'
&& key.type === 'Identifier',
)
)
)
&& parent.parent.type === 'VariableDeclaration'
&& parent.parent.kind === 'const'
&& parent.parent.declarations.length === 1
&& parent.parent.declarations[0] === parent
&& parent.parent.parent.type === 'Program'
) {
const declarator = parent;
const declaration = declarator.parent;
const {id} = declarator;
return function * (fixer) {
const constToken = sourceCode.getFirstToken(declaration);
assertToken(constToken, {
expected: {type: 'Keyword', value: 'const'},
ruleId: 'prefer-module',
});
yield fixer.replaceText(constToken, 'import');
const equalToken = sourceCode.getTokenAfter(id);
assertToken(equalToken, {
expected: {type: 'Punctuator', value: '='},
ruleId: 'prefer-module',
});
yield removeSpacesAfter(id, sourceCode, fixer);
yield removeSpacesAfter(equalToken, sourceCode, fixer);
yield fixer.replaceText(equalToken, ' from ');
yield fixer.remove(callee);
const openingParenthesisToken = sourceCode.getTokenAfter(
callee,
isOpeningParenToken,
);
yield fixer.remove(openingParenthesisToken);
const closingParenthesisToken = sourceCode.getLastToken(requireCall);
yield fixer.remove(closingParenthesisToken);
for (const node of [callee, requireCall, source]) {
yield * removeParentheses(node, fixer, sourceCode);
}
if (id.type === 'Identifier') {
return;
}
const {properties} = id;
for (const property of properties) {
const {key, shorthand} = property;
if (!shorthand) {
const commaToken = sourceCode.getTokenAfter(key);
assertToken(commaToken, {
expected: {type: 'Punctuator', value: ':'},
ruleId: 'prefer-module',
});
yield removeSpacesAfter(key, sourceCode, fixer);
yield removeSpacesAfter(commaToken, sourceCode, fixer);
yield fixer.replaceText(commaToken, ' as ');
}
}
};
}
}
const isTopLevelAssignment = node =>
node.parent.type === 'AssignmentExpression'
&& node.parent.operator === '='
&& node.parent.left === node
&& node.parent.parent.type === 'ExpressionStatement'
&& node.parent.parent.parent.type === 'Program';
const isNamedExport = node =>
node.parent.type === 'MemberExpression'
&& !node.parent.optional
&& !node.parent.computed
&& node.parent.object === node
&& node.parent.property.type === 'Identifier'
&& isTopLevelAssignment(node.parent)
&& node.parent.parent.right.type === 'Identifier';
const isModuleExports = node =>
node.parent.type === 'MemberExpression'
&& !node.parent.optional
&& !node.parent.computed
&& node.parent.object === node
&& node.parent.property.type === 'Identifier'
&& node.parent.property.name === 'exports';
function fixDefaultExport(node, sourceCode) {
return function * (fixer) {
yield fixer.replaceText(node, 'export default ');
yield removeSpacesAfter(node, sourceCode, fixer);
const equalToken = sourceCode.getTokenAfter(node, token => token.type === 'Punctuator' && token.value === '=');
yield fixer.remove(equalToken);
yield removeSpacesAfter(equalToken, sourceCode, fixer);
for (const currentNode of [node.parent, node]) {
yield * removeParentheses(currentNode, fixer, sourceCode);
}
};
}
function fixNamedExport(node, sourceCode) {
return function * (fixer) {
const assignmentExpression = node.parent.parent;
const exported = node.parent.property.name;
const local = assignmentExpression.right.name;
yield fixer.replaceText(assignmentExpression, `export {${local} as ${exported}}`);
yield * removeParentheses(assignmentExpression, fixer, sourceCode);
};
}
function fixExports(node, sourceCode) {
// `exports = bar`
if (isTopLevelAssignment(node)) {
return fixDefaultExport(node, sourceCode);
}
// `exports.foo = bar`
if (isNamedExport(node)) {
return fixNamedExport(node, sourceCode);
}
}
function fixModuleExports(node, sourceCode) {
if (isModuleExports(node)) {
return fixExports(node.parent, sourceCode);
}
}
function create(context) {
const filename = context.getFilename().toLowerCase();
if (filename.endsWith('.cjs')) {
return;
}
const sourceCode = context.getSourceCode();
return {
'ExpressionStatement[directive="use strict"]'(node) {
const problem = {node, messageId: ERROR_USE_STRICT_DIRECTIVE};
const fix = function * (fixer) {
yield fixer.remove(node);
yield removeSpacesAfter(node, sourceCode, fixer);
};
if (filename.endsWith('.mjs')) {
problem.fix = fix;
} else {
problem.suggest = [{messageId: SUGGESTION_USE_STRICT_DIRECTIVE, fix}];
}
return problem;
},
'ReturnStatement:not(:function ReturnStatement)'(node) {
return {
node: sourceCode.getFirstToken(node),
messageId: ERROR_GLOBAL_RETURN,
};
},
[identifierSelector](node) {
if (isShadowed(context.getScope(), node)) {
return;
}
const {name} = node;
const problem = {
node,
messageId: ERROR_IDENTIFIER,
data: {name},
};
switch (name) {
case '__filename':
case '__dirname': {
const messageId = node.name === '__dirname' ? SUGGESTION_DIRNAME : SUGGESTION_FILENAME;
const replacement = node.name === '__dirname'
? 'path.dirname(url.fileURLToPath(import.meta.url))'
: 'url.fileURLToPath(import.meta.url)';
problem.suggest = [{
messageId,
fix: fixer => replaceReferenceIdentifier(node, replacement, fixer),
}];
return problem;
}
case 'require': {
const fix = fixRequireCall(node, sourceCode);
if (fix) {
problem.suggest = [{
messageId: SUGGESTION_IMPORT,
fix,
}];
return problem;
}
break;
}
case 'exports': {
const fix = fixExports(node, sourceCode);
if (fix) {
problem.suggest = [{
messageId: SUGGESTION_EXPORT,
fix,
}];
return problem;
}
break;
}
case 'module': {
const fix = fixModuleExports(node, sourceCode);
if (fix) {
problem.suggest = [{
messageId: SUGGESTION_EXPORT,
fix,
}];
return problem;
}
break;
}
default:
}
return problem;
},
};
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer JavaScript modules (ESM) over CommonJS.',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};