no-restricted-globals.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /**
  2. * @fileoverview Restrict usage of specified globals.
  3. * @author Benoît Zugmeyer
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. const TYPE_NODES = new Set([
  14. "TSTypeReference",
  15. "TSInterfaceHeritage",
  16. "TSClassImplements",
  17. "TSTypeQuery",
  18. "TSQualifiedName",
  19. ]);
  20. const GLOBAL_OBJECTS = new Set(["globalThis", "self", "window"]);
  21. //------------------------------------------------------------------------------
  22. // Rule Definition
  23. //------------------------------------------------------------------------------
  24. const arrayOfGlobals = {
  25. type: "array",
  26. items: {
  27. oneOf: [
  28. {
  29. type: "string",
  30. },
  31. {
  32. type: "object",
  33. properties: {
  34. name: { type: "string" },
  35. message: { type: "string" },
  36. },
  37. required: ["name"],
  38. additionalProperties: false,
  39. },
  40. ],
  41. },
  42. uniqueItems: true,
  43. minItems: 0,
  44. };
  45. /** @type {import('../types').Rule.RuleModule} */
  46. module.exports = {
  47. meta: {
  48. dialects: ["javascript", "typescript"],
  49. language: "javascript",
  50. type: "suggestion",
  51. docs: {
  52. description: "Disallow specified global variables",
  53. recommended: false,
  54. url: "https://eslint.org/docs/latest/rules/no-restricted-globals",
  55. },
  56. schema: {
  57. anyOf: [
  58. arrayOfGlobals,
  59. {
  60. type: "array",
  61. items: [
  62. {
  63. type: "object",
  64. properties: {
  65. globals: arrayOfGlobals,
  66. checkGlobalObject: {
  67. type: "boolean",
  68. },
  69. globalObjects: {
  70. type: "array",
  71. items: {
  72. type: "string",
  73. },
  74. uniqueItems: true,
  75. },
  76. },
  77. required: ["globals"],
  78. additionalProperties: false,
  79. },
  80. ],
  81. additionalItems: false,
  82. },
  83. ],
  84. },
  85. messages: {
  86. defaultMessage: "Unexpected use of '{{name}}'.",
  87. // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
  88. customMessage: "Unexpected use of '{{name}}'. {{customMessage}}",
  89. },
  90. },
  91. create(context) {
  92. const { sourceCode, options } = context;
  93. const isGlobalsObject =
  94. typeof options[0] === "object" &&
  95. Object.hasOwn(options[0], "globals");
  96. const restrictedGlobals = isGlobalsObject
  97. ? options[0].globals
  98. : options;
  99. const checkGlobalObject = isGlobalsObject
  100. ? options[0].checkGlobalObject
  101. : false;
  102. const userGlobalObjects = isGlobalsObject
  103. ? options[0].globalObjects || []
  104. : [];
  105. const globalObjects = new Set([
  106. ...GLOBAL_OBJECTS,
  107. ...userGlobalObjects,
  108. ]);
  109. // If no globals are restricted, we don't need to do anything
  110. if (restrictedGlobals.length === 0) {
  111. return {};
  112. }
  113. const restrictedGlobalMessages = restrictedGlobals.reduce(
  114. (memo, option) => {
  115. if (typeof option === "string") {
  116. memo[option] = null;
  117. } else {
  118. memo[option.name] = option.message;
  119. }
  120. return memo;
  121. },
  122. {},
  123. );
  124. /**
  125. * Report a variable to be used as a restricted global.
  126. * @param {Reference} reference the variable reference
  127. * @returns {void}
  128. * @private
  129. */
  130. function reportReference(reference) {
  131. const name = reference.identifier.name,
  132. customMessage = restrictedGlobalMessages[name],
  133. messageId = customMessage ? "customMessage" : "defaultMessage";
  134. context.report({
  135. node: reference.identifier,
  136. messageId,
  137. data: {
  138. name,
  139. customMessage,
  140. },
  141. });
  142. }
  143. /**
  144. * Check if the given name is a restricted global name.
  145. * @param {string} name name of a variable
  146. * @returns {boolean} whether the variable is a restricted global or not
  147. * @private
  148. */
  149. function isRestricted(name) {
  150. return Object.hasOwn(restrictedGlobalMessages, name);
  151. }
  152. /**
  153. * Check if the given reference occurs within a TypeScript type context.
  154. * @param {Reference} reference The variable reference to check.
  155. * @returns {boolean} Whether the reference is in a type context.
  156. * @private
  157. */
  158. function isInTypeContext(reference) {
  159. const parent = reference.identifier.parent;
  160. return TYPE_NODES.has(parent.type);
  161. }
  162. return {
  163. Program(node) {
  164. const scope = sourceCode.getScope(node);
  165. // Report variables declared elsewhere (ex: variables defined as "global" by eslint)
  166. scope.variables.forEach(variable => {
  167. if (!variable.defs.length && isRestricted(variable.name)) {
  168. variable.references.forEach(reference => {
  169. if (!isInTypeContext(reference)) {
  170. reportReference(reference);
  171. }
  172. });
  173. }
  174. });
  175. // Report variables not declared at all
  176. scope.through.forEach(reference => {
  177. if (
  178. isRestricted(reference.identifier.name) &&
  179. !isInTypeContext(reference)
  180. ) {
  181. reportReference(reference);
  182. }
  183. });
  184. },
  185. "Program:exit"(node) {
  186. if (!checkGlobalObject) {
  187. return;
  188. }
  189. const globalScope = sourceCode.getScope(node);
  190. globalObjects.forEach(globalObjectName => {
  191. const variable = astUtils.getVariableByName(
  192. globalScope,
  193. globalObjectName,
  194. );
  195. if (!variable) {
  196. return;
  197. }
  198. variable.references.forEach(reference => {
  199. const identifier = reference.identifier;
  200. let parent = identifier.parent;
  201. // To detect code like `window.window.Promise`.
  202. while (
  203. astUtils.isSpecificMemberAccess(
  204. parent,
  205. null,
  206. globalObjectName,
  207. )
  208. ) {
  209. parent = parent.parent;
  210. }
  211. const propertyName =
  212. astUtils.getStaticPropertyName(parent);
  213. if (propertyName && isRestricted(propertyName)) {
  214. const customMessage =
  215. restrictedGlobalMessages[propertyName];
  216. const messageId = customMessage
  217. ? "customMessage"
  218. : "defaultMessage";
  219. context.report({
  220. node: parent.property,
  221. messageId,
  222. data: {
  223. name: propertyName,
  224. customMessage,
  225. },
  226. });
  227. }
  228. });
  229. });
  230. },
  231. };
  232. },
  233. };