d2-ui
Version:
300 lines (265 loc) • 8.85 kB
JavaScript
/**
* Copyright 2013 Facebook, 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.
*/
/*jslint node: true*/
"use strict";
var esprima = require('esprima-fb');
var utils = require('./utils');
var getBoundaryNode = utils.getBoundaryNode;
var declareIdentInScope = utils.declareIdentInLocalScope;
var initScopeMetadata = utils.initScopeMetadata;
var Syntax = esprima.Syntax;
/**
* @param {object} node
* @param {object} parentNode
* @return {boolean}
*/
function _nodeIsClosureScopeBoundary(node, parentNode) {
if (node.type === Syntax.Program) {
return true;
}
var parentIsFunction =
parentNode.type === Syntax.FunctionDeclaration
|| parentNode.type === Syntax.FunctionExpression
|| parentNode.type === Syntax.ArrowFunctionExpression;
var parentIsCurlylessArrowFunc =
parentNode.type === Syntax.ArrowFunctionExpression
&& node === parentNode.body;
return parentIsFunction
&& (node.type === Syntax.BlockStatement || parentIsCurlylessArrowFunc);
}
function _nodeIsBlockScopeBoundary(node, parentNode) {
if (node.type === Syntax.Program) {
return false;
}
return node.type === Syntax.BlockStatement
&& parentNode.type === Syntax.CatchClause;
}
/**
* @param {object} node
* @param {array} path
* @param {object} state
*/
function traverse(node, path, state) {
/*jshint -W004*/
// Create a scope stack entry if this is the first node we've encountered in
// its local scope
var startIndex = null;
var parentNode = path[0];
if (!Array.isArray(node) && state.localScope.parentNode !== parentNode) {
if (_nodeIsClosureScopeBoundary(node, parentNode)) {
var scopeIsStrict = state.scopeIsStrict;
if (!scopeIsStrict
&& (node.type === Syntax.BlockStatement
|| node.type === Syntax.Program)) {
scopeIsStrict =
node.body.length > 0
&& node.body[0].type === Syntax.ExpressionStatement
&& node.body[0].expression.type === Syntax.Literal
&& node.body[0].expression.value === 'use strict';
}
if (node.type === Syntax.Program) {
startIndex = state.g.buffer.length;
state = utils.updateState(state, {
scopeIsStrict: scopeIsStrict
});
} else {
startIndex = state.g.buffer.length + 1;
state = utils.updateState(state, {
localScope: {
parentNode: parentNode,
parentScope: state.localScope,
identifiers: {},
tempVarIndex: 0,
tempVars: []
},
scopeIsStrict: scopeIsStrict
});
// All functions have an implicit 'arguments' object in scope
declareIdentInScope('arguments', initScopeMetadata(node), state);
// Include function arg identifiers in the scope boundaries of the
// function
if (parentNode.params.length > 0) {
var param;
var metadata = initScopeMetadata(parentNode, path.slice(1), path[0]);
for (var i = 0; i < parentNode.params.length; i++) {
param = parentNode.params[i];
if (param.type === Syntax.Identifier) {
declareIdentInScope(param.name, metadata, state);
}
}
}
// Include rest arg identifiers in the scope boundaries of their
// functions
if (parentNode.rest) {
var metadata = initScopeMetadata(
parentNode,
path.slice(1),
path[0]
);
declareIdentInScope(parentNode.rest.name, metadata, state);
}
// Named FunctionExpressions scope their name within the body block of
// themselves only
if (parentNode.type === Syntax.FunctionExpression && parentNode.id) {
var metaData =
initScopeMetadata(parentNode, path.parentNodeslice, parentNode);
declareIdentInScope(parentNode.id.name, metaData, state);
}
}
// Traverse and find all local identifiers in this closure first to
// account for function/variable declaration hoisting
collectClosureIdentsAndTraverse(node, path, state);
}
if (_nodeIsBlockScopeBoundary(node, parentNode)) {
startIndex = state.g.buffer.length;
state = utils.updateState(state, {
localScope: {
parentNode: parentNode,
parentScope: state.localScope,
identifiers: {},
tempVarIndex: 0,
tempVars: []
}
});
if (parentNode.type === Syntax.CatchClause) {
var metadata = initScopeMetadata(
parentNode,
path.slice(1),
parentNode
);
declareIdentInScope(parentNode.param.name, metadata, state);
}
collectBlockIdentsAndTraverse(node, path, state);
}
}
// Only catchup() before and after traversing a child node
function traverser(node, path, state) {
node.range && utils.catchup(node.range[0], state);
traverse(node, path, state);
node.range && utils.catchup(node.range[1], state);
}
utils.analyzeAndTraverse(walker, traverser, node, path, state);
// Inject temp variables into the scope.
if (startIndex !== null) {
utils.injectTempVarDeclarations(state, startIndex);
}
}
function collectClosureIdentsAndTraverse(node, path, state) {
utils.analyzeAndTraverse(
visitLocalClosureIdentifiers,
collectClosureIdentsAndTraverse,
node,
path,
state
);
}
function collectBlockIdentsAndTraverse(node, path, state) {
utils.analyzeAndTraverse(
visitLocalBlockIdentifiers,
collectBlockIdentsAndTraverse,
node,
path,
state
);
}
function visitLocalClosureIdentifiers(node, path, state) {
var metaData;
switch (node.type) {
case Syntax.ArrowFunctionExpression:
case Syntax.FunctionExpression:
// Function expressions don't get their names (if there is one) added to
// the closure scope they're defined in
return false;
case Syntax.ClassDeclaration:
case Syntax.ClassExpression:
case Syntax.FunctionDeclaration:
if (node.id) {
metaData = initScopeMetadata(getBoundaryNode(path), path.slice(), node);
declareIdentInScope(node.id.name, metaData, state);
}
return false;
case Syntax.VariableDeclarator:
// Variables have function-local scope
if (path[0].kind === 'var') {
metaData = initScopeMetadata(getBoundaryNode(path), path.slice(), node);
declareIdentInScope(node.id.name, metaData, state);
}
break;
}
}
function visitLocalBlockIdentifiers(node, path, state) {
// TODO: Support 'let' here...maybe...one day...or something...
if (node.type === Syntax.CatchClause) {
return false;
}
}
function walker(node, path, state) {
var visitors = state.g.visitors;
for (var i = 0; i < visitors.length; i++) {
if (visitors[i].test(node, path, state)) {
return visitors[i](traverse, node, path, state);
}
}
}
var _astCache = {};
function getAstForSource(source, options) {
if (_astCache[source] && !options.disableAstCache) {
return _astCache[source];
}
var ast = esprima.parse(source, {
comment: true,
loc: true,
range: true,
sourceType: options.sourceType
});
if (!options.disableAstCache) {
_astCache[source] = ast;
}
return ast;
}
/**
* Applies all available transformations to the source
* @param {array} visitors
* @param {string} source
* @param {?object} options
* @return {object}
*/
function transform(visitors, source, options) {
options = options || {};
var ast;
try {
ast = getAstForSource(source, options);
} catch (e) {
e.message = 'Parse Error: ' + e.message;
throw e;
}
var state = utils.createState(source, ast, options);
state.g.visitors = visitors;
if (options.sourceMap) {
var SourceMapGenerator = require('source-map').SourceMapGenerator;
state.g.sourceMap = new SourceMapGenerator({file: options.filename || 'transformed.js'});
}
traverse(ast, [], state);
utils.catchup(source.length, state);
var ret = {code: state.g.buffer, extra: state.g.extra};
if (options.sourceMap) {
ret.sourceMap = state.g.sourceMap;
ret.sourceMapFilename = options.filename || 'source.js';
}
return ret;
}
exports.transform = transform;
exports.Syntax = Syntax;