Jelajahi Sumber

Added runtime test with `astring` and `const` and `let` variables

sanex3339 5 tahun lalu
induk
melakukan
78b2be628f

+ 114 - 22
test/runtime-tests/JavaScriptObfuscatorRuntime.spec.ts

@@ -1,5 +1,7 @@
 import { assert } from 'chai';
 
+import { TInputOptions } from '../../src/types/options/TInputOptions';
+
 import { StringArrayEncoding } from '../../src/enums/StringArrayEncoding';
 
 import { readFileAsString } from '../helpers/readFileAsString';
@@ -7,30 +9,120 @@ import { readFileAsString } from '../helpers/readFileAsString';
 import { JavaScriptObfuscator } from '../../src/JavaScriptObfuscatorFacade';
 
 describe('JavaScriptObfuscator runtime eval', function () {
+    const options: TInputOptions = {
+        controlFlowFlattening: true,
+        controlFlowFlatteningThreshold: 1,
+        deadCodeInjection: true,
+        deadCodeInjectionThreshold: 1,
+        debugProtection: true,
+        selfDefending: true,
+        splitStrings: true,
+        splitStringsChunkLength: 5,
+        stringArray: true,
+        rotateStringArray: true,
+        stringArrayEncoding: StringArrayEncoding.Rc4,
+        transformObjectKeys: true,
+        unicodeEscapeSequence: true
+    };
+
     this.timeout(100000);
 
-    let obfuscatedCode: string;
-
-    before(() => {
-        const code: string = readFileAsString(__dirname + '/fixtures/sha256.js');
-
-        obfuscatedCode = JavaScriptObfuscator.obfuscate(
-            code,
-            {
-                controlFlowFlattening: true,
-                deadCodeInjection: true,
-                debugProtection: true,
-                selfDefending: true,
-                stringArrayEncoding: StringArrayEncoding.Rc4,
-                transformObjectKeys: true
-            }
-        ).getObfuscatedCode();
+    describe('Astring', () => {
+        let obfuscatedCode: string;
+
+        before(() => {
+            const code: string = readFileAsString(__dirname + '/fixtures/astring.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                options
+            ).getObfuscatedCode();
+        });
+
+        it('should obfuscate code without any runtime errors after obfuscation: Variant #1 astring', () => {
+            assert.equal(
+                eval(`
+                    ${obfuscatedCode}
+                    const code = generate({
+                        "type": "Program",
+                        "body": [
+                            {
+                                "type": "FunctionDeclaration",
+                                "id": {
+                                    "type": "Identifier",
+                                    "name": "test",
+                                    "range": [
+                                        9,
+                                        13
+                                    ]
+                                },
+                                "params": [],
+                                "body": {
+                                    "type": "BlockStatement",
+                                    "body": [
+                                        {
+                                            "type": "ReturnStatement",
+                                            "argument": {
+                                                "type": "Literal",
+                                                "value": "foo",
+                                                "raw": "'foo'",
+                                                "range": [
+                                                    30,
+                                                    35
+                                                ]
+                                            },
+                                            "range": [
+                                                23,
+                                                36
+                                            ]
+                                        }
+                                    ],
+                                    "range": [
+                                        17,
+                                        38
+                                    ]
+                                },
+                                "generator": false,
+                                "expression": false,
+                                "async": false,
+                                "range": [
+                                    0,
+                                    38
+                                ]
+                            }
+                        ],
+                        "sourceType": "module",
+                        "range": [
+                            0,
+                            38
+                        ],
+                        "comments": []
+                    });
+                    
+                    eval(\`\${code} test();\`);
+                 `),
+                'foo'
+            );
+        });
     });
 
-    it('should obfuscate code without any runtime errors after obfuscation: Variant #1 sha256', () => {
-        assert.equal(
-            eval(`${obfuscatedCode} sha256('test');`),
-            '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
-        );
+    describe('Sha256', () => {
+        let obfuscatedCode: string;
+
+        before(() => {
+            const code: string = readFileAsString(__dirname + '/fixtures/sha256.js');
+
+            obfuscatedCode = JavaScriptObfuscator.obfuscate(
+                code,
+                options
+            ).getObfuscatedCode();
+        });
+
+        it('should obfuscate code without any runtime errors after obfuscation: Variant #2 sha256', () => {
+            assert.equal(
+                eval(`${obfuscatedCode} sha256('test');`),
+                '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
+            );
+        });
     });
-});
+});

+ 1060 - 0
test/runtime-tests/fixtures/astring.js

@@ -0,0 +1,1060 @@
+// Astring is a tiny and fast JavaScript code generator from an ESTree-compliant AST.
+//
+// Astring was written by David Bonnet and released under an MIT license.
+//
+// The Git repository for Astring is available at:
+// https://github.com/davidbonnet/astring.git
+//
+// Please use the GitHub bug tracker to report issues:
+// https://github.com/davidbonnet/astring/issues
+
+const { stringify } = JSON
+
+/* istanbul ignore if */
+if (!String.prototype.repeat) {
+    /* istanbul ignore next */
+    throw new Error(
+        'String.prototype.repeat is undefined, see https://github.com/davidbonnet/astring#installation',
+    )
+}
+
+/* istanbul ignore if */
+if (!String.prototype.endsWith) {
+    /* istanbul ignore next */
+    throw new Error(
+        'String.prototype.endsWith is undefined, see https://github.com/davidbonnet/astring#installation',
+    )
+}
+
+const OPERATOR_PRECEDENCE = {
+    '||': 3,
+    '&&': 4,
+    '|': 5,
+    '^': 6,
+    '&': 7,
+    '==': 8,
+    '!=': 8,
+    '===': 8,
+    '!==': 8,
+    '<': 9,
+    '>': 9,
+    '<=': 9,
+    '>=': 9,
+    in: 9,
+    instanceof: 9,
+    '<<': 10,
+    '>>': 10,
+    '>>>': 10,
+    '+': 11,
+    '-': 11,
+    '*': 12,
+    '%': 12,
+    '/': 12,
+    '**': 13,
+}
+
+// Enables parenthesis regardless of precedence
+const NEEDS_PARENTHESES = 17
+
+const EXPRESSIONS_PRECEDENCE = {
+    // Definitions
+    ArrayExpression: 20,
+    TaggedTemplateExpression: 20,
+    ThisExpression: 20,
+    Identifier: 20,
+    Literal: 18,
+    TemplateLiteral: 20,
+    Super: 20,
+    SequenceExpression: 20,
+    // Operations
+    MemberExpression: 19,
+    CallExpression: 19,
+    NewExpression: 19,
+    // Other definitions
+    ArrowFunctionExpression: NEEDS_PARENTHESES,
+    ClassExpression: NEEDS_PARENTHESES,
+    FunctionExpression: NEEDS_PARENTHESES,
+    ObjectExpression: NEEDS_PARENTHESES,
+    // Other operations
+    UpdateExpression: 16,
+    UnaryExpression: 15,
+    BinaryExpression: 14,
+    LogicalExpression: 13,
+    ConditionalExpression: 4,
+    AssignmentExpression: 3,
+    AwaitExpression: 2,
+    YieldExpression: 2,
+    RestElement: 1,
+}
+
+function formatSequence(state, nodes) {
+    /*
+    Writes into `state` a sequence of `nodes`.
+    */
+    const { generator } = state
+    state.write('(')
+    if (nodes != null && nodes.length > 0) {
+        generator[nodes[0].type](nodes[0], state)
+        const { length } = nodes
+        for (let i = 1; i < length; i++) {
+            const param = nodes[i]
+            state.write(', ')
+            generator[param.type](param, state)
+        }
+    }
+    state.write(')')
+}
+
+function expressionNeedsParenthesis(node, parentNode, isRightHand) {
+    const nodePrecedence = EXPRESSIONS_PRECEDENCE[node.type]
+    if (nodePrecedence === NEEDS_PARENTHESES) {
+        return true
+    }
+    const parentNodePrecedence = EXPRESSIONS_PRECEDENCE[parentNode.type]
+    if (nodePrecedence !== parentNodePrecedence) {
+        // Different node types
+        return (
+            (!isRightHand &&
+                nodePrecedence === 15 &&
+                parentNodePrecedence === 14 &&
+                parentNode.operator === '**') ||
+            nodePrecedence < parentNodePrecedence
+        )
+    }
+    if (nodePrecedence !== 13 && nodePrecedence !== 14) {
+        // Not a `LogicalExpression` or `BinaryExpression`
+        return false
+    }
+    if (node.operator === '**' && parentNode.operator === '**') {
+        // Exponentiation operator has right-to-left associativity
+        return !isRightHand
+    }
+    if (isRightHand) {
+        // Parenthesis are used if both operators have the same precedence
+        return (
+            OPERATOR_PRECEDENCE[node.operator] <=
+            OPERATOR_PRECEDENCE[parentNode.operator]
+        )
+    }
+    return (
+        OPERATOR_PRECEDENCE[node.operator] <
+        OPERATOR_PRECEDENCE[parentNode.operator]
+    )
+}
+
+function formatBinaryExpressionPart(state, node, parentNode, isRightHand) {
+    /*
+    Writes into `state` a left-hand or right-hand expression `node`
+    from a binary expression applying the provided `operator`.
+    The `isRightHand` parameter should be `true` if the `node` is a right-hand argument.
+    */
+    const { generator } = state
+    if (expressionNeedsParenthesis(node, parentNode, isRightHand)) {
+        state.write('(')
+        generator[node.type](node, state)
+        state.write(')')
+    } else {
+        generator[node.type](node, state)
+    }
+}
+
+function reindent(state, text, indent, lineEnd) {
+    /*
+    Writes into `state` the `text` string reindented with the provided `indent`.
+    */
+    const lines = text.split('\n')
+    const end = lines.length - 1
+    state.write(lines[0].trim())
+    if (end > 0) {
+        state.write(lineEnd)
+        for (let i = 1; i < end; i++) {
+            state.write(indent + lines[i].trim() + lineEnd)
+        }
+        state.write(indent + lines[end].trim())
+    }
+}
+
+function formatComments(state, comments, indent, lineEnd) {
+    /*
+    Writes into `state` the provided list of `comments`, with the given `indent` and `lineEnd` strings.
+    Line comments will end with `"\n"` regardless of the value of `lineEnd`.
+    Expects to start on a new unindented line.
+    */
+    const { length } = comments
+    for (let i = 0; i < length; i++) {
+        const comment = comments[i]
+        state.write(indent)
+        if (comment.type[0] === 'L') {
+            // Line comment
+            state.write('// ' + comment.value.trim() + '\n')
+        } else {
+            // Block comment
+            state.write('/*')
+            reindent(state, comment.value, indent, lineEnd)
+            state.write('*/' + lineEnd)
+        }
+    }
+}
+
+function hasCallExpression(node) {
+    /*
+    Returns `true` if the provided `node` contains a call expression and `false` otherwise.
+    */
+    let currentNode = node
+    while (currentNode != null) {
+        const { type } = currentNode
+        if (type[0] === 'C' && type[1] === 'a') {
+            // Is CallExpression
+            return true
+        } else if (type[0] === 'M' && type[1] === 'e' && type[2] === 'm') {
+            // Is MemberExpression
+            currentNode = currentNode.object
+        } else {
+            return false
+        }
+    }
+}
+
+function formatVariableDeclaration(state, node) {
+    /*
+    Writes into `state` a variable declaration.
+    */
+    const { generator } = state
+    const { declarations } = node
+    state.write(node.kind + ' ')
+    const { length } = declarations
+    if (length > 0) {
+        generator.VariableDeclarator(declarations[0], state)
+        for (let i = 1; i < length; i++) {
+            state.write(', ')
+            generator.VariableDeclarator(declarations[i], state)
+        }
+    }
+}
+
+let ForInStatement,
+    FunctionDeclaration,
+    RestElement,
+    BinaryExpression,
+    ArrayExpression,
+    BlockStatement
+
+const baseGenerator = {
+    Program(node, state) {
+        const indent = state.indent.repeat(state.indentLevel)
+        const { lineEnd, writeComments } = state
+        if (writeComments && node.comments != null) {
+            formatComments(state, node.comments, indent, lineEnd)
+        }
+        const statements = node.body
+        const { length } = statements
+        for (let i = 0; i < length; i++) {
+            const statement = statements[i]
+            if (writeComments && statement.comments != null) {
+                formatComments(state, statement.comments, indent, lineEnd)
+            }
+            state.write(indent)
+            this[statement.type](statement, state)
+            state.write(lineEnd)
+        }
+        if (writeComments && node.trailingComments != null) {
+            formatComments(state, node.trailingComments, indent, lineEnd)
+        }
+    },
+    BlockStatement: (BlockStatement = function(node, state) {
+        const indent = state.indent.repeat(state.indentLevel++)
+        const { lineEnd, writeComments } = state
+        const statementIndent = indent + state.indent
+        state.write('{')
+        const statements = node.body
+        if (statements != null && statements.length > 0) {
+            state.write(lineEnd)
+            if (writeComments && node.comments != null) {
+                formatComments(state, node.comments, statementIndent, lineEnd)
+            }
+            const { length } = statements
+            for (let i = 0; i < length; i++) {
+                const statement = statements[i]
+                if (writeComments && statement.comments != null) {
+                    formatComments(state, statement.comments, statementIndent, lineEnd)
+                }
+                state.write(statementIndent)
+                this[statement.type](statement, state)
+                state.write(lineEnd)
+            }
+            state.write(indent)
+        } else {
+            if (writeComments && node.comments != null) {
+                state.write(lineEnd)
+                formatComments(state, node.comments, statementIndent, lineEnd)
+                state.write(indent)
+            }
+        }
+        if (writeComments && node.trailingComments != null) {
+            formatComments(state, node.trailingComments, statementIndent, lineEnd)
+        }
+        state.write('}')
+        state.indentLevel--
+    }),
+    ClassBody: BlockStatement,
+    EmptyStatement(node, state) {
+        state.write(';')
+    },
+    ExpressionStatement(node, state) {
+        const precedence = EXPRESSIONS_PRECEDENCE[node.expression.type]
+        if (
+            precedence === NEEDS_PARENTHESES ||
+            (precedence === 3 && node.expression.left.type[0] === 'O')
+        ) {
+            // Should always have parentheses or is an AssignmentExpression to an ObjectPattern
+            state.write('(')
+            this[node.expression.type](node.expression, state)
+            state.write(')')
+        } else {
+            this[node.expression.type](node.expression, state)
+        }
+        state.write(';')
+    },
+    IfStatement(node, state) {
+        state.write('if (')
+        this[node.test.type](node.test, state)
+        state.write(') ')
+        this[node.consequent.type](node.consequent, state)
+        if (node.alternate != null) {
+            state.write(' else ')
+            this[node.alternate.type](node.alternate, state)
+        }
+    },
+    LabeledStatement(node, state) {
+        this[node.label.type](node.label, state)
+        state.write(': ')
+        this[node.body.type](node.body, state)
+    },
+    BreakStatement(node, state) {
+        state.write('break')
+        if (node.label != null) {
+            state.write(' ')
+            this[node.label.type](node.label, state)
+        }
+        state.write(';')
+    },
+    ContinueStatement(node, state) {
+        state.write('continue')
+        if (node.label != null) {
+            state.write(' ')
+            this[node.label.type](node.label, state)
+        }
+        state.write(';')
+    },
+    WithStatement(node, state) {
+        state.write('with (')
+        this[node.object.type](node.object, state)
+        state.write(') ')
+        this[node.body.type](node.body, state)
+    },
+    SwitchStatement(node, state) {
+        const indent = state.indent.repeat(state.indentLevel++)
+        const { lineEnd, writeComments } = state
+        state.indentLevel++
+        const caseIndent = indent + state.indent
+        const statementIndent = caseIndent + state.indent
+        state.write('switch (')
+        this[node.discriminant.type](node.discriminant, state)
+        state.write(') {' + lineEnd)
+        const { cases: occurences } = node
+        const { length: occurencesCount } = occurences
+        for (let i = 0; i < occurencesCount; i++) {
+            const occurence = occurences[i]
+            if (writeComments && occurence.comments != null) {
+                formatComments(state, occurence.comments, caseIndent, lineEnd)
+            }
+            if (occurence.test) {
+                state.write(caseIndent + 'case ')
+                this[occurence.test.type](occurence.test, state)
+                state.write(':' + lineEnd)
+            } else {
+                state.write(caseIndent + 'default:' + lineEnd)
+            }
+            const { consequent } = occurence
+            const { length: consequentCount } = consequent
+            for (let i = 0; i < consequentCount; i++) {
+                const statement = consequent[i]
+                if (writeComments && statement.comments != null) {
+                    formatComments(state, statement.comments, statementIndent, lineEnd)
+                }
+                state.write(statementIndent)
+                this[statement.type](statement, state)
+                state.write(lineEnd)
+            }
+        }
+        state.indentLevel -= 2
+        state.write(indent + '}')
+    },
+    ReturnStatement(node, state) {
+        state.write('return')
+        if (node.argument) {
+            state.write(' ')
+            this[node.argument.type](node.argument, state)
+        }
+        state.write(';')
+    },
+    ThrowStatement(node, state) {
+        state.write('throw ')
+        this[node.argument.type](node.argument, state)
+        state.write(';')
+    },
+    TryStatement(node, state) {
+        state.write('try ')
+        this[node.block.type](node.block, state)
+        if (node.handler) {
+            const { handler } = node
+            if (handler.param == null) {
+                state.write(' catch ')
+            } else {
+                state.write(' catch (')
+                this[handler.param.type](handler.param, state)
+                state.write(') ')
+            }
+            this[handler.body.type](handler.body, state)
+        }
+        if (node.finalizer) {
+            state.write(' finally ')
+            this[node.finalizer.type](node.finalizer, state)
+        }
+    },
+    WhileStatement(node, state) {
+        state.write('while (')
+        this[node.test.type](node.test, state)
+        state.write(') ')
+        this[node.body.type](node.body, state)
+    },
+    DoWhileStatement(node, state) {
+        state.write('do ')
+        this[node.body.type](node.body, state)
+        state.write(' while (')
+        this[node.test.type](node.test, state)
+        state.write(');')
+    },
+    ForStatement(node, state) {
+        state.write('for (')
+        if (node.init != null) {
+            const { init } = node
+            if (init.type[0] === 'V') {
+                formatVariableDeclaration(state, init)
+            } else {
+                this[init.type](init, state)
+            }
+        }
+        state.write('; ')
+        if (node.test) {
+            this[node.test.type](node.test, state)
+        }
+        state.write('; ')
+        if (node.update) {
+            this[node.update.type](node.update, state)
+        }
+        state.write(') ')
+        this[node.body.type](node.body, state)
+    },
+    ForInStatement: (ForInStatement = function(node, state) {
+        state.write(`for ${node.await ? 'await ' : ''}(`)
+        const { left } = node
+        if (left.type[0] === 'V') {
+            formatVariableDeclaration(state, left)
+        } else {
+            this[left.type](left, state)
+        }
+        // Identifying whether node.type is `ForInStatement` or `ForOfStatement`
+        state.write(node.type[3] === 'I' ? ' in ' : ' of ')
+        this[node.right.type](node.right, state)
+        state.write(') ')
+        this[node.body.type](node.body, state)
+    }),
+    ForOfStatement: ForInStatement,
+    DebuggerStatement(node, state) {
+        state.write('debugger;' + state.lineEnd)
+    },
+    FunctionDeclaration: (FunctionDeclaration = function(node, state) {
+        state.write(
+            (node.async ? 'async ' : '') +
+            (node.generator ? 'function* ' : 'function ') +
+            (node.id ? node.id.name : ''),
+            node,
+        )
+        formatSequence(state, node.params)
+        state.write(' ')
+        this[node.body.type](node.body, state)
+    }),
+    FunctionExpression: FunctionDeclaration,
+    VariableDeclaration(node, state) {
+        formatVariableDeclaration(state, node)
+        state.write(';')
+    },
+    VariableDeclarator(node, state) {
+        this[node.id.type](node.id, state)
+        if (node.init != null) {
+            state.write(' = ')
+            this[node.init.type](node.init, state)
+        }
+    },
+    ClassDeclaration(node, state) {
+        state.write('class ' + (node.id ? `${node.id.name} ` : ''), node)
+        if (node.superClass) {
+            state.write('extends ')
+            this[node.superClass.type](node.superClass, state)
+            state.write(' ')
+        }
+        this.ClassBody(node.body, state)
+    },
+    ImportDeclaration(node, state) {
+        state.write('import ')
+        const { specifiers } = node
+        const { length } = specifiers
+        // NOTE: Once babili is fixed, put this after condition
+        // https://github.com/babel/babili/issues/430
+        let i = 0
+        if (length > 0) {
+            for (; i < length; ) {
+                if (i > 0) {
+                    state.write(', ')
+                }
+                const specifier = specifiers[i]
+                const type = specifier.type[6]
+                if (type === 'D') {
+                    // ImportDefaultSpecifier
+                    state.write(specifier.local.name, specifier)
+                    i++
+                } else if (type === 'N') {
+                    // ImportNamespaceSpecifier
+                    state.write('* as ' + specifier.local.name, specifier)
+                    i++
+                } else {
+                    // ImportSpecifier
+                    break
+                }
+            }
+            if (i < length) {
+                state.write('{')
+                for (;;) {
+                    const specifier = specifiers[i]
+                    const { name } = specifier.imported
+                    state.write(name, specifier)
+                    if (name !== specifier.local.name) {
+                        state.write(' as ' + specifier.local.name)
+                    }
+                    if (++i < length) {
+                        state.write(', ')
+                    } else {
+                        break
+                    }
+                }
+                state.write('}')
+            }
+            state.write(' from ')
+        }
+        this.Literal(node.source, state)
+        state.write(';')
+    },
+    ExportDefaultDeclaration(node, state) {
+        state.write('export default ')
+        this[node.declaration.type](node.declaration, state)
+        if (
+            EXPRESSIONS_PRECEDENCE[node.declaration.type] &&
+            node.declaration.type[0] !== 'F'
+        ) {
+            // All expression nodes except `FunctionExpression`
+            state.write(';')
+        }
+    },
+    ExportNamedDeclaration(node, state) {
+        state.write('export ')
+        if (node.declaration) {
+            this[node.declaration.type](node.declaration, state)
+        } else {
+            state.write('{')
+            const { specifiers } = node,
+                { length } = specifiers
+            if (length > 0) {
+                for (let i = 0; ; ) {
+                    const specifier = specifiers[i]
+                    const { name } = specifier.local
+                    state.write(name, specifier)
+                    if (name !== specifier.exported.name) {
+                        state.write(' as ' + specifier.exported.name)
+                    }
+                    if (++i < length) {
+                        state.write(', ')
+                    } else {
+                        break
+                    }
+                }
+            }
+            state.write('}')
+            if (node.source) {
+                state.write(' from ')
+                this.Literal(node.source, state)
+            }
+            state.write(';')
+        }
+    },
+    ExportAllDeclaration(node, state) {
+        state.write('export * from ')
+        this.Literal(node.source, state)
+        state.write(';')
+    },
+    MethodDefinition(node, state) {
+        if (node.static) {
+            state.write('static ')
+        }
+        const kind = node.kind[0]
+        if (kind === 'g' || kind === 's') {
+            // Getter or setter
+            state.write(node.kind + ' ')
+        }
+        if (node.value.async) {
+            state.write('async ')
+        }
+        if (node.value.generator) {
+            state.write('*')
+        }
+        if (node.computed) {
+            state.write('[')
+            this[node.key.type](node.key, state)
+            state.write(']')
+        } else {
+            this[node.key.type](node.key, state)
+        }
+        formatSequence(state, node.value.params)
+        state.write(' ')
+        this[node.value.body.type](node.value.body, state)
+    },
+    ClassExpression(node, state) {
+        this.ClassDeclaration(node, state)
+    },
+    ArrowFunctionExpression(node, state) {
+        state.write(node.async ? 'async ' : '', node)
+        const { params } = node
+        if (params != null) {
+            // Omit parenthesis if only one named parameter
+            if (params.length === 1 && params[0].type[0] === 'I') {
+                // If params[0].type[0] starts with 'I', it can't be `ImportDeclaration` nor `IfStatement` and thus is `Identifier`
+                state.write(params[0].name, params[0])
+            } else {
+                formatSequence(state, node.params)
+            }
+        }
+        state.write(' => ')
+        if (node.body.type[0] === 'O') {
+            // Body is an object expression
+            state.write('(')
+            this.ObjectExpression(node.body, state)
+            state.write(')')
+        } else {
+            this[node.body.type](node.body, state)
+        }
+    },
+    ThisExpression(node, state) {
+        state.write('this', node)
+    },
+    Super(node, state) {
+        state.write('super', node)
+    },
+    RestElement: (RestElement = function(node, state) {
+        state.write('...')
+        this[node.argument.type](node.argument, state)
+    }),
+    SpreadElement: RestElement,
+    YieldExpression(node, state) {
+        state.write(node.delegate ? 'yield*' : 'yield')
+        if (node.argument) {
+            state.write(' ')
+            this[node.argument.type](node.argument, state)
+        }
+    },
+    AwaitExpression(node, state) {
+        state.write('await ')
+        if (node.argument) {
+            this[node.argument.type](node.argument, state)
+        }
+    },
+    TemplateLiteral(node, state) {
+        const { quasis, expressions } = node
+        state.write('`')
+        const { length } = expressions
+        for (let i = 0; i < length; i++) {
+            const expression = expressions[i]
+            this.TemplateElement(quasis[i], state)
+            state.write('${')
+            this[expression.type](expression, state)
+            state.write('}')
+        }
+        state.write(quasis[quasis.length - 1].value.raw)
+        state.write('`')
+    },
+    TemplateElement(node, state) {
+        state.write(node.value.raw)
+    },
+    TaggedTemplateExpression(node, state) {
+        this[node.tag.type](node.tag, state)
+        this[node.quasi.type](node.quasi, state)
+    },
+    ArrayExpression: (ArrayExpression = function(node, state) {
+        state.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) {
+                    state.write(', ')
+                } else {
+                    if (element == null) {
+                        state.write(', ')
+                    }
+                    break
+                }
+            }
+        }
+        state.write(']')
+    }),
+    ArrayPattern: ArrayExpression,
+    ObjectExpression(node, state) {
+        const indent = state.indent.repeat(state.indentLevel++)
+        const { lineEnd, writeComments } = state
+        const propertyIndent = indent + state.indent
+        state.write('{')
+        if (node.properties.length > 0) {
+            state.write(lineEnd)
+            if (writeComments && node.comments != null) {
+                formatComments(state, node.comments, propertyIndent, lineEnd)
+            }
+            const comma = ',' + lineEnd
+            const { properties } = node,
+                { length } = properties
+            for (let i = 0; ; ) {
+                const property = properties[i]
+                if (writeComments && property.comments != null) {
+                    formatComments(state, property.comments, propertyIndent, lineEnd)
+                }
+                state.write(propertyIndent)
+                this[property.type](property, state)
+                if (++i < length) {
+                    state.write(comma)
+                } else {
+                    break
+                }
+            }
+            state.write(lineEnd)
+            if (writeComments && node.trailingComments != null) {
+                formatComments(state, node.trailingComments, propertyIndent, lineEnd)
+            }
+            state.write(indent + '}')
+        } else if (writeComments) {
+            if (node.comments != null) {
+                state.write(lineEnd)
+                formatComments(state, node.comments, propertyIndent, lineEnd)
+                if (node.trailingComments != null) {
+                    formatComments(state, node.trailingComments, propertyIndent, lineEnd)
+                }
+                state.write(indent + '}')
+            } else if (node.trailingComments != null) {
+                state.write(lineEnd)
+                formatComments(state, node.trailingComments, propertyIndent, lineEnd)
+                state.write(indent + '}')
+            } else {
+                state.write('}')
+            }
+        } else {
+            state.write('}')
+        }
+        state.indentLevel--
+    },
+    Property(node, state) {
+        if (node.method || node.kind[0] !== 'i') {
+            // Either a method or of kind `set` or `get` (not `init`)
+            this.MethodDefinition(node, state)
+        } else {
+            if (!node.shorthand) {
+                if (node.computed) {
+                    state.write('[')
+                    this[node.key.type](node.key, state)
+                    state.write(']')
+                } else {
+                    this[node.key.type](node.key, state)
+                }
+                state.write(': ')
+            }
+            this[node.value.type](node.value, state)
+        }
+    },
+    ObjectPattern(node, state) {
+        state.write('{')
+        if (node.properties.length > 0) {
+            const { properties } = node,
+                { length } = properties
+            for (let i = 0; ; ) {
+                this[properties[i].type](properties[i], state)
+                if (++i < length) {
+                    state.write(', ')
+                } else {
+                    break
+                }
+            }
+        }
+        state.write('}')
+    },
+    SequenceExpression(node, state) {
+        formatSequence(state, node.expressions)
+    },
+    UnaryExpression(node, state) {
+        if (node.prefix) {
+            state.write(node.operator)
+            if (node.operator.length > 1) {
+                state.write(' ')
+            }
+            if (
+                EXPRESSIONS_PRECEDENCE[node.argument.type] <
+                EXPRESSIONS_PRECEDENCE.UnaryExpression
+            ) {
+                state.write('(')
+                this[node.argument.type](node.argument, state)
+                state.write(')')
+            } else {
+                this[node.argument.type](node.argument, state)
+            }
+        } else {
+            // FIXME: This case never occurs
+            this[node.argument.type](node.argument, state)
+            state.write(node.operator)
+        }
+    },
+    UpdateExpression(node, state) {
+        // Always applied to identifiers or members, no parenthesis check needed
+        if (node.prefix) {
+            state.write(node.operator)
+            this[node.argument.type](node.argument, state)
+        } else {
+            this[node.argument.type](node.argument, state)
+            state.write(node.operator)
+        }
+    },
+    AssignmentExpression(node, state) {
+        this[node.left.type](node.left, state)
+        state.write(' ' + node.operator + ' ')
+        this[node.right.type](node.right, state)
+    },
+    AssignmentPattern(node, state) {
+        this[node.left.type](node.left, state)
+        state.write(' = ')
+        this[node.right.type](node.right, state)
+    },
+    BinaryExpression: (BinaryExpression = function(node, state) {
+        const isIn = node.operator === 'in'
+        if (isIn) {
+            // Avoids confusion in `for` loops initializers
+            state.write('(')
+        }
+        formatBinaryExpressionPart(state, node.left, node, false)
+        state.write(' ' + node.operator + ' ')
+        formatBinaryExpressionPart(state, node.right, node, true)
+        if (isIn) {
+            state.write(')')
+        }
+    }),
+    LogicalExpression: BinaryExpression,
+    ConditionalExpression(node, state) {
+        if (
+            EXPRESSIONS_PRECEDENCE[node.test.type] >
+            EXPRESSIONS_PRECEDENCE.ConditionalExpression
+        ) {
+            this[node.test.type](node.test, state)
+        } else {
+            state.write('(')
+            this[node.test.type](node.test, state)
+            state.write(')')
+        }
+        state.write(' ? ')
+        this[node.consequent.type](node.consequent, state)
+        state.write(' : ')
+        this[node.alternate.type](node.alternate, state)
+    },
+    NewExpression(node, state) {
+        state.write('new ')
+        if (
+            EXPRESSIONS_PRECEDENCE[node.callee.type] <
+            EXPRESSIONS_PRECEDENCE.CallExpression ||
+            hasCallExpression(node.callee)
+        ) {
+            state.write('(')
+            this[node.callee.type](node.callee, state)
+            state.write(')')
+        } else {
+            this[node.callee.type](node.callee, state)
+        }
+        formatSequence(state, node['arguments'])
+    },
+    CallExpression(node, state) {
+        if (
+            EXPRESSIONS_PRECEDENCE[node.callee.type] <
+            EXPRESSIONS_PRECEDENCE.CallExpression
+        ) {
+            state.write('(')
+            this[node.callee.type](node.callee, state)
+            state.write(')')
+        } else {
+            this[node.callee.type](node.callee, state)
+        }
+        formatSequence(state, node['arguments'])
+    },
+    MemberExpression(node, state) {
+        if (
+            EXPRESSIONS_PRECEDENCE[node.object.type] <
+            EXPRESSIONS_PRECEDENCE.MemberExpression
+        ) {
+            state.write('(')
+            this[node.object.type](node.object, state)
+            state.write(')')
+        } else {
+            this[node.object.type](node.object, state)
+        }
+        if (node.computed) {
+            state.write('[')
+            this[node.property.type](node.property, state)
+            state.write(']')
+        } else {
+            state.write('.')
+            this[node.property.type](node.property, state)
+        }
+    },
+    MetaProperty(node, state) {
+        state.write(node.meta.name + '.' + node.property.name, node)
+    },
+    Identifier(node, state) {
+        state.write(node.name, node)
+    },
+    Literal(node, state) {
+        if (node.raw != null) {
+            state.write(node.raw, node)
+        } else if (node.regex != null) {
+            this.RegExpLiteral(node, state)
+        } else {
+            state.write(stringify(node.value), node)
+        }
+    },
+    RegExpLiteral(node, state) {
+        const { regex } = node
+        state.write(`/${regex.pattern}/${regex.flags}`, node)
+    },
+}
+
+const EMPTY_OBJECT = {}
+
+class State {
+    constructor(options) {
+        const setup = options == null ? EMPTY_OBJECT : options
+        this.output = ''
+        // Functional options
+        if (setup.output != null) {
+            this.output = setup.output
+            this.write = this.writeToStream
+        } else {
+            this.output = ''
+        }
+        this.generator = setup.generator != null ? setup.generator : baseGenerator
+        // Formating setup
+        this.indent = setup.indent != null ? setup.indent : '  '
+        this.lineEnd = setup.lineEnd != null ? setup.lineEnd : '\n'
+        this.indentLevel =
+            setup.startingIndentLevel != null ? setup.startingIndentLevel : 0
+        this.writeComments = setup.comments ? setup.comments : false
+        // Source map
+        if (setup.sourceMap != null) {
+            this.write =
+                setup.output == null ? this.writeAndMap : this.writeToStreamAndMap
+            this.sourceMap = setup.sourceMap
+            this.line = 1
+            this.column = 0
+            this.lineEndSize = this.lineEnd.split('\n').length - 1
+            this.mapping = {
+                original: null,
+                generated: this,
+                name: undefined,
+                source: setup.sourceMap.file || setup.sourceMap._file,
+            }
+        }
+    }
+
+    write(code) {
+        this.output += code
+    }
+
+    writeToStream(code) {
+        this.output.write(code)
+    }
+
+    writeAndMap(code, node) {
+        this.output += code
+        this.map(code, node)
+    }
+
+    writeToStreamAndMap(code, node) {
+        this.output.write(code)
+        this.map(code, node)
+    }
+
+    map(code, node) {
+        if (node != null && node.loc != null) {
+            const { mapping } = this
+            mapping.original = node.loc.start
+            mapping.name = node.name
+            this.sourceMap.addMapping(mapping)
+        }
+        if (code.length > 0) {
+            if (this.lineEndSize > 0) {
+                if (code.endsWith(this.lineEnd)) {
+                    this.line += this.lineEndSize
+                    this.column = 0
+                } else if (code[code.length - 1] === '\n') {
+                    // Case of inline comment
+                    this.line++
+                    this.column = 0
+                } else {
+                    this.column += code.length
+                }
+            } else {
+                if (code[code.length - 1] === '\n') {
+                    // Case of inline comment
+                    this.line++
+                    this.column = 0
+                } else {
+                    this.column += code.length
+                }
+            }
+        }
+    }
+
+    toString() {
+        return this.output
+    }
+}
+
+function generate(node, options) {
+    /*
+    Returns a string representing the rendered code of the provided AST `node`.
+    The `options` are:
+
+    - `indent`: string to use for indentation (defaults to `␣␣`)
+    - `lineEnd`: string to use for line endings (defaults to `\n`)
+    - `startingIndentLevel`: indent level to start from (defaults to `0`)
+    - `comments`: generate comments if `true` (defaults to `false`)
+    - `output`: output stream to write the rendered code to (defaults to `null`)
+    - `generator`: custom code generator (defaults to `baseGenerator`)
+    */
+    const state = new State(options)
+    // Travel through the AST node and generate the code
+    state.generator[node.type](node, state)
+    return state.output
+}