prefer-module.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. 'use strict';
  2. const {isOpeningParenToken} = require('@eslint-community/eslint-utils');
  3. const isShadowed = require('./utils/is-shadowed.js');
  4. const assertToken = require('./utils/assert-token.js');
  5. const {referenceIdentifierSelector} = require('./selectors/index.js');
  6. const {isStaticRequire} = require('./ast/index.js');
  7. const {
  8. removeParentheses,
  9. replaceReferenceIdentifier,
  10. removeSpacesAfter,
  11. } = require('./fix/index.js');
  12. const ERROR_USE_STRICT_DIRECTIVE = 'error/use-strict-directive';
  13. const ERROR_GLOBAL_RETURN = 'error/global-return';
  14. const ERROR_IDENTIFIER = 'error/identifier';
  15. const SUGGESTION_USE_STRICT_DIRECTIVE = 'suggestion/use-strict-directive';
  16. const SUGGESTION_DIRNAME = 'suggestion/dirname';
  17. const SUGGESTION_FILENAME = 'suggestion/filename';
  18. const SUGGESTION_IMPORT = 'suggestion/import';
  19. const SUGGESTION_EXPORT = 'suggestion/export';
  20. const messages = {
  21. [ERROR_USE_STRICT_DIRECTIVE]: 'Do not use "use strict" directive.',
  22. [ERROR_GLOBAL_RETURN]: '"return" should be used inside a function.',
  23. [ERROR_IDENTIFIER]: 'Do not use "{{name}}".',
  24. [SUGGESTION_USE_STRICT_DIRECTIVE]: 'Remove "use strict" directive.',
  25. [SUGGESTION_DIRNAME]: 'Replace "__dirname" with `"…(import.meta.url)"`.',
  26. [SUGGESTION_FILENAME]: 'Replace "__filename" with `"…(import.meta.url)"`.',
  27. [SUGGESTION_IMPORT]: 'Switch to `import`.',
  28. [SUGGESTION_EXPORT]: 'Switch to `export`.',
  29. };
  30. const identifierSelector = referenceIdentifierSelector([
  31. 'exports',
  32. 'require',
  33. 'module',
  34. '__filename',
  35. '__dirname',
  36. ]);
  37. function fixRequireCall(node, sourceCode) {
  38. if (!isStaticRequire(node.parent) || node.parent.callee !== node) {
  39. return;
  40. }
  41. const requireCall = node.parent;
  42. const {
  43. parent,
  44. callee,
  45. arguments: [source],
  46. } = requireCall;
  47. // `require("foo")`
  48. if (parent.type === 'ExpressionStatement' && parent.parent.type === 'Program') {
  49. return function * (fixer) {
  50. yield fixer.replaceText(callee, 'import');
  51. const openingParenthesisToken = sourceCode.getTokenAfter(
  52. callee,
  53. isOpeningParenToken,
  54. );
  55. yield fixer.replaceText(openingParenthesisToken, ' ');
  56. const closingParenthesisToken = sourceCode.getLastToken(requireCall);
  57. yield fixer.remove(closingParenthesisToken);
  58. for (const node of [callee, requireCall, source]) {
  59. yield * removeParentheses(node, fixer, sourceCode);
  60. }
  61. };
  62. }
  63. // `const foo = require("foo")`
  64. // `const {foo} = require("foo")`
  65. if (
  66. parent.type === 'VariableDeclarator'
  67. && parent.init === requireCall
  68. && (
  69. parent.id.type === 'Identifier'
  70. || (
  71. parent.id.type === 'ObjectPattern'
  72. && parent.id.properties.every(
  73. ({type, key, value, computed}) =>
  74. type === 'Property'
  75. && !computed
  76. && value.type === 'Identifier'
  77. && key.type === 'Identifier',
  78. )
  79. )
  80. )
  81. && parent.parent.type === 'VariableDeclaration'
  82. && parent.parent.kind === 'const'
  83. && parent.parent.declarations.length === 1
  84. && parent.parent.declarations[0] === parent
  85. && parent.parent.parent.type === 'Program'
  86. ) {
  87. const declarator = parent;
  88. const declaration = declarator.parent;
  89. const {id} = declarator;
  90. return function * (fixer) {
  91. const constToken = sourceCode.getFirstToken(declaration);
  92. assertToken(constToken, {
  93. expected: {type: 'Keyword', value: 'const'},
  94. ruleId: 'prefer-module',
  95. });
  96. yield fixer.replaceText(constToken, 'import');
  97. const equalToken = sourceCode.getTokenAfter(id);
  98. assertToken(equalToken, {
  99. expected: {type: 'Punctuator', value: '='},
  100. ruleId: 'prefer-module',
  101. });
  102. yield removeSpacesAfter(id, sourceCode, fixer);
  103. yield removeSpacesAfter(equalToken, sourceCode, fixer);
  104. yield fixer.replaceText(equalToken, ' from ');
  105. yield fixer.remove(callee);
  106. const openingParenthesisToken = sourceCode.getTokenAfter(
  107. callee,
  108. isOpeningParenToken,
  109. );
  110. yield fixer.remove(openingParenthesisToken);
  111. const closingParenthesisToken = sourceCode.getLastToken(requireCall);
  112. yield fixer.remove(closingParenthesisToken);
  113. for (const node of [callee, requireCall, source]) {
  114. yield * removeParentheses(node, fixer, sourceCode);
  115. }
  116. if (id.type === 'Identifier') {
  117. return;
  118. }
  119. const {properties} = id;
  120. for (const property of properties) {
  121. const {key, shorthand} = property;
  122. if (!shorthand) {
  123. const commaToken = sourceCode.getTokenAfter(key);
  124. assertToken(commaToken, {
  125. expected: {type: 'Punctuator', value: ':'},
  126. ruleId: 'prefer-module',
  127. });
  128. yield removeSpacesAfter(key, sourceCode, fixer);
  129. yield removeSpacesAfter(commaToken, sourceCode, fixer);
  130. yield fixer.replaceText(commaToken, ' as ');
  131. }
  132. }
  133. };
  134. }
  135. }
  136. const isTopLevelAssignment = node =>
  137. node.parent.type === 'AssignmentExpression'
  138. && node.parent.operator === '='
  139. && node.parent.left === node
  140. && node.parent.parent.type === 'ExpressionStatement'
  141. && node.parent.parent.parent.type === 'Program';
  142. const isNamedExport = node =>
  143. node.parent.type === 'MemberExpression'
  144. && !node.parent.optional
  145. && !node.parent.computed
  146. && node.parent.object === node
  147. && node.parent.property.type === 'Identifier'
  148. && isTopLevelAssignment(node.parent)
  149. && node.parent.parent.right.type === 'Identifier';
  150. const isModuleExports = node =>
  151. node.parent.type === 'MemberExpression'
  152. && !node.parent.optional
  153. && !node.parent.computed
  154. && node.parent.object === node
  155. && node.parent.property.type === 'Identifier'
  156. && node.parent.property.name === 'exports';
  157. function fixDefaultExport(node, sourceCode) {
  158. return function * (fixer) {
  159. yield fixer.replaceText(node, 'export default ');
  160. yield removeSpacesAfter(node, sourceCode, fixer);
  161. const equalToken = sourceCode.getTokenAfter(node, token => token.type === 'Punctuator' && token.value === '=');
  162. yield fixer.remove(equalToken);
  163. yield removeSpacesAfter(equalToken, sourceCode, fixer);
  164. for (const currentNode of [node.parent, node]) {
  165. yield * removeParentheses(currentNode, fixer, sourceCode);
  166. }
  167. };
  168. }
  169. function fixNamedExport(node, sourceCode) {
  170. return function * (fixer) {
  171. const assignmentExpression = node.parent.parent;
  172. const exported = node.parent.property.name;
  173. const local = assignmentExpression.right.name;
  174. yield fixer.replaceText(assignmentExpression, `export {${local} as ${exported}}`);
  175. yield * removeParentheses(assignmentExpression, fixer, sourceCode);
  176. };
  177. }
  178. function fixExports(node, sourceCode) {
  179. // `exports = bar`
  180. if (isTopLevelAssignment(node)) {
  181. return fixDefaultExport(node, sourceCode);
  182. }
  183. // `exports.foo = bar`
  184. if (isNamedExport(node)) {
  185. return fixNamedExport(node, sourceCode);
  186. }
  187. }
  188. function fixModuleExports(node, sourceCode) {
  189. if (isModuleExports(node)) {
  190. return fixExports(node.parent, sourceCode);
  191. }
  192. }
  193. function create(context) {
  194. const filename = context.getFilename().toLowerCase();
  195. if (filename.endsWith('.cjs')) {
  196. return;
  197. }
  198. const sourceCode = context.getSourceCode();
  199. return {
  200. 'ExpressionStatement[directive="use strict"]'(node) {
  201. const problem = {node, messageId: ERROR_USE_STRICT_DIRECTIVE};
  202. const fix = function * (fixer) {
  203. yield fixer.remove(node);
  204. yield removeSpacesAfter(node, sourceCode, fixer);
  205. };
  206. if (filename.endsWith('.mjs')) {
  207. problem.fix = fix;
  208. } else {
  209. problem.suggest = [{messageId: SUGGESTION_USE_STRICT_DIRECTIVE, fix}];
  210. }
  211. return problem;
  212. },
  213. 'ReturnStatement:not(:function ReturnStatement)'(node) {
  214. return {
  215. node: sourceCode.getFirstToken(node),
  216. messageId: ERROR_GLOBAL_RETURN,
  217. };
  218. },
  219. [identifierSelector](node) {
  220. if (isShadowed(context.getScope(), node)) {
  221. return;
  222. }
  223. const {name} = node;
  224. const problem = {
  225. node,
  226. messageId: ERROR_IDENTIFIER,
  227. data: {name},
  228. };
  229. switch (name) {
  230. case '__filename':
  231. case '__dirname': {
  232. const messageId = node.name === '__dirname' ? SUGGESTION_DIRNAME : SUGGESTION_FILENAME;
  233. const replacement = node.name === '__dirname'
  234. ? 'path.dirname(url.fileURLToPath(import.meta.url))'
  235. : 'url.fileURLToPath(import.meta.url)';
  236. problem.suggest = [{
  237. messageId,
  238. fix: fixer => replaceReferenceIdentifier(node, replacement, fixer),
  239. }];
  240. return problem;
  241. }
  242. case 'require': {
  243. const fix = fixRequireCall(node, sourceCode);
  244. if (fix) {
  245. problem.suggest = [{
  246. messageId: SUGGESTION_IMPORT,
  247. fix,
  248. }];
  249. return problem;
  250. }
  251. break;
  252. }
  253. case 'exports': {
  254. const fix = fixExports(node, sourceCode);
  255. if (fix) {
  256. problem.suggest = [{
  257. messageId: SUGGESTION_EXPORT,
  258. fix,
  259. }];
  260. return problem;
  261. }
  262. break;
  263. }
  264. case 'module': {
  265. const fix = fixModuleExports(node, sourceCode);
  266. if (fix) {
  267. problem.suggest = [{
  268. messageId: SUGGESTION_EXPORT,
  269. fix,
  270. }];
  271. return problem;
  272. }
  273. break;
  274. }
  275. default:
  276. }
  277. return problem;
  278. },
  279. };
  280. }
  281. /** @type {import('eslint').Rule.RuleModule} */
  282. module.exports = {
  283. create,
  284. meta: {
  285. type: 'suggestion',
  286. docs: {
  287. description: 'Prefer JavaScript modules (ESM) over CommonJS.',
  288. },
  289. fixable: 'code',
  290. hasSuggestions: true,
  291. messages,
  292. },
  293. };