no-restricted-imports.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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. },
  34. additionalProperties: false,
  35. required: ["name"]
  36. }
  37. ]
  38. },
  39. uniqueItems: true
  40. };
  41. const arrayOfStringsOrObjectPatterns = {
  42. anyOf: [
  43. {
  44. type: "array",
  45. items: {
  46. type: "string"
  47. },
  48. uniqueItems: true
  49. },
  50. {
  51. type: "array",
  52. items: {
  53. type: "object",
  54. properties: {
  55. importNames: {
  56. type: "array",
  57. items: {
  58. type: "string"
  59. },
  60. minItems: 1,
  61. uniqueItems: true
  62. },
  63. group: {
  64. type: "array",
  65. items: {
  66. type: "string"
  67. },
  68. minItems: 1,
  69. uniqueItems: true
  70. },
  71. message: {
  72. type: "string",
  73. minLength: 1
  74. },
  75. caseSensitive: {
  76. type: "boolean"
  77. }
  78. },
  79. additionalProperties: false,
  80. required: ["group"]
  81. },
  82. uniqueItems: true
  83. }
  84. ]
  85. };
  86. /** @type {import('../shared/types').Rule} */
  87. module.exports = {
  88. meta: {
  89. type: "suggestion",
  90. docs: {
  91. description: "Disallow specified modules when loaded by `import`",
  92. recommended: false,
  93. url: "https://eslint.org/docs/rules/no-restricted-imports"
  94. },
  95. messages: {
  96. path: "'{{importSource}}' import is restricted from being used.",
  97. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  98. pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}",
  99. patterns: "'{{importSource}}' import is restricted from being used by a pattern.",
  100. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  101. patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}",
  102. patternAndImportName: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern.",
  103. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  104. patternAndImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
  105. patternAndEverything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern.",
  106. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  107. patternAndEverythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
  108. everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
  109. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  110. everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}",
  111. importName: "'{{importName}}' import from '{{importSource}}' is restricted.",
  112. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  113. importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}"
  114. },
  115. schema: {
  116. anyOf: [
  117. arrayOfStringsOrObjects,
  118. {
  119. type: "array",
  120. items: [{
  121. type: "object",
  122. properties: {
  123. paths: arrayOfStringsOrObjects,
  124. patterns: arrayOfStringsOrObjectPatterns
  125. },
  126. additionalProperties: false
  127. }],
  128. additionalItems: false
  129. }
  130. ]
  131. }
  132. },
  133. create(context) {
  134. const sourceCode = context.getSourceCode();
  135. const options = Array.isArray(context.options) ? context.options : [];
  136. const isPathAndPatternsObject =
  137. typeof options[0] === "object" &&
  138. (Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns"));
  139. const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
  140. const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
  141. if (typeof importSource === "string") {
  142. memo[importSource] = { message: null };
  143. } else {
  144. memo[importSource.name] = {
  145. message: importSource.message,
  146. importNames: importSource.importNames
  147. };
  148. }
  149. return memo;
  150. }, {});
  151. // Handle patterns too, either as strings or groups
  152. let restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
  153. // standardize to array of objects if we have an array of strings
  154. if (restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string") {
  155. restrictedPatterns = [{ group: restrictedPatterns }];
  156. }
  157. // relative paths are supported for this rule
  158. const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive, importNames }) => ({
  159. matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group),
  160. customMessage: message,
  161. importNames
  162. }));
  163. // if no imports are restricted we don't need to check
  164. if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) {
  165. return {};
  166. }
  167. /**
  168. * Report a restricted path.
  169. * @param {string} importSource path of the import
  170. * @param {Map<string,Object[]>} importNames Map of import names that are being imported
  171. * @param {node} node representing the restricted path reference
  172. * @returns {void}
  173. * @private
  174. */
  175. function checkRestrictedPathAndReport(importSource, importNames, node) {
  176. if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
  177. return;
  178. }
  179. const customMessage = restrictedPathMessages[importSource].message;
  180. const restrictedImportNames = restrictedPathMessages[importSource].importNames;
  181. if (restrictedImportNames) {
  182. if (importNames.has("*")) {
  183. const specifierData = importNames.get("*")[0];
  184. context.report({
  185. node,
  186. messageId: customMessage ? "everythingWithCustomMessage" : "everything",
  187. loc: specifierData.loc,
  188. data: {
  189. importSource,
  190. importNames: restrictedImportNames,
  191. customMessage
  192. }
  193. });
  194. }
  195. restrictedImportNames.forEach(importName => {
  196. if (importNames.has(importName)) {
  197. const specifiers = importNames.get(importName);
  198. specifiers.forEach(specifier => {
  199. context.report({
  200. node,
  201. messageId: customMessage ? "importNameWithCustomMessage" : "importName",
  202. loc: specifier.loc,
  203. data: {
  204. importSource,
  205. customMessage,
  206. importName
  207. }
  208. });
  209. });
  210. }
  211. });
  212. } else {
  213. context.report({
  214. node,
  215. messageId: customMessage ? "pathWithCustomMessage" : "path",
  216. data: {
  217. importSource,
  218. customMessage
  219. }
  220. });
  221. }
  222. }
  223. /**
  224. * Report a restricted path specifically for patterns.
  225. * @param {node} node representing the restricted path reference
  226. * @param {Object} group contains an Ignore instance for paths, the customMessage to show on failure,
  227. * and any restricted import names that have been specified in the config
  228. * @param {Map<string,Object[]>} importNames Map of import names that are being imported
  229. * @returns {void}
  230. * @private
  231. */
  232. function reportPathForPatterns(node, group, importNames) {
  233. const importSource = node.source.value.trim();
  234. const customMessage = group.customMessage;
  235. const restrictedImportNames = group.importNames;
  236. /*
  237. * If we are not restricting to any specific import names and just the pattern itself,
  238. * report the error and move on
  239. */
  240. if (!restrictedImportNames) {
  241. context.report({
  242. node,
  243. messageId: customMessage ? "patternWithCustomMessage" : "patterns",
  244. data: {
  245. importSource,
  246. customMessage
  247. }
  248. });
  249. return;
  250. }
  251. if (importNames.has("*")) {
  252. const specifierData = importNames.get("*")[0];
  253. context.report({
  254. node,
  255. messageId: customMessage ? "patternAndEverythingWithCustomMessage" : "patternAndEverything",
  256. loc: specifierData.loc,
  257. data: {
  258. importSource,
  259. importNames: restrictedImportNames,
  260. customMessage
  261. }
  262. });
  263. }
  264. restrictedImportNames.forEach(importName => {
  265. if (!importNames.has(importName)) {
  266. return;
  267. }
  268. const specifiers = importNames.get(importName);
  269. specifiers.forEach(specifier => {
  270. context.report({
  271. node,
  272. messageId: customMessage ? "patternAndImportNameWithCustomMessage" : "patternAndImportName",
  273. loc: specifier.loc,
  274. data: {
  275. importSource,
  276. customMessage,
  277. importName
  278. }
  279. });
  280. });
  281. });
  282. }
  283. /**
  284. * Check if the given importSource is restricted by a pattern.
  285. * @param {string} importSource path of the import
  286. * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails
  287. * @returns {boolean} whether the variable is a restricted pattern or not
  288. * @private
  289. */
  290. function isRestrictedPattern(importSource, group) {
  291. return group.matcher.ignores(importSource);
  292. }
  293. /**
  294. * Checks a node to see if any problems should be reported.
  295. * @param {ASTNode} node The node to check.
  296. * @returns {void}
  297. * @private
  298. */
  299. function checkNode(node) {
  300. const importSource = node.source.value.trim();
  301. const importNames = new Map();
  302. if (node.type === "ExportAllDeclaration") {
  303. const starToken = sourceCode.getFirstToken(node, 1);
  304. importNames.set("*", [{ loc: starToken.loc }]);
  305. } else if (node.specifiers) {
  306. for (const specifier of node.specifiers) {
  307. let name;
  308. const specifierData = { loc: specifier.loc };
  309. if (specifier.type === "ImportDefaultSpecifier") {
  310. name = "default";
  311. } else if (specifier.type === "ImportNamespaceSpecifier") {
  312. name = "*";
  313. } else if (specifier.imported) {
  314. name = astUtils.getModuleExportName(specifier.imported);
  315. } else if (specifier.local) {
  316. name = astUtils.getModuleExportName(specifier.local);
  317. }
  318. if (typeof name === "string") {
  319. if (importNames.has(name)) {
  320. importNames.get(name).push(specifierData);
  321. } else {
  322. importNames.set(name, [specifierData]);
  323. }
  324. }
  325. }
  326. }
  327. checkRestrictedPathAndReport(importSource, importNames, node);
  328. restrictedPatternGroups.forEach(group => {
  329. if (isRestrictedPattern(importSource, group)) {
  330. reportPathForPatterns(node, group, importNames);
  331. }
  332. });
  333. }
  334. return {
  335. ImportDeclaration: checkNode,
  336. ExportNamedDeclaration(node) {
  337. if (node.source) {
  338. checkNode(node);
  339. }
  340. },
  341. ExportAllDeclaration: checkNode
  342. };
  343. }
  344. };