js2coffee
Version:
JavaScript to CoffeeScript compiler
300 lines (259 loc) • 7.63 kB
text/coffeescript
{
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