Home Manual Reference Source Test Repository

src/utils/ast/astSyntax.js

/* eslint-disable eqeqeq */
import ASTUtil                from './ASTUtil';

import expressionPrecedence   from './expressionPrecedence';

let ArrayExpression, BinaryExpression, ForInStatement, FunctionDeclaration;

export default {
   Program(node, state)
   {
      const indent = state.indent.repeat(state.indentLevel);
      const { lineEnd, output } = state;
      const statements = node.body;

      for (let i = 0; i < statements.length; i++)
      {
         const statement = statements[i];

         output.write(indent);

         this[statement.type](statement, state);

         output.write(lineEnd);
      }
   },

   BlockStatement(node, state)
   {
      const indent = state.indent.repeat(state.indentLevel++);
      const { lineEnd, output } = state;
      const statementIndent = indent + state.indent;

      output.write('{');

      const statements = node.body;

      if (statements != null && statements.length > 0)
      {
         output.write(lineEnd);

         for (let i = 0; i < statements.length; i++)
         {
            const statement = statements[i];

            output.write(statementIndent);

            this[statement.type](statement, state);

            output.write(lineEnd);
         }
         output.write(indent);
      }

      output.write('}');
      state.indentLevel--;
   },

   EmptyStatement(node, state)
   {
      state.output.write(';');
   },

   ExpressionStatement(node, state)
   {
      const precedence = expressionPrecedence[node.expression.type];

      if (precedence === 17 || (precedence === 3 && node.expression.left.type[0] === 'O'))
      {
         // Should always have parentheses or is an AssignmentExpression to an ObjectPattern
         state.output.write('(');
         this[node.expression.type](node.expression, state);
         state.output.write(')');
      }
      else
      {
         this[node.expression.type](node.expression, state);
      }

      state.output.write(';');
   },

   IfStatement(node, state)
   {
      const { output } = state;

      output.write('if (');
      output.operators.push('if');

      this[node.test.type](node.test, state);

      output.write(') ');

      this[node.consequent.type](node.consequent, state);

      if (node.alternate != null)
      {
         output.write(' else ');
         output.operators.push('else');

         this[node.alternate.type](node.alternate, state);
      }
   },

   LabeledStatement(node, state)
   {
      this[node.label.type](node.label, state);
      state.output.write(': ');
      this[node.body.type](node.body, state);
   },

   BreakStatement(node, state)
   {
      const { output } = state;

      output.write('break');
      output.operators.push('break');

      if (node.label)
      {
         output.write(' ');
         this[node.label.type](node.label, state);
      }
      output.write(';');
   },

   ContinueStatement(node, state)
   {
      const { output } = state;

      output.write('continue');
      output.operators.push('continue');

      if (node.label)
      {
         output.write(' ');
         this[node.label.type](node.label, state);
      }

      output.write(';');
   },

   WithStatement(node, state)
   {
      const { output } = state;

      output.write('with (');
      output.operators.push('with');

      this[node.object.type](node.object, state);

      output.write(') ');

      this[node.body.type](node.body, state);
   },

   SwitchStatement(node, state)
   {
      const indent = state.indent.repeat(state.indentLevel++);
      const { lineEnd, output } = state;

      state.indentLevel++;

      const caseIndent = indent + state.indent;
      const statementIndent = caseIndent + state.indent;

      output.write('switch (');
      output.operators.push('switch');

      this[node.discriminant.type](node.discriminant, state);

      output.write(`) \{${lineEnd}`);

      const { cases: occurences } = node;
      const { length: occurencesCount } = occurences;

      for (let i = 0; i < occurencesCount; i++)
      {
         const occurence = occurences[i];

         if (occurence.test)
         {
            output.write(`${caseIndent}case `);
            output.operators.push('case');

            this[occurence.test.type](occurence.test, state);

            output.write(`:${lineEnd}`);
         }
         else
         {
            output.write(`${caseIndent}default:${lineEnd}`);
            output.operators.push('default');
         }

         const { consequent } = occurence;
         const { length: consequentCount } = consequent;

         for (let j = 0; j < consequentCount; j++)
         {
            const statement = consequent[j];

            output.write(statementIndent);

            this[statement.type](statement, state);

            output.write(lineEnd);
         }
      }

      state.indentLevel -= 2;

      output.write(`${indent}}`);
   },

   ReturnStatement(node, state)
   {
      const { output } = state;

      output.write('return');
      output.operators.push('return');

      if (node.argument)
      {
         output.write(' ');

         this[node.argument.type](node.argument, state);
      }

      output.write(';');
   },

   ThrowStatement(node, state)
   {
      const { output } = state;

      output.write('throw ');
      output.operators.push('throw');

      this[node.argument.type](node.argument, state);

      output.write(';');
   },

   TryStatement(node, state)
   {
      const { output } = state;

      output.write('try ');
      output.operators.push('try');

      this[node.block.type](node.block, state);

      if (node.handler)
      {
         const { handler } = node;

         output.write(' catch (');
         output.operators.push('catch');

         this[handler.param.type](handler.param, state);

         output.write(') ');

         this[handler.body.type](handler.body, state);
      }

      if (node.finalizer)
      {
         output.write(' finally ');
         output.operators.push('finally');

         this[node.finalizer.type](node.finalizer, state);
      }
   },

   WhileStatement(node, state)
   {
      const { output } = state;

      output.operators.push('while');
      output.write('while (');

      this[node.test.type](node.test, state);

      output.write(') ');

      this[node.body.type](node.body, state);
   },

   DoWhileStatement(node, state)
   {
      const { output } = state;

      output.operators.push('dowhile');
      output.write('do ');

      this[node.body.type](node.body, state);

      output.write(' while (');

      this[node.test.type](node.test, state);

      output.write(');');
   },

   ForStatement(node, state)
   {
      const { output } = state;

      output.write('for (');
      output.operators.push('for');

      if (node.init != null)
      {
         state.noTrailingSemicolon = true;

         this[node.init.type](node.init, state);

         state.noTrailingSemicolon = false;
      }

      output.write('; ');

      if (node.test) { this[node.test.type](node.test, state); }

      output.write('; ');

      if (node.update) { this[node.update.type](node.update, state); }

      output.write(') ');

      this[node.body.type](node.body, state);
   },

   ForInStatement: ForInStatement = function(node, state)
   {
      const { output } = state;

      output.write('for (');

      const { left } = node, { type } = left;

      state.noTrailingSemicolon = true;

      this[type](left, state);

      state.noTrailingSemicolon = false;

      // Identifying whether node.type is `ForInStatement` or `ForOfStatement`
      output.write(node.type[3] === 'I' ? ' in ' : ' of ');
      output.operators.push(node.type[3] === 'I' ? 'forin' : 'forof');

      this[node.right.type](node.right, state);

      output.write(') ');

      this[node.body.type](node.body, state);
   },

   ForOfStatement: ForInStatement,

   DebuggerStatement(node, state)
   {
      state.output.write(`debugger;${state.lineEnd}`);
   },

   FunctionDeclaration: FunctionDeclaration = function(node, state)
   {
      const { output } = state;

      output.write(node.generator ? 'function* ' : 'function ');
      output.operators.push(node.generator ? 'function*' : 'function');

      if (node.id)
      {
         output.write(node.id.name);
         output.operands.push(node.id.name);
      }

      ASTUtil.formatSequence(node.params, state, this);

      output.write(' ');

      this[node.body.type](node.body, state);
   },

   FunctionExpression: FunctionDeclaration,

   VariableDeclaration(node, state)
   {
      const { output } = state;
      const { declarations } = node;

      output.write(`${node.kind} `);
      output.operators.push(node.kind);

      const { length } = declarations;

      if (length > 0)
      {
         this.VariableDeclarator(declarations[0], state);

         for (let i = 1; i < length; i++)
         {
            output.write(', ');

            this.VariableDeclarator(declarations[i], state);
         }
      }

      if (state.noTrailingSemicolon !== true) { output.write(';'); }
   },

   VariableDeclarator(node, state)
   {
      this[node.id.type](node.id, state);

      if (node.init != null)
      {
         state.output.write(' = ');
         state.output.operators.push('=');

         this[node.init.type](node.init, state);
      }
   },

   ClassDeclaration(node, state)
   {
      const { output } = state;

      output.write('class ');
      output.operators.push('class');

      if (node.id) { output.write(`${node.id.name} `); }

      if (node.superClass)
      {
         output.write('extends ');
         output.operators.push('extends');

         this[node.superClass.type](node.superClass, state);

         output.write(' ');
      }

      this.BlockStatement(node.body, state);
   },

   ImportDeclaration(node, state)
   {
      const { output } = state;

      output.write('import ');
      output.operators.push('import');

      const { specifiers } = node;
      const { length } = specifiers;

      if (length > 0)
      {
         let i = 0, specifier;
         while (i < length)
         {
            if (i > 0) { output.write(', '); }

            specifier = specifiers[i];
            const type = specifier.type[6];

            if (type === 'D')
            {
               output.write(specifier.local.name); // ImportDefaultSpecifier
               i++;
            }
            else if (type === 'N')
            {

               output.write(`* as ${specifier.local.name}`);   // ImportNamespaceSpecifier
               i++;
            }
            else
            {
               break;   // ImportSpecifier
            }
         }

         if (i < length)
         {
            output.write('{');

            for (; ;)
            {
               specifier = specifiers[i];
               const { name } = specifier.imported;

               output.write(name);

               if (name !== specifier.local.name) { output.write(` as ${specifier.local.name}`); }

               if (++i < length)
               {
                  output.write(', ');
               }
               else
               {
                  break;
               }
            }

            output.write('}');
         }

         output.write(' from ');
         output.operators.push('from');
      }

      this.Literal(node.source, state);

      output.write(';');
   },

   ExportDefaultDeclaration(node, state)
   {
      const { output } = state;

      output.write('export default ');
      output.operators.push('export');
      output.operators.push('default');

      this[node.declaration.type](node.declaration, state);

      // All expression nodes except `FunctionExpression`
      if (expressionPrecedence[node.declaration.type] && node.declaration.type[0] !== 'F') { output.write(';'); }
   },

   ExportNamedDeclaration(node, state)
   {
      const { output } = state;

      output.write('export ');
      output.operators.push('export');

      if (node.declaration)
      {
         this[node.declaration.type](node.declaration, state);
      }
      else
      {
         output.write('{');
         output.operators.push('{}');

         const { specifiers } = node, { length } = specifiers;

         if (length > 0)
         {
            for (let i = 0; ;)
            {
               const specifier = specifiers[i];
               const { name } = specifier.local;

               output.write(name);

               if (name !== specifier.exported.name) { output.write(` as ${specifier.exported.name}`); }

               if (++i < length)
               {
                  output.write(', ');
               }
               else
               {
                  break;
               }
            }
         }

         output.write('}');

         if (node.source)
         {
            output.write(' from ');

            this.Literal(node.source, state);
         }

         output.write(';');
      }
   },

   ExportAllDeclaration(node, state)
   {
      const { output } = state;

      output.write('export * from ');
      output.operators.push('export');
      output.operators.push('*');

      this.Literal(node.source, state);

      output.write(';');
   },

   MethodDefinition(node, state)
   {
      const { output } = state;

      if (node.static)
      {
         output.write('static ');
         output.operators.push('static');
      }

      switch (node.kind[0])
      {
         case 'g': // `get`
         case 's': // `set`
            output.write(`${node.kind} `);
            output.operators.push(node.kind);
            break;
      }

      if (node.value.generator) { output.write('*'); }

      if (node.computed)
      {
         output.write('[');

         this[node.key.type](node.key, state);

         output.write(']');
      }
      else
      {
         this[node.key.type](node.key, state);
      }

      ASTUtil.formatSequence(node.value.params, state, this);

      output.write(' ');

      this[node.value.body.type](node.value.body, state);
   },

   ClassExpression(node, state)
   {
      this.ClassDeclaration(node, state);
   },

   ArrowFunctionExpression(node, state)
   {
      const { output } = state;
      const { params } = node;

      if (params != null)
      {
         // If params[0].type[0] starts with 'I', it can't be `ImportDeclaration` nor `IfStatement` and thus is
         // `Identifier`
         if (params.length === 1 && params[0].type[0] === 'I')
         {
            output.write(params[0].name);
         }
         else
         {
            ASTUtil.formatSequence(node.params, state, this);
         }
      }

      output.write(' => ');
      output.operators.push('function=>');

      if (node.body.type[0] === 'O')
      {
         output.write('(');

         this.ObjectExpression(node.body, state);

         output.write(')');
      }
      else
      {
         this[node.body.type](node.body, state);
      }
   },

   ThisExpression(node, state)
   {
      state.output.write('this');
      state.output.operators.push('this');
   },

   Super(node, state)
   {
      state.output.write('super');
      state.output.operators.push('super');
   },

   RestElement(node, state)
   {
      state.output.write('...');
      state.output.operators.push('... (rest)');

      this[node.argument.type](node.argument, state);
   },

   SpreadElement(node, state)
   {
      state.output.write('...');
      state.output.operators.push('... (spread)');
      this[node.argument.type](node.argument, state);
   },

   YieldExpression(node, state)
   {
      const { output } = state;

      output.write(node.delegate ? 'yield*' : 'yield');
      output.operators.push(node.delegate ? 'yield*' : 'yield');

      if (node.argument)
      {
         output.write(' ');

         this[node.argument.type](node.argument, state);
      }
   },

   TemplateLiteral(node, state)
   {
      const { output } = state;
      const { quasis, expressions } = node;
      const { length } = expressions;

      output.write('`');

      for (let i = 0; i < length; i++)
      {
         const expression = expressions[i];

         output.write(quasis[i].value.raw);
         output.write('${');

         this[expression.type](expression, state);

         output.write('}');
      }

      output.write(quasis[quasis.length - 1].value.raw);
      output.write('`');
   },

   TaggedTemplateExpression(node, state)
   {
      this[node.tag.type](node.tag, state);
      this[node.quasi.type](node.quasi, state);
   },

   ArrayExpression: ArrayExpression = function(node, state)
   {
      const { output } = state;

      output.operators.push('[]');
      output.write('[');

      if (node.elements.length > 0)
      {
         const { elements } = node, { length } = elements;

         for (let i = 0; ;)
         {
            const element = elements[i];

            if (element != null) { this[element.type](element, state); }

            if (++i < length)
            {
               output.write(', ');
            }
            else
            {
               if (element == null) { output.write(', '); }
               break;
            }
         }
      }

      output.write(']');
   },

   ArrayPattern: ArrayExpression,

   ObjectExpression(node, state)
   {
      const indent = state.indent.repeat(state.indentLevel++);
      const { lineEnd, output } = state;
      const propertyIndent = indent + state.indent;

      output.operators.push('{}');
      output.write('{');

      if (node.properties.length > 0)
      {
         output.write(lineEnd);

         const comma = `,${lineEnd}`, { properties } = node, { length } = properties;

         for (let i = 0; ;)
         {
            const property = properties[i];

            output.write(propertyIndent);

            this.Property(property, state);

            if (++i < length)
            {
               output.write(comma);
            }
            else
            {
               break;
            }
         }

         output.write(lineEnd);
         output.write(`${indent}}`);
      }
      else
      {
         output.write('}');
      }

      state.indentLevel--;
   },

   Property(node, state)
   {
      if (node.method || node.kind[0] !== 'i')
      {
         this.MethodDefinition(node, state); // Either a method or of kind `set` or `get` (not `init`)
      }
      else
      {
         const { output } = state;

         if (!node.shorthand)
         {
            if (node.computed)
            {
               output.operators.push('[]');
               output.write('[');

               this[node.key.type](node.key, state);

               output.write(']');
            }
            else
            {
               this[node.key.type](node.key, state);
            }

            output.operators.push(':');
            output.write(': ');
         }

         this[node.value.type](node.value, state);
      }
   },

   ObjectPattern(node, state)
   {
      const { output } = state;

      output.operators.push('{}');
      output.write('{');

      if (node.properties.length > 0)
      {
         const { properties } = node, { length } = properties;

         for (let i = 0; ;)
         {
            this.Property(properties[i], state);

            if (++i < length)
            {
               output.write(', ');
            }
            else
            {
               break;
            }
         }
      }
      output.write('}');
   },

   SequenceExpression(node, state)
   {
      ASTUtil.formatSequence(node.expressions, state, this);
   },

   UnaryExpression(node, state)
   {
      const { output } = state;

      output.operators.push(`${node.operator} (${node.prefix ? 'pre' : 'post'}fix)`);

      if (node.prefix)
      {
         output.write(node.operator);

         if (node.operator.length > 1) { output.write(' '); }

         if (expressionPrecedence[node.argument.type] < expressionPrecedence.UnaryExpression)
         {
            output.write('(');

            this[node.argument.type](node.argument, state);

            output.write(')');
         }
         else
         {
            this[node.argument.type](node.argument, state);
         }
      }
      else
      {
         // FIXME: This case never occurs
         this[node.argument.type](node.argument, state);

         state.output.write(node.operator);
      }
   },

   UpdateExpression(node, state)
   {
      // Always applied to identifiers or members, no parenthesis check needed
      state.output.operators.push(`${node.operator} (${node.prefix ? 'pre' : 'post'}fix)`);

      if (node.prefix)
      {
         state.output.write(node.operator);

         this[node.argument.type](node.argument, state);
      }
      else
      {
         this[node.argument.type](node.argument, state);

         state.output.write(node.operator);
      }
   },

   AssignmentExpression(node, state)
   {
      this[node.left.type](node.left, state);

      state.output.write(` ${node.operator} `);

      this[node.right.type](node.right, state);

      state.output.operators.push(node.operator);
   },

   AssignmentPattern(node, state)
   {
      this[node.left.type](node.left, state);

      state.output.write(' = ');

      this[node.right.type](node.right, state);

      state.output.operators.push('=');
   },

   BinaryExpression: BinaryExpression = function(node, state)
   {
      const { output } = state;

      output.operators.push(node.operator);

      if (node.operator === 'in')
      {
         // Avoids confusion in `for` loops initializers
         output.write('(');

         ASTUtil.formatBinaryExpressionPart(node.left, node, false, state, this);

         output.write(` ${node.operator} `);

         ASTUtil.formatBinaryExpressionPart(node.right, node, true, state, this);

         output.write(')');
      }
      else
      {
         ASTUtil.formatBinaryExpressionPart(node.left, node, false, state, this);

         output.write(` ${node.operator} `);

         ASTUtil.formatBinaryExpressionPart(node.right, node, true, state, this);
      }
   },

   LogicalExpression: BinaryExpression,

   ConditionalExpression(node, state)
   {
      const { output } = state;

      if (expressionPrecedence[node.test.type] > expressionPrecedence.ConditionalExpression)
      {
         this[node.test.type](node.test, state);
      }
      else
      {
         output.operators.push('()');
         output.write('(');

         this[node.test.type](node.test, state);

         output.write(')');
      }

      output.write(' ? ');

      this[node.consequent.type](node.consequent, state);

      output.write(' : ');

      this[node.alternate.type](node.alternate, state);

      output.operators.push(':?');
   },

   NewExpression(node, state)
   {
      const { output } = state;

      output.write('new ');
      output.operators.push('new');

      if (expressionPrecedence[node.callee.type] < expressionPrecedence.CallExpression ||
       ASTUtil.hasCallExpression(node.callee))
      {
         output.write('(');

         this[node.callee.type](node.callee, state);

         output.write(')');

         output.operators.push('()');
      }
      else
      {
         this[node.callee.type](node.callee, state);
      }

      ASTUtil.formatSequence(node['arguments'], state, this);
   },

   CallExpression(node, state)
   {
      const { output } = state;

      if (expressionPrecedence[node.callee.type] < expressionPrecedence.CallExpression)
      {
         output.write('(');

         this[node.callee.type](node.callee, state);

         output.write(')');
         output.operators.push('()');
      }
      else
      {
         this[node.callee.type](node.callee, state);
      }

      ASTUtil.formatSequence(node['arguments'], state, this);
   },

   MemberExpression(node, state)
   {
      const { output } = state;

      if (expressionPrecedence[node.object.type] < expressionPrecedence.MemberExpression)
      {
         output.write('(');

         this[node.object.type](node.object, state);

         output.write(')');
         output.operators.push('()');
      }
      else
      {
         this[node.object.type](node.object, state);
      }

      if (node.computed)
      {
         output.write('[');

         this[node.property.type](node.property, state);

         output.write(']');

         output.operators.push('[]');
      }
      else
      {
         output.write('.');
         output.operators.push('.');

         this[node.property.type](node.property, state);
      }
   },

   MetaProperty(node, state)
   {
      state.output.write(`${node.meta.name}.${node.property.name}`);

      state.output.operators.push('.');
      state.output.operands.push(node.meta.name);
      state.output.operands.push(node.property.name);
   },

   Identifier(node, state)
   {
      state.output.write(node.name);
      state.output.operands.push(node.name);
   },

   Literal(node, state)
   {
      if (node.raw != null)
      {
         state.output.write(node.raw);
         state.output.operands.push(node.raw);
      }
      else if (node.regex != null)
      {
         this.RegExpLiteral(node, state);
      }
      else
      {
         state.output.write(JSON.stringify(node.value));
      }
   },

   RegExpLiteral(node, state)
   {
      const { regex } = node;

      state.output.write(`new RegExp(${JSON.stringify(regex.pattern)}, ${JSON.stringify(regex.flags)})`);
   },

   // Babylon AST nodes ---------------------------------------------------------------------------------------------

   BooleanLiteral(node, state)
   {
      state.output.write(node.value);
      state.output.operands.push(node.value);
   },

   DirectiveLiteral(node, state)
   {
      state.output.write(node.value);
      state.output.operands.push(node.value);
   },

   NullLiteral(node, state)
   {
      state.output.write('null');
      state.output.operands.push('null');
   },

   NumericLiteral(node, state)
   {
      state.output.write(node.value);
      state.output.operands.push(node.value);
   },

   StringLiteral(node, state)
   {
      if (node.extra != null && node.extra.raw != null)
      {
         state.output.write(node.extra.raw);
         state.output.operands.push(node.extra.raw);
      }
      else
      {
         state.output.write(JSON.stringify(node.value));
         state.output.operands.push(JSON.stringify(node.value));
      }
   }
};