You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
629 lines
18 KiB
629 lines
18 KiB
/* |
|
Copyright (C) 2015 Yusuke Suzuki <utatane.tea@gmail.com> |
|
|
|
Redistribution and use in source and binary forms, with or without |
|
modification, are permitted provided that the following conditions are met: |
|
|
|
* Redistributions of source code must retain the above copyright |
|
notice, this list of conditions and the following disclaimer. |
|
* Redistributions in binary form must reproduce the above copyright |
|
notice, this list of conditions and the following disclaimer in the |
|
documentation and/or other materials provided with the distribution. |
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
|
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY |
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
|
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
*/ |
|
"use strict"; |
|
|
|
/* eslint-disable no-underscore-dangle */ |
|
/* eslint-disable no-undefined */ |
|
|
|
const Syntax = require("estraverse").Syntax; |
|
const esrecurse = require("esrecurse"); |
|
const Reference = require("./reference"); |
|
const Variable = require("./variable"); |
|
const PatternVisitor = require("./pattern-visitor"); |
|
const definition = require("./definition"); |
|
const assert = require("assert"); |
|
|
|
const ParameterDefinition = definition.ParameterDefinition; |
|
const Definition = definition.Definition; |
|
|
|
/** |
|
* Traverse identifier in pattern |
|
* @param {Object} options - options |
|
* @param {pattern} rootPattern - root pattern |
|
* @param {Refencer} referencer - referencer |
|
* @param {callback} callback - callback |
|
* @returns {void} |
|
*/ |
|
function traverseIdentifierInPattern(options, rootPattern, referencer, callback) { |
|
|
|
// Call the callback at left hand identifier nodes, and Collect right hand nodes. |
|
const visitor = new PatternVisitor(options, rootPattern, callback); |
|
|
|
visitor.visit(rootPattern); |
|
|
|
// Process the right hand nodes recursively. |
|
if (referencer !== null && referencer !== undefined) { |
|
visitor.rightHandNodes.forEach(referencer.visit, referencer); |
|
} |
|
} |
|
|
|
// Importing ImportDeclaration. |
|
// http://people.mozilla.org/~jorendorff/es6-draft.html#sec-moduledeclarationinstantiation |
|
// https://github.com/estree/estree/blob/master/es6.md#importdeclaration |
|
// FIXME: Now, we don't create module environment, because the context is |
|
// implementation dependent. |
|
|
|
class Importer extends esrecurse.Visitor { |
|
constructor(declaration, referencer) { |
|
super(null, referencer.options); |
|
this.declaration = declaration; |
|
this.referencer = referencer; |
|
} |
|
|
|
visitImport(id, specifier) { |
|
this.referencer.visitPattern(id, pattern => { |
|
this.referencer.currentScope().__define(pattern, |
|
new Definition( |
|
Variable.ImportBinding, |
|
pattern, |
|
specifier, |
|
this.declaration, |
|
null, |
|
null |
|
)); |
|
}); |
|
} |
|
|
|
ImportNamespaceSpecifier(node) { |
|
const local = (node.local || node.id); |
|
|
|
if (local) { |
|
this.visitImport(local, node); |
|
} |
|
} |
|
|
|
ImportDefaultSpecifier(node) { |
|
const local = (node.local || node.id); |
|
|
|
this.visitImport(local, node); |
|
} |
|
|
|
ImportSpecifier(node) { |
|
const local = (node.local || node.id); |
|
|
|
if (node.name) { |
|
this.visitImport(node.name, node); |
|
} else { |
|
this.visitImport(local, node); |
|
} |
|
} |
|
} |
|
|
|
// Referencing variables and creating bindings. |
|
class Referencer extends esrecurse.Visitor { |
|
constructor(options, scopeManager) { |
|
super(null, options); |
|
this.options = options; |
|
this.scopeManager = scopeManager; |
|
this.parent = null; |
|
this.isInnerMethodDefinition = false; |
|
} |
|
|
|
currentScope() { |
|
return this.scopeManager.__currentScope; |
|
} |
|
|
|
close(node) { |
|
while (this.currentScope() && node === this.currentScope().block) { |
|
this.scopeManager.__currentScope = this.currentScope().__close(this.scopeManager); |
|
} |
|
} |
|
|
|
pushInnerMethodDefinition(isInnerMethodDefinition) { |
|
const previous = this.isInnerMethodDefinition; |
|
|
|
this.isInnerMethodDefinition = isInnerMethodDefinition; |
|
return previous; |
|
} |
|
|
|
popInnerMethodDefinition(isInnerMethodDefinition) { |
|
this.isInnerMethodDefinition = isInnerMethodDefinition; |
|
} |
|
|
|
referencingDefaultValue(pattern, assignments, maybeImplicitGlobal, init) { |
|
const scope = this.currentScope(); |
|
|
|
assignments.forEach(assignment => { |
|
scope.__referencing( |
|
pattern, |
|
Reference.WRITE, |
|
assignment.right, |
|
maybeImplicitGlobal, |
|
pattern !== assignment.left, |
|
init |
|
); |
|
}); |
|
} |
|
|
|
visitPattern(node, options, callback) { |
|
let visitPatternOptions = options; |
|
let visitPatternCallback = callback; |
|
|
|
if (typeof options === "function") { |
|
visitPatternCallback = options; |
|
visitPatternOptions = { processRightHandNodes: false }; |
|
} |
|
|
|
traverseIdentifierInPattern( |
|
this.options, |
|
node, |
|
visitPatternOptions.processRightHandNodes ? this : null, |
|
visitPatternCallback |
|
); |
|
} |
|
|
|
visitFunction(node) { |
|
let i, iz; |
|
|
|
// FunctionDeclaration name is defined in upper scope |
|
// NOTE: Not referring variableScope. It is intended. |
|
// Since |
|
// in ES5, FunctionDeclaration should be in FunctionBody. |
|
// in ES6, FunctionDeclaration should be block scoped. |
|
|
|
if (node.type === Syntax.FunctionDeclaration) { |
|
|
|
// id is defined in upper scope |
|
this.currentScope().__define(node.id, |
|
new Definition( |
|
Variable.FunctionName, |
|
node.id, |
|
node, |
|
null, |
|
null, |
|
null |
|
)); |
|
} |
|
|
|
// FunctionExpression with name creates its special scope; |
|
// FunctionExpressionNameScope. |
|
if (node.type === Syntax.FunctionExpression && node.id) { |
|
this.scopeManager.__nestFunctionExpressionNameScope(node); |
|
} |
|
|
|
// Consider this function is in the MethodDefinition. |
|
this.scopeManager.__nestFunctionScope(node, this.isInnerMethodDefinition); |
|
|
|
const that = this; |
|
|
|
/** |
|
* Visit pattern callback |
|
* @param {pattern} pattern - pattern |
|
* @param {Object} info - info |
|
* @returns {void} |
|
*/ |
|
function visitPatternCallback(pattern, info) { |
|
that.currentScope().__define(pattern, |
|
new ParameterDefinition( |
|
pattern, |
|
node, |
|
i, |
|
info.rest |
|
)); |
|
|
|
that.referencingDefaultValue(pattern, info.assignments, null, true); |
|
} |
|
|
|
// Process parameter declarations. |
|
for (i = 0, iz = node.params.length; i < iz; ++i) { |
|
this.visitPattern(node.params[i], { processRightHandNodes: true }, visitPatternCallback); |
|
} |
|
|
|
// if there's a rest argument, add that |
|
if (node.rest) { |
|
this.visitPattern({ |
|
type: "RestElement", |
|
argument: node.rest |
|
}, pattern => { |
|
this.currentScope().__define(pattern, |
|
new ParameterDefinition( |
|
pattern, |
|
node, |
|
node.params.length, |
|
true |
|
)); |
|
}); |
|
} |
|
|
|
// In TypeScript there are a number of function-like constructs which have no body, |
|
// so check it exists before traversing |
|
if (node.body) { |
|
|
|
// Skip BlockStatement to prevent creating BlockStatement scope. |
|
if (node.body.type === Syntax.BlockStatement) { |
|
this.visitChildren(node.body); |
|
} else { |
|
this.visit(node.body); |
|
} |
|
} |
|
|
|
this.close(node); |
|
} |
|
|
|
visitClass(node) { |
|
if (node.type === Syntax.ClassDeclaration) { |
|
this.currentScope().__define(node.id, |
|
new Definition( |
|
Variable.ClassName, |
|
node.id, |
|
node, |
|
null, |
|
null, |
|
null |
|
)); |
|
} |
|
|
|
this.visit(node.superClass); |
|
|
|
this.scopeManager.__nestClassScope(node); |
|
|
|
if (node.id) { |
|
this.currentScope().__define(node.id, |
|
new Definition( |
|
Variable.ClassName, |
|
node.id, |
|
node |
|
)); |
|
} |
|
this.visit(node.body); |
|
|
|
this.close(node); |
|
} |
|
|
|
visitProperty(node) { |
|
let previous; |
|
|
|
if (node.computed) { |
|
this.visit(node.key); |
|
} |
|
|
|
const isMethodDefinition = node.type === Syntax.MethodDefinition; |
|
|
|
if (isMethodDefinition) { |
|
previous = this.pushInnerMethodDefinition(true); |
|
} |
|
this.visit(node.value); |
|
if (isMethodDefinition) { |
|
this.popInnerMethodDefinition(previous); |
|
} |
|
} |
|
|
|
visitForIn(node) { |
|
if (node.left.type === Syntax.VariableDeclaration && node.left.kind !== "var") { |
|
this.scopeManager.__nestForScope(node); |
|
} |
|
|
|
if (node.left.type === Syntax.VariableDeclaration) { |
|
this.visit(node.left); |
|
this.visitPattern(node.left.declarations[0].id, pattern => { |
|
this.currentScope().__referencing(pattern, Reference.WRITE, node.right, null, true, true); |
|
}); |
|
} else { |
|
this.visitPattern(node.left, { processRightHandNodes: true }, (pattern, info) => { |
|
let maybeImplicitGlobal = null; |
|
|
|
if (!this.currentScope().isStrict) { |
|
maybeImplicitGlobal = { |
|
pattern, |
|
node |
|
}; |
|
} |
|
this.referencingDefaultValue(pattern, info.assignments, maybeImplicitGlobal, false); |
|
this.currentScope().__referencing(pattern, Reference.WRITE, node.right, maybeImplicitGlobal, true, false); |
|
}); |
|
} |
|
this.visit(node.right); |
|
this.visit(node.body); |
|
|
|
this.close(node); |
|
} |
|
|
|
visitVariableDeclaration(variableTargetScope, type, node, index) { |
|
|
|
const decl = node.declarations[index]; |
|
const init = decl.init; |
|
|
|
this.visitPattern(decl.id, { processRightHandNodes: true }, (pattern, info) => { |
|
variableTargetScope.__define( |
|
pattern, |
|
new Definition( |
|
type, |
|
pattern, |
|
decl, |
|
node, |
|
index, |
|
node.kind |
|
) |
|
); |
|
|
|
this.referencingDefaultValue(pattern, info.assignments, null, true); |
|
if (init) { |
|
this.currentScope().__referencing(pattern, Reference.WRITE, init, null, !info.topLevel, true); |
|
} |
|
}); |
|
} |
|
|
|
AssignmentExpression(node) { |
|
if (PatternVisitor.isPattern(node.left)) { |
|
if (node.operator === "=") { |
|
this.visitPattern(node.left, { processRightHandNodes: true }, (pattern, info) => { |
|
let maybeImplicitGlobal = null; |
|
|
|
if (!this.currentScope().isStrict) { |
|
maybeImplicitGlobal = { |
|
pattern, |
|
node |
|
}; |
|
} |
|
this.referencingDefaultValue(pattern, info.assignments, maybeImplicitGlobal, false); |
|
this.currentScope().__referencing(pattern, Reference.WRITE, node.right, maybeImplicitGlobal, !info.topLevel, false); |
|
}); |
|
} else { |
|
this.currentScope().__referencing(node.left, Reference.RW, node.right); |
|
} |
|
} else { |
|
this.visit(node.left); |
|
} |
|
this.visit(node.right); |
|
} |
|
|
|
CatchClause(node) { |
|
this.scopeManager.__nestCatchScope(node); |
|
|
|
this.visitPattern(node.param, { processRightHandNodes: true }, (pattern, info) => { |
|
this.currentScope().__define(pattern, |
|
new Definition( |
|
Variable.CatchClause, |
|
node.param, |
|
node, |
|
null, |
|
null, |
|
null |
|
)); |
|
this.referencingDefaultValue(pattern, info.assignments, null, true); |
|
}); |
|
this.visit(node.body); |
|
|
|
this.close(node); |
|
} |
|
|
|
Program(node) { |
|
this.scopeManager.__nestGlobalScope(node); |
|
|
|
if (this.scopeManager.__isNodejsScope()) { |
|
|
|
// Force strictness of GlobalScope to false when using node.js scope. |
|
this.currentScope().isStrict = false; |
|
this.scopeManager.__nestFunctionScope(node, false); |
|
} |
|
|
|
if (this.scopeManager.__isES6() && this.scopeManager.isModule()) { |
|
this.scopeManager.__nestModuleScope(node); |
|
} |
|
|
|
if (this.scopeManager.isStrictModeSupported() && this.scopeManager.isImpliedStrict()) { |
|
this.currentScope().isStrict = true; |
|
} |
|
|
|
this.visitChildren(node); |
|
this.close(node); |
|
} |
|
|
|
Identifier(node) { |
|
this.currentScope().__referencing(node); |
|
} |
|
|
|
UpdateExpression(node) { |
|
if (PatternVisitor.isPattern(node.argument)) { |
|
this.currentScope().__referencing(node.argument, Reference.RW, null); |
|
} else { |
|
this.visitChildren(node); |
|
} |
|
} |
|
|
|
MemberExpression(node) { |
|
this.visit(node.object); |
|
if (node.computed) { |
|
this.visit(node.property); |
|
} |
|
} |
|
|
|
Property(node) { |
|
this.visitProperty(node); |
|
} |
|
|
|
MethodDefinition(node) { |
|
this.visitProperty(node); |
|
} |
|
|
|
BreakStatement() {} // eslint-disable-line class-methods-use-this |
|
|
|
ContinueStatement() {} // eslint-disable-line class-methods-use-this |
|
|
|
LabeledStatement(node) { |
|
this.visit(node.body); |
|
} |
|
|
|
ForStatement(node) { |
|
|
|
// Create ForStatement declaration. |
|
// NOTE: In ES6, ForStatement dynamically generates |
|
// per iteration environment. However, escope is |
|
// a static analyzer, we only generate one scope for ForStatement. |
|
if (node.init && node.init.type === Syntax.VariableDeclaration && node.init.kind !== "var") { |
|
this.scopeManager.__nestForScope(node); |
|
} |
|
|
|
this.visitChildren(node); |
|
|
|
this.close(node); |
|
} |
|
|
|
ClassExpression(node) { |
|
this.visitClass(node); |
|
} |
|
|
|
ClassDeclaration(node) { |
|
this.visitClass(node); |
|
} |
|
|
|
CallExpression(node) { |
|
|
|
// Check this is direct call to eval |
|
if (!this.scopeManager.__ignoreEval() && node.callee.type === Syntax.Identifier && node.callee.name === "eval") { |
|
|
|
// NOTE: This should be `variableScope`. Since direct eval call always creates Lexical environment and |
|
// let / const should be enclosed into it. Only VariableDeclaration affects on the caller's environment. |
|
this.currentScope().variableScope.__detectEval(); |
|
} |
|
this.visitChildren(node); |
|
} |
|
|
|
BlockStatement(node) { |
|
if (this.scopeManager.__isES6()) { |
|
this.scopeManager.__nestBlockScope(node); |
|
} |
|
|
|
this.visitChildren(node); |
|
|
|
this.close(node); |
|
} |
|
|
|
ThisExpression() { |
|
this.currentScope().variableScope.__detectThis(); |
|
} |
|
|
|
WithStatement(node) { |
|
this.visit(node.object); |
|
|
|
// Then nest scope for WithStatement. |
|
this.scopeManager.__nestWithScope(node); |
|
|
|
this.visit(node.body); |
|
|
|
this.close(node); |
|
} |
|
|
|
VariableDeclaration(node) { |
|
const variableTargetScope = (node.kind === "var") ? this.currentScope().variableScope : this.currentScope(); |
|
|
|
for (let i = 0, iz = node.declarations.length; i < iz; ++i) { |
|
const decl = node.declarations[i]; |
|
|
|
this.visitVariableDeclaration(variableTargetScope, Variable.Variable, node, i); |
|
if (decl.init) { |
|
this.visit(decl.init); |
|
} |
|
} |
|
} |
|
|
|
// sec 13.11.8 |
|
SwitchStatement(node) { |
|
this.visit(node.discriminant); |
|
|
|
if (this.scopeManager.__isES6()) { |
|
this.scopeManager.__nestSwitchScope(node); |
|
} |
|
|
|
for (let i = 0, iz = node.cases.length; i < iz; ++i) { |
|
this.visit(node.cases[i]); |
|
} |
|
|
|
this.close(node); |
|
} |
|
|
|
FunctionDeclaration(node) { |
|
this.visitFunction(node); |
|
} |
|
|
|
FunctionExpression(node) { |
|
this.visitFunction(node); |
|
} |
|
|
|
ForOfStatement(node) { |
|
this.visitForIn(node); |
|
} |
|
|
|
ForInStatement(node) { |
|
this.visitForIn(node); |
|
} |
|
|
|
ArrowFunctionExpression(node) { |
|
this.visitFunction(node); |
|
} |
|
|
|
ImportDeclaration(node) { |
|
assert(this.scopeManager.__isES6() && this.scopeManager.isModule(), "ImportDeclaration should appear when the mode is ES6 and in the module context."); |
|
|
|
const importer = new Importer(node, this); |
|
|
|
importer.visit(node); |
|
} |
|
|
|
visitExportDeclaration(node) { |
|
if (node.source) { |
|
return; |
|
} |
|
if (node.declaration) { |
|
this.visit(node.declaration); |
|
return; |
|
} |
|
|
|
this.visitChildren(node); |
|
} |
|
|
|
// TODO: ExportDeclaration doesn't exist. for bc? |
|
ExportDeclaration(node) { |
|
this.visitExportDeclaration(node); |
|
} |
|
|
|
ExportAllDeclaration(node) { |
|
this.visitExportDeclaration(node); |
|
} |
|
|
|
ExportDefaultDeclaration(node) { |
|
this.visitExportDeclaration(node); |
|
} |
|
|
|
ExportNamedDeclaration(node) { |
|
this.visitExportDeclaration(node); |
|
} |
|
|
|
ExportSpecifier(node) { |
|
|
|
// TODO: `node.id` doesn't exist. for bc? |
|
const local = (node.id || node.local); |
|
|
|
this.visit(local); |
|
} |
|
|
|
MetaProperty() { // eslint-disable-line class-methods-use-this |
|
|
|
// do nothing. |
|
} |
|
} |
|
|
|
module.exports = Referencer; |
|
|
|
/* vim: set sw=4 ts=4 et tw=80 : */
|
|
|