UNPKG

rocambole

Version:

Recursively walk and transform EcmaScript AST

433 lines (367 loc) 16 kB
/*global describe:false, it:false, beforeEach:false, afterEach:false */ "use strict"; var expect = require('expect.js'); var rocambole = require('../'); describe('parse', function () { it('should parse string and return AST', function () { var ast = rocambole.parse('(function(){ return 123 })'); expect( ast.type ).to.equal( 'Program' ); expect( ast.body[0].type ).to.equal( 'ExpressionStatement' ); }); it('should include tokens before and after "program" end', function () { var ast = rocambole.parse('//foo\n(function(){ return 123 })\n//bar\n'); expect( ast.startToken.value ).to.equal( 'foo' ); expect( ast.endToken.value ).to.equal( '\n' ); ast = rocambole.parse('\n//foo\n(function(){ return 123 })\n//dolor'); expect( ast.startToken.value ).to.equal( '\n' ); expect( ast.endToken.value ).to.equal( 'dolor' ); }); it('should work with any kind of line breaks & spaces', function () { var ast = rocambole.parse('\nvar n\r\n=\n10;\r\r \t\t \n', {loc : true}); var br_1 = ast.startToken; expect( br_1.type ).to.be( 'LineBreak' ); expect( br_1.value ).to.be( '\n' ); expect( br_1.range ).to.eql( [0, 1] ); expect( br_1.loc ).to.eql({ start : { line : 1, column : 0 }, end : { line : 1, column : 1 } }); var ws_1 = ast.startToken.next.next; expect( ws_1.type ).to.be( 'WhiteSpace' ); expect( ws_1.value ).to.be( ' ' ); var br_2 = br_1.next.next.next.next; expect( br_2.type ).to.be( 'LineBreak' ); expect( br_2.value ).to.be( '\r\n' ); expect( br_2.range ).to.eql( [6, 8] ); expect( br_2.loc ).to.eql({ start : { line : 2, column : 5 }, end : { line : 2, column : 7 } }); // it's important to notice that esprima doesn't parse "\r" as line // break, so if it is not at EOF it will give conflicting "loc" info. var br_6 = ast.endToken; expect( br_6.type ).to.be( 'LineBreak' ); expect( br_6.value ).to.be( '\n' ); expect( br_6.range ).to.eql( [21, 22] ); expect( br_6.loc ).to.eql({ start : { line : 6, column : 6 }, end : { line : 6, column : 7 } }); var ws_2 = ast.endToken.prev; expect( ws_2.type ).to.be( 'WhiteSpace' ); expect( ws_2.value ).to.be( ' \t\t ' ); expect( ws_2.range ).to.eql( [15, 21] ); expect( ws_2.loc ).to.eql({ start : { line : 6, column : 0 }, end : { line : 6, column : 6 } }); var br_5 = ws_2.prev; expect( br_5.type ).to.be( 'LineBreak' ); expect( br_5.value ).to.be( '\r' ); var br_4 = br_5.prev; expect( br_4.type ).to.be( 'LineBreak' ); expect( br_4.value ).to.be( '\r' ); }); it('should not include any char that isn\'t a white space on a WhiteSpace token [issue #3]', function () { var ast = rocambole.parse("\n/* foo */\n/* bar */\nfunction foo(){\n var bar = 'baz';\n\n //foo\n //bar\n\n var lorem = 'ipsum';\n return bar + lorem;\n}"); var tk = ast.startToken; var nComments = 0; while (tk) { if (tk.type === 'WhiteSpace') { expect( tk.value ).to.match( /^[\s\t]+$/ ); } else if (tk.type === 'LineBreak') { expect( tk.value ).to.equal( '\n' ); } else if (tk.type === 'LineComment') { expect( tk.raw ).to.match( /^\/\/\w{3}$/ ); nComments++; } else if (tk.type === 'BlockComment') { expect( tk.raw ).to.match( /^\/\* \w{3} \*\/$/ ); nComments++; } tk = tk.next; } expect( nComments ).to.be( 4 ); }); it('should instrument object expression "value" node', function () { // this was a bug introduced while trying to improve performance var ast = rocambole.parse('amet(123, a, {flag : true});'); var exp = ast.body[0].expression; expect( exp.startToken ).not.to.be(undefined); expect( exp.callee.startToken ).not.to.be(undefined); expect( exp['arguments'][0].startToken ).not.to.be(undefined); expect( exp['arguments'][1].startToken ).not.to.be(undefined); expect( exp['arguments'][2].startToken ).not.to.be(undefined); expect( exp['arguments'][2].properties[0].startToken ).not.to.be(undefined); expect( exp['arguments'][2].properties[0].key.startToken ).not.to.be(undefined); expect( exp['arguments'][2].properties[0].value.startToken ).not.to.be(undefined); }); describe('Node', function () { var ast, program, expressionStatement, fnExpression, block, returnStatement; beforeEach(function(){ ast = rocambole.parse('/* block */\n(function(){\n return 123; // line\n})'); program = ast; expressionStatement = ast.body[0]; fnExpression = expressionStatement.expression; block = fnExpression.body; returnStatement = block.body[0]; }); describe('node.parent', function () { it('should add reference to parent node', function () { expect( program.parent ).to.equal( undefined ); expect( expressionStatement.parent ).to.equal( program ); expect( fnExpression.parent ).to.equal( expressionStatement ); expect( block.parent ).to.equal( fnExpression ); }); }); describe('node.toString()', function(){ it('should return the node source', function () { expect( returnStatement.type ).to.equal( 'ReturnStatement' ); expect( returnStatement.toString() ).to.equal( 'return 123;' ); }); it('should use raw value of comments', function () { expect( block.toString() ).to.equal( '{\n return 123; // line\n}' ); }); it('should use raw value of comments', function () { expect( ast.toString() ).to.equal( '/* block */\n(function(){\n return 123; // line\n})' ); }); }); describe('depth', function () { it('should add depth property to nodes', function () { expect( program.depth ).to.equal( 0 ); expect( expressionStatement.depth ).to.equal( 1 ); expect( fnExpression.depth ).to.equal( 2 ); expect( block.depth ).to.equal( 3 ); expect( returnStatement.depth ).to.equal( 4 ); }); }); describe('node.endToken', function () { it('should return last token inside node', function () { expect( program.endToken.value ).to.equal( ')' ); expect( expressionStatement.endToken.value ).to.equal( ')' ); expect( fnExpression.endToken.value ).to.equal( '}' ); expect( block.endToken.value ).to.equal( '}' ); expect( returnStatement.endToken.value ).to.equal( ';' ); }); it('should capture end token properly', function () { var ast = rocambole.parse('[1,2,[3,4,[5,6,[7,8,9]]]];'); var exp = ast.body[0].expression; expect( exp.endToken.value ).to.equal( ']' ); expect( exp.elements[0].value ).to.equal( 1 ); expect( exp.elements[0].startToken.value ).to.equal( '1' ); expect( exp.elements[0].endToken.value ).to.equal( '1' ); }); }); describe('node.startToken', function () { it('should return first token inside node', function () { expect( program.startToken.value ).to.equal( ' block ' ); expect( expressionStatement.startToken.value ).to.equal( '(' ); expect( fnExpression.startToken.value ).to.equal( 'function' ); expect( block.startToken.value ).to.equal( '{' ); expect( returnStatement.startToken.value ).to.equal( 'return' ); }); }); describe('Node.next & Node.prev', function () { it('should return reference to previous and next nodes', function () { var ast = rocambole.parse("\n/* foo */\n/* bar */\nfunction foo(){\n var bar = 'baz';\n var lorem = 'ipsum';\n return bar + lorem;\n}"); var block = ast.body[0].body.body; var firstNode = block[0]; var secondNode = block[1]; var lastNode = block[2]; expect( firstNode.prev ).to.equal( undefined ); expect( firstNode.next ).to.equal( secondNode ); expect( secondNode.prev ).to.equal( firstNode ); expect( secondNode.next ).to.equal( lastNode ); expect( lastNode.prev ).to.equal( secondNode ); expect( lastNode.next ).to.equal( undefined ); }); }); }); describe('Token', function () { it('should instrument tokens', function () { var ast = rocambole.parse('function foo(){ return "bar"; }'); var tokens = ast.tokens; expect( tokens[0].prev ).to.be(undefined); expect( tokens[0].next ).to.be( tokens[1] ); expect( tokens[1].prev ).to.be( tokens[0] ); expect( tokens[1].next ).to.be( tokens[2] ); }); it('should add range and loc info to comment tokens', function () { var ast = rocambole.parse('\n/* foo\n bar\n*/\nfunction foo(){ return "bar"; }\n// end', {loc:true}); var blockComment = ast.startToken.next; expect( blockComment.range ).to.eql( [1, 16] ); expect( blockComment.loc ).to.eql({ start : { line : 2, column : 0 }, end : { line : 4, column : 2 } }); var lineComment = ast.endToken; expect( lineComment.range ).to.eql( [49, 55] ); expect( lineComment.loc ).to.eql({ start : { line : 6, column : 0 }, end : { line : 6, column : 6 } }); }); it('should add originalIndent info to block comments', function () { var ast = rocambole.parse(' /* foo */\n\t\t// bar'); expect( ast.startToken.next.originalIndent ).to.be(' '); }); it('should not add originalIndent info to line comments', function () { var ast = rocambole.parse(' /* foo */\n\t\t// bar'); expect( ast.endToken.originalIndent ).to.be(undefined); }); it('should not add as originalIndent if prev token is not white space', function () { var ast = rocambole.parse('lorem;/* foo */\n\t\t// bar'); expect( ast.startToken.next.next.originalIndent ).to.be(undefined); }); it('should not add as originalIndent if prev token is not on a new line', function () { var ast = rocambole.parse('lorem; /* foo */\n\t\t// bar'); expect( ast.startToken.next.next.next.originalIndent ).to.be(undefined); }); it('should add as originalIndent if on a new line', function () { var ast = rocambole.parse('lorem;\n /* foo */\n\t\t// bar'); expect( ast.startToken.next.next.next.next.originalIndent ).to.be(' '); }); }); describe('export BYPASS_RECURSION', function () { it('should export BYPASS_RECURSION', function () { expect( rocambole.BYPASS_RECURSION.root ).to.be(true); }); }); describe('empty program', function () { it('should not throw if program is empty', function () { expect(function(){ rocambole.parse(''); }).not.throwError(); }); it('should return augmented AST', function () { var ast = rocambole.parse(''); expect(ast).to.eql({ type: 'Program', body: [], range: [0,0], comments: [], tokens: [], // we check toString behavior later toString: ast.toString, startToken: null, endToken: null, depth: 0 }); }); it('toString should return proper value', function() { var ast = rocambole.parse(''); expect(ast.toString()).to.be(''); }); }); describe('support anything that implements `toString` as input', function () { it('should support arrays', function () { var ast = rocambole.parse([1,2,3]); expect(ast.body[0].toString()).to.eql('1,2,3'); }); it('should support functions', function () { var ast = rocambole.parse(function doStuff(){ doStuff(1, 2); }); expect(ast.body[0].type).to.be('FunctionDeclaration'); }); }); describe('sparse array', function() { // yes, people shold not be writting code like this, but we should not // bail when that happens it('should not fail on sparse arrays', function() { var ast = rocambole.parse('[,3,[,4]]'); expect(ast.toString()).to.eql('[,3,[,4]]'); var elements = ast.body[0].expression.elements; expect(elements[0]).to.be(null); expect(elements[1].type).to.be('Literal'); expect(elements[1].value).to.be(3); }); }); describe('custom parseFn', function() { var _parseFn; var _parseContext; var _parseOptions; var empty = { type: 'Program', body: [], range: [0,0], comments: [], tokens: [] }; beforeEach(function() { _parseFn = rocambole.parseFn; _parseContext = rocambole.parseContext; _parseOptions = rocambole.parseOptions; }); afterEach(function() { rocambole.parseFn = _parseFn; rocambole.parseContext = _parseContext; rocambole.parseOptions = _parseOptions; rocambole.parseOptions.tokens = true; }); it('should allow global override of parseFn', function() { var obj = {}; expect(rocambole.parseOptions).to.eql({ range: true, comment: true, tokens: true }); rocambole.parseOptions.tokens = 567; rocambole.parseContext = obj; rocambole.parseFn = function(source, opts) { expect(this).to.be(obj); expect(opts).to.eql({ loc: true, foo: 'bar', range: 123, tokens: 567, comment: true }); expect(source).to.eql('bar()'); return empty; }; var result = rocambole.parse('bar()', { loc: true, foo: 'bar', range: 123 }); expect(result).to.be(empty); }); }); });