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;
- },
- };
|