зеркало из https://github.com/mozilla/gecko-dev.git
227 строки
6.4 KiB
JavaScript
227 строки
6.4 KiB
JavaScript
/**
|
|
* @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 escope = require("escope");
|
|
const estraverse = require("estraverse");
|
|
|
|
/**
|
|
* 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 = undefined;
|
|
if (pos !== -1) {
|
|
value = name.substring(pos + 1, name.length);
|
|
name = name.substring(0, pos);
|
|
}
|
|
|
|
items[name] = {
|
|
value: (value === "true"),
|
|
comment: 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();
|
|
|
|
/**
|
|
* 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) {
|
|
this.path = filePath;
|
|
this.dirname = path.dirname(this.path)
|
|
this.root = helpers.getRootDir(this.path);
|
|
this.isWorker = helpers.getIsWorker(this.path);
|
|
}
|
|
|
|
GlobalsForNode.prototype = {
|
|
Program(node) {
|
|
if (!this.isWorker) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{name: "importScripts", writable: false},
|
|
// Only available to workers.
|
|
{name: "FileReaderSync", writable: false},
|
|
{name: "onmessage", writable: true},
|
|
// Only available to chrome workers, but since we don't know which is which,
|
|
// we make it available anyway.
|
|
{name: "ctypes", writable: false}
|
|
];
|
|
},
|
|
|
|
BlockComment(node, parents) {
|
|
let value = node.value.trim();
|
|
let match = /^import-globals-from\s+(.+)$/.exec(value);
|
|
if (!match) {
|
|
return [];
|
|
}
|
|
|
|
let filePath = match[1].trim();
|
|
|
|
if (!path.isAbsolute(filePath)) {
|
|
filePath = path.resolve(this.dirname, filePath);
|
|
}
|
|
|
|
return module.exports.getGlobalsForFile(filePath);
|
|
},
|
|
|
|
ExpressionStatement(node, parents) {
|
|
let isGlobal = helpers.getIsGlobalScope(parents);
|
|
let globals = helpers.convertExpressionToGlobals(node, isGlobal, this.root);
|
|
// Map these globals now, as getGlobalsForFile is pre-mapped.
|
|
globals = globals.map(name => { return { name, writable: true }});
|
|
|
|
if (this.isWorker) {
|
|
let workerDetails = helpers.convertWorkerExpressionToGlobals(node,
|
|
isGlobal, this.root, this.dirname);
|
|
globals = globals.concat(workerDetails.map(name => {
|
|
return { name, writable: true };
|
|
}));
|
|
}
|
|
|
|
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} path
|
|
* 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.
|
|
*/
|
|
getGlobalsForFile(path) {
|
|
if (globalCache.has(path)) {
|
|
return globalCache.get(path);
|
|
}
|
|
|
|
let content = fs.readFileSync(path, "utf8");
|
|
|
|
// Parse the content into an AST
|
|
let ast = helpers.getAST(content);
|
|
|
|
// Discover global declarations
|
|
let scopeManager = escope.analyze(ast);
|
|
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(path);
|
|
|
|
helpers.walkAST(ast, (type, node, parents) => {
|
|
// We have to discover any globals that ESLint would have defined through
|
|
// comment directives
|
|
if (type == "BlockComment") {
|
|
let value = node.value.trim();
|
|
value = value.replace(/\n/g, '');
|
|
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
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type in handler) {
|
|
let newGlobals = handler[type](node, parents);
|
|
globals.push.apply(globals, newGlobals);
|
|
}
|
|
});
|
|
|
|
globalCache.set(path, globals);
|
|
|
|
return 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();
|
|
}
|
|
};
|
|
|
|
// 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();
|
|
}
|
|
let globals = handler[type](node, context.getAncestors());
|
|
helpers.addGlobals(globals, globalScope);
|
|
}
|
|
}
|
|
|
|
return parser;
|
|
}
|
|
};
|