summaryrefslogtreecommitdiffstats
path: root/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js')
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js434
1 files changed, 434 insertions, 0 deletions
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
new file mode 100644
index 0000000000..2c43c18799
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
@@ -0,0 +1,434 @@
+/**
+ * @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;
+ },
+};