eslint-plugin-unicorn
Version:
Various awesome ESLint rules
327 lines (287 loc) • 9 kB
JavaScript
'use strict';
const {isOpeningParenToken} = require('eslint-utils');
const isShadowed = require('./utils/is-shadowed.js');
const removeSpacesAfter = require('./utils/remove-spaces-after.js');
const isStaticRequire = require('./utils/is-static-require.js');
const replaceReferenceIdentifier = require('./utils/replace-reference-identifier.js');
const {getParentheses} = require('./utils/parentheses.js');
const assertToken = require('./utils/assert-token.js');
const {referenceIdentifierSelector} = require('./selectors/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_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_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 * removeParentheses(nodeOrNodes, fixer, sourceCode) {
for (const node of Array.isArray(nodeOrNodes) ? nodeOrNodes : [nodeOrNodes]) {
const parentheses = getParentheses(node, sourceCode);
for (const token of parentheses) {
yield fixer.remove(token);
}
}
}
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);
yield * removeParentheses([callee, requireCall, source], 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);
yield * removeParentheses([callee, requireCall, source], 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);
yield * removeParentheses([node.parent, node], 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.getPhysicalFilename();
if (filename.toLowerCase().endsWith('.cjs')) {
return {};
}
const sourceCode = context.getSourceCode();
return {
'ExpressionStatement[directive="use strict"]'(node) {
return {
node,
messageId: ERROR_USE_STRICT_DIRECTIVE,
* fix(fixer) {
yield fixer.remove(node);
yield removeSpacesAfter(node, sourceCode, fixer);
}
};
},
'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;
}
};
}
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer JavaScript modules (ESM) over CommonJS.'
},
fixable: 'code',
messages,
hasSuggestions: true
}
};