no-restricted-imports.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. /**
  2. * @fileoverview Restrict usage of specified node imports.
  3. * @author Guy Ellis
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. const ignore = require("ignore");
  14. const arrayOfStringsOrObjects = {
  15. type: "array",
  16. items: {
  17. anyOf: [
  18. { type: "string" },
  19. {
  20. type: "object",
  21. properties: {
  22. name: { type: "string" },
  23. message: {
  24. type: "string",
  25. minLength: 1
  26. },
  27. importNames: {
  28. type: "array",
  29. items: {
  30. type: "string"
  31. }
  32. },
  33. allowImportNames: {
  34. type: "array",
  35. items: {
  36. type: "string"
  37. }
  38. }
  39. },
  40. additionalProperties: false,
  41. required: ["name"],
  42. not: { required: ["importNames", "allowImportNames"] }
  43. }
  44. ]
  45. },
  46. uniqueItems: true
  47. };
  48. const arrayOfStringsOrObjectPatterns = {
  49. anyOf: [
  50. {
  51. type: "array",
  52. items: {
  53. type: "string"
  54. },
  55. uniqueItems: true
  56. },
  57. {
  58. type: "array",
  59. items: {
  60. type: "object",
  61. properties: {
  62. importNames: {
  63. type: "array",
  64. items: {
  65. type: "string"
  66. },
  67. minItems: 1,
  68. uniqueItems: true
  69. },
  70. allowImportNames: {
  71. type: "array",
  72. items: {
  73. type: "string"
  74. },
  75. minItems: 1,
  76. uniqueItems: true
  77. },
  78. group: {
  79. type: "array",
  80. items: {
  81. type: "string"
  82. },
  83. minItems: 1,
  84. uniqueItems: true
  85. },
  86. regex: {
  87. type: "string"
  88. },
  89. importNamePattern: {
  90. type: "string"
  91. },
  92. allowImportNamePattern: {
  93. type: "string"
  94. },
  95. message: {
  96. type: "string",
  97. minLength: 1
  98. },
  99. caseSensitive: {
  100. type: "boolean"
  101. }
  102. },
  103. additionalProperties: false,
  104. not: {
  105. anyOf: [
  106. { required: ["importNames", "allowImportNames"] },
  107. { required: ["importNamePattern", "allowImportNamePattern"] },
  108. { required: ["importNames", "allowImportNamePattern"] },
  109. { required: ["importNamePattern", "allowImportNames"] },
  110. { required: ["allowImportNames", "allowImportNamePattern"] }
  111. ]
  112. },
  113. oneOf: [
  114. { required: ["group"] },
  115. { required: ["regex"] }
  116. ]
  117. },
  118. uniqueItems: true
  119. }
  120. ]
  121. };
  122. /** @type {import('../shared/types').Rule} */
  123. module.exports = {
  124. meta: {
  125. type: "suggestion",
  126. docs: {
  127. description: "Disallow specified modules when loaded by `import`",
  128. recommended: false,
  129. url: "https://eslint.org/docs/latest/rules/no-restricted-imports"
  130. },
  131. messages: {
  132. path: "'{{importSource}}' import is restricted from being used.",
  133. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  134. pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}",
  135. patterns: "'{{importSource}}' import is restricted from being used by a pattern.",
  136. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  137. patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}",
  138. patternAndImportName: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern.",
  139. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  140. patternAndImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
  141. patternAndEverything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern.",
  142. patternAndEverythingWithRegexImportName: "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used.",
  143. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  144. patternAndEverythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
  145. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  146. patternAndEverythingWithRegexImportNameAndCustomMessage: "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used. {{customMessage}}",
  147. everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
  148. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  149. everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}",
  150. importName: "'{{importName}}' import from '{{importSource}}' is restricted.",
  151. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  152. importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}",
  153. allowedImportName: "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed.",
  154. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  155. allowedImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed. {{customMessage}}",
  156. everythingWithAllowImportNames: "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed.",
  157. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  158. everythingWithAllowImportNamesAndCustomMessage: "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed. {{customMessage}}",
  159. allowedImportNamePattern: "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'.",
  160. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  161. allowedImportNamePatternWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'. {{customMessage}}",
  162. everythingWithAllowedImportNamePattern: "* import is invalid because only imports that match the pattern '{{allowedImportNamePattern}}' from '{{importSource}}' are allowed.",
  163. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  164. everythingWithAllowedImportNamePatternWithCustomMessage: "* import is invalid because only imports that match the pattern '{{allowedImportNamePattern}}' from '{{importSource}}' are allowed. {{customMessage}}"
  165. },
  166. schema: {
  167. anyOf: [
  168. arrayOfStringsOrObjects,
  169. {
  170. type: "array",
  171. items: [{
  172. type: "object",
  173. properties: {
  174. paths: arrayOfStringsOrObjects,
  175. patterns: arrayOfStringsOrObjectPatterns
  176. },
  177. additionalProperties: false
  178. }],
  179. additionalItems: false
  180. }
  181. ]
  182. }
  183. },
  184. create(context) {
  185. const sourceCode = context.sourceCode;
  186. const options = Array.isArray(context.options) ? context.options : [];
  187. const isPathAndPatternsObject =
  188. typeof options[0] === "object" &&
  189. (Object.hasOwn(options[0], "paths") || Object.hasOwn(options[0], "patterns"));
  190. const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
  191. const groupedRestrictedPaths = restrictedPaths.reduce((memo, importSource) => {
  192. const path = typeof importSource === "string"
  193. ? importSource
  194. : importSource.name;
  195. if (!memo[path]) {
  196. memo[path] = [];
  197. }
  198. if (typeof importSource === "string") {
  199. memo[path].push({});
  200. } else {
  201. memo[path].push({
  202. message: importSource.message,
  203. importNames: importSource.importNames,
  204. allowImportNames: importSource.allowImportNames
  205. });
  206. }
  207. return memo;
  208. }, Object.create(null));
  209. // Handle patterns too, either as strings or groups
  210. let restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
  211. // standardize to array of objects if we have an array of strings
  212. if (restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string") {
  213. restrictedPatterns = [{ group: restrictedPatterns }];
  214. }
  215. // relative paths are supported for this rule
  216. const restrictedPatternGroups = restrictedPatterns.map(
  217. ({ group, regex, message, caseSensitive, importNames, importNamePattern, allowImportNames, allowImportNamePattern }) => (
  218. {
  219. ...(group ? { matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group) } : {}),
  220. ...(typeof regex === "string" ? { regexMatcher: new RegExp(regex, caseSensitive ? "u" : "iu") } : {}),
  221. customMessage: message,
  222. importNames,
  223. importNamePattern,
  224. allowImportNames,
  225. allowImportNamePattern
  226. }
  227. )
  228. );
  229. // if no imports are restricted we don't need to check
  230. if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) {
  231. return {};
  232. }
  233. /**
  234. * Report a restricted path.
  235. * @param {string} importSource path of the import
  236. * @param {Map<string,Object[]>} importNames Map of import names that are being imported
  237. * @param {node} node representing the restricted path reference
  238. * @returns {void}
  239. * @private
  240. */
  241. function checkRestrictedPathAndReport(importSource, importNames, node) {
  242. if (!Object.hasOwn(groupedRestrictedPaths, importSource)) {
  243. return;
  244. }
  245. groupedRestrictedPaths[importSource].forEach(restrictedPathEntry => {
  246. const customMessage = restrictedPathEntry.message;
  247. const restrictedImportNames = restrictedPathEntry.importNames;
  248. const allowedImportNames = restrictedPathEntry.allowImportNames;
  249. if (!restrictedImportNames && !allowedImportNames) {
  250. context.report({
  251. node,
  252. messageId: customMessage ? "pathWithCustomMessage" : "path",
  253. data: {
  254. importSource,
  255. customMessage
  256. }
  257. });
  258. return;
  259. }
  260. importNames.forEach((specifiers, importName) => {
  261. if (importName === "*") {
  262. const [specifier] = specifiers;
  263. if (restrictedImportNames) {
  264. context.report({
  265. node,
  266. messageId: customMessage ? "everythingWithCustomMessage" : "everything",
  267. loc: specifier.loc,
  268. data: {
  269. importSource,
  270. importNames: restrictedImportNames,
  271. customMessage
  272. }
  273. });
  274. } else if (allowedImportNames) {
  275. context.report({
  276. node,
  277. messageId: customMessage ? "everythingWithAllowImportNamesAndCustomMessage" : "everythingWithAllowImportNames",
  278. loc: specifier.loc,
  279. data: {
  280. importSource,
  281. allowedImportNames,
  282. customMessage
  283. }
  284. });
  285. }
  286. return;
  287. }
  288. if (restrictedImportNames && restrictedImportNames.includes(importName)) {
  289. specifiers.forEach(specifier => {
  290. context.report({
  291. node,
  292. messageId: customMessage ? "importNameWithCustomMessage" : "importName",
  293. loc: specifier.loc,
  294. data: {
  295. importSource,
  296. customMessage,
  297. importName
  298. }
  299. });
  300. });
  301. }
  302. if (allowedImportNames && !allowedImportNames.includes(importName)) {
  303. specifiers.forEach(specifier => {
  304. context.report({
  305. node,
  306. loc: specifier.loc,
  307. messageId: customMessage ? "allowedImportNameWithCustomMessage" : "allowedImportName",
  308. data: {
  309. importSource,
  310. customMessage,
  311. importName,
  312. allowedImportNames
  313. }
  314. });
  315. });
  316. }
  317. });
  318. });
  319. }
  320. /**
  321. * Report a restricted path specifically for patterns.
  322. * @param {node} node representing the restricted path reference
  323. * @param {Object} group contains an Ignore instance for paths, the customMessage to show on failure,
  324. * and any restricted import names that have been specified in the config
  325. * @param {Map<string,Object[]>} importNames Map of import names that are being imported
  326. * @returns {void}
  327. * @private
  328. */
  329. function reportPathForPatterns(node, group, importNames) {
  330. const importSource = node.source.value.trim();
  331. const customMessage = group.customMessage;
  332. const restrictedImportNames = group.importNames;
  333. const restrictedImportNamePattern = group.importNamePattern ? new RegExp(group.importNamePattern, "u") : null;
  334. const allowedImportNames = group.allowImportNames;
  335. const allowedImportNamePattern = group.allowImportNamePattern ? new RegExp(group.allowImportNamePattern, "u") : null;
  336. /**
  337. * If we are not restricting to any specific import names and just the pattern itself,
  338. * report the error and move on
  339. */
  340. if (!restrictedImportNames && !allowedImportNames && !restrictedImportNamePattern && !allowedImportNamePattern) {
  341. context.report({
  342. node,
  343. messageId: customMessage ? "patternWithCustomMessage" : "patterns",
  344. data: {
  345. importSource,
  346. customMessage
  347. }
  348. });
  349. return;
  350. }
  351. importNames.forEach((specifiers, importName) => {
  352. if (importName === "*") {
  353. const [specifier] = specifiers;
  354. if (restrictedImportNames) {
  355. context.report({
  356. node,
  357. messageId: customMessage ? "patternAndEverythingWithCustomMessage" : "patternAndEverything",
  358. loc: specifier.loc,
  359. data: {
  360. importSource,
  361. importNames: restrictedImportNames,
  362. customMessage
  363. }
  364. });
  365. } else if (allowedImportNames) {
  366. context.report({
  367. node,
  368. messageId: customMessage ? "everythingWithAllowImportNamesAndCustomMessage" : "everythingWithAllowImportNames",
  369. loc: specifier.loc,
  370. data: {
  371. importSource,
  372. allowedImportNames,
  373. customMessage
  374. }
  375. });
  376. } else if (allowedImportNamePattern) {
  377. context.report({
  378. node,
  379. messageId: customMessage ? "everythingWithAllowedImportNamePatternWithCustomMessage" : "everythingWithAllowedImportNamePattern",
  380. loc: specifier.loc,
  381. data: {
  382. importSource,
  383. allowedImportNamePattern,
  384. customMessage
  385. }
  386. });
  387. } else {
  388. context.report({
  389. node,
  390. messageId: customMessage ? "patternAndEverythingWithRegexImportNameAndCustomMessage" : "patternAndEverythingWithRegexImportName",
  391. loc: specifier.loc,
  392. data: {
  393. importSource,
  394. importNames: restrictedImportNamePattern,
  395. customMessage
  396. }
  397. });
  398. }
  399. return;
  400. }
  401. if (
  402. (restrictedImportNames && restrictedImportNames.includes(importName)) ||
  403. (restrictedImportNamePattern && restrictedImportNamePattern.test(importName))
  404. ) {
  405. specifiers.forEach(specifier => {
  406. context.report({
  407. node,
  408. messageId: customMessage ? "patternAndImportNameWithCustomMessage" : "patternAndImportName",
  409. loc: specifier.loc,
  410. data: {
  411. importSource,
  412. customMessage,
  413. importName
  414. }
  415. });
  416. });
  417. }
  418. if (allowedImportNames && !allowedImportNames.includes(importName)) {
  419. specifiers.forEach(specifier => {
  420. context.report({
  421. node,
  422. messageId: customMessage ? "allowedImportNameWithCustomMessage" : "allowedImportName",
  423. loc: specifier.loc,
  424. data: {
  425. importSource,
  426. customMessage,
  427. importName,
  428. allowedImportNames
  429. }
  430. });
  431. });
  432. } else if (allowedImportNamePattern && !allowedImportNamePattern.test(importName)) {
  433. specifiers.forEach(specifier => {
  434. context.report({
  435. node,
  436. messageId: customMessage ? "allowedImportNamePatternWithCustomMessage" : "allowedImportNamePattern",
  437. loc: specifier.loc,
  438. data: {
  439. importSource,
  440. customMessage,
  441. importName,
  442. allowedImportNamePattern
  443. }
  444. });
  445. });
  446. }
  447. });
  448. }
  449. /**
  450. * Check if the given importSource is restricted by a pattern.
  451. * @param {string} importSource path of the import
  452. * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails
  453. * @returns {boolean} whether the variable is a restricted pattern or not
  454. * @private
  455. */
  456. function isRestrictedPattern(importSource, group) {
  457. return group.regexMatcher ? group.regexMatcher.test(importSource) : group.matcher.ignores(importSource);
  458. }
  459. /**
  460. * Checks a node to see if any problems should be reported.
  461. * @param {ASTNode} node The node to check.
  462. * @returns {void}
  463. * @private
  464. */
  465. function checkNode(node) {
  466. const importSource = node.source.value.trim();
  467. const importNames = new Map();
  468. if (node.type === "ExportAllDeclaration") {
  469. const starToken = sourceCode.getFirstToken(node, 1);
  470. importNames.set("*", [{ loc: starToken.loc }]);
  471. } else if (node.specifiers) {
  472. for (const specifier of node.specifiers) {
  473. let name;
  474. const specifierData = { loc: specifier.loc };
  475. if (specifier.type === "ImportDefaultSpecifier") {
  476. name = "default";
  477. } else if (specifier.type === "ImportNamespaceSpecifier") {
  478. name = "*";
  479. } else if (specifier.imported) {
  480. name = astUtils.getModuleExportName(specifier.imported);
  481. } else if (specifier.local) {
  482. name = astUtils.getModuleExportName(specifier.local);
  483. }
  484. if (typeof name === "string") {
  485. if (importNames.has(name)) {
  486. importNames.get(name).push(specifierData);
  487. } else {
  488. importNames.set(name, [specifierData]);
  489. }
  490. }
  491. }
  492. }
  493. checkRestrictedPathAndReport(importSource, importNames, node);
  494. restrictedPatternGroups.forEach(group => {
  495. if (isRestrictedPattern(importSource, group)) {
  496. reportPathForPatterns(node, group, importNames);
  497. }
  498. });
  499. }
  500. return {
  501. ImportDeclaration: checkNode,
  502. ExportNamedDeclaration(node) {
  503. if (node.source) {
  504. checkNode(node);
  505. }
  506. },
  507. ExportAllDeclaration: checkNode
  508. };
  509. }
  510. };