Source code

Revision control

Copy as Markdown

Other Tools

/* 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/>. */
import * as t from "@babel/types";
import getFunctionName from "../utils/getFunctionName";
import { getAst } from "../utils/ast";
/**
* "implicit"
* Variables added automaticly like "this" and "arguments"
*
* "var"
* Variables declared with "var" or non-block function declarations
*
* "let"
* Variables declared with "let".
*
* "const"
* Variables declared with "const", or added as const
* bindings like inner function expressions and inner class names.
*
* "import"
* Imported binding names exposed from other modules.
*
* "global"
* Variables that reference undeclared global values.
*/
// Location information about the expression immediartely surrounding a
// given binding reference.
function isGeneratedId(id) {
return !/\/originalSource/.test(id);
}
export function parseSourceScopes(sourceId) {
const ast = getAst(sourceId);
if (!ast || !Object.keys(ast).length) {
return null;
}
return buildScopeList(ast, sourceId);
}
export function buildScopeList(ast, sourceId) {
const { global, lexical } = createGlobalScope(ast, sourceId);
const state = {
// The id for the source that scope list is generated for
sourceId,
// A map of any free variables(variables which are used within the current scope but not
// declared within the scope). This changes when a new scope is created.
freeVariables: new Map(),
// A stack of all the free variables created across all the scopes that have
// been created.
freeVariableStack: [],
inType: null,
// The current scope, a new scope is potentially created on a visit to each node
// depending in the criteria. Initially set to the lexical global scope which is the
// child to the global scope.
scope: lexical,
// A stack of all the existing scopes, this is mainly used retrieve the parent scope
// (which is the last scope push onto the stack) on exiting a visited node.
scopeStack: [],
declarationBindingIds: new Set(),
};
t.traverse(ast, scopeCollectionVisitor, state);
for (const [key, freeVariables] of state.freeVariables) {
let binding = global.bindings[key];
if (!binding) {
binding = {
type: "global",
refs: [],
};
global.bindings[key] = binding;
}
binding.refs = freeVariables.concat(binding.refs);
}
// TODO: This should probably check for ".mjs" extension on the
// original file, and should also be skipped if the the generated
// code is an ES6 module rather than a script.
if (
isGeneratedId(sourceId) ||
(ast.program.sourceType === "script" && !looksLikeCommonJS(global))
) {
stripModuleScope(global);
}
return toParsedScopes([global], sourceId) || [];
}
function toParsedScopes(children, sourceId) {
if (!children || children.length === 0) {
return undefined;
}
return children.map(scope => ({
// Removing unneed information from TempScope such as parent reference.
// We also need to convert BabelLocation to the Location type.
start: scope.loc.start,
end: scope.loc.end,
type:
scope.type === "module" || scope.type === "function-body"
? "block"
: scope.type,
scopeKind: "",
displayName: scope.displayName,
bindings: scope.bindings,
children: toParsedScopes(scope.children, sourceId),
}));
}
/**
* Create a new scope object and link the scope to it parent.
*
* @param {String} type - scope type
* @param {String} displayName - The scope display name
* @param {Object} parent - The parent object scope
* @param {Object} loc - The start and end postions (line/columns) of the scope
* @returns {Object} The newly created scope
*/
function createTempScope(type, displayName, parent, loc) {
const scope = {
type,
displayName,
parent,
// A list of all the child scopes
children: [],
loc,
// All the bindings defined in this scope
// bindings = [binding, ...]
// binding = { type: "", refs: []}
bindings: Object.create(null),
};
if (parent) {
parent.children.push(scope);
}
return scope;
}
// Sets a new current scope and creates a new map to store the free variables
// that may exist in this scope.
function pushTempScope(state, type, displayName, loc) {
const scope = createTempScope(type, displayName, state.scope, loc);
state.scope = scope;
state.freeVariableStack.push(state.freeVariables);
state.freeVariables = new Map();
return scope;
}
function isNode(node, type) {
return node ? node.type === type : false;
}
// Walks up the scope tree to the top most variable scope
function getVarScope(scope) {
let s = scope;
while (s.type !== "function" && s.type !== "module") {
if (!s.parent) {
return s;
}
s = s.parent;
}
return s;
}
function fromBabelLocation(location, sourceId) {
return {
sourceId,
line: location.line,
column: location.column,
};
}
function parseDeclarator(
declaratorId,
targetScope,
type,
locationType,
declaration,
state
) {
if (isNode(declaratorId, "Identifier")) {
let existing = targetScope.bindings[declaratorId.name];
if (!existing) {
existing = {
type,
refs: [],
};
targetScope.bindings[declaratorId.name] = existing;
}
state.declarationBindingIds.add(declaratorId);
existing.refs.push({
type: locationType,
start: fromBabelLocation(declaratorId.loc.start, state.sourceId),
end: fromBabelLocation(declaratorId.loc.end, state.sourceId),
declaration: {
start: fromBabelLocation(declaration.loc.start, state.sourceId),
end: fromBabelLocation(declaration.loc.end, state.sourceId),
},
});
} else if (isNode(declaratorId, "ObjectPattern")) {
declaratorId.properties.forEach(prop => {
parseDeclarator(
prop.value,
targetScope,
type,
locationType,
declaration,
state
);
});
} else if (isNode(declaratorId, "ArrayPattern")) {
declaratorId.elements.forEach(item => {
parseDeclarator(
item,
targetScope,
type,
locationType,
declaration,
state
);
});
} else if (isNode(declaratorId, "AssignmentPattern")) {
parseDeclarator(
declaratorId.left,
targetScope,
type,
locationType,
declaration,
state
);
} else if (isNode(declaratorId, "RestElement")) {
parseDeclarator(
declaratorId.argument,
targetScope,
type,
locationType,
declaration,
state
);
} else if (t.isTSParameterProperty(declaratorId)) {
parseDeclarator(
declaratorId.parameter,
targetScope,
type,
locationType,
declaration,
state
);
}
}
function isLetOrConst(node) {
return node.kind === "let" || node.kind === "const";
}
function hasLexicalDeclaration(node, parent) {
const nodes = [];
if (t.isSwitchStatement(node)) {
for (const caseNode of node.cases) {
nodes.push(...caseNode.consequent);
}
} else {
nodes.push(...node.body);
}
const isFunctionBody = t.isFunction(parent, { body: node });
return nodes.some(
child =>
isLexicalVariable(child) ||
t.isClassDeclaration(child) ||
(!isFunctionBody && t.isFunctionDeclaration(child))
);
}
function isLexicalVariable(node) {
return isNode(node, "VariableDeclaration") && isLetOrConst(node);
}
// Creates the global scopes for this source, the overall global scope
// and a lexical global scope.
function createGlobalScope(ast, sourceId) {
const global = createTempScope("object", "Global", null, {
start: fromBabelLocation(ast.loc.start, sourceId),
end: fromBabelLocation(ast.loc.end, sourceId),
});
const lexical = createTempScope("block", "Lexical Global", global, {
start: fromBabelLocation(ast.loc.start, sourceId),
end: fromBabelLocation(ast.loc.end, sourceId),
});
return { global, lexical };
}
const scopeCollectionVisitor = {
// eslint-disable-next-line complexity
enter(node, ancestors, state) {
state.scopeStack.push(state.scope);
const parentNode =
ancestors.length === 0 ? null : ancestors[ancestors.length - 1].node;
if (state.inType) {
return;
}
if (t.isProgram(node)) {
const scope = pushTempScope(state, "module", "Module", {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
scope.bindings.this = {
type: "implicit",
refs: [],
};
} else if (t.isFunction(node)) {
let { scope } = state;
if (t.isFunctionExpression(node) && isNode(node.id, "Identifier")) {
scope = pushTempScope(state, "block", "Function Expression", {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
state.declarationBindingIds.add(node.id);
scope.bindings[node.id.name] = {
type: "const",
refs: [
{
type: "fn-expr",
start: fromBabelLocation(node.id.loc.start, state.sourceId),
end: fromBabelLocation(node.id.loc.end, state.sourceId),
declaration: {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
},
},
],
};
}
if (t.isFunctionDeclaration(node) && isNode(node.id, "Identifier")) {
// This ignores Annex B function declaration hoisting, which
// is probably a fine assumption.
state.declarationBindingIds.add(node.id);
const refs = [
{
type: "fn-decl",
start: fromBabelLocation(node.id.loc.start, state.sourceId),
end: fromBabelLocation(node.id.loc.end, state.sourceId),
declaration: {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
},
},
];
if (scope.type === "block") {
scope.bindings[node.id.name] = {
type: "let",
refs,
};
} else {
// Add the binding to the ancestor scope
getVarScope(scope).bindings[node.id.name] = {
type: "var",
refs,
};
}
}
scope = pushTempScope(
state,
"function",
getFunctionName(node, parentNode),
{
// Being at the start of a function doesn't count as
// being inside of it.
start: fromBabelLocation(
node.params[0] ? node.params[0].loc.start : node.loc.start,
state.sourceId
),
end: fromBabelLocation(node.loc.end, state.sourceId),
}
);
node.params.forEach(param =>
parseDeclarator(param, scope, "var", "fn-param", node, state)
);
if (!t.isArrowFunctionExpression(node)) {
scope.bindings.this = {
type: "implicit",
refs: [],
};
scope.bindings.arguments = {
type: "implicit",
refs: [],
};
}
if (
t.isBlockStatement(node.body) &&
hasLexicalDeclaration(node.body, node)
) {
scope = pushTempScope(state, "function-body", "Function Body", {
start: fromBabelLocation(node.body.loc.start, state.sourceId),
end: fromBabelLocation(node.body.loc.end, state.sourceId),
});
}
} else if (t.isClass(node)) {
if (t.isIdentifier(node.id)) {
// For decorated classes, the AST considers the first the decorator
// to be the start of the class. For the purposes of mapping class
// declarations however, we really want to look for the "class Foo"
// piece. To achieve that, we estimate the location of the declaration
// instead.
let declStart = node.loc.start;
if (node.decorators && node.decorators.length) {
// Estimate the location of the "class" keyword since it
// is unlikely to be a different line than the class name.
declStart = {
line: node.id.loc.start.line,
column: node.id.loc.start.column - "class ".length,
};
}
const declaration = {
start: fromBabelLocation(declStart, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
};
if (t.isClassDeclaration(node)) {
state.declarationBindingIds.add(node.id);
state.scope.bindings[node.id.name] = {
type: "let",
refs: [
{
type: "class-decl",
start: fromBabelLocation(node.id.loc.start, state.sourceId),
end: fromBabelLocation(node.id.loc.end, state.sourceId),
declaration,
},
],
};
}
const scope = pushTempScope(state, "block", "Class", {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
state.declarationBindingIds.add(node.id);
scope.bindings[node.id.name] = {
type: "const",
refs: [
{
type: "class-inner",
start: fromBabelLocation(node.id.loc.start, state.sourceId),
end: fromBabelLocation(node.id.loc.end, state.sourceId),
declaration,
},
],
};
}
} else if (t.isForXStatement(node) || t.isForStatement(node)) {
const init = node.init || node.left;
if (isNode(init, "VariableDeclaration") && isLetOrConst(init)) {
// Debugger will create new lexical environment for the for.
pushTempScope(state, "block", "For", {
// Being at the start of a for loop doesn't count as
// being inside it.
start: fromBabelLocation(init.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
}
} else if (t.isCatchClause(node)) {
const scope = pushTempScope(state, "block", "Catch", {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
parseDeclarator(node.param, scope, "var", "catch", node, state);
} else if (
t.isBlockStatement(node) &&
// Function body's are handled in the function logic above.
!t.isFunction(parentNode) &&
hasLexicalDeclaration(node, parentNode)
) {
// Debugger will create new lexical environment for the block.
pushTempScope(state, "block", "Block", {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
} else if (
t.isVariableDeclaration(node) &&
(node.kind === "var" ||
// Lexical declarations in for statements are handled above.
!t.isForStatement(parentNode, { init: node }) ||
!t.isForXStatement(parentNode, { left: node }))
) {
// Finds right lexical environment
const hoistAt = !isLetOrConst(node)
? getVarScope(state.scope)
: state.scope;
node.declarations.forEach(declarator => {
parseDeclarator(
declarator.id,
hoistAt,
node.kind,
node.kind,
node,
state
);
});
} else if (
t.isImportDeclaration(node) &&
(!node.importKind || node.importKind === "value")
) {
node.specifiers.forEach(spec => {
if (spec.importKind && spec.importKind !== "value") {
return;
}
if (t.isImportNamespaceSpecifier(spec)) {
state.declarationBindingIds.add(spec.local);
state.scope.bindings[spec.local.name] = {
// Imported namespaces aren't live import bindings, they are
// just normal const bindings.
type: "const",
refs: [
{
type: "import-ns-decl",
start: fromBabelLocation(spec.local.loc.start, state.sourceId),
end: fromBabelLocation(spec.local.loc.end, state.sourceId),
declaration: {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
},
},
],
};
} else {
state.declarationBindingIds.add(spec.local);
state.scope.bindings[spec.local.name] = {
type: "import",
refs: [
{
type: "import-decl",
start: fromBabelLocation(spec.local.loc.start, state.sourceId),
end: fromBabelLocation(spec.local.loc.end, state.sourceId),
importName: t.isImportDefaultSpecifier(spec)
? "default"
: spec.imported.name,
declaration: {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
},
},
],
};
}
});
} else if (t.isTSEnumDeclaration(node)) {
state.declarationBindingIds.add(node.id);
state.scope.bindings[node.id.name] = {
type: "const",
refs: [
{
type: "ts-enum-decl",
start: fromBabelLocation(node.id.loc.start, state.sourceId),
end: fromBabelLocation(node.id.loc.end, state.sourceId),
declaration: {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
},
},
],
};
} else if (t.isTSModuleDeclaration(node)) {
state.declarationBindingIds.add(node.id);
state.scope.bindings[node.id.name] = {
type: "const",
refs: [
{
type: "ts-namespace-decl",
start: fromBabelLocation(node.id.loc.start, state.sourceId),
end: fromBabelLocation(node.id.loc.end, state.sourceId),
declaration: {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
},
},
],
};
} else if (t.isTSModuleBlock(node)) {
pushTempScope(state, "block", "TypeScript Namespace", {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
} else if (
t.isIdentifier(node) &&
t.isReferenced(node, parentNode) &&
// Babel doesn't cover this in 'isReferenced' yet, but it should
// eventually.
!t.isTSEnumMember(parentNode, { id: node }) &&
!t.isTSModuleDeclaration(parentNode, { id: node }) &&
// isReferenced above fails to see `var { foo } = ...` as a non-reference
// because the direct parent is not enough to know that the pattern is
// used within a variable declaration.
!state.declarationBindingIds.has(node)
) {
let freeVariables = state.freeVariables.get(node.name);
if (!freeVariables) {
freeVariables = [];
state.freeVariables.set(node.name, freeVariables);
}
freeVariables.push({
type: "ref",
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
meta: buildMetaBindings(state.sourceId, node, ancestors),
});
} else if (isOpeningJSXIdentifier(node, ancestors)) {
let freeVariables = state.freeVariables.get(node.name);
if (!freeVariables) {
freeVariables = [];
state.freeVariables.set(node.name, freeVariables);
}
freeVariables.push({
type: "ref",
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
meta: buildMetaBindings(state.sourceId, node, ancestors),
});
} else if (t.isThisExpression(node)) {
let freeVariables = state.freeVariables.get("this");
if (!freeVariables) {
freeVariables = [];
state.freeVariables.set("this", freeVariables);
}
freeVariables.push({
type: "ref",
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
meta: buildMetaBindings(state.sourceId, node, ancestors),
});
} else if (t.isClassProperty(parentNode, { value: node })) {
const scope = pushTempScope(state, "function", "Class Field", {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
scope.bindings.this = {
type: "implicit",
refs: [],
};
scope.bindings.arguments = {
type: "implicit",
refs: [],
};
} else if (
t.isSwitchStatement(node) &&
hasLexicalDeclaration(node, parentNode)
) {
pushTempScope(state, "block", "Switch", {
start: fromBabelLocation(node.loc.start, state.sourceId),
end: fromBabelLocation(node.loc.end, state.sourceId),
});
}
if (
// In general Flow expressions are deleted, so they can't contain
// runtime bindings, but typecasts are the one exception there.
(t.isFlow(node) && !t.isTypeCastExpression(node)) ||
// In general TS items are deleted, but TS has a few wrapper node
// types that can contain general JS expressions.
(node.type.startsWith("TS") &&
!t.isTSTypeAssertion(node) &&
!t.isTSAsExpression(node) &&
!t.isTSNonNullExpression(node) &&
!t.isTSModuleDeclaration(node) &&
!t.isTSModuleBlock(node) &&
!t.isTSParameterProperty(node) &&
!t.isTSExportAssignment(node))
) {
// Flag this node as a root "type" node. All items inside of this
// will be skipped entirely.
state.inType = node;
}
},
exit(node, ancestors, state) {
const currentScope = state.scope;
const parentScope = state.scopeStack.pop();
if (!parentScope) {
throw new Error("Assertion failure - unsynchronized pop");
}
state.scope = parentScope;
// It is possible, as in the case of function expressions, that a single
// node has added multiple scopes, so we need to traverse upward here
// rather than jumping stright to 'parentScope'.
for (
let scope = currentScope;
scope && scope !== parentScope;
scope = scope.parent
) {
const { freeVariables } = state;
state.freeVariables = state.freeVariableStack.pop();
const parentFreeVariables = state.freeVariables;
// Match up any free variables that match this scope's bindings and
// merge then into the refs.
for (const key of Object.keys(scope.bindings)) {
const binding = scope.bindings[key];
const freeVars = freeVariables.get(key);
if (freeVars) {
binding.refs.push(...freeVars);
freeVariables.delete(key);
}
}
// Move any undeclared references in this scope into the parent for
// processing in higher scopes.
for (const [key, value] of freeVariables) {
let refs = parentFreeVariables.get(key);
if (!refs) {
refs = [];
parentFreeVariables.set(key, refs);
}
refs.push(...value);
}
}
if (state.inType === node) {
state.inType = null;
}
},
};
function isOpeningJSXIdentifier(node, ancestors) {
if (!t.isJSXIdentifier(node)) {
return false;
}
for (let i = ancestors.length - 1; i >= 0; i--) {
const { node: parent, key } = ancestors[i];
if (t.isJSXOpeningElement(parent) && key === "name") {
return true;
} else if (!t.isJSXMemberExpression(parent) || key !== "object") {
break;
}
}
return false;
}
function buildMetaBindings(
sourceId,
node,
ancestors,
parentIndex = ancestors.length - 1
) {
if (parentIndex <= 1) {
return null;
}
const parent = ancestors[parentIndex].node;
const grandparent = ancestors[parentIndex - 1].node;
// Consider "0, foo" to be equivalent to "foo".
if (
t.isSequenceExpression(parent) &&
parent.expressions.length === 2 &&
t.isNumericLiteral(parent.expressions[0]) &&
parent.expressions[1] === node
) {
let { start, end } = parent.loc;
if (t.isCallExpression(grandparent, { callee: parent })) {
// Attempt to expand the range around parentheses, e.g.
// (0, foo.bar)()
start = grandparent.loc.start;
end = Object.assign({}, end);
end.column += 1;
}
return {
type: "inherit",
start: fromBabelLocation(start, sourceId),
end: fromBabelLocation(end, sourceId),
parent: buildMetaBindings(sourceId, parent, ancestors, parentIndex - 1),
};
}
// Consider "Object(foo)", and "__webpack_require__.i(foo)" to be
// equivalent to "foo" since they are essentially identity functions.
if (
t.isCallExpression(parent) &&
(t.isIdentifier(parent.callee, { name: "Object" }) ||
(t.isMemberExpression(parent.callee, { computed: false }) &&
t.isIdentifier(parent.callee.object, { name: "__webpack_require__" }) &&
t.isIdentifier(parent.callee.property, { name: "i" }))) &&
parent.arguments.length === 1 &&
parent.arguments[0] === node
) {
return {
type: "inherit",
start: fromBabelLocation(parent.loc.start, sourceId),
end: fromBabelLocation(parent.loc.end, sourceId),
parent: buildMetaBindings(sourceId, parent, ancestors, parentIndex - 1),
};
}
if (t.isMemberExpression(parent, { object: node })) {
if (parent.computed) {
if (t.isStringLiteral(parent.property)) {
return {
type: "member",
start: fromBabelLocation(parent.loc.start, sourceId),
end: fromBabelLocation(parent.loc.end, sourceId),
property: parent.property.value,
parent: buildMetaBindings(
sourceId,
parent,
ancestors,
parentIndex - 1
),
};
}
} else {
return {
type: "member",
start: fromBabelLocation(parent.loc.start, sourceId),
end: fromBabelLocation(parent.loc.end, sourceId),
property: parent.property.name,
parent: buildMetaBindings(sourceId, parent, ancestors, parentIndex - 1),
};
}
}
if (
t.isCallExpression(parent, { callee: node }) &&
!parent.arguments.length
) {
return {
type: "call",
start: fromBabelLocation(parent.loc.start, sourceId),
end: fromBabelLocation(parent.loc.end, sourceId),
parent: buildMetaBindings(sourceId, parent, ancestors, parentIndex - 1),
};
}
return null;
}
function looksLikeCommonJS(rootScope) {
const hasRefs = name =>
rootScope.bindings[name] && !!rootScope.bindings[name].refs.length;
return (
hasRefs("__dirname") ||
hasRefs("__filename") ||
hasRefs("require") ||
hasRefs("exports") ||
hasRefs("module")
);
}
function stripModuleScope(rootScope) {
const rootLexicalScope = rootScope.children[0];
const moduleScope = rootLexicalScope.children[0];
if (moduleScope.type !== "module") {
throw new Error("Assertion failure - should be module");
}
Object.keys(moduleScope.bindings).forEach(name => {
const binding = moduleScope.bindings[name];
if (binding.type === "let" || binding.type === "const") {
rootLexicalScope.bindings[name] = binding;
} else {
rootScope.bindings[name] = binding;
}
});
rootLexicalScope.children = moduleScope.children;
rootLexicalScope.children.forEach(child => {
child.parent = rootLexicalScope;
});
}