no-restricted-imports.js 10 KB

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