mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-10 10:18:38 +09:00
524 lines
14 KiB
JavaScript
524 lines
14 KiB
JavaScript
/**
|
|
* @fileoverview A collection of helper functions.
|
|
* 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";
|
|
|
|
var escope = require("escope");
|
|
var espree = require("espree");
|
|
var estraverse = require("estraverse");
|
|
var path = require("path");
|
|
var fs = require("fs");
|
|
var ini = require("ini-parser");
|
|
|
|
var modules = null;
|
|
var directoryManifests = new Map();
|
|
|
|
var definitions = [
|
|
/^loader\.lazyGetter\(this, "(\w+)"/,
|
|
/^loader\.lazyImporter\(this, "(\w+)"/,
|
|
/^loader\.lazyServiceGetter\(this, "(\w+)"/,
|
|
/^loader\.lazyRequireGetter\(this, "(\w+)"/,
|
|
/^XPCOMUtils\.defineLazyGetter\(this, "(\w+)"/,
|
|
/^XPCOMUtils\.defineLazyModuleGetter\(this, "(\w+)"/,
|
|
/^XPCOMUtils\.defineLazyServiceGetter\(this, "(\w+)"/,
|
|
/^XPCOMUtils\.defineConstant\(this, "(\w+)"/,
|
|
/^DevToolsUtils\.defineLazyModuleGetter\(this, "(\w+)"/,
|
|
/^DevToolsUtils\.defineLazyGetter\(this, "(\w+)"/,
|
|
/^Object\.defineProperty\(this, "(\w+)"/,
|
|
/^Reflect\.defineProperty\(this, "(\w+)"/,
|
|
/^this\.__defineGetter__\("(\w+)"/,
|
|
/^this\.(\w+) =/
|
|
];
|
|
|
|
var imports = [
|
|
/^(?:Cu|Components\.utils)\.import\(".*\/((.*?)\.jsm?)"(?:, this)?\)/,
|
|
];
|
|
|
|
module.exports = {
|
|
/**
|
|
* Gets the abstract syntax tree (AST) of the JavaScript source code contained
|
|
* in sourceText.
|
|
*
|
|
* @param {String} sourceText
|
|
* Text containing valid JavaScript.
|
|
*
|
|
* @return {Object}
|
|
* The resulting AST.
|
|
*/
|
|
getAST: function(sourceText) {
|
|
// Use a permissive config file to allow parsing of anything that Espree
|
|
// can parse.
|
|
var config = this.getPermissiveConfig();
|
|
|
|
return espree.parse(sourceText, config);
|
|
},
|
|
|
|
/**
|
|
* A simplistic conversion of some AST nodes to a standard string form.
|
|
*
|
|
* @param {Object} node
|
|
* The AST node to convert.
|
|
*
|
|
* @return {String}
|
|
* The JS source for the node.
|
|
*/
|
|
getASTSource: function(node) {
|
|
switch (node.type) {
|
|
case "MemberExpression":
|
|
if (node.computed)
|
|
throw new Error("getASTSource unsupported computed MemberExpression");
|
|
return this.getASTSource(node.object) + "." + this.getASTSource(node.property);
|
|
case "ThisExpression":
|
|
return "this";
|
|
case "Identifier":
|
|
return node.name;
|
|
case "Literal":
|
|
return JSON.stringify(node.value);
|
|
case "CallExpression":
|
|
var args = node.arguments.map(a => this.getASTSource(a)).join(", ");
|
|
return this.getASTSource(node.callee) + "(" + args + ")";
|
|
case "ObjectExpression":
|
|
return "{}";
|
|
case "ExpressionStatement":
|
|
return this.getASTSource(node.expression) + ";";
|
|
case "FunctionExpression":
|
|
return "function() {}";
|
|
case "ArrowFunctionExpression":
|
|
return "() => {}";
|
|
case "AssignmentExpression":
|
|
return this.getASTSource(node.left) + " = " + this.getASTSource(node.right);
|
|
default:
|
|
throw new Error("getASTSource unsupported node type: " + node.type);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* This walks an AST in a manner similar to ESLint passing node and comment
|
|
* events to the listener. The listener is expected to be a simple function
|
|
* which accepts node type, node and parents arguments.
|
|
*
|
|
* @param {Object} ast
|
|
* The AST to walk.
|
|
* @param {Function} listener
|
|
* A callback function to call for the nodes. Passed three arguments,
|
|
* event type, node and an array of parent nodes for the current node.
|
|
*/
|
|
walkAST(ast, listener) {
|
|
let parents = [];
|
|
|
|
let seenComments = new Set();
|
|
function sendCommentEvents(comments) {
|
|
if (!comments) {
|
|
return;
|
|
}
|
|
|
|
for (let comment of comments) {
|
|
if (seenComments.has(comment)) {
|
|
return;
|
|
}
|
|
seenComments.add(comment);
|
|
|
|
listener(comment.type + "Comment", comment, parents);
|
|
}
|
|
}
|
|
|
|
estraverse.traverse(ast, {
|
|
enter(node, parent) {
|
|
// Comments are held in node.comments for empty programs
|
|
let leadingComments = node.leadingComments;
|
|
if (node.type === "Program" && node.body.length == 0) {
|
|
leadingComments = node.comments;
|
|
}
|
|
|
|
sendCommentEvents(leadingComments);
|
|
listener(node.type, node, parents);
|
|
sendCommentEvents(node.trailingComments);
|
|
|
|
parents.push(node);
|
|
},
|
|
|
|
leave(node, parent) {
|
|
// TODO send comment exit events
|
|
listener(node.type + ":exit", node, parents);
|
|
|
|
if (parents.length == 0) {
|
|
throw new Error("Left more nodes than entered.");
|
|
}
|
|
parents.pop();
|
|
}
|
|
});
|
|
if (parents.length) {
|
|
throw new Error("Entered more nodes than left.");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Attempts to convert an ExpressionStatement to likely global variable
|
|
* definitions.
|
|
*
|
|
* @param {Object} node
|
|
* The AST node to convert.
|
|
* @param {boolean} isGlobal
|
|
* True if the current node is in the global scope.
|
|
* @param {String} repository
|
|
* The root of the repository.
|
|
*
|
|
* @return {Array}
|
|
* An array of variable names defined.
|
|
*/
|
|
convertExpressionToGlobals: function(node, isGlobal, repository) {
|
|
if (!modules) {
|
|
modules = require(path.join(repository, "tools", "lint", "eslint", "modules.json"));
|
|
}
|
|
|
|
try {
|
|
var source = this.getASTSource(node);
|
|
}
|
|
catch (e) {
|
|
return [];
|
|
}
|
|
|
|
for (var reg of definitions) {
|
|
var match = source.match(reg);
|
|
if (match) {
|
|
// Must be in the global scope
|
|
if (!isGlobal) {
|
|
return [];
|
|
}
|
|
|
|
return [match[1]];
|
|
}
|
|
}
|
|
|
|
for (reg of imports) {
|
|
var match = source.match(reg);
|
|
if (match) {
|
|
// The two argument form is only acceptable in the global scope
|
|
if (node.expression.arguments.length > 1 && !isGlobal) {
|
|
return [];
|
|
}
|
|
|
|
if (match[1] in modules) {
|
|
return modules[match[1]];
|
|
}
|
|
|
|
return [match[2]];
|
|
}
|
|
}
|
|
|
|
return [];
|
|
},
|
|
|
|
/**
|
|
* Add a variable to the current scope.
|
|
* HACK: This relies on eslint internals so it could break at any time.
|
|
*
|
|
* @param {String} name
|
|
* The variable name to add to the scope.
|
|
* @param {ASTScope} scope
|
|
* The scope to add to.
|
|
* @param {boolean} writable
|
|
* Whether the global can be overwritten.
|
|
*/
|
|
addVarToScope: function(name, scope, writable) {
|
|
scope.__defineGeneric(name, scope.set, scope.variables, null, null);
|
|
|
|
let variable = scope.set.get(name);
|
|
variable.eslintExplicitGlobal = false;
|
|
variable.writeable = writable;
|
|
|
|
// Walk to the global scope which holds all undeclared variables.
|
|
while (scope.type != "global") {
|
|
scope = scope.upper;
|
|
}
|
|
|
|
// "through" contains all references with no found definition.
|
|
scope.through = scope.through.filter(function(reference) {
|
|
if (reference.identifier.name != name) {
|
|
return true;
|
|
}
|
|
|
|
// Links the variable and the reference.
|
|
// And this reference is removed from `Scope#through`.
|
|
reference.resolved = variable;
|
|
variable.references.push(reference);
|
|
return false;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Adds a set of globals to a scope.
|
|
*
|
|
* @param {Array} globalVars
|
|
* An array of global variable names.
|
|
* @param {ASTScope} scope
|
|
* The scope.
|
|
*/
|
|
addGlobals: function(globalVars, scope) {
|
|
globalVars.forEach(v => this.addVarToScope(v.name, scope, v.writable));
|
|
},
|
|
|
|
/**
|
|
* To allow espree to parse almost any JavaScript we need as many features as
|
|
* possible turned on. This method returns that config.
|
|
*
|
|
* @return {Object}
|
|
* Espree compatible permissive config.
|
|
*/
|
|
getPermissiveConfig: function() {
|
|
return {
|
|
range: true,
|
|
loc: true,
|
|
comment: true,
|
|
attachComment: true,
|
|
ecmaVersion: 8,
|
|
sourceType: "script",
|
|
ecmaFeatures: {
|
|
experimentalObjectRestSpread: true,
|
|
globalReturn: true,
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Check whether the context is the global scope.
|
|
*
|
|
* @param {Array} ancestors
|
|
* The parents of the current node.
|
|
*
|
|
* @return {Boolean}
|
|
* True or false
|
|
*/
|
|
getIsGlobalScope: function(ancestors) {
|
|
for (let parent of ancestors) {
|
|
if (parent.type == "FunctionExpression" ||
|
|
parent.type == "FunctionDeclaration") {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Check whether we might be in a test head file.
|
|
*
|
|
* @param {RuleContext} scope
|
|
* You should pass this from within a rule
|
|
* e.g. helpers.getIsHeadFile(this)
|
|
*
|
|
* @return {Boolean}
|
|
* True or false
|
|
*/
|
|
getIsHeadFile: function(scope) {
|
|
var pathAndFilename = this.cleanUpPath(scope.getFilename());
|
|
|
|
return /.*[\\/]head(_.+)?\.js$/.test(pathAndFilename);
|
|
},
|
|
|
|
/**
|
|
* Gets the head files for a potential test file
|
|
*
|
|
* @param {RuleContext} scope
|
|
* You should pass this from within a rule
|
|
* e.g. helpers.getIsHeadFile(this)
|
|
*
|
|
* @return {String[]}
|
|
* Paths to head files to load for the test
|
|
*/
|
|
getTestHeadFiles: function(scope) {
|
|
if (!this.getIsTest(scope)) {
|
|
return [];
|
|
}
|
|
|
|
let filepath = this.cleanUpPath(scope.getFilename());
|
|
let dir = path.dirname(filepath);
|
|
|
|
let names = fs.readdirSync(dir)
|
|
.filter(name => name.startsWith("head") && name.endsWith(".js"))
|
|
.map(name => path.join(dir, name));
|
|
return names;
|
|
},
|
|
|
|
/**
|
|
* Gets all the test manifest data for a directory
|
|
*
|
|
* @param {String} dir
|
|
* The directory
|
|
*
|
|
* @return {Array}
|
|
* An array of objects with file and manifest properties
|
|
*/
|
|
getManifestsForDirectory: function(dir) {
|
|
if (directoryManifests.has(dir)) {
|
|
return directoryManifests.get(dir);
|
|
}
|
|
|
|
let manifests = [];
|
|
|
|
let names = fs.readdirSync(dir);
|
|
for (let name of names) {
|
|
if (!name.endsWith(".ini")) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
let manifest = ini.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
|
|
manifests.push({
|
|
file: path.join(dir, name),
|
|
manifest
|
|
})
|
|
} catch (e) {
|
|
}
|
|
}
|
|
|
|
directoryManifests.set(dir, manifests);
|
|
return manifests;
|
|
},
|
|
|
|
/**
|
|
* Gets the manifest file a test is listed in
|
|
*
|
|
* @param {RuleContext} scope
|
|
* You should pass this from within a rule
|
|
* e.g. helpers.getIsHeadFile(this)
|
|
*
|
|
* @return {String}
|
|
* The path to the test manifest file
|
|
*/
|
|
getTestManifest: function(scope) {
|
|
let filepath = this.cleanUpPath(scope.getFilename());
|
|
|
|
let dir = path.dirname(filepath);
|
|
let filename = path.basename(filepath);
|
|
|
|
for (let manifest of this.getManifestsForDirectory(dir)) {
|
|
if (filename in manifest.manifest) {
|
|
return manifest.file;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Check whether we are in a test of some kind.
|
|
*
|
|
* @param {RuleContext} scope
|
|
* You should pass this from within a rule
|
|
* e.g. helpers.getIsTest(this)
|
|
*
|
|
* @return {Boolean}
|
|
* True or false
|
|
*/
|
|
getIsTest: function(scope) {
|
|
// Regardless of the manifest name being in a manifest means we're a test.
|
|
let manifest = this.getTestManifest(scope);
|
|
if (manifest) {
|
|
return true;
|
|
}
|
|
|
|
return !!this.getTestType(scope);
|
|
},
|
|
|
|
/**
|
|
* Gets the type of test or null if this isn't a test.
|
|
*
|
|
* @param {RuleContext} scope
|
|
* You should pass this from within a rule
|
|
* e.g. helpers.getIsHeadFile(this)
|
|
*
|
|
* @return {String or null}
|
|
* Test type: xpcshell, browser, chrome, mochitest
|
|
*/
|
|
getTestType: function(scope) {
|
|
let manifest = this.getTestManifest(scope);
|
|
if (manifest) {
|
|
let name = path.basename(manifest);
|
|
for (let testType of ["browser", "xpcshell", "chrome", "mochitest"]) {
|
|
if (name.startsWith(testType)) {
|
|
return testType;
|
|
}
|
|
}
|
|
}
|
|
|
|
let filepath = this.cleanUpPath(scope.getFilename());
|
|
let filename = path.basename(filepath);
|
|
|
|
if (filename.startsWith("browser_")) {
|
|
return "browser";
|
|
}
|
|
|
|
if (filename.startsWith("test_")) {
|
|
return "xpcshell";
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Gets the root directory of the repository by walking up directories until
|
|
* a .eslintignore file is found.
|
|
* @param {String} fileName
|
|
* The absolute path of a file in the repository
|
|
*
|
|
* @return {String} The absolute path of the repository directory
|
|
*/
|
|
getRootDir: function(fileName) {
|
|
var dirName = path.dirname(fileName);
|
|
|
|
while (dirName && !fs.existsSync(path.join(dirName, ".eslintignore"))) {
|
|
dirName = path.dirname(dirName);
|
|
}
|
|
|
|
if (!dirName) {
|
|
throw new Error("Unable to find root of repository");
|
|
}
|
|
|
|
return dirName;
|
|
},
|
|
|
|
/**
|
|
* ESLint may be executed from various places: from mach, at the root of the
|
|
* repository, or from a directory in the repository when, for instance,
|
|
* executed by a text editor's plugin.
|
|
* The value returned by context.getFileName() varies because of this.
|
|
* This helper function makes sure to return an absolute file path for the
|
|
* current context, by looking at process.cwd().
|
|
* @param {Context} context
|
|
* @return {String} The absolute path
|
|
*/
|
|
getAbsoluteFilePath: function(context) {
|
|
var fileName = this.cleanUpPath(context.getFilename());
|
|
var cwd = process.cwd();
|
|
|
|
if (path.isAbsolute(fileName)) {
|
|
// Case 2: executed from the repo's root with mach:
|
|
// fileName: /path/to/mozilla/repo/a/b/c/d.js
|
|
// cwd: /path/to/mozilla/repo
|
|
return fileName;
|
|
} else if (path.basename(fileName) == fileName) {
|
|
// Case 1b: executed from a nested directory, fileName is the base name
|
|
// without any path info (happens in Atom with linter-eslint)
|
|
return path.join(cwd, fileName);
|
|
} else {
|
|
// Case 1: executed form in a nested directory, e.g. from a text editor:
|
|
// fileName: a/b/c/d.js
|
|
// cwd: /path/to/mozilla/repo/a/b/c
|
|
var dirName = path.dirname(fileName);
|
|
return cwd.slice(0, cwd.length - dirName.length) + fileName;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* When ESLint is run from SublimeText, paths retrieved from
|
|
* context.getFileName contain leading and trailing double-quote characters.
|
|
* These characters need to be removed.
|
|
*/
|
|
cleanUpPath: function(path) {
|
|
return path.replace(/^"/, "").replace(/"$/, "");
|
|
}
|
|
};
|