| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 | /** * @fileoverview functions for scanning an AST for globals including *               traversing referenced scripts. * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */"use strict";const path = require("path");const fs = require("fs");const helpers = require("./helpers");const htmlparser = require("htmlparser2");/** * Parses a list of "name:boolean_value" or/and "name" options divided by comma * or whitespace. * * This function was copied from eslint.js * * @param {string} string The string to parse. * @param {Comment} comment The comment node which has the string. * @returns {Object} Result map object of names and boolean values */function parseBooleanConfig(string, comment) {  let items = {};  // Collapse whitespace around : to make parsing easier  string = string.replace(/\s*:\s*/g, ":");  // Collapse whitespace around ,  string = string.replace(/\s*,\s*/g, ",");  string.split(/\s|,+/).forEach(function(name) {    if (!name) {      return;    }    let pos = name.indexOf(":");    let value;    if (pos !== -1) {      value = name.substring(pos + 1, name.length);      name = name.substring(0, pos);    }    items[name] = {      value: value === "true",      comment,    };  });  return items;}/** * Global discovery can require parsing many files. This map of * {String} => {Object} caches what globals were discovered for a file path. */const globalCache = new Map();/** * Global discovery can occasionally meet circular dependencies due to the way * js files are included via html/xhtml files etc. This set is used to avoid * getting into loops whilst the discovery is in progress. */var globalDiscoveryInProgressForFiles = new Set();/** * When looking for globals in HTML files, it can be common to have more than * one script tag with inline javascript. These will normally be called together, * so we store the globals for just the last HTML file processed. */var lastHTMLGlobals = {};/** * An object that returns found globals for given AST node types. Each prototype * property should be named for a node type and accepts a node parameter and a * parents parameter which is a list of the parent nodes of the current node. * Each returns an array of globals found. * * @param  {String} filePath *         The absolute path of the file being parsed. */function GlobalsForNode(filePath, context) {  this.path = filePath;  this.context = context;  if (this.path) {    this.dirname = path.dirname(this.path);  } else {    this.dirname = null;  }}GlobalsForNode.prototype = {  Program(node) {    let globals = [];    for (let comment of node.comments) {      if (comment.type !== "Block") {        continue;      }      let value = comment.value.trim();      value = value.replace(/\n/g, "");      // We have to discover any globals that ESLint would have defined through      // comment directives.      let match = /^globals?\s+(.+)/.exec(value);      if (match) {        let values = parseBooleanConfig(match[1].trim(), node);        for (let name of Object.keys(values)) {          globals.push({            name,            writable: values[name].value,          });        }        // We matched globals, so we won't match import-globals-from.        continue;      }      match = /^import-globals-from\s+(.+)$/.exec(value);      if (!match) {        continue;      }      if (!this.dirname) {        // If this is testing context without path, ignore import.        return globals;      }      let filePath = match[1].trim();      if (filePath.endsWith(".mjs")) {        if (this.context) {          this.context.report(            comment,            "import-globals-from does not support module files - use a direct import instead"          );        } else {          // Fall back to throwing an error, as we do not have a context in all situations,          // e.g. when loading the environment.          throw new Error(            "import-globals-from does not support module files - use a direct import instead"          );        }        continue;      }      if (!path.isAbsolute(filePath)) {        filePath = path.resolve(this.dirname, filePath);      } else {        filePath = path.join(helpers.rootDir, filePath);      }      globals = globals.concat(module.exports.getGlobalsForFile(filePath));    }    return globals;  },  ExpressionStatement(node, parents, globalScope) {    let isGlobal = helpers.getIsGlobalThis(parents);    let globals = [];    // Note: We check the expression types here and only call the necessary    // functions to aid performance.    if (node.expression.type === "AssignmentExpression") {      globals = helpers.convertThisAssignmentExpressionToGlobals(        node,        isGlobal      );    } else if (node.expression.type === "CallExpression") {      globals = helpers.convertCallExpressionToGlobals(node, isGlobal);    }    // Here we assume that if importScripts is set in the global scope, then    // this is a worker. It would be nice if eslint gave us a way of getting    // the environment directly.    //    // If this is testing context without path, ignore import.    if (globalScope && globalScope.set.get("importScripts") && this.dirname) {      let workerDetails = helpers.convertWorkerExpressionToGlobals(        node,        isGlobal,        this.dirname      );      globals = globals.concat(workerDetails);    }    return globals;  },};module.exports = {  /**   * Returns all globals for a given file. Recursively searches through   * import-globals-from directives and also includes globals defined by   * standard eslint directives.   *   * @param  {String} filePath   *         The absolute path of the file to be parsed.   * @param  {Object} astOptions   *         Extra options to pass to the parser.   * @return {Array}   *         An array of objects that contain details about the globals:   *         - {String} name   *                    The name of the global.   *         - {Boolean} writable   *                     If the global is writeable or not.   */  getGlobalsForFile(filePath, astOptions = {}) {    if (globalCache.has(filePath)) {      return globalCache.get(filePath);    }    if (globalDiscoveryInProgressForFiles.has(filePath)) {      // We're already processing this file, so return an empty set for now -      // the initial processing will pick up on the globals for this file.      return [];    }    globalDiscoveryInProgressForFiles.add(filePath);    let content = fs.readFileSync(filePath, "utf8");    // Parse the content into an AST    let { ast, scopeManager, visitorKeys } = helpers.parseCode(      content,      astOptions    );    // Discover global declarations    let globalScope = scopeManager.acquire(ast);    let globals = Object.keys(globalScope.variables).map(v => ({      name: globalScope.variables[v].name,      writable: true,    }));    // Walk over the AST to find any of our custom globals    let handler = new GlobalsForNode(filePath);    helpers.walkAST(ast, visitorKeys, (type, node, parents) => {      if (type in handler) {        let newGlobals = handler[type](node, parents, globalScope);        globals.push.apply(globals, newGlobals);      }    });    globalCache.set(filePath, globals);    globalDiscoveryInProgressForFiles.delete(filePath);    return globals;  },  /**   * Returns all globals for a code.   * This is only for testing.   *   * @param  {String} code   *         The JS code   * @param  {Object} astOptions   *         Extra options to pass to the parser.   * @return {Array}   *         An array of objects that contain details about the globals:   *         - {String} name   *                    The name of the global.   *         - {Boolean} writable   *                     If the global is writeable or not.   */  getGlobalsForCode(code, astOptions = {}) {    // Parse the content into an AST    let { ast, scopeManager, visitorKeys } = helpers.parseCode(      code,      astOptions,      { useBabel: false }    );    // Discover global declarations    let globalScope = scopeManager.acquire(ast);    let globals = Object.keys(globalScope.variables).map(v => ({      name: globalScope.variables[v].name,      writable: true,    }));    // Walk over the AST to find any of our custom globals    let handler = new GlobalsForNode(null);    helpers.walkAST(ast, visitorKeys, (type, node, parents) => {      if (type in handler) {        let newGlobals = handler[type](node, parents, globalScope);        globals.push.apply(globals, newGlobals);      }    });    return globals;  },  /**   * Returns all the globals for an html file that are defined by imported   * scripts (i.e. <script src="foo.js">).   *   * This function will cache results for one html file only - we expect   * this to be called sequentially for each chunk of a HTML file, rather   * than chucks of different files in random order.   *   * @param  {String} filePath   *         The absolute path of the file to be parsed.   * @return {Array}   *         An array of objects that contain details about the globals:   *         - {String} name   *                    The name of the global.   *         - {Boolean} writable   *                     If the global is writeable or not.   */  getImportedGlobalsForHTMLFile(filePath) {    if (lastHTMLGlobals.filename === filePath) {      return lastHTMLGlobals.globals;    }    let dir = path.dirname(filePath);    let globals = [];    let content = fs.readFileSync(filePath, "utf8");    let scriptSrcs = [];    // We use htmlparser as this ensures we find the script tags correctly.    let parser = new htmlparser.Parser(      {        onopentag(name, attribs) {          if (name === "script" && "src" in attribs) {            scriptSrcs.push({              src: attribs.src,              type:                "type" in attribs && attribs.type == "module"                  ? "module"                  : "script",            });          }        },      },      {        xmlMode: filePath.endsWith("xhtml"),      }    );    parser.parseComplete(content);    for (let script of scriptSrcs) {      // Ensure that the script src isn't just "".      if (!script.src) {        continue;      }      let scriptName;      if (script.src.includes("http:")) {        // We don't handle this currently as the paths are complex to match.      } else if (script.src.includes("chrome")) {        // This is one way of referencing test files.        script.src = script.src.replace("chrome://mochikit/content/", "/");        scriptName = path.join(          helpers.rootDir,          "testing",          "mochitest",          script.src        );      } else if (script.src.includes("SimpleTest")) {        // This is another way of referencing test files...        scriptName = path.join(          helpers.rootDir,          "testing",          "mochitest",          script.src        );      } else if (script.src.startsWith("/tests/")) {        scriptName = path.join(helpers.rootDir, script.src.substring(7));      } else {        // Fallback to hoping this is a relative path.        scriptName = path.join(dir, script.src);      }      if (scriptName && fs.existsSync(scriptName)) {        globals.push(          ...module.exports.getGlobalsForFile(scriptName, {            ecmaVersion: helpers.getECMAVersion(),            sourceType: script.type,          })        );      }    }    lastHTMLGlobals.filePath = filePath;    return (lastHTMLGlobals.globals = globals);  },  /**   * Intended to be used as-is for an ESLint rule that parses for globals in   * the current file and recurses through import-globals-from directives.   *   * @param  {Object} context   *         The ESLint parsing context.   */  getESLintGlobalParser(context) {    let globalScope;    let parser = {      Program(node) {        globalScope = context.getScope();      },    };    let filename = context.getFilename();    let extraHTMLGlobals = [];    if (filename.endsWith(".html") || filename.endsWith(".xhtml")) {      extraHTMLGlobals = module.exports.getImportedGlobalsForHTMLFile(filename);    }    // Install thin wrappers around GlobalsForNode    let handler = new GlobalsForNode(helpers.getAbsoluteFilePath(context));    for (let type of Object.keys(GlobalsForNode.prototype)) {      parser[type] = function(node) {        if (type === "Program") {          globalScope = context.getScope();          helpers.addGlobals(extraHTMLGlobals, globalScope);        }        let globals = handler[type](node, context.getAncestors(), globalScope);        helpers.addGlobals(          globals,          globalScope,          node.type !== "Program" && node        );      };    }    return parser;  },};
 |