helpers.js 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  1. /**
  2. * @fileoverview A collection of helper functions.
  3. * This Source Code Form is subject to the terms of the Mozilla Public
  4. * License, v. 2.0. If a copy of the MPL was not distributed with this
  5. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. */
  7. "use strict";
  8. const parser = require("@babel/eslint-parser");
  9. const { analyze } = require("eslint-scope");
  10. const { KEYS: defaultVisitorKeys } = require("eslint-visitor-keys");
  11. const estraverse = require("estraverse");
  12. const path = require("path");
  13. const fs = require("fs");
  14. const ini = require("multi-ini");
  15. const recommendedConfig = require("./configs/recommended");
  16. var gRootDir = null;
  17. var directoryManifests = new Map();
  18. const callExpressionDefinitions = [
  19. /^loader\.lazyGetter\((?:globalThis|this), "(\w+)"/,
  20. /^loader\.lazyServiceGetter\((?:globalThis|this), "(\w+)"/,
  21. /^loader\.lazyRequireGetter\((?:globalThis|this), "(\w+)"/,
  22. /^XPCOMUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/,
  23. /^XPCOMUtils\.defineLazyModuleGetter\((?:globalThis|this), "(\w+)"/,
  24. /^ChromeUtils\.defineModuleGetter\((?:globalThis|this), "(\w+)"/,
  25. /^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|this), "(\w+)"/,
  26. /^XPCOMUtils\.defineLazyProxy\((?:globalThis|this), "(\w+)"/,
  27. /^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|this), "(\w+)"/,
  28. /^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|this), "(\w+)"/,
  29. /^XPCOMUtils\.defineConstant\((?:globalThis|this), "(\w+)"/,
  30. /^DevToolsUtils\.defineLazyModuleGetter\((?:globalThis|this), "(\w+)"/,
  31. /^DevToolsUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/,
  32. /^Object\.defineProperty\((?:globalThis|this), "(\w+)"/,
  33. /^Reflect\.defineProperty\((?:globalThis|this), "(\w+)"/,
  34. /^this\.__defineGetter__\("(\w+)"/,
  35. ];
  36. const callExpressionMultiDefinitions = [
  37. "XPCOMUtils.defineLazyGlobalGetters(this,",
  38. "XPCOMUtils.defineLazyGlobalGetters(globalThis,",
  39. "XPCOMUtils.defineLazyModuleGetters(this,",
  40. "XPCOMUtils.defineLazyModuleGetters(globalThis,",
  41. "XPCOMUtils.defineLazyServiceGetters(this,",
  42. "XPCOMUtils.defineLazyServiceGetters(globalThis,",
  43. "ChromeUtils.defineESModuleGetters(this,",
  44. "ChromeUtils.defineESModuleGetters(globalThis,",
  45. "loader.lazyRequireGetter(this,",
  46. "loader.lazyRequireGetter(globalThis,",
  47. ];
  48. const workerImportFilenameMatch = /(.*\/)*((.*?)\.jsm?)/;
  49. let xpidlData;
  50. module.exports = {
  51. get iniParser() {
  52. if (!this._iniParser) {
  53. this._iniParser = new ini.Parser();
  54. }
  55. return this._iniParser;
  56. },
  57. get servicesData() {
  58. return require("./services.json");
  59. },
  60. /**
  61. * Obtains xpidl data from the object directory specified in the
  62. * environment.
  63. *
  64. * @returns {Map<string, object>}
  65. * A map of interface names to the interface details.
  66. */
  67. get xpidlData() {
  68. let xpidlDir;
  69. if (process.env.TASK_ID && !process.env.MOZ_XPT_ARTIFACTS_DIR) {
  70. throw new Error(
  71. "MOZ_XPT_ARTIFACTS_DIR must be set for this rule in automation"
  72. );
  73. }
  74. xpidlDir = process.env.MOZ_XPT_ARTIFACTS_DIR;
  75. if (!xpidlDir && process.env.MOZ_OBJDIR) {
  76. xpidlDir = `${process.env.MOZ_OBJDIR}/dist/xpt_artifacts/`;
  77. if (!fs.existsSync(xpidlDir)) {
  78. xpidlDir = `${process.env.MOZ_OBJDIR}/config/makefiles/xpidl/`;
  79. }
  80. }
  81. if (!xpidlDir) {
  82. throw new Error(
  83. "MOZ_OBJDIR must be defined in the environment for this rule, i.e. MOZ_OBJDIR=objdir-ff ./mach ..."
  84. );
  85. }
  86. if (xpidlData) {
  87. return xpidlData;
  88. }
  89. let files = fs.readdirSync(`${xpidlDir}`);
  90. // `Makefile` is an expected file in the directory.
  91. if (files.length <= 1) {
  92. throw new Error("Missing xpidl data files, maybe you need to build?");
  93. }
  94. xpidlData = new Map();
  95. for (let file of files) {
  96. if (!file.endsWith(".xpt")) {
  97. continue;
  98. }
  99. let data = JSON.parse(fs.readFileSync(path.join(`${xpidlDir}`, file)));
  100. for (let details of data) {
  101. xpidlData.set(details.name, details);
  102. }
  103. }
  104. return xpidlData;
  105. },
  106. /**
  107. * Gets the abstract syntax tree (AST) of the JavaScript source code contained
  108. * in sourceText. This matches the results for an eslint parser, see
  109. * https://eslint.org/docs/developer-guide/working-with-custom-parsers.
  110. *
  111. * @param {String} sourceText
  112. * Text containing valid JavaScript.
  113. * @param {Object} astOptions
  114. * Extra configuration to pass to the espree parser, these will override
  115. * the configuration from getPermissiveConfig().
  116. * @param {Object} configOptions
  117. * Extra options for getPermissiveConfig().
  118. *
  119. * @return {Object}
  120. * Returns an object containing `ast`, `scopeManager` and
  121. * `visitorKeys`
  122. */
  123. parseCode(sourceText, astOptions = {}, configOptions = {}) {
  124. // Use a permissive config file to allow parsing of anything that Espree
  125. // can parse.
  126. let config = { ...this.getPermissiveConfig(configOptions), ...astOptions };
  127. let parseResult =
  128. "parseForESLint" in parser
  129. ? parser.parseForESLint(sourceText, config)
  130. : { ast: parser.parse(sourceText, config) };
  131. let visitorKeys = parseResult.visitorKeys || defaultVisitorKeys;
  132. visitorKeys.ExperimentalRestProperty = visitorKeys.RestElement;
  133. visitorKeys.ExperimentalSpreadProperty = visitorKeys.SpreadElement;
  134. return {
  135. ast: parseResult.ast,
  136. scopeManager: parseResult.scopeManager || analyze(parseResult.ast),
  137. visitorKeys,
  138. };
  139. },
  140. /**
  141. * A simplistic conversion of some AST nodes to a standard string form.
  142. *
  143. * @param {Object} node
  144. * The AST node to convert.
  145. *
  146. * @return {String}
  147. * The JS source for the node.
  148. */
  149. getASTSource(node, context) {
  150. switch (node.type) {
  151. case "MemberExpression":
  152. if (node.computed) {
  153. let filename = context && context.getFilename();
  154. throw new Error(
  155. `getASTSource unsupported computed MemberExpression in ${filename}`
  156. );
  157. }
  158. return (
  159. this.getASTSource(node.object) +
  160. "." +
  161. this.getASTSource(node.property)
  162. );
  163. case "ThisExpression":
  164. return "this";
  165. case "Identifier":
  166. return node.name;
  167. case "Literal":
  168. return JSON.stringify(node.value);
  169. case "CallExpression":
  170. var args = node.arguments.map(a => this.getASTSource(a)).join(", ");
  171. return this.getASTSource(node.callee) + "(" + args + ")";
  172. case "ObjectExpression":
  173. return "{}";
  174. case "ExpressionStatement":
  175. return this.getASTSource(node.expression) + ";";
  176. case "FunctionExpression":
  177. return "function() {}";
  178. case "ArrayExpression":
  179. return "[" + node.elements.map(this.getASTSource, this).join(",") + "]";
  180. case "ArrowFunctionExpression":
  181. return "() => {}";
  182. case "AssignmentExpression":
  183. return (
  184. this.getASTSource(node.left) + " = " + this.getASTSource(node.right)
  185. );
  186. case "BinaryExpression":
  187. return (
  188. this.getASTSource(node.left) +
  189. " " +
  190. node.operator +
  191. " " +
  192. this.getASTSource(node.right)
  193. );
  194. case "UnaryExpression":
  195. return node.operator + " " + this.getASTSource(node.argument);
  196. default:
  197. throw new Error("getASTSource unsupported node type: " + node.type);
  198. }
  199. },
  200. /**
  201. * This walks an AST in a manner similar to ESLint passing node events to the
  202. * listener. The listener is expected to be a simple function
  203. * which accepts node type, node and parents arguments.
  204. *
  205. * @param {Object} ast
  206. * The AST to walk.
  207. * @param {Array} visitorKeys
  208. * The visitor keys to use for the AST.
  209. * @param {Function} listener
  210. * A callback function to call for the nodes. Passed three arguments,
  211. * event type, node and an array of parent nodes for the current node.
  212. */
  213. walkAST(ast, visitorKeys, listener) {
  214. let parents = [];
  215. estraverse.traverse(ast, {
  216. enter(node, parent) {
  217. listener(node.type, node, parents);
  218. parents.push(node);
  219. },
  220. leave(node, parent) {
  221. if (!parents.length) {
  222. throw new Error("Left more nodes than entered.");
  223. }
  224. parents.pop();
  225. },
  226. keys: visitorKeys,
  227. });
  228. if (parents.length) {
  229. throw new Error("Entered more nodes than left.");
  230. }
  231. },
  232. /**
  233. * Attempts to convert an ExpressionStatement to likely global variable
  234. * definitions.
  235. *
  236. * @param {Object} node
  237. * The AST node to convert.
  238. * @param {boolean} isGlobal
  239. * True if the current node is in the global scope.
  240. *
  241. * @return {Array}
  242. * An array of objects that contain details about the globals:
  243. * - {String} name
  244. * The name of the global.
  245. * - {Boolean} writable
  246. * If the global is writeable or not.
  247. */
  248. convertWorkerExpressionToGlobals(node, isGlobal, dirname) {
  249. var getGlobalsForFile = require("./globals").getGlobalsForFile;
  250. let results = [];
  251. let expr = node.expression;
  252. if (
  253. node.expression.type === "CallExpression" &&
  254. expr.callee &&
  255. expr.callee.type === "Identifier" &&
  256. expr.callee.name === "importScripts"
  257. ) {
  258. for (var arg of expr.arguments) {
  259. var match = arg.value && arg.value.match(workerImportFilenameMatch);
  260. if (match) {
  261. if (!match[1]) {
  262. let filePath = path.resolve(dirname, match[2]);
  263. if (fs.existsSync(filePath)) {
  264. let additionalGlobals = getGlobalsForFile(filePath);
  265. results = results.concat(additionalGlobals);
  266. }
  267. }
  268. // Import with relative/absolute path should explicitly use
  269. // `import-globals-from` comment.
  270. }
  271. }
  272. }
  273. return results;
  274. },
  275. /**
  276. * Attempts to convert an AssignmentExpression into a global variable
  277. * definition if it applies to `this` in the global scope.
  278. *
  279. * @param {Object} node
  280. * The AST node to convert.
  281. * @param {boolean} isGlobal
  282. * True if the current node is in the global scope.
  283. *
  284. * @return {Array}
  285. * An array of objects that contain details about the globals:
  286. * - {String} name
  287. * The name of the global.
  288. * - {Boolean} writable
  289. * If the global is writeable or not.
  290. */
  291. convertThisAssignmentExpressionToGlobals(node, isGlobal) {
  292. if (
  293. isGlobal &&
  294. node.expression.left &&
  295. node.expression.left.object &&
  296. node.expression.left.object.type === "ThisExpression" &&
  297. node.expression.left.property &&
  298. node.expression.left.property.type === "Identifier"
  299. ) {
  300. return [{ name: node.expression.left.property.name, writable: true }];
  301. }
  302. return [];
  303. },
  304. /**
  305. * Attempts to convert an CallExpressions that look like module imports
  306. * into global variable definitions.
  307. *
  308. * @param {Object} node
  309. * The AST node to convert.
  310. * @param {boolean} isGlobal
  311. * True if the current node is in the global scope.
  312. *
  313. * @return {Array}
  314. * An array of objects that contain details about the globals:
  315. * - {String} name
  316. * The name of the global.
  317. * - {Boolean} writable
  318. * If the global is writeable or not.
  319. */
  320. convertCallExpressionToGlobals(node, isGlobal) {
  321. let express = node.expression;
  322. if (
  323. express.type === "CallExpression" &&
  324. express.callee.type === "MemberExpression" &&
  325. express.callee.object &&
  326. express.callee.object.type === "Identifier" &&
  327. express.arguments.length === 1 &&
  328. express.arguments[0].type === "ArrayExpression" &&
  329. express.callee.property.type === "Identifier" &&
  330. express.callee.property.name === "importGlobalProperties"
  331. ) {
  332. return express.arguments[0].elements.map(literal => {
  333. return {
  334. explicit: true,
  335. name: literal.value,
  336. writable: false,
  337. };
  338. });
  339. }
  340. let source;
  341. try {
  342. source = this.getASTSource(node);
  343. } catch (e) {
  344. return [];
  345. }
  346. // The definition matches below must be in the global scope for us to define
  347. // a global, so bail out early if we're not a global.
  348. if (!isGlobal) {
  349. return [];
  350. }
  351. for (let reg of callExpressionDefinitions) {
  352. let match = source.match(reg);
  353. if (match) {
  354. return [{ name: match[1], writable: true, explicit: true }];
  355. }
  356. }
  357. if (
  358. callExpressionMultiDefinitions.some(expr => source.startsWith(expr)) &&
  359. node.expression.arguments[1]
  360. ) {
  361. let arg = node.expression.arguments[1];
  362. if (arg.type === "ObjectExpression") {
  363. return arg.properties
  364. .map(p => ({
  365. name: p.type === "Property" && p.key.name,
  366. writable: true,
  367. explicit: true,
  368. }))
  369. .filter(g => g.name);
  370. }
  371. if (arg.type === "ArrayExpression") {
  372. return arg.elements
  373. .map(p => ({
  374. name: p.type === "Literal" && p.value,
  375. writable: true,
  376. explicit: true,
  377. }))
  378. .filter(g => typeof g.name == "string");
  379. }
  380. }
  381. if (
  382. node.expression.callee.type == "MemberExpression" &&
  383. node.expression.callee.property.type == "Identifier" &&
  384. node.expression.callee.property.name == "defineLazyScriptGetter"
  385. ) {
  386. // The case where we have a single symbol as a string has already been
  387. // handled by the regexp, so we have an array of symbols here.
  388. return node.expression.arguments[1].elements.map(n => ({
  389. name: n.value,
  390. writable: true,
  391. explicit: true,
  392. }));
  393. }
  394. return [];
  395. },
  396. /**
  397. * Add a variable to the current scope.
  398. * HACK: This relies on eslint internals so it could break at any time.
  399. *
  400. * @param {String} name
  401. * The variable name to add to the scope.
  402. * @param {ASTScope} scope
  403. * The scope to add to.
  404. * @param {boolean} writable
  405. * Whether the global can be overwritten.
  406. * @param {Object} [node]
  407. * The AST node that defined the globals.
  408. */
  409. addVarToScope(name, scope, writable, node) {
  410. scope.__defineGeneric(name, scope.set, scope.variables, null, null);
  411. let variable = scope.set.get(name);
  412. variable.eslintExplicitGlobal = false;
  413. variable.writeable = writable;
  414. if (node) {
  415. variable.defs.push({
  416. type: "Variable",
  417. node,
  418. name: { name, parent: node.parent },
  419. });
  420. variable.identifiers.push(node);
  421. }
  422. // Walk to the global scope which holds all undeclared variables.
  423. while (scope.type != "global") {
  424. scope = scope.upper;
  425. }
  426. // "through" contains all references with no found definition.
  427. scope.through = scope.through.filter(function(reference) {
  428. if (reference.identifier.name != name) {
  429. return true;
  430. }
  431. // Links the variable and the reference.
  432. // And this reference is removed from `Scope#through`.
  433. reference.resolved = variable;
  434. variable.references.push(reference);
  435. return false;
  436. });
  437. },
  438. /**
  439. * Adds a set of globals to a scope.
  440. *
  441. * @param {Array} globalVars
  442. * An array of global variable names.
  443. * @param {ASTScope} scope
  444. * The scope.
  445. * @param {Object} [node]
  446. * The AST node that defined the globals.
  447. */
  448. addGlobals(globalVars, scope, node) {
  449. globalVars.forEach(v =>
  450. this.addVarToScope(v.name, scope, v.writable, v.explicit && node)
  451. );
  452. },
  453. /**
  454. * To allow espree to parse almost any JavaScript we need as many features as
  455. * possible turned on. This method returns that config.
  456. *
  457. * @param {Object} options
  458. * {
  459. * useBabel: {boolean} whether to set babelOptions.
  460. * }
  461. * @return {Object}
  462. * Espree compatible permissive config.
  463. */
  464. getPermissiveConfig({ useBabel = true } = {}) {
  465. const config = {
  466. range: true,
  467. requireConfigFile: false,
  468. babelOptions: {
  469. // configFile: path.join(gRootDir, ".babel-eslint.rc.js"),
  470. // parserOpts: {
  471. // plugins: [
  472. // "@babel/plugin-proposal-class-static-block",
  473. // "@babel/plugin-syntax-class-properties",
  474. // "@babel/plugin-syntax-jsx",
  475. // ],
  476. // },
  477. },
  478. loc: true,
  479. comment: true,
  480. attachComment: true,
  481. ecmaVersion: this.getECMAVersion(),
  482. sourceType: "script",
  483. };
  484. if (useBabel && this.isMozillaCentralBased()) {
  485. config.babelOptions.configFile = path.join(
  486. gRootDir,
  487. ".babel-eslint.rc.js"
  488. );
  489. }
  490. return config;
  491. },
  492. /**
  493. * Returns the ECMA version of the recommended config.
  494. *
  495. * @return {Number} The ECMA version of the recommended config.
  496. */
  497. getECMAVersion() {
  498. return recommendedConfig.parserOptions.ecmaVersion;
  499. },
  500. /**
  501. * Check whether it's inside top-level script.
  502. *
  503. * @param {Array} ancestors
  504. * The parents of the current node.
  505. *
  506. * @return {Boolean}
  507. * True or false
  508. */
  509. getIsTopLevelScript(ancestors) {
  510. for (let parent of ancestors) {
  511. switch (parent.type) {
  512. case "ArrowFunctionExpression":
  513. case "FunctionDeclaration":
  514. case "FunctionExpression":
  515. case "PropertyDefinition":
  516. case "StaticBlock":
  517. return false;
  518. }
  519. }
  520. return true;
  521. },
  522. isTopLevel(ancestors) {
  523. for (let parent of ancestors) {
  524. switch (parent.type) {
  525. case "ArrowFunctionExpression":
  526. case "FunctionDeclaration":
  527. case "FunctionExpression":
  528. case "PropertyDefinition":
  529. case "StaticBlock":
  530. case "BlockStatement":
  531. return false;
  532. }
  533. }
  534. return true;
  535. },
  536. /**
  537. * Check whether `this` expression points the global this.
  538. *
  539. * @param {Array} ancestors
  540. * The parents of the current node.
  541. *
  542. * @return {Boolean}
  543. * True or false
  544. */
  545. getIsGlobalThis(ancestors) {
  546. for (let parent of ancestors) {
  547. switch (parent.type) {
  548. case "FunctionDeclaration":
  549. case "FunctionExpression":
  550. case "PropertyDefinition":
  551. case "StaticBlock":
  552. return false;
  553. }
  554. }
  555. return true;
  556. },
  557. /**
  558. * Check whether the node is evaluated at top-level script unconditionally.
  559. *
  560. * @param {Array} ancestors
  561. * The parents of the current node.
  562. *
  563. * @return {Boolean}
  564. * True or false
  565. */
  566. getIsTopLevelAndUnconditionallyExecuted(ancestors) {
  567. for (let parent of ancestors) {
  568. switch (parent.type) {
  569. // Control flow
  570. case "IfStatement":
  571. case "SwitchStatement":
  572. case "TryStatement":
  573. case "WhileStatement":
  574. case "DoWhileStatement":
  575. case "ForStatement":
  576. case "ForInStatement":
  577. case "ForOfStatement":
  578. return false;
  579. // Function
  580. case "FunctionDeclaration":
  581. case "FunctionExpression":
  582. case "ArrowFunctionExpression":
  583. case "ClassBody":
  584. return false;
  585. // Branch
  586. case "LogicalExpression":
  587. case "ConditionalExpression":
  588. case "ChainExpression":
  589. return false;
  590. case "AssignmentExpression":
  591. switch (parent.operator) {
  592. // Branch
  593. case "||=":
  594. case "&&=":
  595. case "??=":
  596. return false;
  597. }
  598. break;
  599. // Implicit branch (default value)
  600. case "ObjectPattern":
  601. case "ArrayPattern":
  602. return false;
  603. }
  604. }
  605. return true;
  606. },
  607. /**
  608. * Check whether we might be in a test head file.
  609. *
  610. * @param {RuleContext} scope
  611. * You should pass this from within a rule
  612. * e.g. helpers.getIsHeadFile(context)
  613. *
  614. * @return {Boolean}
  615. * True or false
  616. */
  617. getIsHeadFile(scope) {
  618. var pathAndFilename = this.cleanUpPath(scope.getFilename());
  619. return /.*[\\/]head(_.+)?\.js$/.test(pathAndFilename);
  620. },
  621. /**
  622. * Gets the head files for a potential test file
  623. *
  624. * @param {RuleContext} scope
  625. * You should pass this from within a rule
  626. * e.g. helpers.getIsHeadFile(context)
  627. *
  628. * @return {String[]}
  629. * Paths to head files to load for the test
  630. */
  631. getTestHeadFiles(scope) {
  632. if (!this.getIsTest(scope)) {
  633. return [];
  634. }
  635. let filepath = this.cleanUpPath(scope.getFilename());
  636. let dir = path.dirname(filepath);
  637. let names = fs
  638. .readdirSync(dir)
  639. .filter(
  640. name =>
  641. (name.startsWith("head") || name.startsWith("xpcshell-head")) &&
  642. name.endsWith(".js")
  643. )
  644. .map(name => path.join(dir, name));
  645. return names;
  646. },
  647. /**
  648. * Gets all the test manifest data for a directory
  649. *
  650. * @param {String} dir
  651. * The directory
  652. *
  653. * @return {Array}
  654. * An array of objects with file and manifest properties
  655. */
  656. getManifestsForDirectory(dir) {
  657. if (directoryManifests.has(dir)) {
  658. return directoryManifests.get(dir);
  659. }
  660. let manifests = [];
  661. let names = [];
  662. try {
  663. names = fs.readdirSync(dir);
  664. } catch (err) {
  665. // Ignore directory not found, it might be faked by a test
  666. if (err.code !== "ENOENT") {
  667. throw err;
  668. }
  669. }
  670. for (let name of names) {
  671. if (!name.endsWith(".ini")) {
  672. continue;
  673. }
  674. try {
  675. let manifest = this.iniParser.parse(
  676. fs.readFileSync(path.join(dir, name), "utf8").split("\n")
  677. );
  678. manifests.push({
  679. file: path.join(dir, name),
  680. manifest,
  681. });
  682. } catch (e) {}
  683. }
  684. directoryManifests.set(dir, manifests);
  685. return manifests;
  686. },
  687. /**
  688. * Gets the manifest file a test is listed in
  689. *
  690. * @param {RuleContext} scope
  691. * You should pass this from within a rule
  692. * e.g. helpers.getIsHeadFile(context)
  693. *
  694. * @return {String}
  695. * The path to the test manifest file
  696. */
  697. getTestManifest(scope) {
  698. let filepath = this.cleanUpPath(scope.getFilename());
  699. let dir = path.dirname(filepath);
  700. let filename = path.basename(filepath);
  701. for (let manifest of this.getManifestsForDirectory(dir)) {
  702. if (filename in manifest.manifest) {
  703. return manifest.file;
  704. }
  705. }
  706. return null;
  707. },
  708. /**
  709. * Check whether we are in a test of some kind.
  710. *
  711. * @param {RuleContext} scope
  712. * You should pass this from within a rule
  713. * e.g. helpers.getIsTest(context)
  714. *
  715. * @return {Boolean}
  716. * True or false
  717. */
  718. getIsTest(scope) {
  719. // Regardless of the manifest name being in a manifest means we're a test.
  720. let manifest = this.getTestManifest(scope);
  721. if (manifest) {
  722. return true;
  723. }
  724. return !!this.getTestType(scope);
  725. },
  726. /*
  727. * Check if this is an .sjs file.
  728. */
  729. getIsSjs(scope) {
  730. let filepath = this.cleanUpPath(scope.getFilename());
  731. return path.extname(filepath) == ".sjs";
  732. },
  733. /**
  734. * Gets the type of test or null if this isn't a test.
  735. *
  736. * @param {RuleContext} scope
  737. * You should pass this from within a rule
  738. * e.g. helpers.getIsHeadFile(context)
  739. *
  740. * @return {String or null}
  741. * Test type: xpcshell, browser, chrome, mochitest
  742. */
  743. getTestType(scope) {
  744. let testTypes = ["browser", "xpcshell", "chrome", "mochitest", "a11y"];
  745. let manifest = this.getTestManifest(scope);
  746. if (manifest) {
  747. let name = path.basename(manifest);
  748. for (let testType of testTypes) {
  749. if (name.startsWith(testType)) {
  750. return testType;
  751. }
  752. }
  753. }
  754. let filepath = this.cleanUpPath(scope.getFilename());
  755. let filename = path.basename(filepath);
  756. if (filename.startsWith("browser_")) {
  757. return "browser";
  758. }
  759. if (filename.startsWith("test_")) {
  760. let parent = path.basename(path.dirname(filepath));
  761. for (let testType of testTypes) {
  762. if (parent.startsWith(testType)) {
  763. return testType;
  764. }
  765. }
  766. // It likely is a test, we're just not sure what kind.
  767. return "unknown";
  768. }
  769. // Likely not a test
  770. return null;
  771. },
  772. getIsWorker(filePath) {
  773. let filename = path.basename(this.cleanUpPath(filePath)).toLowerCase();
  774. return filename.includes("worker");
  775. },
  776. /**
  777. * Gets the root directory of the repository by walking up directories from
  778. * this file until a .eslintignore file is found. If this fails, the same
  779. * procedure will be attempted from the current working dir.
  780. * @return {String} The absolute path of the repository directory
  781. */
  782. get rootDir() {
  783. if (!gRootDir) {
  784. function searchUpForIgnore(dirName, filename) {
  785. let parsed = path.parse(dirName);
  786. while (parsed.root !== dirName) {
  787. if (fs.existsSync(path.join(dirName, filename))) {
  788. return dirName;
  789. }
  790. // Move up a level
  791. dirName = parsed.dir;
  792. parsed = path.parse(dirName);
  793. }
  794. return null;
  795. }
  796. let possibleRoot = searchUpForIgnore(
  797. path.dirname(module.filename),
  798. ".eslintignore"
  799. );
  800. if (!possibleRoot) {
  801. possibleRoot = searchUpForIgnore(path.resolve(), ".eslintignore");
  802. }
  803. if (!possibleRoot) {
  804. possibleRoot = searchUpForIgnore(path.resolve(), "package.json");
  805. }
  806. if (!possibleRoot) {
  807. // We've couldn't find a root from the module or CWD, so lets just go
  808. // for the CWD. We really don't want to throw if possible, as that
  809. // tends to give confusing results when used with ESLint.
  810. possibleRoot = process.cwd();
  811. }
  812. gRootDir = possibleRoot;
  813. }
  814. return gRootDir;
  815. },
  816. /**
  817. * ESLint may be executed from various places: from mach, at the root of the
  818. * repository, or from a directory in the repository when, for instance,
  819. * executed by a text editor's plugin.
  820. * The value returned by context.getFileName() varies because of this.
  821. * This helper function makes sure to return an absolute file path for the
  822. * current context, by looking at process.cwd().
  823. * @param {Context} context
  824. * @return {String} The absolute path
  825. */
  826. getAbsoluteFilePath(context) {
  827. var fileName = this.cleanUpPath(context.getFilename());
  828. var cwd = process.cwd();
  829. if (path.isAbsolute(fileName)) {
  830. // Case 2: executed from the repo's root with mach:
  831. // fileName: /path/to/mozilla/repo/a/b/c/d.js
  832. // cwd: /path/to/mozilla/repo
  833. return fileName;
  834. } else if (path.basename(fileName) == fileName) {
  835. // Case 1b: executed from a nested directory, fileName is the base name
  836. // without any path info (happens in Atom with linter-eslint)
  837. return path.join(cwd, fileName);
  838. }
  839. // Case 1: executed form in a nested directory, e.g. from a text editor:
  840. // fileName: a/b/c/d.js
  841. // cwd: /path/to/mozilla/repo/a/b/c
  842. var dirName = path.dirname(fileName);
  843. return cwd.slice(0, cwd.length - dirName.length) + fileName;
  844. },
  845. /**
  846. * When ESLint is run from SublimeText, paths retrieved from
  847. * context.getFileName contain leading and trailing double-quote characters.
  848. * These characters need to be removed.
  849. */
  850. cleanUpPath(pathName) {
  851. return pathName.replace(/^"/, "").replace(/"$/, "");
  852. },
  853. get globalScriptPaths() {
  854. return [
  855. path.join(this.rootDir, "browser", "base", "content", "browser.xhtml"),
  856. path.join(
  857. this.rootDir,
  858. "browser",
  859. "base",
  860. "content",
  861. "global-scripts.inc"
  862. ),
  863. ];
  864. },
  865. isMozillaCentralBased() {
  866. return fs.existsSync(this.globalScriptPaths[0]);
  867. },
  868. getSavedEnvironmentItems(environment) {
  869. return require("./environments/saved-globals.json").environments[
  870. environment
  871. ];
  872. },
  873. getSavedRuleData(rule) {
  874. return require("./rules/saved-rules-data.json").rulesData[rule];
  875. },
  876. getBuildEnvironment() {
  877. var { execFileSync } = require("child_process");
  878. var output = execFileSync(
  879. path.join(this.rootDir, "mach"),
  880. ["environment", "--format=json"],
  881. { silent: true }
  882. );
  883. return JSON.parse(output);
  884. },
  885. /**
  886. * Extract the path of require (and require-like) helpers used in DevTools.
  887. */
  888. getDevToolsRequirePath(node) {
  889. if (
  890. node.callee.type == "Identifier" &&
  891. node.callee.name == "require" &&
  892. node.arguments.length == 1 &&
  893. node.arguments[0].type == "Literal"
  894. ) {
  895. return node.arguments[0].value;
  896. } else if (
  897. node.callee.type == "MemberExpression" &&
  898. node.callee.property.type == "Identifier" &&
  899. node.callee.property.name == "lazyRequireGetter" &&
  900. node.arguments.length >= 3 &&
  901. node.arguments[2].type == "Literal"
  902. ) {
  903. return node.arguments[2].value;
  904. }
  905. return null;
  906. },
  907. /**
  908. * Returns property name from MemberExpression. Also accepts Identifier for consistency.
  909. * @param {import("estree").MemberExpression | import("estree").Identifier} node
  910. * @returns {string | null}
  911. *
  912. * @example `foo` gives "foo"
  913. * @example `foo.bar` gives "bar"
  914. * @example `foo.bar.baz` gives "baz"
  915. */
  916. maybeGetMemberPropertyName(node) {
  917. if (node.type === "MemberExpression") {
  918. return node.property.name;
  919. }
  920. if (node.type === "Identifier") {
  921. return node.name;
  922. }
  923. return null;
  924. },
  925. };