shift-scope
Version:
produce a scope tree for Shift format ASTs
340 lines (294 loc) • 11.9 kB
JavaScript
/**
* Copyright 2015 Shape Security, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const MultiMap = require('multimap');
const { reduce, MonoidalReducer } = require('shift-reducer');
const ScopeState = require('./scope-state');
const { Accessibility, Reference } = require('./reference');
const { DeclarationType } = require('./declaration');
const { ScopeType } = require('./scope');
const StrictnessReducer = require('./strictness-reducer');
function asSimpleFunctionDeclarationName(statement) {
return statement.type === 'FunctionDeclaration' && !statement.isGenerator && !statement.isAsync
? statement.name
: statement.type === 'LabeledStatement'
? asSimpleFunctionDeclarationName(statement.body)
: null;
}
function getUnnestedSimpleFunctionDeclarationNames(statements) {
let names = statements.map(asSimpleFunctionDeclarationName).filter(f => f != null);
// if a function declaration occurs twice in the same scope, neither can be B.3.3 hoisted
// see https://github.com/tc39/ecma262/issues/913
let hist = names.reduce((memo, id) => {
if (id.name in memo) {
++memo[id.name];
} else {
memo[id.name] = 1;
}
return memo;
}, Object.create(null));
return names.filter(id => hist[id.name] === 1);
}
module.exports = class ScopeAnalyzer extends MonoidalReducer {
constructor(program) {
super(ScopeState);
this.sloppySet = program.type === 'Script' ? StrictnessReducer.analyze(program) : new Set;
}
fold(list, a) {
return list.reduce((memo, x) => this.append(memo, x), a == null ? this.identity : a);
}
static analyze(program) {
return reduce(new this(program), program).children[0];
}
finishFunction(fnNode, params, body) {
const isArrowFn = fnNode.type === 'ArrowExpression';
const fnType = isArrowFn ? ScopeType.ARROW_FUNCTION : ScopeType.FUNCTION;
if (params.hasParameterExpressions) {
return params
.withoutParameterExpressions()
.concat(body.finish(fnNode, fnType, { shouldResolveArguments: false, paramsToBlockB33Hoisting: params, shouldB33: this.sloppySet.has(fnNode) }))
.finish(fnNode, ScopeType.PARAMETERS, { shouldResolveArguments: !isArrowFn });
}
return params.concat(body).finish(fnNode, fnType, { shouldResolveArguments: !isArrowFn, shouldB33: this.sloppySet.has(fnNode) });
}
reduceArrowExpression(node, { params, body }) {
return this.finishFunction(node, params, body);
}
reduceAssignmentExpression(node, { binding, expression }) {
return super.reduceAssignmentExpression(node, {
binding: binding.addReferences(Accessibility.WRITE),
expression,
});
}
reduceAssignmentTargetIdentifier(node) {
return new ScopeState({ atsForParent: [node] });
}
reduceBindingIdentifier(node) {
if (node.name === '*default*') {
return new ScopeState;
}
return new ScopeState({ bindingsForParent: [node] });
}
reduceBindingPropertyIdentifier(node, { binding, init }) {
const s = super.reduceBindingPropertyIdentifier(node, { binding, init });
if (init) {
return s.withParameterExpressions();
}
return s;
}
reduceBindingPropertyProperty(node, { name, binding }) {
const s = super.reduceBindingPropertyProperty(node, { name, binding });
if (node.name.type === 'ComputedPropertyName') {
return s.withParameterExpressions();
}
return s;
}
reduceBindingWithDefault(node, { binding, init }) {
return super.reduceBindingWithDefault(node, { binding, init }).withParameterExpressions();
}
reduceBlock(node, { statements }) {
return super
.reduceBlock(node, { statements })
.withPotentialVarFunctions(getUnnestedSimpleFunctionDeclarationNames(node.statements))
.finish(node, ScopeType.BLOCK);
}
reduceCallExpression(node, { callee, arguments: _arguments }) {
const s = super.reduceCallExpression(node, { callee, arguments: _arguments });
if (node.callee.type === 'IdentifierExpression' && node.callee.name === 'eval') {
return s.taint();
}
return s;
}
reduceCatchClause(node, { binding, body }) {
return super
.reduceCatchClause(node, {
binding: binding == null ? null : binding.addDeclarations(DeclarationType.CATCH_PARAMETER),
body,
})
.finish(node, ScopeType.CATCH);
}
reduceClassDeclaration(node, { name, super: _super, elements }) {
let s = super
.reduceClassDeclaration(node, { name, super: _super, elements })
.addDeclarations(DeclarationType.CLASS_NAME)
.finish(node, ScopeType.CLASS_NAME);
return s.concat(name.addDeclarations(DeclarationType.CLASS_DECLARATION));
}
reduceClassExpression(node, { name, super: _super, elements }) {
return super
.reduceClassExpression(node, { name, super: _super, elements })
.addDeclarations(DeclarationType.CLASS_NAME)
.finish(node, ScopeType.CLASS_NAME);
}
reduceCompoundAssignmentExpression(node, { binding, expression }) {
return super.reduceCompoundAssignmentExpression(node, {
binding: binding.addReferences(Accessibility.READWRITE),
expression,
});
}
reduceComputedMemberExpression(node, { object, expression }) {
return super
.reduceComputedMemberExpression(node, { object, expression })
.withParameterExpressions();
}
reduceForInStatement(node, { left, right, body }) {
return super
.reduceForInStatement(node, { left: left.addReferences(Accessibility.WRITE), right, body })
.finish(node, ScopeType.BLOCK);
}
reduceForAwaitStatement(node, { left, right, body }) {
return super
.reduceForAwaitStatement(node, { left: left.addReferences(Accessibility.WRITE), right, body })
.finish(node, ScopeType.BLOCK);
}
reduceForOfStatement(node, { left, right, body }) {
return super
.reduceForOfStatement(node, { left: left.addReferences(Accessibility.WRITE), right, body })
.finish(node, ScopeType.BLOCK);
}
reduceForStatement(node, { init, test, update, body }) {
return super
.reduceForStatement(node, {
init: init ? init.withoutBindingsForParent() : init,
test,
update,
body,
})
.finish(node, ScopeType.BLOCK);
}
reduceFormalParameters(node, { items, rest }) {
let s = rest ? rest : new ScopeState;
items.forEach((item, ind) => {
s = s.concat(
item.hasParameterExpressions
? item.finish(node.items[ind], ScopeType.PARAMETER_EXPRESSION)
: item
);
});
return s.addDeclarations(DeclarationType.PARAMETER);
}
reduceFunctionDeclaration(node, { name, params, body }) {
return name.concat(this.finishFunction(node, params, body)).addFunctionDeclaration();
}
reduceFunctionExpression(node, { name, params, body }) {
let s = this.finishFunction(node, params, body);
if (name) {
return name
.concat(s)
.addDeclarations(DeclarationType.FUNCTION_NAME)
.finish(node, ScopeType.FUNCTION_NAME);
}
return s;
}
reduceGetter(node, { name, body }) {
return name.concat(
body.finish(node, ScopeType.FUNCTION, {
shouldResolveArguments: true,
shouldB33: this.sloppySet.has(node),
})
);
}
reduceIdentifierExpression(node) {
return new ScopeState({
freeIdentifiers: new MultiMap([[node.name, new Reference(node, Accessibility.READ)]]),
});
}
reduceIfStatement(node, { test, consequent, alternate }) {
// These "blocks" are synthetic; see https://tc39.es/ecma262/#sec-functiondeclarations-in-ifstatement-statement-clauses
let consequentFunctionDeclName = asSimpleFunctionDeclarationName(node.consequent);
if (consequentFunctionDeclName != null) {
consequent = consequent.withPotentialVarFunctions([consequentFunctionDeclName])
.finish(node.consequent, ScopeType.BLOCK);
}
if (node.alternate != null) {
let alternateFunctionDeclName = asSimpleFunctionDeclarationName(node.alternate);
if (alternateFunctionDeclName != null) {
alternate = alternate.withPotentialVarFunctions([alternateFunctionDeclName])
.finish(node.alternate, ScopeType.BLOCK);
}
}
return super
.reduceIfStatement(node, { test, consequent, alternate });
}
reduceImport(node, { moduleSpecifier, defaultBinding, namedImports }) {
return super
.reduceImport(node, { moduleSpecifier, defaultBinding, namedImports })
.addDeclarations(DeclarationType.IMPORT);
}
reduceMethod(node, { name, params, body }) {
return name.concat(this.finishFunction(node, params, body));
}
reduceModule(node, { directives, items }) {
return super.reduceModule(node, { directives, items }).finish(node, ScopeType.MODULE);
}
reduceScript(node, { directives, statements }) {
return super.reduceScript(node, { directives, statements }).finish(node, ScopeType.SCRIPT, { shouldB33: !node.directives.some(d => d.rawValue === 'use strict') });
}
reduceSetter(node, { name, param, body }) {
if (param.hasParameterExpressions) {
param = param.finish(node, ScopeType.PARAMETER_EXPRESSION);
}
return name.concat(
this.finishFunction(node, param.addDeclarations(DeclarationType.PARAMETER), body)
);
}
reduceSwitchStatement(node, { discriminant, cases }) {
return this
.fold(cases)
.withPotentialVarFunctions(getUnnestedSimpleFunctionDeclarationNames([].concat(...node.cases.map(c => c.consequent))))
.finish(node, ScopeType.BLOCK)
.concat(discriminant);
}
reduceSwitchStatementWithDefault(node, { discriminant, preDefaultCases, defaultCase, postDefaultCases }) {
const functionDeclarations = getUnnestedSimpleFunctionDeclarationNames([].concat(
...node.preDefaultCases.concat([node.defaultCase], node.postDefaultCases).map(c => c.consequent)
));
const cases = preDefaultCases.concat([defaultCase], postDefaultCases);
return this
.fold(cases)
.withPotentialVarFunctions(functionDeclarations)
.finish(node, ScopeType.BLOCK)
.concat(discriminant);
}
reduceUnaryExpression(node, { operand }) {
if (node.operator === 'delete' && node.operand.type === 'IdentifierExpression') {
// 'delete x' is a special case.
return new ScopeState({ freeIdentifiers: new MultiMap([[node.operand.name, new Reference(node.operand, Accessibility.DELETE)]]) });
}
return super.reduceUnaryExpression(node, { operand });
}
reduceUpdateExpression(node, { operand }) {
return operand.addReferences(Accessibility.READWRITE);
}
reduceVariableDeclaration(node, { declarators }) {
return super
.reduceVariableDeclaration(node, { declarators })
.addDeclarations(DeclarationType.fromVarDeclKind(node.kind), true);
// passes bindingsForParent up, for for-in and for-of to add their write-references
}
reduceVariableDeclarationStatement(node, { declaration }) {
return declaration.withoutBindingsForParent();
}
reduceVariableDeclarator(node, { binding, init }) {
const s = super.reduceVariableDeclarator(node, { binding, init });
if (init) {
return s.addReferences(Accessibility.WRITE, true);
}
return s;
}
reduceWithStatement(node, { object, body }) {
return super.reduceWithStatement(node, { object, body: body.finish(node, ScopeType.WITH) });
}
};