no-restricted-imports.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /**
  2. * @fileoverview Restrict usage of specified node imports.
  3. * @author Guy Ellis
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Rule Definition
  8. //------------------------------------------------------------------------------
  9. const ignore = require("ignore");
  10. const arrayOfStringsOrObjects = {
  11. type: "array",
  12. items: {
  13. anyOf: [
  14. { type: "string" },
  15. {
  16. type: "object",
  17. properties: {
  18. name: { type: "string" },
  19. message: {
  20. type: "string",
  21. minLength: 1
  22. },
  23. importNames: {
  24. type: "array",
  25. items: {
  26. type: "string"
  27. }
  28. }
  29. },
  30. additionalProperties: false,
  31. required: ["name"]
  32. }
  33. ]
  34. },
  35. uniqueItems: true
  36. };
  37. const arrayOfStringsOrObjectPatterns = {
  38. anyOf: [
  39. {
  40. type: "array",
  41. items: {
  42. type: "string"
  43. },
  44. uniqueItems: true
  45. },
  46. {
  47. type: "array",
  48. items: {
  49. type: "object",
  50. properties: {
  51. group: {
  52. type: "array",
  53. items: {
  54. type: "string"
  55. },
  56. minItems: 1,
  57. uniqueItems: true
  58. },
  59. message: {
  60. type: "string",
  61. minLength: 1
  62. }
  63. },
  64. additionalProperties: false,
  65. required: ["group"]
  66. },
  67. uniqueItems: true
  68. }
  69. ]
  70. };
  71. module.exports = {
  72. meta: {
  73. type: "suggestion",
  74. docs: {
  75. description: "disallow specified modules when loaded by `import`",
  76. category: "ECMAScript 6",
  77. recommended: false,
  78. url: "https://eslint.org/docs/rules/no-restricted-imports"
  79. },
  80. messages: {
  81. path: "'{{importSource}}' import is restricted from being used.",
  82. // eslint-disable-next-line eslint-plugin/report-message-format
  83. pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}",
  84. patterns: "'{{importSource}}' import is restricted from being used by a pattern.",
  85. // eslint-disable-next-line eslint-plugin/report-message-format
  86. patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}",
  87. everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
  88. // eslint-disable-next-line eslint-plugin/report-message-format
  89. everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}",
  90. importName: "'{{importName}}' import from '{{importSource}}' is restricted.",
  91. // eslint-disable-next-line eslint-plugin/report-message-format
  92. importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}"
  93. },
  94. schema: {
  95. anyOf: [
  96. arrayOfStringsOrObjects,
  97. {
  98. type: "array",
  99. items: [{
  100. type: "object",
  101. properties: {
  102. paths: arrayOfStringsOrObjects,
  103. patterns: arrayOfStringsOrObjectPatterns
  104. },
  105. additionalProperties: false
  106. }],
  107. additionalItems: false
  108. }
  109. ]
  110. }
  111. },
  112. create(context) {
  113. const sourceCode = context.getSourceCode();
  114. const options = Array.isArray(context.options) ? context.options : [];
  115. const isPathAndPatternsObject =
  116. typeof options[0] === "object" &&
  117. (Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns"));
  118. const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
  119. const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
  120. if (typeof importSource === "string") {
  121. memo[importSource] = { message: null };
  122. } else {
  123. memo[importSource.name] = {
  124. message: importSource.message,
  125. importNames: importSource.importNames
  126. };
  127. }
  128. return memo;
  129. }, {});
  130. // Handle patterns too, either as strings or groups
  131. const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
  132. const restrictedPatternGroups = restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string"
  133. ? [{ matcher: ignore().add(restrictedPatterns) }]
  134. : restrictedPatterns.map(({ group, message }) => ({ matcher: ignore().add(group), customMessage: message }));
  135. // if no imports are restricted we don"t need to check
  136. if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) {
  137. return {};
  138. }
  139. /**
  140. * Report a restricted path.
  141. * @param {string} importSource path of the import
  142. * @param {Map<string,Object[]>} importNames Map of import names that are being imported
  143. * @param {node} node representing the restricted path reference
  144. * @returns {void}
  145. * @private
  146. */
  147. function checkRestrictedPathAndReport(importSource, importNames, node) {
  148. if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
  149. return;
  150. }
  151. const customMessage = restrictedPathMessages[importSource].message;
  152. const restrictedImportNames = restrictedPathMessages[importSource].importNames;
  153. if (restrictedImportNames) {
  154. if (importNames.has("*")) {
  155. const specifierData = importNames.get("*")[0];
  156. context.report({
  157. node,
  158. messageId: customMessage ? "everythingWithCustomMessage" : "everything",
  159. loc: specifierData.loc,
  160. data: {
  161. importSource,
  162. importNames: restrictedImportNames,
  163. customMessage
  164. }
  165. });
  166. }
  167. restrictedImportNames.forEach(importName => {
  168. if (importNames.has(importName)) {
  169. const specifiers = importNames.get(importName);
  170. specifiers.forEach(specifier => {
  171. context.report({
  172. node,
  173. messageId: customMessage ? "importNameWithCustomMessage" : "importName",
  174. loc: specifier.loc,
  175. data: {
  176. importSource,
  177. customMessage,
  178. importName
  179. }
  180. });
  181. });
  182. }
  183. });
  184. } else {
  185. context.report({
  186. node,
  187. messageId: customMessage ? "pathWithCustomMessage" : "path",
  188. data: {
  189. importSource,
  190. customMessage
  191. }
  192. });
  193. }
  194. }
  195. /**
  196. * Report a restricted path specifically for patterns.
  197. * @param {node} node representing the restricted path reference
  198. * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails
  199. * @returns {void}
  200. * @private
  201. */
  202. function reportPathForPatterns(node, group) {
  203. const importSource = node.source.value.trim();
  204. context.report({
  205. node,
  206. messageId: group.customMessage ? "patternWithCustomMessage" : "patterns",
  207. data: {
  208. importSource,
  209. customMessage: group.customMessage
  210. }
  211. });
  212. }
  213. /**
  214. * Check if the given importSource is restricted by a pattern.
  215. * @param {string} importSource path of the import
  216. * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails
  217. * @returns {boolean} whether the variable is a restricted pattern or not
  218. * @private
  219. */
  220. function isRestrictedPattern(importSource, group) {
  221. return group.matcher.ignores(importSource);
  222. }
  223. /**
  224. * Checks a node to see if any problems should be reported.
  225. * @param {ASTNode} node The node to check.
  226. * @returns {void}
  227. * @private
  228. */
  229. function checkNode(node) {
  230. const importSource = node.source.value.trim();
  231. const importNames = new Map();
  232. if (node.type === "ExportAllDeclaration") {
  233. const starToken = sourceCode.getFirstToken(node, 1);
  234. importNames.set("*", [{ loc: starToken.loc }]);
  235. } else if (node.specifiers) {
  236. for (const specifier of node.specifiers) {
  237. let name;
  238. const specifierData = { loc: specifier.loc };
  239. if (specifier.type === "ImportDefaultSpecifier") {
  240. name = "default";
  241. } else if (specifier.type === "ImportNamespaceSpecifier") {
  242. name = "*";
  243. } else if (specifier.imported) {
  244. name = specifier.imported.name;
  245. } else if (specifier.local) {
  246. name = specifier.local.name;
  247. }
  248. if (name) {
  249. if (importNames.has(name)) {
  250. importNames.get(name).push(specifierData);
  251. } else {
  252. importNames.set(name, [specifierData]);
  253. }
  254. }
  255. }
  256. }
  257. checkRestrictedPathAndReport(importSource, importNames, node);
  258. restrictedPatternGroups.forEach(group => {
  259. if (isRestrictedPattern(importSource, group)) {
  260. reportPathForPatterns(node, group);
  261. }
  262. });
  263. }
  264. return {
  265. ImportDeclaration: checkNode,
  266. ExportNamedDeclaration(node) {
  267. if (node.source) {
  268. checkNode(node);
  269. }
  270. },
  271. ExportAllDeclaration: checkNode
  272. };
  273. }
  274. };