/*
  Copyright JS Foundation and other contributors, https://js.foundation/

  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.
*/

var assert = require('assert'),
    esprima = require('../');

describe('esprima.Syntax', function () {

    it('should enumerate all syntax node types', function () {
        assert.deepEqual(esprima.Syntax, {
            ArrayExpression: 'ArrayExpression',
            ArrayPattern: 'ArrayPattern',
            ArrowFunctionExpression: 'ArrowFunctionExpression',
            AssignmentExpression: 'AssignmentExpression',
            AssignmentPattern: 'AssignmentPattern',
            AwaitExpression: 'AwaitExpression',
            BinaryExpression: 'BinaryExpression',
            BlockStatement: 'BlockStatement',
            BreakStatement: 'BreakStatement',
            CallExpression: 'CallExpression',
            CatchClause: 'CatchClause',
            ClassBody: 'ClassBody',
            ClassDeclaration: 'ClassDeclaration',
            ClassExpression: 'ClassExpression',
            ConditionalExpression: 'ConditionalExpression',
            ContinueStatement: 'ContinueStatement',
            DebuggerStatement: 'DebuggerStatement',
            DoWhileStatement: 'DoWhileStatement',
            EmptyStatement: 'EmptyStatement',
            ExportAllDeclaration: 'ExportAllDeclaration',
            ExportDefaultDeclaration: 'ExportDefaultDeclaration',
            ExportNamedDeclaration: 'ExportNamedDeclaration',
            ExportSpecifier: 'ExportSpecifier',
            ExpressionStatement: 'ExpressionStatement',
            ForInStatement: 'ForInStatement',
            ForOfStatement: 'ForOfStatement',
            ForStatement: 'ForStatement',
            FunctionDeclaration: 'FunctionDeclaration',
            FunctionExpression: 'FunctionExpression',
            Identifier: 'Identifier',
            IfStatement: 'IfStatement',
            ImportDeclaration: 'ImportDeclaration',
            ImportDefaultSpecifier: 'ImportDefaultSpecifier',
            ImportNamespaceSpecifier: 'ImportNamespaceSpecifier',
            ImportSpecifier: 'ImportSpecifier',
            LabeledStatement: 'LabeledStatement',
            Literal: 'Literal',
            LogicalExpression: 'LogicalExpression',
            MemberExpression: 'MemberExpression',
            MetaProperty: 'MetaProperty',
            MethodDefinition: 'MethodDefinition',
            NewExpression: 'NewExpression',
            ObjectExpression: 'ObjectExpression',
            ObjectPattern: 'ObjectPattern',
            Program: 'Program',
            Property: 'Property',
            RestElement: 'RestElement',
            ReturnStatement: 'ReturnStatement',
            SequenceExpression: 'SequenceExpression',
            SpreadElement: 'SpreadElement',
            Super: 'Super',
            SwitchCase: 'SwitchCase',
            SwitchStatement: 'SwitchStatement',
            TaggedTemplateExpression: 'TaggedTemplateExpression',
            TemplateElement: 'TemplateElement',
            TemplateLiteral: 'TemplateLiteral',
            ThisExpression: 'ThisExpression',
            ThrowStatement: 'ThrowStatement',
            TryStatement: 'TryStatement',
            UnaryExpression: 'UnaryExpression',
            UpdateExpression: 'UpdateExpression',
            VariableDeclaration: 'VariableDeclaration',
            VariableDeclarator: 'VariableDeclarator',
            WhileStatement: 'WhileStatement',
            WithStatement: 'WithStatement',
            YieldExpression: 'YieldExpression'
        });
    });

});

describe('esprima.parse', function () {

    it('should not accept zero parameter', function () {
        assert.throws(function () {
            esprima.parse();
        });
    });

    it('should accept one string parameter', function () {
        assert.doesNotThrow(function () {
            esprima.parse('function f(){}');
        });
    });

    it('should accept a String object', function () {
        assert.doesNotThrow(function () {
            esprima.parse(new String('some.property = value'));
        });
    });

    it('should accept two parameters (source and parsing options)', function () {
        var options = { range: false };
        assert.doesNotThrow(function () {
            esprima.parse('var x', options);
        });
    });

    it('should accept three parameters (source, options, and delegate)', function () {
        var options = { range: false };
        assert.doesNotThrow(function () {
            esprima.parse('var x', options, function f(entry) {
                return entry;
            });
        });
    });

    it('should exclude location information by default', function () {
        var ast = esprima.parse('answer = 42');
        assert.ifError(ast.range);
        assert.ifError(ast.loc);
    });

    it('should exclude comments by default', function () {
        var ast = esprima.parse('42 // answer');
        assert.ifError(ast.comments);
    });

    it('should exclude list of tokens by default', function () {
        var ast = esprima.parse('3.14159');
        assert.ifError(ast.tokens);
    });

    it('should include index-based location for the nodes when specified', function () {
        var ast = esprima.parse('answer = 42', { range: true });
        assert.deepEqual(ast.range, [0, 11]);
        assert.ifError(ast.loc);
    });

    it('should include line and column-based location for the nodes when specified', function () {
        var ast = esprima.parse('answer = 42', { loc: true });
        assert.deepEqual(ast.loc.start, { line: 1, column: 0 });
        assert.deepEqual(ast.loc.end, { line: 1, column: 11 });
        assert.ifError(ast.loc.source);
        assert.ifError(ast.range);
    });

    it('should include source information for the nodes when specified', function () {
        var ast = esprima.parse('answer = 42', { loc: true, source: 'example.js' });
        assert.deepEqual(ast.loc.source, 'example.js');
        assert.ifError(ast.range);
    });

    it('should include the list of comments when specified', function () {
        var ast = esprima.parse('42 // answer', { comment: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0], { type: 'Line', value: ' answer' });
    });

    it('should include the list of comments with index-based location when specified', function () {
        var ast = esprima.parse('42 // answer', { comment: true, range: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0], { type: 'Line', value: ' answer', range: [3, 12] });
    });

    it('should include the list of comments with line and column-based location when specified', function () {
        var ast = esprima.parse('42 // answer', { comment: true, loc: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0].type, 'Line');
        assert.deepEqual(ast.comments[0].value, ' answer');
        assert.deepEqual(ast.comments[0].loc.start, { line: 1, column: 3 });
        assert.deepEqual(ast.comments[0].loc.end, { line: 1, column: 12 });
        assert.ifError(ast.comments[0].range);
    });

    it('should be able to attach comments', function () {
        var ast, statement, expression, comments;

        ast = esprima.parse('/* universe */ 42', { attachComment: true });
        statement = ast.body[0];
        expression = statement.expression;
        comments = statement.leadingComments;

        assert.deepEqual(expression, { type: 'Literal', value: 42, raw: '42' });
        assert.deepEqual(statement.leadingComments, [{ type: 'Block', value: ' universe ', range: [0, 14] }]);
    });

    it('should not implicity collect comments when comment attachment is specified', function () {
        var ast = esprima.parse('/* universe */ 42', { attachComment: true });
        assert.equal(ast.comments, undefined);
    });

    it('should include the list of tokens when specified', function () {
        var ast = esprima.parse('x = 1', { tokens: true });
        assert.deepEqual(ast.tokens.length, 3);
        assert.deepEqual(ast.tokens[0], { type: 'Identifier', value: 'x' });
        assert.deepEqual(ast.tokens[1], { type: 'Punctuator', value: '=' });
        assert.deepEqual(ast.tokens[2], { type: 'Numeric', value: '1' });
    });

    it('should include the list of tokens with index-based location when specified', function () {
        var ast = esprima.parse('y = 2', { tokens: true, range: true });
        assert.deepEqual(ast.tokens[0], { type: 'Identifier', value: 'y', range: [0, 1] });
        assert.deepEqual(ast.tokens[1], { type: 'Punctuator', value: '=', range: [2, 3] });
        assert.deepEqual(ast.tokens[2], { type: 'Numeric', value: '2', range: [4, 5] });
    });

    it('should include the list of tokens with line and column-based location when specified', function () {
        var ast = esprima.parse('z = 3', { tokens: true, loc: true });
        assert.deepEqual(ast.tokens.length, 3);
        assert.deepEqual(ast.tokens[0].type, 'Identifier');
        assert.deepEqual(ast.tokens[0].value, 'z');
        assert.deepEqual(ast.tokens[0].loc.start, { line: 1, column: 0 });
        assert.deepEqual(ast.tokens[0].loc.end, { line: 1, column: 1 });
        assert.ifError(ast.tokens[0].range);
    });

    it('should not understand JSX syntax by default', function () {
        assert.throws(function () {
            esprima.parse('<head/>');
        });
    });

    it('should not understand JSX syntax if it is explicitly not to be supported', function () {
        assert.throws(function () {
            esprima.parse('<b/>', { jsx: false });
        });
    });

    it('should understand JSX syntax if specified', function () {
        var ast, statement, expression;
        ast = esprima.parse('<title/>', { jsx: true });
        statement = ast.body[0];
        expression = statement.expression;

        assert.deepEqual(expression.type, 'JSXElement');
        assert.deepEqual(expression.openingElement.type, 'JSXOpeningElement');
        assert.deepEqual(expression.openingElement.name, { type: 'JSXIdentifier', name: 'title' });
        assert.deepEqual(expression.closingElement, null);
    });

    it('should never produce shallow copied nodes', function () {
        var ast, pattern, expr;
        ast = esprima.parse('let {a, b} = {x, y, z}');
        pattern = ast.body[0].declarations[0].id;
        expr = ast.body[0].declarations[0].init;
        pattern.properties[0].key.name = 'foo';
        expr.properties[0].key.name = 'bar';

        assert.deepEqual(pattern.properties[0].value, { type: 'Identifier', name: 'a' });
        assert.deepEqual(pattern.properties[1].value, { type: 'Identifier', name: 'b' });
        assert.deepEqual(expr.properties[0].value, { type: 'Identifier', name: 'x' });
        assert.deepEqual(expr.properties[1].value, { type: 'Identifier', name: 'y' });
        assert.deepEqual(expr.properties[2].value, { type: 'Identifier', name: 'z' });
    });
});

describe('esprima.parseModule', function () {

    it('should not accept zero parameter', function () {
        assert.throws(function () {
            esprima.parseModule();
        });
    });

    it('should accept one string parameter', function () {
        assert.doesNotThrow(function () {
            esprima.parseModule('function f(){}');
        });
    });

    it('should accept a String object', function () {
        assert.doesNotThrow(function () {
            esprima.parseModule(new String('some.property = value'));
        });
    });

    it('should accept two parameters (source and parsing options)', function () {
        var options = { range: false };
        assert.doesNotThrow(function () {
            esprima.parseModule('var x', options);
        });
    });

    it('should accept three parameters (source, options, and delegate)', function () {
        var options = { range: false };
        assert.doesNotThrow(function () {
            esprima.parseModule('var x', options, function f(entry) {
                return entry;
            });
        });
    });

    it('should exclude location information by default', function () {
        var ast = esprima.parseModule('answer = 42');
        assert.ifError(ast.range);
        assert.ifError(ast.loc);
    });

    it('should exclude comments by default', function () {
        var ast = esprima.parseModule('42 // answer');
        assert.ifError(ast.comments);
    });

    it('should exclude list of tokens by default', function () {
        var ast = esprima.parseModule('3.14159');
        assert.ifError(ast.tokens);
    });

    it('should include index-based location for the nodes when specified', function () {
        var ast = esprima.parseModule('answer = 42', { range: true });
        assert.deepEqual(ast.range, [0, 11]);
        assert.ifError(ast.loc);
    });

    it('should include line and column-based location for the nodes when specified', function () {
        var ast = esprima.parseModule('answer = 42', { loc: true });
        assert.deepEqual(ast.loc.start, { line: 1, column: 0 });
        assert.deepEqual(ast.loc.end, { line: 1, column: 11 });
        assert.ifError(ast.loc.source);
        assert.ifError(ast.range);
    });

    it('should include source information for the nodes when specified', function () {
        var ast = esprima.parseModule('answer = 42', { loc: true, source: 'example.js' });
        assert.deepEqual(ast.loc.source, 'example.js');
        assert.ifError(ast.range);
    });

    it('should include the list of comments when specified', function () {
        var ast = esprima.parseModule('42 // answer', { comment: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0], { type: 'Line', value: ' answer' });
    });

    it('should include the list of comments with index-based location when specified', function () {
        var ast = esprima.parseModule('42 // answer', { comment: true, range: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0], { type: 'Line', value: ' answer', range: [3, 12] });
    });

    it('should include the list of comments with line and column-based location when specified', function () {
        var ast = esprima.parseModule('42 // answer', { comment: true, loc: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0].type, 'Line');
        assert.deepEqual(ast.comments[0].value, ' answer');
        assert.deepEqual(ast.comments[0].loc.start, { line: 1, column: 3 });
        assert.deepEqual(ast.comments[0].loc.end, { line: 1, column: 12 });
        assert.ifError(ast.comments[0].range);
    });

    it('should be able to attach comments', function () {
        var ast, statement, expression, comments;

        ast = esprima.parseModule('/* universe */ 42', { attachComment: true });
        statement = ast.body[0];
        expression = statement.expression;
        comments = statement.leadingComments;

        assert.deepEqual(expression, { type: 'Literal', value: 42, raw: '42' });
        assert.deepEqual(statement.leadingComments, [{ type: 'Block', value: ' universe ', range: [0, 14] }]);
    });

    it('should not implicity collect comments when comment attachment is specified', function () {
        var ast = esprima.parseModule('/* universe */ 42', { attachComment: true });
        assert.equal(ast.comments, undefined);
    });

    it('should include the list of tokens when specified', function () {
        var ast = esprima.parseModule('x = 1', { tokens: true });
        assert.deepEqual(ast.tokens.length, 3);
        assert.deepEqual(ast.tokens[0], { type: 'Identifier', value: 'x' });
        assert.deepEqual(ast.tokens[1], { type: 'Punctuator', value: '=' });
        assert.deepEqual(ast.tokens[2], { type: 'Numeric', value: '1' });
    });

    it('should include the list of tokens with index-based location when specified', function () {
        var ast = esprima.parseModule('y = 2', { tokens: true, range: true });
        assert.deepEqual(ast.tokens[0], { type: 'Identifier', value: 'y', range: [0, 1] });
        assert.deepEqual(ast.tokens[1], { type: 'Punctuator', value: '=', range: [2, 3] });
        assert.deepEqual(ast.tokens[2], { type: 'Numeric', value: '2', range: [4, 5] });
    });

    it('should include the list of tokens with line and column-based location when specified', function () {
        var ast = esprima.parseModule('z = 3', { tokens: true, loc: true });
        assert.deepEqual(ast.tokens.length, 3);
        assert.deepEqual(ast.tokens[0].type, 'Identifier');
        assert.deepEqual(ast.tokens[0].value, 'z');
        assert.deepEqual(ast.tokens[0].loc.start, { line: 1, column: 0 });
        assert.deepEqual(ast.tokens[0].loc.end, { line: 1, column: 1 });
        assert.ifError(ast.tokens[0].range);
    });

    it('should not understand JSX syntax by default', function () {
        assert.throws(function () {
            esprima.parseModule('<head/>');
        });
    });

    it('should not understand JSX syntax if it is explicitly not to be supported', function () {
        assert.throws(function () {
            esprima.parseModule('<b/>', { jsx: false });
        });
    });

    it('should understand JSX syntax if specified', function () {
        var ast, statement, expression;
        ast = esprima.parseModule('<title/>', { jsx: true });
        statement = ast.body[0];
        expression = statement.expression;

        assert.deepEqual(expression.type, 'JSXElement');
        assert.deepEqual(expression.openingElement.type, 'JSXOpeningElement');
        assert.deepEqual(expression.openingElement.name, { type: 'JSXIdentifier', name: 'title' });
        assert.deepEqual(expression.closingElement, null);
    });

    it('should never produce shallow copied nodes', function () {
        var ast, pattern, expr;
        ast = esprima.parseModule('let {a, b} = {x, y, z}');
        pattern = ast.body[0].declarations[0].id;
        expr = ast.body[0].declarations[0].init;
        pattern.properties[0].key.name = 'foo';
        expr.properties[0].key.name = 'bar';

        assert.deepEqual(pattern.properties[0].value, { type: 'Identifier', name: 'a' });
        assert.deepEqual(pattern.properties[1].value, { type: 'Identifier', name: 'b' });
        assert.deepEqual(expr.properties[0].value, { type: 'Identifier', name: 'x' });
        assert.deepEqual(expr.properties[1].value, { type: 'Identifier', name: 'y' });
        assert.deepEqual(expr.properties[2].value, { type: 'Identifier', name: 'z' });
    });

    it('should perform strict mode parsing', function () {
        assert.throws(function () {
            esprima.parseModule('with (x) {}');
        });
    });

});

describe('esprima.parseScript', function () {

    it('should not accept zero parameter', function () {
        assert.throws(function () {
            esprima.parseScript();
        });
    });

    it('should accept one string parameter', function () {
        assert.doesNotThrow(function () {
            esprima.parseScript('function f(){}');
        });
    });

    it('should accept a String object', function () {
        assert.doesNotThrow(function () {
            esprima.parseScript(new String('some.property = value'));
        });
    });

    it('should accept two parameters (source and parsing options)', function () {
        var options = { range: false };
        assert.doesNotThrow(function () {
            esprima.parseScript('var x', options);
        });
    });

    it('should accept three parameters (source, options, and delegate)', function () {
        var options = { range: false };
        assert.doesNotThrow(function () {
            esprima.parseScript('var x', options, function f(entry) {
                return entry;
            });
        });
    });

    it('should exclude location information by default', function () {
        var ast = esprima.parseScript('answer = 42');
        assert.ifError(ast.range);
        assert.ifError(ast.loc);
    });

    it('should exclude comments by default', function () {
        var ast = esprima.parseScript('42 // answer');
        assert.ifError(ast.comments);
    });

    it('should exclude list of tokens by default', function () {
        var ast = esprima.parseScript('3.14159');
        assert.ifError(ast.tokens);
    });

    it('should include index-based location for the nodes when specified', function () {
        var ast = esprima.parseScript('answer = 42', { range: true });
        assert.deepEqual(ast.range, [0, 11]);
        assert.ifError(ast.loc);
    });

    it('should include line and column-based location for the nodes when specified', function () {
        var ast = esprima.parseScript('answer = 42', { loc: true });
        assert.deepEqual(ast.loc.start, { line: 1, column: 0 });
        assert.deepEqual(ast.loc.end, { line: 1, column: 11 });
        assert.ifError(ast.loc.source);
        assert.ifError(ast.range);
    });

    it('should include source information for the nodes when specified', function () {
        var ast = esprima.parseScript('answer = 42', { loc: true, source: 'example.js' });
        assert.deepEqual(ast.loc.source, 'example.js');
        assert.ifError(ast.range);
    });

    it('should include the list of comments when specified', function () {
        var ast = esprima.parseScript('42 // answer', { comment: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0], { type: 'Line', value: ' answer' });
    });

    it('should include the list of comments with index-based location when specified', function () {
        var ast = esprima.parseScript('42 // answer', { comment: true, range: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0], { type: 'Line', value: ' answer', range: [3, 12] });
    });

    it('should include the list of comments with line and column-based location when specified', function () {
        var ast = esprima.parseScript('42 // answer', { comment: true, loc: true });
        assert.deepEqual(ast.comments.length, 1);
        assert.deepEqual(ast.comments[0].type, 'Line');
        assert.deepEqual(ast.comments[0].value, ' answer');
        assert.deepEqual(ast.comments[0].loc.start, { line: 1, column: 3 });
        assert.deepEqual(ast.comments[0].loc.end, { line: 1, column: 12 });
        assert.ifError(ast.comments[0].range);
    });

    it('should be able to attach comments', function () {
        var ast, statement, expression, comments;

        ast = esprima.parseScript('/* universe */ 42', { attachComment: true });
        statement = ast.body[0];
        expression = statement.expression;
        comments = statement.leadingComments;

        assert.deepEqual(expression, { type: 'Literal', value: 42, raw: '42' });
        assert.deepEqual(statement.leadingComments, [{ type: 'Block', value: ' universe ', range: [0, 14] }]);
    });

    it('should not implicity collect comments when comment attachment is specified', function () {
        var ast = esprima.parseScript('/* universe */ 42', { attachComment: true });
        assert.equal(ast.comments, undefined);
    });

    it('should include the list of tokens when specified', function () {
        var ast = esprima.parseScript('x = 1', { tokens: true });
        assert.deepEqual(ast.tokens.length, 3);
        assert.deepEqual(ast.tokens[0], { type: 'Identifier', value: 'x' });
        assert.deepEqual(ast.tokens[1], { type: 'Punctuator', value: '=' });
        assert.deepEqual(ast.tokens[2], { type: 'Numeric', value: '1' });
    });

    it('should include the list of tokens with index-based location when specified', function () {
        var ast = esprima.parseScript('y = 2', { tokens: true, range: true });
        assert.deepEqual(ast.tokens[0], { type: 'Identifier', value: 'y', range: [0, 1] });
        assert.deepEqual(ast.tokens[1], { type: 'Punctuator', value: '=', range: [2, 3] });
        assert.deepEqual(ast.tokens[2], { type: 'Numeric', value: '2', range: [4, 5] });
    });

    it('should include the list of tokens with line and column-based location when specified', function () {
        var ast = esprima.parseScript('z = 3', { tokens: true, loc: true });
        assert.deepEqual(ast.tokens.length, 3);
        assert.deepEqual(ast.tokens[0].type, 'Identifier');
        assert.deepEqual(ast.tokens[0].value, 'z');
        assert.deepEqual(ast.tokens[0].loc.start, { line: 1, column: 0 });
        assert.deepEqual(ast.tokens[0].loc.end, { line: 1, column: 1 });
        assert.ifError(ast.tokens[0].range);
    });

    it('should not understand JSX syntax by default', function () {
        assert.throws(function () {
            esprima.parseScript('<head/>');
        });
    });

    it('should not understand JSX syntax if it is explicitly not to be supported', function () {
        assert.throws(function () {
            esprima.parseScript('<b/>', { jsx: false });
        });
    });

    it('should understand JSX syntax if specified', function () {
        var ast, statement, expression;
        ast = esprima.parseScript('<title/>', { jsx: true });
        statement = ast.body[0];
        expression = statement.expression;

        assert.deepEqual(expression.type, 'JSXElement');
        assert.deepEqual(expression.openingElement.type, 'JSXOpeningElement');
        assert.deepEqual(expression.openingElement.name, { type: 'JSXIdentifier', name: 'title' });
        assert.deepEqual(expression.closingElement, null);
    });

    it('should never produce shallow copied nodes', function () {
        var ast, pattern, expr;
        ast = esprima.parseScript('let {a, b} = {x, y, z}');
        pattern = ast.body[0].declarations[0].id;
        expr = ast.body[0].declarations[0].init;
        pattern.properties[0].key.name = 'foo';
        expr.properties[0].key.name = 'bar';

        assert.deepEqual(pattern.properties[0].value, { type: 'Identifier', name: 'a' });
        assert.deepEqual(pattern.properties[1].value, { type: 'Identifier', name: 'b' });
        assert.deepEqual(expr.properties[0].value, { type: 'Identifier', name: 'x' });
        assert.deepEqual(expr.properties[1].value, { type: 'Identifier', name: 'y' });
        assert.deepEqual(expr.properties[2].value, { type: 'Identifier', name: 'z' });
    });
});

describe('esprima.parse delegate', function () {

    it('should receive all the nodes', function () {
        var list = [];
        function collect(node) {
            list.push(node.type);
        }
        esprima.parse('/* universe */ answer = 42', {}, collect);

        assert.deepEqual(list.length, 5);
        assert.deepEqual(list, ['Identifier', 'Literal', 'AssignmentExpression', 'ExpressionStatement', 'Program']);
    });

    it('should receive the comments if specified', function () {
        var list = [];
        function collect(node) {
            list.push(node);
        }
        esprima.parse('/* prolog */ answer = 42 // epilog', { comment: true }, collect);

        assert.deepEqual(list.length, 7);
        assert.deepEqual(list[0], { type: 'BlockComment', value: ' prolog ' });
        assert.deepEqual(list[1], { type: 'Identifier', name: 'answer' });
        assert.deepEqual(list[2], { type: 'LineComment', value: ' epilog' });
        assert.deepEqual(list[3], { type: 'Literal', value: 42, raw: '42' });
        assert.deepEqual(list[4].type, 'AssignmentExpression');
        assert.deepEqual(list[5].type, 'ExpressionStatement');
        assert.deepEqual(list[6].type, 'Program');
    });

    it('should be able to walk the tree and pick a node', function () {
        var constant = null;
        function walk(node) {
            if (node.type === 'Literal') {
                constant = node;
            }
        }
        esprima.parse('answer = 42 // universe', {}, walk);
        assert.deepEqual(constant, { type: 'Literal', value: 42, raw: '42' });
    });

    it('should be able to mutate each node', function () {
        var ast = esprima.parse('42', { range: true }, function (node) {
            node.start = node.range[0];
            node.end = node.range[1];
        });
        assert.deepEqual(ast.start, 0);
        assert.deepEqual(ast.end, 2);
    });

    it('should be affected by cover grammars', function () {
        var list = [];
        esprima.parse('(x, y) => z', {}, function (n) {
            list.push(n.type)
        });

        // (x, y) will be a SequenceExpression first before being reinterpreted as
        // the formal parameter list for an ArrowFunctionExpression

        assert.deepEqual(list.length, 7);
        assert.deepEqual(list[0], 'Identifier');               // x
        assert.deepEqual(list[1], 'Identifier');               // y
        assert.deepEqual(list[2], 'SequenceExpression');       // x, y
        assert.deepEqual(list[3], 'Identifier');               // z
        assert.deepEqual(list[4], 'ArrowFunctionExpression');  // (x, y) => z
        assert.deepEqual(list[5], 'ExpressionStatement');
    });

    it('should be affected by async arrow function cover grammar', function () {
        var list = [];
        esprima.parse('async (a) => 1', {}, function (n) {
            list.push(n.type)
        });

        // async (a) will be a CallExpression first before being reinterpreted as
        // the formal parameter list for an (async) ArrowFunctionExpression

        assert.deepEqual(list.length, 7);
        assert.deepEqual(list[0], 'Identifier');               // async
        assert.deepEqual(list[1], 'Identifier');               // a
        assert.deepEqual(list[2], 'CallExpression');           // async (a)
        assert.deepEqual(list[3], 'Literal');                  // 1
        assert.deepEqual(list[4], 'ArrowFunctionExpression');  // async (a) => 1
        assert.deepEqual(list[5], 'ExpressionStatement');
    });

    it('should receive metadata of each node', function () {
        var starts = [], ends = [];
        esprima.parse('x = y + z', { range: false }, function (node, metadata) {
            starts.push(metadata.start);
            ends.push(metadata.end);
        });

        assert.deepEqual(starts.length, 7);
        assert.deepEqual(starts[0], { line: 1, column: 0, offset: 0 }); // x
        assert.deepEqual(starts[1], { line: 1, column: 4, offset: 4 }); // y
        assert.deepEqual(starts[2], { line: 1, column: 8, offset: 8 }); // z
        assert.deepEqual(starts[3], { line: 1, column: 4, offset: 4 }); // y + z
        assert.deepEqual(starts[4], { line: 1, column: 0, offset: 0 }); // x = y + z
        assert.deepEqual(starts[5], { line: 1, column: 0, offset: 0 }); // x = y + z
        assert.deepEqual(starts[6], { line: 1, column: 0, offset: 0 }); // x = y + z

        assert.deepEqual(ends.length, 7);
        assert.deepEqual(ends[0], { line: 1, column: 1, offset: 1 }); // x
        assert.deepEqual(ends[1], { line: 1, column: 5, offset: 5 }); // y
        assert.deepEqual(ends[2], { line: 1, column: 9, offset: 9 }); // z
        assert.deepEqual(ends[3], { line: 1, column: 9, offset: 9 }); // y + z
        assert.deepEqual(ends[4], { line: 1, column: 9, offset: 9 }); // x = y + z
        assert.deepEqual(ends[5], { line: 1, column: 9, offset: 9 }); // x = y + z
        assert.deepEqual(ends[6], { line: 1, column: 9, offset: 9 }); // x = y + z
    });

    it('should receive metadata of comments', function () {
        var starts = [], ends = [];
        esprima.parse('42 // answer', { comment: true }, function (node, metadata) {
            starts.push(metadata.start);
            ends.push(metadata.end);
        });

        assert.deepEqual(starts.length, 4);
        assert.deepEqual(starts[0], { line: 1, column: 3, offset: 3 });
        assert.deepEqual(starts[1], { line: 1, column: 0, offset: 0 });
        assert.deepEqual(starts[2], { line: 1, column: 0, offset: 0 });
        assert.deepEqual(starts[3], { line: 1, column: 0, offset: 0 });

        assert.deepEqual(ends.length, 4);
        assert.deepEqual(ends[0], { line: 1, column: 12, offset: 12 });
        assert.deepEqual(ends[1], { line: 1, column: 2, offset: 2 });
        assert.deepEqual(ends[2], { line: 1, column: 12, offset: 12 });
        assert.deepEqual(ends[3], { line: 1, column: 12, offset: 12 });
    });

    it('can be used for custom comment attachment', function () {
        var code, attacher, tree;

        function LineAttacher() {
            this.variableDeclarations = [];
            this.lineComments = [];
        }

        // Attach a line comment to a variable declaration in the same line.
        // Example: `var foo = 42; // bar` will have "bar" in the new property
        // called `annotation` of the variable declaration statement.

        LineAttacher.prototype.attach = function () {
            var i, j, declaration, comment;

            for (i = 0; i < this.variableDeclarations.length; ++i) {
                declaration = this.variableDeclarations[i];
                for (j = 0; j < this.lineComments.length; ++j) {
                    comment = this.lineComments[j];
                    if (declaration.line === comment.line) {
                        declaration.node.annotation = comment.comment;
                        break;
                    }
                }
            }
        }

        LineAttacher.prototype.visit = function (node, metadata) {
            if (node.type === 'VariableDeclaration') {
                this.variableDeclarations.push({
                    node: node,
                    line: metadata.start.line
                });
            } else if (node.type === 'LineComment') {
                this.lineComments.push({
                    comment: node,
                    line: metadata.start.line
                });
            }
            this.attach();
        }

        code = [
            'var x = 42 // answer',
            'var y = 3',
            '// foobar'
        ].join('\n');

        attacher = new LineAttacher();
        tree = esprima.parse(code, { comment: true }, function (node, metadata) {
            attacher.visit(node, metadata);
        });

        // There will be two variable declaration statement.
        // Only the first one will have `annotation` since there is
        // a line comment in the same line.
        assert.deepEqual(tree.body.length, 2);
        assert.deepEqual(tree.body[0].annotation, { type: 'LineComment', value: ' answer' });
        assert.deepEqual(tree.body[1].annotation, undefined);
    });

});

describe('esprima.tokenize', function () {

    it('should not accept zero parameter', function () {
        assert.throws(function () {
            esprima.tokenize();
        });
    });

    it('should accept one string parameter', function () {
        assert.doesNotThrow(function () {
            esprima.tokenize('function f(){}');
        });
    });

    it('should accept a String object', function () {
        assert.doesNotThrow(function () {
            esprima.tokenize(new String('some.property = value'));
        });
    });

    it('should accept two parameters (source and options)', function () {
        var options = { range: false };
        assert.doesNotThrow(function () {
            esprima.tokenize('var x', options);
        });
    });

    it('should accept three parameters (source, options, and delegate)', function () {
        var options = { range: false };
        assert.doesNotThrow(function () {
            esprima.tokenize('var x', options, function f(entry) {
                return entry;
            });
        });
    });

    it('should exclude location information by default', function () {
        var tokens = esprima.tokenize('answer = 42');
        assert.deepEqual(tokens.length, 3);
        assert.ifError(tokens[0].loc);
        assert.ifError(tokens[0].range);
    });

    it('should exclude list of errors in non-tolerant mode', function () {
        var tokens = esprima.tokenize('x', { tolerant: false });
        assert.deepEqual(tokens, [{ type: 'Identifier', value: 'x' }]);
    });

    it('should include index-based location for the tokens when specified', function () {
        var tokens = esprima.tokenize('answer = 42', { range: true });
        assert.deepEqual(tokens.length, 3);
        assert.deepEqual(tokens[0], { type: 'Identifier', value: 'answer', range: [0, 6] });
        assert.deepEqual(tokens[1], { type: 'Punctuator', value: '=', range: [7, 8] });
        assert.deepEqual(tokens[2], { type: 'Numeric', value: '42', range: [9, 11] });
        assert.ifError(tokens[0].loc);
    });

    it('should include line and column-based location for the tokens when specified', function () {
        var tokens = esprima.tokenize('answer = 42', { loc: true });
        assert.deepEqual(tokens.length, 3);
        assert.deepEqual(tokens[0].type, 'Identifier');
        assert.deepEqual(tokens[0].value, 'answer');
        assert.deepEqual(tokens[0].loc.start, { line: 1, column: 0 });
        assert.deepEqual(tokens[0].loc.end, { line: 1, column: 6 });
        assert.ifError(tokens[0].range);
    });

    it('should also include the comments when specified', function () {
        var tokens = esprima.tokenize('/*TODO*/ xyz', { comment: true });
        assert.deepEqual(tokens.length, 2);
        assert.deepEqual(tokens[0], { type: 'BlockComment', value: 'TODO' });
        assert.ifError(tokens[0].loc);
        assert.ifError(tokens[0].range);
    });

});

describe('esprima.tokenize delegate', function () {

    it('should receive all the tokens', function () {
        var list = [];
        esprima.tokenize('p = 1, r', {}, function (entry) {
            list.push(entry);
        });
        assert.deepEqual(list.length, 5);
        assert.deepEqual(list[0], { type: 'Identifier', value: 'p' });
        assert.deepEqual(list[1], { type: 'Punctuator', value: '=' });
        assert.deepEqual(list[2], { type: 'Numeric', value: '1' });
        assert.deepEqual(list[3], { type: 'Punctuator', value: ',' });
        assert.deepEqual(list[4], { type: 'Identifier', value: 'r' });
    });

    it('should receive all the tokens with the location information', function () {
        var list = [];
        esprima.tokenize('s = 3', { range: true }, function (entry) {
            list.push(entry);
        });
        assert.deepEqual(list.length, 3);
        assert.deepEqual(list[0], { type: 'Identifier', value: 's', range: [0, 1] });
        assert.deepEqual(list[1], { type: 'Punctuator', value: '=', range: [2, 3] });
        assert.deepEqual(list[2], { type: 'Numeric', value: '3', range: [4, 5] });
    });

    it('should be able to simplify the token structure', function () {
        var tokens = esprima.tokenize('var regex = /abc/pqs', { range: true }, function (entry) {
            return entry.type;
        });
        assert.deepEqual(tokens.length, 4);
        assert.deepEqual(tokens[0], 'Keyword');
        assert.deepEqual(tokens[1], 'Identifier');
        assert.deepEqual(tokens[2], 'Punctuator');
        assert.deepEqual(tokens[3], 'RegularExpression');
    });

    it('should be able to modify the token structure', function () {
        var tokens = esprima.tokenize('var regex = /abc/pqs', { range: true }, function (entry) {
            var t = {};
            t[entry.type] = entry.value;
            t.start = entry.range[0];
            t.end = entry.range[1];
            return t;
        });
        assert.deepEqual(tokens.length, 4);
        assert.deepEqual(tokens[0], { Keyword: 'var', start: 0, end: 3 });
        assert.deepEqual(tokens[1], { Identifier: 'regex', start: 4, end: 9 });
        assert.deepEqual(tokens[2], { Punctuator: '=', start: 10, end: 11 });
        assert.deepEqual(tokens[3], { RegularExpression: '/abc/pqs', start: 12, end: 20 });
    });

});
