UNPKG

js2coffee

Version:

JavaScript to CoffeeScript compiler

503 lines (401 loc) 12.4 kB
{ prependAll quote newline space delimit commaDelimit toIndent joinLines } = require('../helpers') TransformerBase = require('../transforms/base') BuilderBase = require('./base') ###* # Builder : new Builder(ast, [options]) # Generates output based on a JavaScript AST. # # s = new Builder(ast, { filename: 'input.js', source: '...' }) # s.get() # => { code: '...', map: { ... } } # # The params `options` and `source` are optional. The source code is used to # generate meaningful errors. ### module.exports = class Builder extends BuilderBase constructor: (ast, options={}) -> super @_indent = 0 ###* # visitors: # The visitors of each node. ### Program: (node) -> @comments = node.comments @BlockStatement(node) ExpressionStatement: (node) -> newline @walk(node.expression) AssignmentExpression: (node) -> re = @paren space [ @walk(node.left) node.operator @walk(node.right) ] # Space out 'a = ->' if node.right.type is 'FunctionExpression' re = [ "\n", @indent(), re, "\n" ] re Identifier: (node) -> [ node.name ] UnaryExpression: (node) -> isNestedUnary = -> node.argument.type is 'UnaryExpression' isWord = -> (/^[a-z]+$/i).test(node.operator) if isNestedUnary() or isWord() @paren [ node.operator, ' ', @walk(node.argument) ] else @paren [ node.operator, @walk(node.argument) ] # Operator (+) BinaryExpression: (node) -> operator = node.operator operator = 'of' if operator is 'in' @paren space [ @walk(node.left), operator, @walk(node.right) ] Literal: (node) -> if typeof node.value is 'string' [ quote(node.value) ] else [ node.raw ] MemberExpression: (node) -> right = if node.computed [ '[', @walk(node.property), ']' ] else if node._prefixed [ @walk(node.property) ] else [ '.', @walk(node.property) ] @paren [ @walk(node.object), right ] LogicalExpression: (node) -> opers = '||': 'or' '&&': 'and' oper = opers[node.operator] @paren [ @walk(node.left), ' ', oper, ' ', @walk(node.right) ] ThisExpression: (node) -> if node._prefix [ "@" ] else [ "this" ] CallExpression: (node, ctx) -> callee = @walk(node.callee) list = @makeSequence(node.arguments) node._isStatement = ctx.parent.type is 'ExpressionStatement' hasArgs = list.length > 0 if node._isStatement and hasArgs space [ callee, list ] else [ callee, @paren(list, true) ] IfStatement: (node) -> alt = node.alternate if alt?.type is 'IfStatement' els = @indent [ "else ", @walk(node.alternate, 'IfStatement') ] else if alt?.type is 'BlockStatement' els = @indent (i) => [ i, "else", "\n", @walk(node.alternate) ] else if alt? els = @indent (i) => [ i, "else", "\n", @indent(@walk(node.alternate)) ] else els = [] @indent (i) => test = @walk(node.test) consequent = @walk(node.consequent) if node.consequent.type isnt 'BlockStatement' consequent = @indent(consequent) word = if node._negative then 'unless' else 'if' [ word, ' ', test, "\n", consequent, els ] BlockStatement: (node) -> @makeStatements(node, node.body) makeStatements: (node, body) -> walked = body.map(@walk) ret = [] for item, i in walked if body[i].type isnt "BlockStatement" ret.push @indent() ret.push item ret LineComment: (node) -> [ "#", node.value, "\n" ] BlockComment: (node) -> lines = ('###' + node.value + '###').split("\n") output = [ delimit(lines, [ "\n", @indent() ]), "\n" ] [ "\n", @indent(), output, "\n" ] ReturnStatement: (node) -> if node.argument space [ "return", [ @walk(node.argument), "\n" ] ] else [ "return", "\n" ] ArrayExpression: (node) -> items = node.elements.length isSingleLine = items is 1 if items is 0 [ "[]" ] else if isSingleLine space [ "[", node.elements.map(@walk), "]" ] else @indent (indent) => elements = node.elements.map (e) => newline @walk(e) contents = prependAll(elements, @indent()) [ "[", "\n", contents, indent, "]" ] ObjectExpression: (node, ctx) -> props = node.properties.length isBraced = node._braced # Empty if props is 0 [ "{}" ] # Single prop ({ a: 2 }) else if props is 1 props = node.properties.map(@walk) if isBraced @paren space [ "{", props, "}" ] else @paren [ props ] # Last expression in scope (`function() { ({a:2}); }`) else if node._last props = node.properties.map(@walk) return delimit(props, [ "\n", @indent() ]) # Multiple props ({ a: 2, b: 3 }) else props = @indent => props = node.properties.map(@walk) [ "\n", joinLines(props, @indent()) ] if isBraced @paren [ "{", props, "\n", @indent(), "}" ] else @paren [ props ] Property: (node) -> if node.kind isnt 'init' throw new Error("Property: not sure about kind " + node.kind) space [ [@walk(node.key), ":"], @walk(node.value) ] # TODO: convert VariableDeclaration into AssignmentExpression VariableDeclaration: (node) -> declarators = node.declarations.map(@walk) delimit(declarators, @indent()) VariableDeclarator: (node) -> re = [ @walk(node.id), ' = ', newline(@walk(node.init)) ] # Space out 'a = ->' if node.init.type is 'FunctionExpression' re = [ "\n", @indent(), re, "\n" ] re FunctionExpression: (node, ctx) -> params = @makeParams(node.params, node.defaults) expr = @indent (i) => [ params, "->", "\n", @walk(node.body) ] if node._parenthesized [ "(", expr, @indent(), ")" ] else expr EmptyStatement: (node) -> [ ] SequenceExpression: (node) -> exprs = node.expressions.map (expr) => [ @walk(expr), "\n" ] delimit(exprs, @indent()) NewExpression: (node) -> callee = if node.callee?.type is 'Identifier' [ @walk(node.callee) ] else [ '(', @walk(node.callee), ')' ] args = if node.arguments?.length [ '(', @makeSequence(node.arguments), ')' ] else [] @paren [ "new ", callee, args ] WhileStatement: (node) -> [ "while ", @walk(node.test), "\n", @makeLoopBody(node.body) ] CoffeeLoopStatement: (node) -> [ "loop", "\n", @makeLoopBody(node.body) ] BreakStatement: (node) -> [ "break", "\n" ] ContinueStatement: (node) -> [ "continue", "\n" ] DebuggerStatement: (node) -> [ "debugger", "\n" ] TryStatement: (node) -> # block, handler, finalizer _try = @indent => [ "try", "\n", @walk(node.block) ] _catch = @indent (indent) => [ indent, @walk(node.handler) ] _finally = if node.finalizer? @indent (indent) => [ indent, "finally", "\n", @walk(node.finalizer) ] else [] [ _try, _catch, _finally ] CatchClause: (node) -> [ "catch ", @walk(node.param), "\n", @walk(node.body) ] ThrowStatement: (node) -> [ "throw ", @walk(node.argument), "\n" ] # Ternary operator (`a ? b : c`) ConditionalExpression: (node) -> @paren space [ "if", @walk(node.test), "then", @walk(node.consequent), "else", @walk(node.alternate) ] # Increment (`a++`) UpdateExpression: (node) -> if node.prefix [ node.operator, @walk(node.argument) ] else [ @walk(node.argument), node.operator ] SwitchStatement: (node) -> body = @indent => @makeStatements(node, node.cases) item = @walk(node.discriminant) if node.discriminant.type is 'ConditionalExpression' item = [ "(", item, ")" ] [ "switch ", item, "\n", body ] # Custom node type for comma-separated expressions (`when a, b`) CoffeeListExpression: (node) -> @makeSequence(node.expressions) SwitchCase: (node) -> left = if node.test [ "when ", @walk(node.test) ] else [ "else" ] right = @indent => @makeStatements(node, node.consequent) [ left, "\n", right ] ForInStatement: (node) -> if node.left.type isnt 'VariableDeclaration' id = @walk(node.left) # TODO: move this transformation to the lib/transforms/ propagator = { type: 'ExpressionStatement' expression: { type: 'CoffeeEscapedExpression', raw: "#{id} = #{id}" } } node.body.body = [ propagator ].concat(node.body.body) else id = @walk(node.left.declarations[0].id) body = @makeLoopBody(node.body) [ "for ", id, " of ", @walk(node.right), "\n", body ] makeLoopBody: (body) -> isBlock = body?.type is 'BlockStatement' # TODO: move this transformation to the lib/transforms/ if not body or (isBlock and body.body.length is 0) @indent => [ @indent(), "continue", "\n" ] else if isBlock @indent => @walk(body) else @indent => [ @indent(), @walk(body) ] CoffeeEscapedExpression: (node) -> if node._parenthesized [ '(`', node.raw, '`)' ] else [ '`', node.raw, '`' ] CoffeePrototypeExpression: (node) -> if node.computed [ @walk(node.object), '::[', @walk(node.property), ']' ] else [ @walk(node.object), '::', @walk(node.property) ] CoffeeDoExpression: (node) -> space [ 'do', @walk(node.function) ] ###* # makeSequence(): # Builds a comma-separated sequence of nodes. # TODO: turn this into a transformation ### makeSequence: (list) -> for arg, i in list isLast = i is (list.length-1) if not isLast if arg.type is "FunctionExpression" arg._parenthesized = true else if arg.type is "ObjectExpression" arg._braced = true commaDelimit(list.map(@walk)) ###* # makeParams(): # Builds parameters for a function list. ### makeParams: (params, defaults) -> list = [] # Account for defaults ("function fn(a = b)") for param, i in params if defaults[i] def = @walk(defaults[i]) list.push [@walk(param), ' = ', def] else list.push @walk(param) if params.length [ '(', delimit(list, ', '), ') '] else [] ###* # In a call expression, ensure that non-last function arguments get # parenthesized (eg, `setTimeout (-> x), 500`). ### parenthesizeArguments: (node) -> for arg, i in node.arguments isLast = i is (node.arguments.length-1) if arg.type is "FunctionExpression" if not isLast arg._parenthesized = true ### # Utilities ### ###* # indent(): # Indentation utility with 3 different functions. # # - `@indent(-> ...)` - adds an indent level. # - `@indent([ ... ])` - adds indentation. # - `@indent()` - returns the current indent level as a string. # # When invoked with a function, the indentation level is increased by 1, and # the function is invoked. This is similar to escodegen's `withIndent`. # # @indent => # [ '...' ] # # The past indent level is passed to the function as the first argument. # # @indent (indent) => # [ indent, 'if', ... ] # # When invoked with an array, it will indent it. # # @indent [ 'if...' ] # #=> [ ' ', [ 'if...' ] ] # # When invoked without arguments, it returns the current indentation as a # string. # # @indent() ### indent: (fn) -> if typeof fn is "function" previous = @indent() @_indent += 1 result = fn(previous) @_indent -= 1 result else if fn [ @indent(), fn ] else tab = toIndent(@options.indent) Array(@_indent + 1).join(tab) ###* # paren(): # Parenthesizes if the node's parenthesize flag is on (or `parenthesized` is # true) ### paren: (output, parenthesized) -> parenthesized ?= @path[@path.length-1]?._parenthesized isBlock = output.toString().match /\n$/ if parenthesized if isBlock [ '(', output, @indent(), ')' ] else [ '(', output, ')' ] else output ###* # onUnknownNode(): # Invoked when the node is not known. Throw an error. ### onUnknownNode: (node, ctx) -> @syntaxError(node, "#{node.type} is not supported") syntaxError: TransformerBase::syntaxError