globals.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. /**
  2. * @fileoverview functions for scanning an AST for globals including
  3. * traversing referenced scripts.
  4. * This Source Code Form is subject to the terms of the Mozilla Public
  5. * License, v. 2.0. If a copy of the MPL was not distributed with this
  6. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  7. */
  8. "use strict";
  9. const path = require("path");
  10. const fs = require("fs");
  11. const helpers = require("./helpers");
  12. const htmlparser = require("htmlparser2");
  13. /**
  14. * Parses a list of "name:boolean_value" or/and "name" options divided by comma
  15. * or whitespace.
  16. *
  17. * This function was copied from eslint.js
  18. *
  19. * @param {string} string The string to parse.
  20. * @param {Comment} comment The comment node which has the string.
  21. * @returns {Object} Result map object of names and boolean values
  22. */
  23. function parseBooleanConfig(string, comment) {
  24. let items = {};
  25. // Collapse whitespace around : to make parsing easier
  26. string = string.replace(/\s*:\s*/g, ":");
  27. // Collapse whitespace around ,
  28. string = string.replace(/\s*,\s*/g, ",");
  29. string.split(/\s|,+/).forEach(function(name) {
  30. if (!name) {
  31. return;
  32. }
  33. let pos = name.indexOf(":");
  34. let value;
  35. if (pos !== -1) {
  36. value = name.substring(pos + 1, name.length);
  37. name = name.substring(0, pos);
  38. }
  39. items[name] = {
  40. value: value === "true",
  41. comment,
  42. };
  43. });
  44. return items;
  45. }
  46. /**
  47. * Global discovery can require parsing many files. This map of
  48. * {String} => {Object} caches what globals were discovered for a file path.
  49. */
  50. const globalCache = new Map();
  51. /**
  52. * Global discovery can occasionally meet circular dependencies due to the way
  53. * js files are included via html/xhtml files etc. This set is used to avoid
  54. * getting into loops whilst the discovery is in progress.
  55. */
  56. var globalDiscoveryInProgressForFiles = new Set();
  57. /**
  58. * When looking for globals in HTML files, it can be common to have more than
  59. * one script tag with inline javascript. These will normally be called together,
  60. * so we store the globals for just the last HTML file processed.
  61. */
  62. var lastHTMLGlobals = {};
  63. /**
  64. * An object that returns found globals for given AST node types. Each prototype
  65. * property should be named for a node type and accepts a node parameter and a
  66. * parents parameter which is a list of the parent nodes of the current node.
  67. * Each returns an array of globals found.
  68. *
  69. * @param {String} filePath
  70. * The absolute path of the file being parsed.
  71. */
  72. function GlobalsForNode(filePath, context) {
  73. this.path = filePath;
  74. this.context = context;
  75. if (this.path) {
  76. this.dirname = path.dirname(this.path);
  77. } else {
  78. this.dirname = null;
  79. }
  80. }
  81. GlobalsForNode.prototype = {
  82. Program(node) {
  83. let globals = [];
  84. for (let comment of node.comments) {
  85. if (comment.type !== "Block") {
  86. continue;
  87. }
  88. let value = comment.value.trim();
  89. value = value.replace(/\n/g, "");
  90. // We have to discover any globals that ESLint would have defined through
  91. // comment directives.
  92. let match = /^globals?\s+(.+)/.exec(value);
  93. if (match) {
  94. let values = parseBooleanConfig(match[1].trim(), node);
  95. for (let name of Object.keys(values)) {
  96. globals.push({
  97. name,
  98. writable: values[name].value,
  99. });
  100. }
  101. // We matched globals, so we won't match import-globals-from.
  102. continue;
  103. }
  104. match = /^import-globals-from\s+(.+)$/.exec(value);
  105. if (!match) {
  106. continue;
  107. }
  108. if (!this.dirname) {
  109. // If this is testing context without path, ignore import.
  110. return globals;
  111. }
  112. let filePath = match[1].trim();
  113. if (filePath.endsWith(".mjs")) {
  114. if (this.context) {
  115. this.context.report(
  116. comment,
  117. "import-globals-from does not support module files - use a direct import instead"
  118. );
  119. } else {
  120. // Fall back to throwing an error, as we do not have a context in all situations,
  121. // e.g. when loading the environment.
  122. throw new Error(
  123. "import-globals-from does not support module files - use a direct import instead"
  124. );
  125. }
  126. continue;
  127. }
  128. if (!path.isAbsolute(filePath)) {
  129. filePath = path.resolve(this.dirname, filePath);
  130. } else {
  131. filePath = path.join(helpers.rootDir, filePath);
  132. }
  133. globals = globals.concat(module.exports.getGlobalsForFile(filePath));
  134. }
  135. return globals;
  136. },
  137. ExpressionStatement(node, parents, globalScope) {
  138. let isGlobal = helpers.getIsGlobalThis(parents);
  139. let globals = [];
  140. // Note: We check the expression types here and only call the necessary
  141. // functions to aid performance.
  142. if (node.expression.type === "AssignmentExpression") {
  143. globals = helpers.convertThisAssignmentExpressionToGlobals(
  144. node,
  145. isGlobal
  146. );
  147. } else if (node.expression.type === "CallExpression") {
  148. globals = helpers.convertCallExpressionToGlobals(node, isGlobal);
  149. }
  150. // Here we assume that if importScripts is set in the global scope, then
  151. // this is a worker. It would be nice if eslint gave us a way of getting
  152. // the environment directly.
  153. //
  154. // If this is testing context without path, ignore import.
  155. if (globalScope && globalScope.set.get("importScripts") && this.dirname) {
  156. let workerDetails = helpers.convertWorkerExpressionToGlobals(
  157. node,
  158. isGlobal,
  159. this.dirname
  160. );
  161. globals = globals.concat(workerDetails);
  162. }
  163. return globals;
  164. },
  165. };
  166. module.exports = {
  167. /**
  168. * Returns all globals for a given file. Recursively searches through
  169. * import-globals-from directives and also includes globals defined by
  170. * standard eslint directives.
  171. *
  172. * @param {String} filePath
  173. * The absolute path of the file to be parsed.
  174. * @param {Object} astOptions
  175. * Extra options to pass to the parser.
  176. * @return {Array}
  177. * An array of objects that contain details about the globals:
  178. * - {String} name
  179. * The name of the global.
  180. * - {Boolean} writable
  181. * If the global is writeable or not.
  182. */
  183. getGlobalsForFile(filePath, astOptions = {}) {
  184. if (globalCache.has(filePath)) {
  185. return globalCache.get(filePath);
  186. }
  187. if (globalDiscoveryInProgressForFiles.has(filePath)) {
  188. // We're already processing this file, so return an empty set for now -
  189. // the initial processing will pick up on the globals for this file.
  190. return [];
  191. }
  192. globalDiscoveryInProgressForFiles.add(filePath);
  193. let content = fs.readFileSync(filePath, "utf8");
  194. // Parse the content into an AST
  195. let { ast, scopeManager, visitorKeys } = helpers.parseCode(
  196. content,
  197. astOptions
  198. );
  199. // Discover global declarations
  200. let globalScope = scopeManager.acquire(ast);
  201. let globals = Object.keys(globalScope.variables).map(v => ({
  202. name: globalScope.variables[v].name,
  203. writable: true,
  204. }));
  205. // Walk over the AST to find any of our custom globals
  206. let handler = new GlobalsForNode(filePath);
  207. helpers.walkAST(ast, visitorKeys, (type, node, parents) => {
  208. if (type in handler) {
  209. let newGlobals = handler[type](node, parents, globalScope);
  210. globals.push.apply(globals, newGlobals);
  211. }
  212. });
  213. globalCache.set(filePath, globals);
  214. globalDiscoveryInProgressForFiles.delete(filePath);
  215. return globals;
  216. },
  217. /**
  218. * Returns all globals for a code.
  219. * This is only for testing.
  220. *
  221. * @param {String} code
  222. * The JS code
  223. * @param {Object} astOptions
  224. * Extra options to pass to the parser.
  225. * @return {Array}
  226. * An array of objects that contain details about the globals:
  227. * - {String} name
  228. * The name of the global.
  229. * - {Boolean} writable
  230. * If the global is writeable or not.
  231. */
  232. getGlobalsForCode(code, astOptions = {}) {
  233. // Parse the content into an AST
  234. let { ast, scopeManager, visitorKeys } = helpers.parseCode(
  235. code,
  236. astOptions,
  237. { useBabel: false }
  238. );
  239. // Discover global declarations
  240. let globalScope = scopeManager.acquire(ast);
  241. let globals = Object.keys(globalScope.variables).map(v => ({
  242. name: globalScope.variables[v].name,
  243. writable: true,
  244. }));
  245. // Walk over the AST to find any of our custom globals
  246. let handler = new GlobalsForNode(null);
  247. helpers.walkAST(ast, visitorKeys, (type, node, parents) => {
  248. if (type in handler) {
  249. let newGlobals = handler[type](node, parents, globalScope);
  250. globals.push.apply(globals, newGlobals);
  251. }
  252. });
  253. return globals;
  254. },
  255. /**
  256. * Returns all the globals for an html file that are defined by imported
  257. * scripts (i.e. <script src="foo.js">).
  258. *
  259. * This function will cache results for one html file only - we expect
  260. * this to be called sequentially for each chunk of a HTML file, rather
  261. * than chucks of different files in random order.
  262. *
  263. * @param {String} filePath
  264. * The absolute path of the file to be parsed.
  265. * @return {Array}
  266. * An array of objects that contain details about the globals:
  267. * - {String} name
  268. * The name of the global.
  269. * - {Boolean} writable
  270. * If the global is writeable or not.
  271. */
  272. getImportedGlobalsForHTMLFile(filePath) {
  273. if (lastHTMLGlobals.filename === filePath) {
  274. return lastHTMLGlobals.globals;
  275. }
  276. let dir = path.dirname(filePath);
  277. let globals = [];
  278. let content = fs.readFileSync(filePath, "utf8");
  279. let scriptSrcs = [];
  280. // We use htmlparser as this ensures we find the script tags correctly.
  281. let parser = new htmlparser.Parser(
  282. {
  283. onopentag(name, attribs) {
  284. if (name === "script" && "src" in attribs) {
  285. scriptSrcs.push({
  286. src: attribs.src,
  287. type:
  288. "type" in attribs && attribs.type == "module"
  289. ? "module"
  290. : "script",
  291. });
  292. }
  293. },
  294. },
  295. {
  296. xmlMode: filePath.endsWith("xhtml"),
  297. }
  298. );
  299. parser.parseComplete(content);
  300. for (let script of scriptSrcs) {
  301. // Ensure that the script src isn't just "".
  302. if (!script.src) {
  303. continue;
  304. }
  305. let scriptName;
  306. if (script.src.includes("http:")) {
  307. // We don't handle this currently as the paths are complex to match.
  308. } else if (script.src.includes("chrome")) {
  309. // This is one way of referencing test files.
  310. script.src = script.src.replace("chrome://mochikit/content/", "/");
  311. scriptName = path.join(
  312. helpers.rootDir,
  313. "testing",
  314. "mochitest",
  315. script.src
  316. );
  317. } else if (script.src.includes("SimpleTest")) {
  318. // This is another way of referencing test files...
  319. scriptName = path.join(
  320. helpers.rootDir,
  321. "testing",
  322. "mochitest",
  323. script.src
  324. );
  325. } else if (script.src.startsWith("/tests/")) {
  326. scriptName = path.join(helpers.rootDir, script.src.substring(7));
  327. } else {
  328. // Fallback to hoping this is a relative path.
  329. scriptName = path.join(dir, script.src);
  330. }
  331. if (scriptName && fs.existsSync(scriptName)) {
  332. globals.push(
  333. ...module.exports.getGlobalsForFile(scriptName, {
  334. ecmaVersion: helpers.getECMAVersion(),
  335. sourceType: script.type,
  336. })
  337. );
  338. }
  339. }
  340. lastHTMLGlobals.filePath = filePath;
  341. return (lastHTMLGlobals.globals = globals);
  342. },
  343. /**
  344. * Intended to be used as-is for an ESLint rule that parses for globals in
  345. * the current file and recurses through import-globals-from directives.
  346. *
  347. * @param {Object} context
  348. * The ESLint parsing context.
  349. */
  350. getESLintGlobalParser(context) {
  351. let globalScope;
  352. let parser = {
  353. Program(node) {
  354. globalScope = context.getScope();
  355. },
  356. };
  357. let filename = context.getFilename();
  358. let extraHTMLGlobals = [];
  359. if (filename.endsWith(".html") || filename.endsWith(".xhtml")) {
  360. extraHTMLGlobals = module.exports.getImportedGlobalsForHTMLFile(filename);
  361. }
  362. // Install thin wrappers around GlobalsForNode
  363. let handler = new GlobalsForNode(helpers.getAbsoluteFilePath(context));
  364. for (let type of Object.keys(GlobalsForNode.prototype)) {
  365. parser[type] = function(node) {
  366. if (type === "Program") {
  367. globalScope = context.getScope();
  368. helpers.addGlobals(extraHTMLGlobals, globalScope);
  369. }
  370. let globals = handler[type](node, context.getAncestors(), globalScope);
  371. helpers.addGlobals(
  372. globals,
  373. globalScope,
  374. node.type !== "Program" && node
  375. );
  376. };
  377. }
  378. return parser;
  379. },
  380. };