diff --git a/test/runtime-tests/JavaScriptObfuscatorRuntime.spec.ts b/test/runtime-tests/JavaScriptObfuscatorRuntime.spec.ts index 497e9621..71971972 100644 --- a/test/runtime-tests/JavaScriptObfuscatorRuntime.spec.ts +++ b/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; + describe('Astring', () => { + let obfuscatedCode: string; - before(() => { - const code: string = readFileAsString(__dirname + '/fixtures/sha256.js'); + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/astring.js'); - obfuscatedCode = JavaScriptObfuscator.obfuscate( - code, - { - controlFlowFlattening: true, - deadCodeInjection: true, - debugProtection: true, - selfDefending: true, - stringArrayEncoding: StringArrayEncoding.Rc4, - transformObjectKeys: true - } - ).getObfuscatedCode(); + 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' + ); + }); }); -}); +}); \ No newline at end of file diff --git a/test/runtime-tests/fixtures/astring.js b/test/runtime-tests/fixtures/astring.js new file mode 100644 index 00000000..6db0741b --- /dev/null +++ b/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 +} \ No newline at end of file