import-style.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. 'use strict';
  2. const {defaultsDeep} = require('lodash');
  3. const {getStringIfConstant} = require('eslint-utils');
  4. const eslintTemplateVisitor = require('eslint-template-visitor');
  5. const {callExpressionSelector} = require('./selectors/index.js');
  6. const MESSAGE_ID = 'importStyle';
  7. const messages = {
  8. [MESSAGE_ID]: 'Use {{allowedStyles}} import for module `{{moduleName}}`.',
  9. };
  10. const getActualImportDeclarationStyles = importDeclaration => {
  11. const {specifiers} = importDeclaration;
  12. if (specifiers.length === 0) {
  13. return ['unassigned'];
  14. }
  15. const styles = new Set();
  16. for (const specifier of specifiers) {
  17. if (specifier.type === 'ImportDefaultSpecifier') {
  18. styles.add('default');
  19. continue;
  20. }
  21. if (specifier.type === 'ImportNamespaceSpecifier') {
  22. styles.add('namespace');
  23. continue;
  24. }
  25. if (specifier.type === 'ImportSpecifier') {
  26. if (specifier.imported.type === 'Identifier' && specifier.imported.name === 'default') {
  27. styles.add('default');
  28. continue;
  29. }
  30. styles.add('named');
  31. continue;
  32. }
  33. }
  34. return [...styles];
  35. };
  36. const getActualExportDeclarationStyles = exportDeclaration => {
  37. const {specifiers} = exportDeclaration;
  38. if (specifiers.length === 0) {
  39. return ['unassigned'];
  40. }
  41. const styles = new Set();
  42. for (const specifier of specifiers) {
  43. if (specifier.type === 'ExportSpecifier') {
  44. if (specifier.exported.type === 'Identifier' && specifier.exported.name === 'default') {
  45. styles.add('default');
  46. continue;
  47. }
  48. styles.add('named');
  49. continue;
  50. }
  51. }
  52. return [...styles];
  53. };
  54. const getActualAssignmentTargetImportStyles = assignmentTarget => {
  55. if (assignmentTarget.type === 'Identifier' || assignmentTarget.type === 'ArrayPattern') {
  56. return ['namespace'];
  57. }
  58. if (assignmentTarget.type === 'ObjectPattern') {
  59. if (assignmentTarget.properties.length === 0) {
  60. return ['unassigned'];
  61. }
  62. const styles = new Set();
  63. for (const property of assignmentTarget.properties) {
  64. if (property.type === 'RestElement') {
  65. styles.add('named');
  66. continue;
  67. }
  68. if (property.key.type === 'Identifier') {
  69. if (property.key.name === 'default') {
  70. styles.add('default');
  71. } else {
  72. styles.add('named');
  73. }
  74. }
  75. }
  76. return [...styles];
  77. }
  78. // Next line is not test-coverable until unforceable changes to the language
  79. // like an addition of new AST node types usable in `const __HERE__ = foo;`.
  80. // An exotic custom parser or a bug in one could cover it too.
  81. /* istanbul ignore next */
  82. return [];
  83. };
  84. const joinOr = words => words
  85. .map((word, index) => {
  86. if (index === words.length - 1) {
  87. return word;
  88. }
  89. if (index === words.length - 2) {
  90. return word + ' or';
  91. }
  92. return word + ',';
  93. })
  94. .join(' ');
  95. // Keep this alphabetically sorted for easier maintenance
  96. const defaultStyles = {
  97. chalk: {
  98. default: true,
  99. },
  100. path: {
  101. default: true,
  102. },
  103. util: {
  104. named: true,
  105. },
  106. };
  107. const templates = eslintTemplateVisitor({
  108. parserOptions: {
  109. sourceType: 'module',
  110. ecmaVersion: 2018,
  111. },
  112. });
  113. const variableDeclarationVariable = templates.variableDeclarationVariable();
  114. const assignmentTargetVariable = templates.variable();
  115. const moduleNameVariable = templates.variable();
  116. const assignedDynamicImportTemplate = templates.template`async () => {
  117. ${variableDeclarationVariable} ${assignmentTargetVariable} = await import(${moduleNameVariable});
  118. }`.narrow('BlockStatement > :has(AwaitExpression)');
  119. const assignedRequireTemplate = templates.template`
  120. ${variableDeclarationVariable} ${assignmentTargetVariable} = require(${moduleNameVariable});
  121. `;
  122. /** @param {import('eslint').Rule.RuleContext} context */
  123. const create = context => {
  124. let [
  125. {
  126. styles = {},
  127. extendDefaultStyles = true,
  128. checkImport = true,
  129. checkDynamicImport = true,
  130. checkExportFrom = false,
  131. checkRequire = true,
  132. } = {},
  133. ] = context.options;
  134. styles = extendDefaultStyles
  135. ? defaultsDeep({}, styles, defaultStyles)
  136. : styles;
  137. styles = new Map(
  138. Object.entries(styles).map(
  139. ([moduleName, styles]) =>
  140. [moduleName, new Set(Object.entries(styles).filter(([, isAllowed]) => isAllowed).map(([style]) => style))],
  141. ),
  142. );
  143. const report = (node, moduleName, actualImportStyles, allowedImportStyles, isRequire = false) => {
  144. if (!allowedImportStyles || allowedImportStyles.size === 0) {
  145. return;
  146. }
  147. let effectiveAllowedImportStyles = allowedImportStyles;
  148. // For `require`, `'default'` style allows both `x = require('x')` (`'namespace'` style) and
  149. // `{default: x} = require('x')` (`'default'` style) since we don't know in advance
  150. // whether `'x'` is a compiled ES6 module (with `default` key) or a CommonJS module and `require`
  151. // does not provide any automatic interop for this, so the user may have to use either of these.
  152. if (isRequire && allowedImportStyles.has('default') && !allowedImportStyles.has('namespace')) {
  153. effectiveAllowedImportStyles = new Set(allowedImportStyles);
  154. effectiveAllowedImportStyles.add('namespace');
  155. }
  156. if (actualImportStyles.every(style => effectiveAllowedImportStyles.has(style))) {
  157. return;
  158. }
  159. const data = {
  160. allowedStyles: joinOr([...allowedImportStyles.keys()]),
  161. moduleName,
  162. };
  163. context.report({
  164. node,
  165. messageId: MESSAGE_ID,
  166. data,
  167. });
  168. };
  169. let visitor = {};
  170. if (checkImport) {
  171. visitor = {
  172. ...visitor,
  173. ImportDeclaration(node) {
  174. const moduleName = getStringIfConstant(node.source, context.getScope());
  175. const allowedImportStyles = styles.get(moduleName);
  176. const actualImportStyles = getActualImportDeclarationStyles(node);
  177. report(node, moduleName, actualImportStyles, allowedImportStyles);
  178. },
  179. };
  180. }
  181. if (checkDynamicImport) {
  182. visitor = {
  183. ...visitor,
  184. 'ExpressionStatement > ImportExpression'(node) {
  185. const moduleName = getStringIfConstant(node.source, context.getScope());
  186. const allowedImportStyles = styles.get(moduleName);
  187. const actualImportStyles = ['unassigned'];
  188. report(node, moduleName, actualImportStyles, allowedImportStyles);
  189. },
  190. [assignedDynamicImportTemplate](node) {
  191. const assignmentTargetNode = assignedDynamicImportTemplate.context.getMatch(assignmentTargetVariable);
  192. const moduleNameNode = assignedDynamicImportTemplate.context.getMatch(moduleNameVariable);
  193. const moduleName = getStringIfConstant(moduleNameNode, context.getScope());
  194. if (!moduleName) {
  195. return;
  196. }
  197. const allowedImportStyles = styles.get(moduleName);
  198. const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
  199. report(node, moduleName, actualImportStyles, allowedImportStyles);
  200. },
  201. };
  202. }
  203. if (checkExportFrom) {
  204. visitor = {
  205. ...visitor,
  206. ExportAllDeclaration(node) {
  207. const moduleName = getStringIfConstant(node.source, context.getScope());
  208. const allowedImportStyles = styles.get(moduleName);
  209. const actualImportStyles = ['namespace'];
  210. report(node, moduleName, actualImportStyles, allowedImportStyles);
  211. },
  212. ExportNamedDeclaration(node) {
  213. const moduleName = getStringIfConstant(node.source, context.getScope());
  214. const allowedImportStyles = styles.get(moduleName);
  215. const actualImportStyles = getActualExportDeclarationStyles(node);
  216. report(node, moduleName, actualImportStyles, allowedImportStyles);
  217. },
  218. };
  219. }
  220. if (checkRequire) {
  221. visitor = {
  222. ...visitor,
  223. [`ExpressionStatement > ${callExpressionSelector({name: 'require', argumentsLength: 1})}.expression`](node) {
  224. const moduleName = getStringIfConstant(node.arguments[0], context.getScope());
  225. const allowedImportStyles = styles.get(moduleName);
  226. const actualImportStyles = ['unassigned'];
  227. report(node, moduleName, actualImportStyles, allowedImportStyles, true);
  228. },
  229. [assignedRequireTemplate](node) {
  230. const assignmentTargetNode = assignedRequireTemplate.context.getMatch(assignmentTargetVariable);
  231. const moduleNameNode = assignedRequireTemplate.context.getMatch(moduleNameVariable);
  232. const moduleName = getStringIfConstant(moduleNameNode, context.getScope());
  233. if (!moduleName) {
  234. return;
  235. }
  236. const allowedImportStyles = styles.get(moduleName);
  237. const actualImportStyles = getActualAssignmentTargetImportStyles(assignmentTargetNode);
  238. report(node, moduleName, actualImportStyles, allowedImportStyles, true);
  239. },
  240. };
  241. }
  242. return templates.visitor(visitor);
  243. };
  244. const schema = {
  245. type: 'array',
  246. additionalItems: false,
  247. items: [
  248. {
  249. type: 'object',
  250. additionalProperties: false,
  251. properties: {
  252. checkImport: {
  253. type: 'boolean',
  254. },
  255. checkDynamicImport: {
  256. type: 'boolean',
  257. },
  258. checkExportFrom: {
  259. type: 'boolean',
  260. },
  261. checkRequire: {
  262. type: 'boolean',
  263. },
  264. extendDefaultStyles: {
  265. type: 'boolean',
  266. },
  267. styles: {
  268. $ref: '#/definitions/moduleStyles',
  269. },
  270. },
  271. },
  272. ],
  273. definitions: {
  274. moduleStyles: {
  275. type: 'object',
  276. additionalProperties: {
  277. $ref: '#/definitions/styles',
  278. },
  279. },
  280. styles: {
  281. anyOf: [
  282. {
  283. enum: [
  284. false,
  285. ],
  286. },
  287. {
  288. $ref: '#/definitions/booleanObject',
  289. },
  290. ],
  291. },
  292. booleanObject: {
  293. type: 'object',
  294. additionalProperties: {
  295. type: 'boolean',
  296. },
  297. },
  298. },
  299. };
  300. /** @type {import('eslint').Rule.RuleModule} */
  301. module.exports = {
  302. create,
  303. meta: {
  304. type: 'problem',
  305. docs: {
  306. description: 'Enforce specific import styles per module.',
  307. },
  308. schema,
  309. messages,
  310. },
  311. };