d2-ui
Version:
482 lines (421 loc) • 16.2 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.
*
* @emails jeffmo@fb.com
*/
require('mock-modules').autoMockOff();
describe('jstransform', function() {
var transformFn;
var Syntax = require('esprima-fb').Syntax;
beforeEach(function() {
require('mock-modules').dumpCache();
transformFn = require('../jstransform').transform;
});
function _runVisitor(source, nodeCount, visitor) {
var actualVisitationCount = 0;
function shimVisitor(traverse, node, path, state) {
actualVisitationCount++;
return visitor(traverse, node, path, state);
}
shimVisitor.test = visitor.test;
transformFn([shimVisitor], source);
expect(actualVisitationCount).toBe(nodeCount);
}
function testScopeBoundary(source, localIdents, nodeCount, visitorTest) {
function visitor(traverse, node, path, state) {
var actualLocalIdents = Object.keys(state.localScope.identifiers);
expect(actualLocalIdents.sort()).toEqual(localIdents.sort());
}
visitor.test = visitorTest;
_runVisitor(source, nodeCount, visitor);
}
function testParentScope(source, parentIdents, nodeCount, visitorTest) {
function visitor(traverse, node, path, state) {
parentIdents = parentIdents && parentIdents.sort();
var parentScope = state.localScope.parentScope;
var actualParentIdents =
parentScope && Object.keys(parentScope.identifiers).sort();
expect(actualParentIdents).toEqual(parentIdents);
}
visitor.test = visitorTest;
_runVisitor(source, nodeCount, visitor);
}
describe('closure scope boundaries', function() {
it('creates a scope boundary around Program scope', function() {
var source =
'var foo;' +
'var bar, baz;' +
'function blah() {}';
var idents = ['foo', 'bar', 'baz', 'blah'];
testScopeBoundary(source, idents, 3, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
});
it('creates a scope boundary around FunctionDeclarations', function() {
var source =
'var foo;' +
'function blah() {' +
' var bar;' +
' function nested() {' +
' var baz;' +
' }' +
'}';
var programIdents = ['foo', 'blah'];
var blahIdents = ['arguments', 'bar', 'nested'];
var nestedIdents = ['arguments', 'baz'];
testScopeBoundary(source, programIdents, 2, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, blahIdents, 2, function(node, path) {
// All direct children of blah()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionDeclaration &&
path[1].id.name === 'blah';
});
testScopeBoundary(source, nestedIdents, 1, function(node, path) {
// All direct children of nested()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionDeclaration &&
path[1].id.name === 'nested';
});
});
it('creates a scope boundary around MethodDefinitions', function() {
var source =
'var foo;' +
'class ClassA {' +
' blah() {' +
' var bar;' +
' }' +
' another() {' +
' var baz;' +
' }' +
'}';
var programIdents = ['foo', 'ClassA'];
var blahIdents = ['arguments', 'bar'];
var anotherIdents = ['arguments', 'baz'];
testScopeBoundary(source, programIdents, 2, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, blahIdents, 1, function(node, path) {
// All direct children of blah()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionExpression &&
path[2] && path[2].type === Syntax.MethodDefinition &&
path[2].key.name === 'blah';
});
testScopeBoundary(source, anotherIdents, 1, function(node, path) {
// All direct children of another()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionExpression &&
path[2] && path[2].type === Syntax.MethodDefinition &&
path[2].key.name === 'another';
});
});
it('creates a scope boundary around concise ArrowFunc exprs', function() {
var source =
'var foo;' +
'var bar = baz => baz;';
var programIdents = ['foo', 'bar'];
var barIdents = ['arguments', 'baz'];
testScopeBoundary(source, programIdents, 2, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, barIdents, 1, function(node, path) {
return path[0] && path[0].type === Syntax.ArrowFunctionExpression
&& path[0].body === node;
});
});
it('uses VariableDeclarations to determine scope boundary', function() {
var source =
'var foo = 1;' +
'function bar() {' +
' foo++;' +
' function baz() {' +
' var foo = 2;' +
' }' +
'}';
var programIdents = ['foo', 'bar'];
var barIdents = ['arguments', 'baz'];
var bazIdents = ['arguments', 'foo'];
testScopeBoundary(source, programIdents, 2, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, barIdents, 2, function(node, path) {
// All direct children of blah()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionDeclaration &&
path[1].id.name === 'bar';
});
testScopeBoundary(source, bazIdents, 1, function(node, path) {
// All direct children of baz()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionDeclaration &&
path[1].id.name === 'baz';
});
});
it('includes function args in functions scope boundary', function() {
var source =
'var foo;' +
'function blah(bar) {' +
' var baz;' +
'}' +
'var blah2 = bar2 => {var baz;};' +
'var blah3 = bar3 => bar3;';
var programIdents = ['foo', 'blah', 'blah2', 'blah3'];
var blahIdents = ['arguments', 'bar', 'baz'];
var blah2Idents = ['arguments', 'bar2', 'baz'];
var blah3Idents = ['arguments', 'bar3'];
testScopeBoundary(source, programIdents, 4, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, blahIdents, 1, function(node, path) {
// All direct children of blah()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionDeclaration &&
path[1].id.name === 'blah';
});
testScopeBoundary(source, blah2Idents, 1, function(node, path) {
// All direct children of blah2()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.ArrowFunctionExpression &&
path[2].id.name === 'blah2';
});
testScopeBoundary(source, blah3Idents, 1, function(node, path) {
// All direct children of blah3()
return path[0] && path[0].type === Syntax.ArrowFunctionExpression &&
path[0].body === node &&
path[1].id.name === 'blah3';
});
});
it('includes rest param args in function scope boundaries', function() {
var source =
'var foo;' +
'function blah(...bar) {' +
' var baz;' +
'}' +
'var blah2 = (...bar2) => {var baz;};' +
'var blah3 = (...bar3) => bar3;';
var programIdents = ['foo', 'blah', 'blah2', 'blah3'];
var blahIdents = ['arguments', 'bar', 'baz'];
var blah2Idents = ['arguments', 'bar2', 'baz'];
var blah3Idents = ['arguments', 'bar3'];
testScopeBoundary(source, programIdents, 4, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, blahIdents, 1, function(node, path) {
// All direct children of blah()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionDeclaration &&
path[1].id.name === 'blah';
});
testScopeBoundary(source, blah2Idents, 1, function(node, path) {
// All direct children of blah2()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.ArrowFunctionExpression &&
path[2].id.name === 'blah2';
});
testScopeBoundary(source, blah3Idents, 1, function(node, path) {
// All direct children of blah3()
return path[0] && path[0].type === Syntax.ArrowFunctionExpression &&
path[0].body === node &&
path[1].id.name === 'blah3';
});
});
it('puts FunctionExpression names within function scope', function() {
var source =
'var foo;' +
'var bar = function baz() {' +
' var blah;' +
'};';
var programIdents = ['foo', 'bar'];
var bazIdents = ['arguments', 'baz', 'blah'];
testScopeBoundary(source, programIdents, 2, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, bazIdents, 1, function(node, path) {
// All direct children of baz()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionExpression &&
path[1].id.name === 'baz';
});
});
});
describe('block scope boundaries', function() {
it('creates a scope boundary around CatchClauses with params', function() {
var source =
'var blah = 0;' +
'try {' +
'} catch (e) {' +
' blah++;' +
'}';
var programIdents = ['blah'];
var catchIdents = ['e'];
testScopeBoundary(source, programIdents, 2, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, catchIdents, 1, function(node, path) {
// All direct children of catch(e) block
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.CatchClause;
});
});
it('includes vars defined in CatchClauses in the parent scope', function() {
var source =
'try {' +
'} catch (e) {' +
' var blah;' +
'}';
var programIdents = ['blah'];
var catchIdents = ['e'];
testScopeBoundary(source, programIdents, 1, function(node, path) {
return path[0] && path[0].type === Syntax.Program;
});
testScopeBoundary(source, catchIdents, 1, function(node, path) {
// All direct children of catch(e) block
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.CatchClause;
});
});
});
describe('scope chain linking', function() {
it('links parent scope boundaries', function() {
var source =
'var foo;' +
'function blah() {' +
' var bar;' +
' function nested() {' +
' var baz;' +
' }' +
'}';
var programIdents = ['foo', 'blah'];
var blahIdents = ['arguments', 'bar', 'nested'];
testParentScope(source, programIdents, 2, function(node, path) {
// All direct children of blah()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionDeclaration &&
path[1].id.name === 'blah';
});
testParentScope(source, blahIdents, 1, function(node, path) {
// All direct children of nested()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionDeclaration &&
path[1].id.name === 'nested';
});
});
it('nests MethodDefinition boundaries under parent scope', function() {
var source =
'var foo;' +
'class ClassA {' +
' blah() {' +
' var bar;' +
' }' +
'}';
var programIdents = ['foo', 'ClassA'];
testParentScope(source, programIdents, 1, function(node, path) {
// All direct children of blah()
return path[0] && path[0].type === Syntax.BlockStatement &&
path[1] && path[1].type === Syntax.FunctionExpression &&
path[2] && path[2].type === Syntax.MethodDefinition &&
path[2].key.name === 'blah';
});
});
});
describe('"use strict" tracking', function() {
function testStrictness(expectedStrict, source) {
var visitedNodes = 0;
function visitor(traverse, node, path, state) {
visitedNodes++;
expect(state.scopeIsStrict).toBe(expectedStrict);
}
visitor.test = function(node, path, state) {
return node.type === Syntax.Literal
&& node.value === 'testStr';
};
transformFn([visitor], source);
expect(visitedNodes).toBe(1);
}
it('detects program-level strictness', function() {
testStrictness(false, '"testStr";');
testStrictness(true, '"use strict"; "testStr";');
});
it('detects non-inherited strictness', function() {
testStrictness(true, [
'function foo() {',
' "use strict";',
' "testStr";',
'}'
].join('\n'));
});
it('detects program-inherited strictness', function() {
testStrictness(true, [
'"use strict";',
'function foo() {',
' "testStr";',
'}'
].join('\n'));
});
it('detects function-inherited strictness', function() {
testStrictness(true, [
'function foo() {',
' "use strict";',
' function bar() {',
' "testStr";',
' }',
'}'
].join('\n'));
});
it('does not detect sibling strictness', function() {
testStrictness(false, [
'function foo() {',
' "use strict";',
'}',
'function bar() {',
' "testStr";',
'}'
].join('\n'));
});
});
describe('visitors', function() {
it('should visit nodes in order', function() {
var source = [
'// Foo comment',
'function foo() {}',
'',
'// Bar comment',
'function bar() {}'
].join('\n');
var actualNodes = [];
function visitFunction(traverse, node, path, state) {
actualNodes.push([node.id.name, node.range[0]]);
}
visitFunction.test = function(node, path, state) {
return node.type === Syntax.FunctionDeclaration;
};
function visitComments(traverse, node, path, state) {
actualNodes.push([node.value, node.range[0]]);
}
visitComments.test = function(node, path, state) {
return node.type === 'Line';
};
transformFn([visitComments, visitFunction], source);
expect(actualNodes).toEqual([
[' Foo comment', 0],
['foo', 15],
[' Bar comment', 34],
['bar', 49]
]);
});
});
});