123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- '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,
- },
- };
|