UNPKG

js2coffee

Version:

JavaScript to CoffeeScript compiler

300 lines (259 loc) 7.63 kB
{ buildError clone replace toIndent } = require('../helpers') ###** # TransformerBase: # Base class of all transformation steps, such as [FunctionTransforms] and # [OtherTransforms]. This is a thin wrapper around *estraverse* to make things # easier, as well as to add extra features like scope tracking and more. # # class MyTransform extends TransformerBase # Program: (node) -> # return { replacementNodeHere } # # FunctionDeclaration: (node) -> # ... # # ctx = {} # TransformerBase.run ast, options, [ MyTransform ], ctx # # # result: # ast # ctx.warnings # # Each visitor is a method with the node type as the name. # # class MyTransform extends TransformerBase # Literal: (node) -> # # do stuff with `node` # # return the replacement when done # # FunctionDeclaration: (node) -> # # do stuff with `node` too # # From within the handlers, you can call some utility functions: # # @skip() # @break() # @syntaxError(node, "'with' is not supported") # @warn(node, "warning goes here") # # You have access to these variables: # # ~ @depth: The depth of the current node # ~ @node: The current node. # ~ @controller: The estraverse instance # # It also keeps track of scope. For every function body it traverses into (eg: # FunctionExpression.body), you get a `@ctx` variable that is only available # from *within that scope* and the scopes below it. # # ~ @scope: the Node that is the current scope. This is usually a BlockStatement # or a Program. # ~ @ctx: Context variables for the scope. You can store anything here and it # will be remembered for the current scope and the scopes below it. # # It also has a few hooks that you can override: # # ~ onScopeEnter: when scopes are entered (via `pushScope()`) # ~ onScopeExit: when scopes are exited (via `popScope()`) # ~ onEnter: enter of a node # ~ onExit: exit of a node # ~ onBeforeEnter: before the enter of a node # ~ onBeforeExit: before the exit of a node ### module.exports = class TransformerBase ### # Run multiple transformations ### @run: (ast, options, classes, ctx) -> # Combine `classes` into one mega-class Xformer = class extends TransformerBase safeExtend Xformer, classes # Run that xform = new Xformer(ast, options) result = xform.run() # Collect warnings into `ctx`, then return ctx.warnings ?= [] ctx.warnings = ctx.warnings.concat(xform.warnings) result constructor: (@ast, @options) -> @scopes = [] @ctx = { vars: [] } @warnings = [] ###* # run(): # Runs estraverse on `@ast`, and invokes functions on enter and exit # depending on the node type. This is also in change of changing `@depth`, # `@node`, `@controller` (etc) every step of the way. # # new Transformer(ast).run() ### run: -> @recurse @ast ###* # recurse(): # Delegate function of `run()`. See [run()] for details. # # This is sometimes called on its own to recurse down a certain path which # will otherwise be skipped. ### recurse: (root) -> self = this @depth = 0 runner = (direction, node, parent) => @node = node @depth += if direction is 'Enter' then +1 else -1 fnName = if direction is 'Enter' \ then "#{node.type}" else "#{node.type}Exit" @["onBefore#{direction}"]?(node, parent) result = @[fnName]?(node, parent) @["on#{direction}"]?(node, parent) result @estraverse().replace root, enter: (node, parent) -> self.controller = this runner("Enter", node, parent) leave: (node, parent) -> runner("Exit", node, parent) root ###* # skip(): # Skips a certain node from being parsed. # # class MyTransform extends TransformerBase # Identifier: -> # @skip() ### skip: -> @controller?.skip() ###* # remove(): # Removes a node from the tree. # # class MyTransform extends TransformerBase # Identifier: -> # @remove() ### # remove: -> @controller?.remove() ###* # estraverse(): # Returns `estraverse`. It's monkey-patched to work with CoffeeScript ASTs. # # @estraverse().replace ast, ... ### estraverse: -> @_estraverse ?= do -> es = require('estraverse') es.VisitorKeys.CoffeeEscapedExpression = [] es.VisitorKeys.CoffeeListExpression = ['test', 'body'] es.VisitorKeys.CoffeePrototypeExpression = ['object', 'property', 'computed'] es.VisitorKeys.CoffeeLoopStatement = ['body'] es.VisitorKeys.CoffeeDoExpression = ['function'] es.VisitorKeys.BlockComment = [] es.VisitorKeys.LineComment = [] es ###* # pushStack() : @pushStack(node) # Pushes a scope to the scope stack. Every time the scope changes, `@scope` # and `@ctx` gets changed. ### pushStack: (node) -> [ oldScope, oldCtx ] = [ @scope, @ctx ] @scopes.push [ @scope , @ctx ] @ctx = clone(@ctx) @scope = node @onScopeEnter?(@scope, @ctx, oldScope, oldCtx) popStack: () -> [ oldScope, oldCtx ] = [ @scope, @ctx ] [ @scope, @ctx ] = @scopes.pop() @onScopeExit?(@scope, @ctx, oldScope, oldCtx) ###* # syntaxError() : @syntaxError(node, message) # Throws a syntax error for the given `node` with a given `message`. # # @syntaxError node, "Not supported" ### syntaxError: (node, description) -> err = buildError( start: node.loc?.start, end: node.loc?.end, description: description , @options.source, @options.filename) throw err ###* # warn() : @warn(node, message) # Add a warning # # @warn node, "Variable was defined twice" ### warn: (node, description) -> @warnings.push start: node.loc?.start end: node.loc?.end filename: @options.filename description: description ###* # Defaults: these are default handlers that will automatially change `@scope`. ### Program: (node) -> @pushStack node node ProgramExit: (node) -> @popStack() node FunctionExpression: (node) -> @pushStack node.body node FunctionExpressionExit: (node) -> @popStack() node escapeJs: (node, options = {}) -> replace node, type: 'CoffeeEscapedExpression' _parenthesized: options.parenthesized raw: require('escodegen').generate node, format: indent: style: toIndent(@options.indent) ### # Extends a class `dest`'s prototype with those from other classes in `classes`. # Throws an error if there's a clash. ### safeExtend = (dest, classes) -> added = {} classes.forEach (klass) -> for key, fn of klass:: if klass::hasOwnProperty(key) if added[key] and key isnt 'constructor' dest::[key] = chain(dest::[key], fn) else dest::[key] = fn added[key] = true dest # Chains two Visitor functions together. Returns a function that will run the # `old` function, and feed its output node to the `new` function. # # class X extends TransformBase # CallExpression: (node) -> # # class Y extends TransformBase # CallExpression: (node) -> # # fn = chain(X::CallExpression, Y::CallExpression) # chain = (old, noo) -> (node, args...) -> type = node.type result = old.bind(this)(node, args...) # If the `old` function modified the node type, don't continue. # This is the case for eg, CallExpression turning into BinaryExpression if result.type is type result = noo.bind(this)(result, args...) result