/src/nodes.coffee
http://github.com/jashkenas/coffee-script · CoffeeScript · 6111 lines · 3085 code · 559 blank · 2467 comment · 610 complexity · 75430d71fe3a3205f3c9b75d5441a518 MD5 · raw file
Large files are truncated click here to view the full file
- # `nodes.coffee` contains all of the node classes for the syntax tree. Most
- # nodes are created as the result of actions in the [grammar](grammar.html),
- # but some are created by other nodes as a method of code generation. To convert
- # the syntax tree into a string of JavaScript code, call `compile()` on the root.
- Error.stackTraceLimit = Infinity
- {Scope} = require './scope'
- {isUnassignable, JS_FORBIDDEN} = require './lexer'
- # Import the helpers we plan to use.
- {compact, flatten, extend, merge, del, starts, ends, some,
- addDataToNode, attachCommentsToNode, locationDataToString,
- throwSyntaxError, replaceUnicodeCodePointEscapes,
- isFunction, isPlainObject, isNumber, parseNumber} = require './helpers'
- # Functions required by parser.
- exports.extend = extend
- exports.addDataToNode = addDataToNode
- # Constant functions for nodes that don’t need customization.
- YES = -> yes
- NO = -> no
- THIS = -> this
- NEGATE = -> @negated = not @negated; this
- #### CodeFragment
- # The various nodes defined below all compile to a collection of **CodeFragment** objects.
- # A CodeFragments is a block of generated code, and the location in the source file where the code
- # came from. CodeFragments can be assembled together into working code just by catting together
- # all the CodeFragments' `code` snippets, in order.
- exports.CodeFragment = class CodeFragment
- constructor: (parent, code) ->
- @code = "#{code}"
- @type = parent?.constructor?.name or 'unknown'
- @locationData = parent?.locationData
- @comments = parent?.comments
- toString: ->
- # This is only intended for debugging.
- "#{@code}#{if @locationData then ": " + locationDataToString(@locationData) else ''}"
- # Convert an array of CodeFragments into a string.
- fragmentsToText = (fragments) ->
- (fragment.code for fragment in fragments).join('')
- #### Base
- # The **Base** is the abstract base class for all nodes in the syntax tree.
- # Each subclass implements the `compileNode` method, which performs the
- # code generation for that node. To compile a node to JavaScript,
- # call `compile` on it, which wraps `compileNode` in some generic extra smarts,
- # to know when the generated code needs to be wrapped up in a closure.
- # An options hash is passed and cloned throughout, containing information about
- # the environment from higher in the tree (such as if a returned value is
- # being requested by the surrounding function), information about the current
- # scope, and indentation level.
- exports.Base = class Base
- compile: (o, lvl) ->
- fragmentsToText @compileToFragments o, lvl
- # Occasionally a node is compiled multiple times, for example to get the name
- # of a variable to add to scope tracking. When we know that a “premature”
- # compilation won’t result in comments being output, set those comments aside
- # so that they’re preserved for a later `compile` call that will result in
- # the comments being included in the output.
- compileWithoutComments: (o, lvl, method = 'compile') ->
- if @comments
- @ignoreTheseCommentsTemporarily = @comments
- delete @comments
- unwrapped = @unwrapAll()
- if unwrapped.comments
- unwrapped.ignoreTheseCommentsTemporarily = unwrapped.comments
- delete unwrapped.comments
- fragments = @[method] o, lvl
- if @ignoreTheseCommentsTemporarily
- @comments = @ignoreTheseCommentsTemporarily
- delete @ignoreTheseCommentsTemporarily
- if unwrapped.ignoreTheseCommentsTemporarily
- unwrapped.comments = unwrapped.ignoreTheseCommentsTemporarily
- delete unwrapped.ignoreTheseCommentsTemporarily
- fragments
- compileNodeWithoutComments: (o, lvl) ->
- @compileWithoutComments o, lvl, 'compileNode'
- # Common logic for determining whether to wrap this node in a closure before
- # compiling it, or to compile directly. We need to wrap if this node is a
- # *statement*, and it's not a *pureStatement*, and we're not at
- # the top level of a block (which would be unnecessary), and we haven't
- # already been asked to return the result (because statements know how to
- # return results).
- compileToFragments: (o, lvl) ->
- o = extend {}, o
- o.level = lvl if lvl
- node = @unfoldSoak(o) or this
- node.tab = o.indent
- fragments = if o.level is LEVEL_TOP or not node.isStatement(o)
- node.compileNode o
- else
- node.compileClosure o
- @compileCommentFragments o, node, fragments
- fragments
- compileToFragmentsWithoutComments: (o, lvl) ->
- @compileWithoutComments o, lvl, 'compileToFragments'
- # Statements converted into expressions via closure-wrapping share a scope
- # object with their parent closure, to preserve the expected lexical scope.
- compileClosure: (o) ->
- @checkForPureStatementInExpression()
- o.sharedScope = yes
- func = new Code [], Block.wrap [this]
- args = []
- if @contains ((node) -> node instanceof SuperCall)
- func.bound = yes
- else if (argumentsNode = @contains isLiteralArguments) or @contains isLiteralThis
- args = [new ThisLiteral]
- if argumentsNode
- meth = 'apply'
- args.push new IdentifierLiteral 'arguments'
- else
- meth = 'call'
- func = new Value func, [new Access new PropertyName meth]
- parts = (new Call func, args).compileNode o
- switch
- when func.isGenerator or func.base?.isGenerator
- parts.unshift @makeCode "(yield* "
- parts.push @makeCode ")"
- when func.isAsync or func.base?.isAsync
- parts.unshift @makeCode "(await "
- parts.push @makeCode ")"
- parts
- compileCommentFragments: (o, node, fragments) ->
- return fragments unless node.comments
- # This is where comments, that are attached to nodes as a `comments`
- # property, become `CodeFragment`s. “Inline block comments,” e.g.
- # `/* */`-delimited comments that are interspersed within code on a line,
- # are added to the current `fragments` stream. All other fragments are
- # attached as properties to the nearest preceding or following fragment,
- # to remain stowaways until they get properly output in `compileComments`
- # later on.
- unshiftCommentFragment = (commentFragment) ->
- if commentFragment.unshift
- # Find the first non-comment fragment and insert `commentFragment`
- # before it.
- unshiftAfterComments fragments, commentFragment
- else
- if fragments.length isnt 0
- precedingFragment = fragments[fragments.length - 1]
- if commentFragment.newLine and precedingFragment.code isnt '' and
- not /\n\s*$/.test precedingFragment.code
- commentFragment.code = "\n#{commentFragment.code}"
- fragments.push commentFragment
- for comment in node.comments when comment not in @compiledComments
- @compiledComments.push comment # Don’t output this comment twice.
- # For block/here comments, denoted by `###`, that are inline comments
- # like `1 + ### comment ### 2`, create fragments and insert them into
- # the fragments array.
- # Otherwise attach comment fragments to their closest fragment for now,
- # so they can be inserted into the output later after all the newlines
- # have been added.
- if comment.here # Block comment, delimited by `###`.
- commentFragment = new HereComment(comment).compileNode o
- else # Line comment, delimited by `#`.
- commentFragment = new LineComment(comment).compileNode o
- if (commentFragment.isHereComment and not commentFragment.newLine) or
- node.includeCommentFragments()
- # Inline block comments, like `1 + /* comment */ 2`, or a node whose
- # `compileToFragments` method has logic for outputting comments.
- unshiftCommentFragment commentFragment
- else
- fragments.push @makeCode '' if fragments.length is 0
- if commentFragment.unshift
- fragments[0].precedingComments ?= []
- fragments[0].precedingComments.push commentFragment
- else
- fragments[fragments.length - 1].followingComments ?= []
- fragments[fragments.length - 1].followingComments.push commentFragment
- fragments
- # If the code generation wishes to use the result of a complex expression
- # in multiple places, ensure that the expression is only ever evaluated once,
- # by assigning it to a temporary variable. Pass a level to precompile.
- #
- # If `level` is passed, then returns `[val, ref]`, where `val` is the compiled value, and `ref`
- # is the compiled reference. If `level` is not passed, this returns `[val, ref]` where
- # the two values are raw nodes which have not been compiled.
- cache: (o, level, shouldCache) ->
- complex = if shouldCache? then shouldCache this else @shouldCache()
- if complex
- ref = new IdentifierLiteral o.scope.freeVariable 'ref'
- sub = new Assign ref, this
- if level then [sub.compileToFragments(o, level), [@makeCode(ref.value)]] else [sub, ref]
- else
- ref = if level then @compileToFragments o, level else this
- [ref, ref]
- # Occasionally it may be useful to make an expression behave as if it was 'hoisted', whereby the
- # result of the expression is available before its location in the source, but the expression's
- # variable scope corresponds to the source position. This is used extensively to deal with executable
- # class bodies in classes.
- #
- # Calling this method mutates the node, proxying the `compileNode` and `compileToFragments`
- # methods to store their result for later replacing the `target` node, which is returned by the
- # call.
- hoist: ->
- @hoisted = yes
- target = new HoistTarget @
- compileNode = @compileNode
- compileToFragments = @compileToFragments
- @compileNode = (o) ->
- target.update compileNode, o
- @compileToFragments = (o) ->
- target.update compileToFragments, o
- target
- cacheToCodeFragments: (cacheValues) ->
- [fragmentsToText(cacheValues[0]), fragmentsToText(cacheValues[1])]
- # Construct a node that returns the current node’s result.
- # Note that this is overridden for smarter behavior for
- # many statement nodes (e.g. `If`, `For`).
- makeReturn: (results, mark) ->
- if mark
- # Mark this node as implicitly returned, so that it can be part of the
- # node metadata returned in the AST.
- @canBeReturned = yes
- return
- node = @unwrapAll()
- if results
- new Call new Literal("#{results}.push"), [node]
- else
- new Return node
- # Does this node, or any of its children, contain a node of a certain kind?
- # Recursively traverses down the *children* nodes and returns the first one
- # that verifies `pred`. Otherwise return undefined. `contains` does not cross
- # scope boundaries.
- contains: (pred) ->
- node = undefined
- @traverseChildren no, (n) ->
- if pred n
- node = n
- return no
- node
- # Pull out the last node of a node list.
- lastNode: (list) ->
- if list.length is 0 then null else list[list.length - 1]
- # Debugging representation of the node, for inspecting the parse tree.
- # This is what `coffee --nodes` prints out.
- toString: (idt = '', name = @constructor.name) ->
- tree = '\n' + idt + name
- tree += '?' if @soak
- @eachChild (node) -> tree += node.toString idt + TAB
- tree
- checkForPureStatementInExpression: ->
- if jumpNode = @jumps()
- jumpNode.error 'cannot use a pure statement in an expression'
- # Plain JavaScript object representation of the node, that can be serialized
- # as JSON. This is what the `ast` option in the Node API returns.
- # We try to follow the [Babel AST spec](https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md)
- # as closely as possible, for improved interoperability with other tools.
- # **WARNING: DO NOT OVERRIDE THIS METHOD IN CHILD CLASSES.**
- # Only override the component `ast*` methods as needed.
- ast: (o, level) ->
- # Merge `level` into `o` and perform other universal checks.
- o = @astInitialize o, level
- # Create serializable representation of this node.
- astNode = @astNode o
- # Mark AST nodes that correspond to expressions that (implicitly) return.
- # We can’t do this as part of `astNode` because we need to assemble child
- # nodes first before marking the parent being returned.
- if @astNode? and @canBeReturned
- Object.assign astNode, {returns: yes}
- astNode
- astInitialize: (o, level) ->
- o = Object.assign {}, o
- o.level = level if level?
- if o.level > LEVEL_TOP
- @checkForPureStatementInExpression()
- # `@makeReturn` must be called before `astProperties`, because the latter may call
- # `.ast()` for child nodes and those nodes would need the return logic from `makeReturn`
- # already executed by then.
- @makeReturn null, yes if @isStatement(o) and o.level isnt LEVEL_TOP and o.scope?
- o
- astNode: (o) ->
- # Every abstract syntax tree node object has four categories of properties:
- # - type, stored in the `type` field and a string like `NumberLiteral`.
- # - location data, stored in the `loc`, `start`, `end` and `range` fields.
- # - properties specific to this node, like `parsedValue`.
- # - properties that are themselves child nodes, like `body`.
- # These fields are all intermixed in the Babel spec; `type` and `start` and
- # `parsedValue` are all top level fields in the AST node object. We have
- # separate methods for returning each category, that we merge together here.
- Object.assign {}, {type: @astType(o)}, @astProperties(o), @astLocationData()
- # By default, a node class has no specific properties.
- astProperties: -> {}
- # By default, a node class’s AST `type` is its class name.
- astType: -> @constructor.name
- # The AST location data is a rearranged version of our Jison location data,
- # mutated into the structure that the Babel spec uses.
- astLocationData: ->
- jisonLocationDataToAstLocationData @locationData
- # Determines whether an AST node needs an `ExpressionStatement` wrapper.
- # Typically matches our `isStatement()` logic but this allows overriding.
- isStatementAst: (o) ->
- @isStatement o
- # Passes each child to a function, breaking when the function returns `false`.
- eachChild: (func) ->
- return this unless @children
- for attr in @children when @[attr]
- for child in flatten [@[attr]]
- return this if func(child) is false
- this
- traverseChildren: (crossScope, func) ->
- @eachChild (child) ->
- recur = func(child)
- child.traverseChildren(crossScope, func) unless recur is no
- # `replaceInContext` will traverse children looking for a node for which `match` returns
- # true. Once found, the matching node will be replaced by the result of calling `replacement`.
- replaceInContext: (match, replacement) ->
- return false unless @children
- for attr in @children when children = @[attr]
- if Array.isArray children
- for child, i in children
- if match child
- children[i..i] = replacement child, @
- return true
- else
- return true if child.replaceInContext match, replacement
- else if match children
- @[attr] = replacement children, @
- return true
- else
- return true if children.replaceInContext match, replacement
- invert: ->
- new Op '!', this
- unwrapAll: ->
- node = this
- continue until node is node = node.unwrap()
- node
- # Default implementations of the common node properties and methods. Nodes
- # will override these with custom logic, if needed.
- # `children` are the properties to recurse into when tree walking. The
- # `children` list *is* the structure of the AST. The `parent` pointer, and
- # the pointer to the `children` are how you can traverse the tree.
- children: []
- # `isStatement` has to do with “everything is an expression”. A few things
- # can’t be expressions, such as `break`. Things that `isStatement` returns
- # `true` for are things that can’t be used as expressions. There are some
- # error messages that come from `nodes.coffee` due to statements ending up
- # in expression position.
- isStatement: NO
- # Track comments that have been compiled into fragments, to avoid outputting
- # them twice.
- compiledComments: []
- # `includeCommentFragments` lets `compileCommentFragments` know whether this node
- # has special awareness of how to handle comments within its output.
- includeCommentFragments: NO
- # `jumps` tells you if an expression, or an internal part of an expression
- # has a flow control construct (like `break`, or `continue`, or `return`,
- # or `throw`) that jumps out of the normal flow of control and can’t be
- # used as a value. This is important because things like this make no sense;
- # we have to disallow them.
- jumps: NO
- # If `node.shouldCache() is false`, it is safe to use `node` more than once.
- # Otherwise you need to store the value of `node` in a variable and output
- # that variable several times instead. Kind of like this: `5` need not be
- # cached. `returnFive()`, however, could have side effects as a result of
- # evaluating it more than once, and therefore we need to cache it. The
- # parameter is named `shouldCache` rather than `mustCache` because there are
- # also cases where we might not need to cache but where we want to, for
- # example a long expression that may well be idempotent but we want to cache
- # for brevity.
- shouldCache: YES
- isChainable: NO
- isAssignable: NO
- isNumber: NO
- unwrap: THIS
- unfoldSoak: NO
- # Is this node used to assign a certain variable?
- assigns: NO
- # For this node and all descendents, set the location data to `locationData`
- # if the location data is not already set.
- updateLocationDataIfMissing: (locationData, force) ->
- @forceUpdateLocation = yes if force
- return this if @locationData and not @forceUpdateLocation
- delete @forceUpdateLocation
- @locationData = locationData
- @eachChild (child) ->
- child.updateLocationDataIfMissing locationData
- # Add location data from another node
- withLocationDataFrom: ({locationData}) ->
- @updateLocationDataIfMissing locationData
- # Add location data and comments from another node
- withLocationDataAndCommentsFrom: (node) ->
- @withLocationDataFrom node
- {comments} = node
- @comments = comments if comments?.length
- this
- # Throw a SyntaxError associated with this node’s location.
- error: (message) ->
- throwSyntaxError message, @locationData
- makeCode: (code) ->
- new CodeFragment this, code
- wrapInParentheses: (fragments) ->
- [@makeCode('('), fragments..., @makeCode(')')]
- wrapInBraces: (fragments) ->
- [@makeCode('{'), fragments..., @makeCode('}')]
- # `fragmentsList` is an array of arrays of fragments. Each array in fragmentsList will be
- # concatenated together, with `joinStr` added in between each, to produce a final flat array
- # of fragments.
- joinFragmentArrays: (fragmentsList, joinStr) ->
- answer = []
- for fragments, i in fragmentsList
- if i then answer.push @makeCode joinStr
- answer = answer.concat fragments
- answer
- #### HoistTarget
- # A **HoistTargetNode** represents the output location in the node tree for a hoisted node.
- # See Base#hoist.
- exports.HoistTarget = class HoistTarget extends Base
- # Expands hoisted fragments in the given array
- @expand = (fragments) ->
- for fragment, i in fragments by -1 when fragment.fragments
- fragments[i..i] = @expand fragment.fragments
- fragments
- constructor: (@source) ->
- super()
- # Holds presentational options to apply when the source node is compiled.
- @options = {}
- # Placeholder fragments to be replaced by the source node’s compilation.
- @targetFragments = { fragments: [] }
- isStatement: (o) ->
- @source.isStatement o
- # Update the target fragments with the result of compiling the source.
- # Calls the given compile function with the node and options (overriden with the target
- # presentational options).
- update: (compile, o) ->
- @targetFragments.fragments = compile.call @source, merge o, @options
- # Copies the target indent and level, and returns the placeholder fragments
- compileToFragments: (o, level) ->
- @options.indent = o.indent
- @options.level = level ? o.level
- [ @targetFragments ]
- compileNode: (o) ->
- @compileToFragments o
- compileClosure: (o) ->
- @compileToFragments o
- #### Root
- # The root node of the node tree
- exports.Root = class Root extends Base
- constructor: (@body) ->
- super()
- children: ['body']
- # Wrap everything in a safety closure, unless requested not to. It would be
- # better not to generate them in the first place, but for now, clean up
- # obvious double-parentheses.
- compileNode: (o) ->
- o.indent = if o.bare then '' else TAB
- o.level = LEVEL_TOP
- o.compiling = yes
- @initializeScope o
- fragments = @body.compileRoot o
- return fragments if o.bare
- [].concat @makeCode("(function() {\n"), fragments, @makeCode("\n}).call(this);\n")
- initializeScope: (o) ->
- o.scope = new Scope null, @body, null, o.referencedVars ? []
- # Mark given local variables in the root scope as parameters so they don’t
- # end up being declared on the root block.
- o.scope.parameter name for name in o.locals or []
- commentsAst: ->
- @allComments ?=
- for commentToken in (@allCommentTokens ? []) when not commentToken.heregex
- if commentToken.here
- new HereComment commentToken
- else
- new LineComment commentToken
- comment.ast() for comment in @allComments
- astNode: (o) ->
- o.level = LEVEL_TOP
- @initializeScope o
- super o
- astType: -> 'File'
- astProperties: (o) ->
- @body.isRootBlock = yes
- return
- program: Object.assign @body.ast(o), @astLocationData()
- comments: @commentsAst()
- #### Block
- # The block is the list of expressions that forms the body of an
- # indented block of code -- the implementation of a function, a clause in an
- # `if`, `switch`, or `try`, and so on...
- exports.Block = class Block extends Base
- constructor: (nodes) ->
- super()
- @expressions = compact flatten nodes or []
- children: ['expressions']
- # Tack an expression on to the end of this expression list.
- push: (node) ->
- @expressions.push node
- this
- # Remove and return the last expression of this expression list.
- pop: ->
- @expressions.pop()
- # Add an expression at the beginning of this expression list.
- unshift: (node) ->
- @expressions.unshift node
- this
- # If this Block consists of just a single node, unwrap it by pulling
- # it back out.
- unwrap: ->
- if @expressions.length is 1 then @expressions[0] else this
- # Is this an empty block of code?
- isEmpty: ->
- not @expressions.length
- isStatement: (o) ->
- for exp in @expressions when exp.isStatement o
- return yes
- no
- jumps: (o) ->
- for exp in @expressions
- return jumpNode if jumpNode = exp.jumps o
- # A Block node does not return its entire body, rather it
- # ensures that the final expression is returned.
- makeReturn: (results, mark) ->
- len = @expressions.length
- [..., lastExp] = @expressions
- lastExp = lastExp?.unwrap() or no
- # We also need to check that we’re not returning a JSX tag if there’s an
- # adjacent one at the same level; JSX doesn’t allow that.
- if lastExp and lastExp instanceof Parens and lastExp.body.expressions.length > 1
- {body:{expressions}} = lastExp
- [..., penult, last] = expressions
- penult = penult.unwrap()
- last = last.unwrap()
- if penult instanceof JSXElement and last instanceof JSXElement
- expressions[expressions.length - 1].error 'Adjacent JSX elements must be wrapped in an enclosing tag'
- if mark
- @expressions[len - 1]?.makeReturn results, mark
- return
- while len--
- expr = @expressions[len]
- @expressions[len] = expr.makeReturn results
- @expressions.splice(len, 1) if expr instanceof Return and not expr.expression
- break
- this
- compile: (o, lvl) ->
- return new Root(this).withLocationDataFrom(this).compile o, lvl unless o.scope
- super o, lvl
- # Compile all expressions within the **Block** body. If we need to return
- # the result, and it’s an expression, simply return it. If it’s a statement,
- # ask the statement to do so.
- compileNode: (o) ->
- @tab = o.indent
- top = o.level is LEVEL_TOP
- compiledNodes = []
- for node, index in @expressions
- if node.hoisted
- # This is a hoisted expression.
- # We want to compile this and ignore the result.
- node.compileToFragments o
- continue
- node = (node.unfoldSoak(o) or node)
- if node instanceof Block
- # This is a nested block. We don’t do anything special here like
- # enclose it in a new scope; we just compile the statements in this
- # block along with our own.
- compiledNodes.push node.compileNode o
- else if top
- node.front = yes
- fragments = node.compileToFragments o
- unless node.isStatement o
- fragments = indentInitial fragments, @
- [..., lastFragment] = fragments
- unless lastFragment.code is '' or lastFragment.isComment
- fragments.push @makeCode ';'
- compiledNodes.push fragments
- else
- compiledNodes.push node.compileToFragments o, LEVEL_LIST
- if top
- if @spaced
- return [].concat @joinFragmentArrays(compiledNodes, '\n\n'), @makeCode('\n')
- else
- return @joinFragmentArrays(compiledNodes, '\n')
- if compiledNodes.length
- answer = @joinFragmentArrays(compiledNodes, ', ')
- else
- answer = [@makeCode 'void 0']
- if compiledNodes.length > 1 and o.level >= LEVEL_LIST then @wrapInParentheses answer else answer
- compileRoot: (o) ->
- @spaced = yes
- fragments = @compileWithDeclarations o
- HoistTarget.expand fragments
- @compileComments fragments
- # Compile the expressions body for the contents of a function, with
- # declarations of all inner variables pushed up to the top.
- compileWithDeclarations: (o) ->
- fragments = []
- post = []
- for exp, i in @expressions
- exp = exp.unwrap()
- break unless exp instanceof Literal
- o = merge(o, level: LEVEL_TOP)
- if i
- rest = @expressions.splice i, 9e9
- [spaced, @spaced] = [@spaced, no]
- [fragments, @spaced] = [@compileNode(o), spaced]
- @expressions = rest
- post = @compileNode o
- {scope} = o
- if scope.expressions is this
- declars = o.scope.hasDeclarations()
- assigns = scope.hasAssignments
- if declars or assigns
- fragments.push @makeCode '\n' if i
- fragments.push @makeCode "#{@tab}var "
- if declars
- declaredVariables = scope.declaredVariables()
- for declaredVariable, declaredVariablesIndex in declaredVariables
- fragments.push @makeCode declaredVariable
- if Object::hasOwnProperty.call o.scope.comments, declaredVariable
- fragments.push o.scope.comments[declaredVariable]...
- if declaredVariablesIndex isnt declaredVariables.length - 1
- fragments.push @makeCode ', '
- if assigns
- fragments.push @makeCode ",\n#{@tab + TAB}" if declars
- fragments.push @makeCode scope.assignedVariables().join(",\n#{@tab + TAB}")
- fragments.push @makeCode ";\n#{if @spaced then '\n' else ''}"
- else if fragments.length and post.length
- fragments.push @makeCode "\n"
- fragments.concat post
- compileComments: (fragments) ->
- for fragment, fragmentIndex in fragments
- # Insert comments into the output at the next or previous newline.
- # If there are no newlines at which to place comments, create them.
- if fragment.precedingComments
- # Determine the indentation level of the fragment that we are about
- # to insert comments before, and use that indentation level for our
- # inserted comments. At this point, the fragments’ `code` property
- # is the generated output JavaScript, and CoffeeScript always
- # generates output indented by two spaces; so all we need to do is
- # search for a `code` property that begins with at least two spaces.
- fragmentIndent = ''
- for pastFragment in fragments[0...(fragmentIndex + 1)] by -1
- indent = /^ {2,}/m.exec pastFragment.code
- if indent
- fragmentIndent = indent[0]
- break
- else if '\n' in pastFragment.code
- break
- code = "\n#{fragmentIndent}" + (
- for commentFragment in fragment.precedingComments
- if commentFragment.isHereComment and commentFragment.multiline
- multident commentFragment.code, fragmentIndent, no
- else
- commentFragment.code
- ).join("\n#{fragmentIndent}").replace /^(\s*)$/gm, ''
- for pastFragment, pastFragmentIndex in fragments[0...(fragmentIndex + 1)] by -1
- newLineIndex = pastFragment.code.lastIndexOf '\n'
- if newLineIndex is -1
- # Keep searching previous fragments until we can’t go back any
- # further, either because there are no fragments left or we’ve
- # discovered that we’re in a code block that is interpolated
- # inside a string.
- if pastFragmentIndex is 0
- pastFragment.code = '\n' + pastFragment.code
- newLineIndex = 0
- else if pastFragment.isStringWithInterpolations and pastFragment.code is '{'
- code = code[1..] + '\n' # Move newline to end.
- newLineIndex = 1
- else
- continue
- delete fragment.precedingComments
- pastFragment.code = pastFragment.code[0...newLineIndex] +
- code + pastFragment.code[newLineIndex..]
- break
- # Yes, this is awfully similar to the previous `if` block, but if you
- # look closely you’ll find lots of tiny differences that make this
- # confusing if it were abstracted into a function that both blocks share.
- if fragment.followingComments
- # Does the first trailing comment follow at the end of a line of code,
- # like `; // Comment`, or does it start a new line after a line of code?
- trail = fragment.followingComments[0].trail
- fragmentIndent = ''
- # Find the indent of the next line of code, if we have any non-trailing
- # comments to output. We need to first find the next newline, as these
- # comments will be output after that; and then the indent of the line
- # that follows the next newline.
- unless trail and fragment.followingComments.length is 1
- onNextLine = no
- for upcomingFragment in fragments[fragmentIndex...]
- unless onNextLine
- if '\n' in upcomingFragment.code
- onNextLine = yes
- else
- continue
- else
- indent = /^ {2,}/m.exec upcomingFragment.code
- if indent
- fragmentIndent = indent[0]
- break
- else if '\n' in upcomingFragment.code
- break
- # Is this comment following the indent inserted by bare mode?
- # If so, there’s no need to indent this further.
- code = if fragmentIndex is 1 and /^\s+$/.test fragments[0].code
- ''
- else if trail
- ' '
- else
- "\n#{fragmentIndent}"
- # Assemble properly indented comments.
- code += (
- for commentFragment in fragment.followingComments
- if commentFragment.isHereComment and commentFragment.multiline
- multident commentFragment.code, fragmentIndent, no
- else
- commentFragment.code
- ).join("\n#{fragmentIndent}").replace /^(\s*)$/gm, ''
- for upcomingFragment, upcomingFragmentIndex in fragments[fragmentIndex...]
- newLineIndex = upcomingFragment.code.indexOf '\n'
- if newLineIndex is -1
- # Keep searching upcoming fragments until we can’t go any
- # further, either because there are no fragments left or we’ve
- # discovered that we’re in a code block that is interpolated
- # inside a string.
- if upcomingFragmentIndex is fragments.length - 1
- upcomingFragment.code = upcomingFragment.code + '\n'
- newLineIndex = upcomingFragment.code.length
- else if upcomingFragment.isStringWithInterpolations and upcomingFragment.code is '}'
- code = "#{code}\n"
- newLineIndex = 0
- else
- continue
- delete fragment.followingComments
- # Avoid inserting extra blank lines.
- code = code.replace /^\n/, '' if upcomingFragment.code is '\n'
- upcomingFragment.code = upcomingFragment.code[0...newLineIndex] +
- code + upcomingFragment.code[newLineIndex..]
- break
- fragments
- # Wrap up the given nodes as a **Block**, unless it already happens
- # to be one.
- @wrap: (nodes) ->
- return nodes[0] if nodes.length is 1 and nodes[0] instanceof Block
- new Block nodes
- astNode: (o) ->
- if (o.level? and o.level isnt LEVEL_TOP) and @expressions.length
- return (new Sequence(@expressions).withLocationDataFrom @).ast o
- super o
- astType: ->
- if @isRootBlock
- 'Program'
- else if @isClassBody
- 'ClassBody'
- else
- 'BlockStatement'
- astProperties: (o) ->
- checkForDirectives = del o, 'checkForDirectives'
- sniffDirectives @expressions, notFinalExpression: checkForDirectives if @isRootBlock or checkForDirectives
- directives = []
- body = []
- for expression in @expressions
- expressionAst = expression.ast o
- # Ignore generated PassthroughLiteral
- if not expressionAst?
- continue
- else if expression instanceof Directive
- directives.push expressionAst
- # If an expression is a statement, it can be added to the body as is.
- else if expression.isStatementAst o
- body.push expressionAst
- # Otherwise, we need to wrap it in an `ExpressionStatement` AST node.
- else
- body.push Object.assign
- type: 'ExpressionStatement'
- expression: expressionAst
- ,
- expression.astLocationData()
- return {
- # For now, we’re not including `sourceType` on the `Program` AST node.
- # Its value could be either `'script'` or `'module'`, and there’s no way
- # for CoffeeScript to always know which it should be. The presence of an
- # `import` or `export` statement in source code would imply that it should
- # be a `module`, but a project may consist of mostly such files and also
- # an outlier file that lacks `import` or `export` but is still imported
- # into the project and therefore expects to be treated as a `module`.
- # Determining the value of `sourceType` is essentially the same challenge
- # posed by determining the parse goal of a JavaScript file, also `module`
- # or `script`, and so if Node figures out a way to do so for `.js` files
- # then CoffeeScript can copy Node’s algorithm.
- # sourceType: 'module'
- body, directives
- }
- astLocationData: ->
- return if @isRootBlock and not @locationData?
- super()
- # A directive e.g. 'use strict'.
- # Currently only used during AST generation.
- exports.Directive = class Directive extends Base
- constructor: (@value) ->
- super()
- astProperties: (o) ->
- return
- value: Object.assign {},
- @value.ast o
- type: 'DirectiveLiteral'
- #### Literal
- # `Literal` is a base class for static values that can be passed through
- # directly into JavaScript without translation, such as: strings, numbers,
- # `true`, `false`, `null`...
- exports.Literal = class Literal extends Base
- constructor: (@value) ->
- super()
- shouldCache: NO
- assigns: (name) ->
- name is @value
- compileNode: (o) ->
- [@makeCode @value]
- astProperties: ->
- return
- value: @value
- toString: ->
- # This is only intended for debugging.
- " #{if @isStatement() then super() else @constructor.name}: #{@value}"
- exports.NumberLiteral = class NumberLiteral extends Literal
- constructor: (@value, {@parsedValue} = {}) ->
- super()
- unless @parsedValue?
- if isNumber @value
- @parsedValue = @value
- @value = "#{@value}"
- else
- @parsedValue = parseNumber @value
- isBigInt: ->
- /n$/.test @value
- astType: ->
- if @isBigInt()
- 'BigIntLiteral'
- else
- 'NumericLiteral'
- astProperties: ->
- return
- value:
- if @isBigInt()
- @parsedValue.toString()
- else
- @parsedValue
- extra:
- rawValue:
- if @isBigInt()
- @parsedValue.toString()
- else
- @parsedValue
- raw: @value
- exports.InfinityLiteral = class InfinityLiteral extends NumberLiteral
- constructor: (@value, {@originalValue = 'Infinity'} = {}) ->
- super()
- compileNode: ->
- [@makeCode '2e308']
- astNode: (o) ->
- unless @originalValue is 'Infinity'
- return new NumberLiteral(@value).withLocationDataFrom(@).ast o
- super o
- astType: -> 'Identifier'
- astProperties: ->
- return
- name: 'Infinity'
- declaration: no
- exports.NaNLiteral = class NaNLiteral extends NumberLiteral
- constructor: ->
- super 'NaN'
- compileNode: (o) ->
- code = [@makeCode '0/0']
- if o.level >= LEVEL_OP then @wrapInParentheses code else code
- astType: -> 'Identifier'
- astProperties: ->
- return
- name: 'NaN'
- declaration: no
- exports.StringLiteral = class StringLiteral extends Literal
- constructor: (@originalValue, {@quote, @initialChunk, @finalChunk, @indent, @double, @heregex} = {}) ->
- super ''
- @quote = null if @quote is '///'
- @fromSourceString = @quote?
- @quote ?= '"'
- heredoc = @isFromHeredoc()
- val = @originalValue
- if @heregex
- val = val.replace HEREGEX_OMIT, '$1$2'
- val = replaceUnicodeCodePointEscapes val, flags: @heregex.flags
- else
- val = val.replace STRING_OMIT, '$1'
- val =
- unless @fromSourceString
- val
- else if heredoc
- indentRegex = /// \n#{@indent} ///g if @indent
- val = val.replace indentRegex, '\n' if indentRegex
- val = val.replace LEADING_BLANK_LINE, '' if @initialChunk
- val = val.replace TRAILING_BLANK_LINE, '' if @finalChunk
- val
- else
- val.replace SIMPLE_STRING_OMIT, (match, offset) =>
- if (@initialChunk and offset is 0) or
- (@finalChunk and offset + match.length is val.length)
- ''
- else
- ' '
- @delimiter = @quote.charAt 0
- @value = makeDelimitedLiteral val, {
- @delimiter
- @double
- }
- @unquotedValueForTemplateLiteral = makeDelimitedLiteral val, {
- delimiter: '`'
- @double
- escapeNewlines: no
- includeDelimiters: no
- convertTrailingNullEscapes: yes
- }
- @unquotedValueForJSX = makeDelimitedLiteral val, {
- @double
- escapeNewlines: no
- includeDelimiters: no
- escapeDelimiter: no
- }
- compileNode: (o) ->
- return StringWithInterpolations.fromStringLiteral(@).compileNode o if @shouldGenerateTemplateLiteral()
- return [@makeCode @unquotedValueForJSX] if @jsx
- super o
- # `StringLiteral`s can represent either entire literal strings
- # or pieces of text inside of e.g. an interpolated string.
- # When parsed as the former but needing to be treated as the latter
- # (e.g. the string part of a tagged template literal), this will return
- # a copy of the `StringLiteral` with the quotes trimmed from its location
- # data (like it would have if parsed as part of an interpolated string).
- withoutQuotesInLocationData: ->
- endsWithNewline = @originalValue[-1..] is '\n'
- locationData = Object.assign {}, @locationData
- locationData.first_column += @quote.length
- if endsWithNewline
- locationData.last_line -= 1
- locationData.last_column =
- if locationData.last_line is locationData.first_line
- locationData.first_column + @originalValue.length - '\n'.length
- else
- @originalValue[...-1].length - '\n'.length - @originalValue[...-1].lastIndexOf('\n')
- else
- locationData.last_column -= @quote.length
- locationData.last_column_exclusive -= @quote.length
- locationData.range = [
- locationData.range[0] + @quote.length
- locationData.range[1] - @quote.length
- ]
- copy = new StringLiteral @originalValue, {@quote, @initialChunk, @finalChunk, @indent, @double, @heregex}
- copy.locationData = locationData
- copy
- isFromHeredoc: ->
- @quote.length is 3
- shouldGenerateTemplateLiteral: ->
- @isFromHeredoc()
- astNode: (o) ->
- return StringWithInterpolations.fromStringLiteral(@).ast o if @shouldGenerateTemplateLiteral()
- super o
- astProperties: ->
- return
- value: @originalValue
- extra:
- raw: "#{@delimiter}#{@originalValue}#{@delimiter}"
- exports.RegexLiteral = class RegexLiteral extends Literal
- constructor: (value, {@delimiter = '/', @heregexCommentTokens = []} = {}) ->
- super ''
- heregex = @delimiter is '///'
- endDelimiterIndex = value.lastIndexOf '/'
- @flags = value[endDelimiterIndex + 1..]
- val = @originalValue = value[1...endDelimiterIndex]
- val = val.replace HEREGEX_OMIT, '$1$2' if heregex
- val = replaceUnicodeCodePointEscapes val, {@flags}
- @value = "#{makeDelimitedLiteral val, delimiter: '/'}#{@flags}"
- REGEX_REGEX: /// ^ / (.*) / \w* $ ///
- astType: -> 'RegExpLiteral'
- astProperties: (o) ->
- [, pattern] = @REGEX_REGEX.exec @value
- return {
- value: undefined
- pattern, @flags, @delimiter
- originalPattern: @originalValue
- extra:
- raw: @value
- originalRaw: "#{@delimiter}#{@originalValue}#{@delimiter}#{@flags}"
- rawValue: undefined
- comments:
- for heregexCommentToken in @heregexCommentTokens
- if heregexCommentToken.here
- new HereComment(heregexCommentToken).ast o
- else
- new LineComment(heregexCommentToken).ast o
- }
- exports.PassthroughLiteral = class PassthroughLiteral extends Literal
- constructor: (@originalValue, {@here, @generated} = {}) ->
- super ''
- @value = @originalValue.replace /\\+(`|$)/g, (string) ->
- # `string` is always a value like '\`', '\\\`', '\\\\\`', etc.
- # By reducing it to its latter half, we turn '\`' to '`', '\\\`' to '\`', etc.
- string[-Math.ceil(string.length / 2)..]
- astNode: (o) ->
- return null if @generated
- super o
- astProperties: ->
- return {
- value: @originalValue
- here: !!@here
- }
- exports.IdentifierLiteral = class IdentifierLiteral extends Literal
- isAssignable: YES
- eachName: (iterator) ->
- iterator @
- astType: ->
- if @jsx
- 'JSXIdentifier'
- else
- 'Identifier'
- astProperties: ->
- return
- name: @value
- declaration: !!@isDeclaration
- exports.PropertyName = class PropertyName extends Literal
- isAssignable: YES
- astType: ->
- if @jsx
- 'JSXIdentifier'
- else
- 'Identifier'
- astProperties: ->
- return
- name: @value
- declaration: no
- exports.ComputedPropertyName = class ComputedPropertyName extends PropertyName
- compileNode: (o) ->
- [@makeCode('['), @value.compileToFragments(o, LEVEL_LIST)..., @makeCode(']')]
- astNode: (o) ->
- @value.ast o
- exports.StatementLiteral = class StatementLiteral extends Literal
- isStatement: YES
- makeReturn: THIS
- jumps: (o) ->
- return this if @value is 'break' and not (o?.loop or o?.block)
- return this if @value is 'continue' and not o?.loop
- compileNode: (o) ->
- [@makeCode "#{@tab}#{@value};"]
- astType: ->
- switch @value
- when 'continue' then 'ContinueStatement'
- when 'break' then 'BreakStatement'
- when 'debugger' then 'DebuggerStatement'
- exports.ThisLiteral = class ThisLiteral extends Literal
- constructor: (value) ->
- super 'this'
- @shorthand = value is '@'
- compileNode: (o) ->
- code = if o.scope.method?.bound then o.scope.method.context else @value
- [@makeCode code]
- astType: -> 'ThisExpression'
- astProperties: ->
- return
- shorthand: @shorthand
- exports.UndefinedLiteral = class UndefinedLiteral extends Literal
- constructor: ->
- super 'undefined'
- compileNode: (o) ->
- [@makeCode if o.level >= LEVEL_ACCESS then '(void 0)' else 'void 0']
- astType: -> 'Identifier'
- astProperties: ->
- return
- name: @value
- declaration: no
- exports.NullLiteral = class NullLiteral extends Literal
- constructor: ->
- super 'null'
- exports.BooleanLiteral = class BooleanLiteral extends Literal
- constructor: (value, {@originalValue} = {}) ->
- super value
- @originalValue ?= @value
- astProperties: ->
- value: if @value is 'true' then yes else no
- name: @originalValue
- exports.DefaultLiteral = class DefaultLiteral extends Literal
- astType: -> 'Identifier'
- astProperties: ->
- return
- name: 'default'
- declaration: no
- #### Return
- # A `return` is a *pureStatement*—wrapping it in a closure wouldn’t make sense.
- exports.Return = class Return extends Base
- constructor: (@expression, {@belongsToFuncDirectiveReturn} = {}) ->
- super()
- children: ['expression']
- isStatement: YES
- makeReturn: THIS
- jumps: THIS
- compileToFragments: (o, level) ->
- expr = @expression?.makeReturn()
- if expr and expr not instanceof Return then expr.compileToFragments o, level else super o, level
- compileNode: (o) ->
- answer = []
- # TODO: If we call `expression.compile()` here twice, we’ll sometimes
- # get back different results!
- if @expression
- answer = @expression.compileToFragments o, LEVEL_PAREN
- unshiftAfterComments answer, @makeCode "#{@tab}return "
- # Since the `return` got indented by `@tab`, preceding comments that are
- # multiline need to be indented.
- for fragment in answer
- if fragment.isHereComment and '\n' in fragment.code
- fragment.code = multident fragment.code, @tab
- else if fragment.isLineComment
- fragment.code = "#{@tab}#{fragment.code}"
- else
- break
- else
- answer.push @makeCode "#{@tab}return"
- answer.push @makeCode ';'
- answer
- checkForPureStatementInExpression: ->
- # don’t flag `return` from `await return`/`yield return` as invalid.
- return if @belongsToFuncDirectiveReturn
- super()
- astType: -> 'ReturnStatement'
- astProperties: (o) ->
- argument: @expression?.ast(o, LEVEL_PAREN) ? null
- # Parent class for `YieldReturn`/`AwaitReturn`.
- exports.FuncDirectiveReturn = class FuncDirectiveReturn extends Return
- constructor: (expression, {@returnKeyword}) ->
- super expression
- compileNode: (o) ->
- @checkScope o
- super o
- checkScope: (o) ->
- unless o.scope.parent?
- @error "#{@keyword} can only occur inside functions"
- isStatementAst: NO
- astNode: (o) ->
- @checkScope o
- new Op @keyword,
- new Return @expression, belongsToFuncDirectiveReturn: yes
- .withLocationDataFrom(
- if @expression?
- locationData: mergeLocationData @returnKeyword.locationData, @expression.locationData
- else
- @returnKeyword
- )
- .withLocationDataFrom @
- .ast o
- # `yield return` works exactly like `return`, except that it turns the function
- # into a generator.
- exports.YieldReturn = class YieldReturn extends FuncDirectiveReturn
- keyword: 'yield'
- exports.AwaitReturn = class AwaitReturn extends FuncDirectiveReturn
- keyword: 'await'
- #### Value
- # A value, variable or literal or parenthesized, indexed or dotted into,
- # or vanilla.
- exports.Value = class Value extends Base
- constructor: (base, props, tag, isDefaultValue = no) ->
- super()
- return base if not props and base instanceof Value
- @base = base
- @properties = props or []
- @tag = tag
- @[tag] = yes if tag
- @isDefaultValue = isDefaultValue
- # If this is a `@foo =` assignment, if there are comments on `@` move them
- # to be on `foo`.
- if @base?.comments and @base instanceof ThisLiteral and @properties[0]?.name?
- moveComments @base, @properties[0].name
- children: ['base', 'properties']
- # Add a property (or *properties* ) `Access` to the list.
- add: (props) ->
- @properties = @properties.concat props
- @forceUpdateLocation = yes
- this
- hasProperties: ->
- @properties.length isnt 0
- bareLiteral: (type) ->
- not @properties.length and @base instanceof type
- # Some boolean checks for the benefit of other nodes.
- isArray : -> @bareLiteral(Arr)
- isRange : -> @bareLiteral(Range)
- shouldCache : -> @hasProperties() or @base.shouldCache()
- isAssignable : (opts) -> @hasProperties() or @base.isAssignable opts
- isNumber : -> @bareLiteral(NumberLiteral)
- isString : -> @bareLiteral(StringLiteral)
- isRegex : -> @bareLiteral(RegexLiteral)
- isUndefined : -> @bareLiteral(UndefinedLiteral)
- isNull : -> @bareLiteral(NullLiteral)
- isBoolean : -> @bareLiteral(BooleanLiteral)
- isAtomic : ->
- for node in @properties.concat @base
- return no if node.soak or node instanceof Call or node instanceof Op and node.operator is 'do'
- yes
- isNotCallable : -> @isNumber() or @isString() or @isRegex() or
- @isArray() or @isRange() or @isSplice() or @isObject() or
- @isUndefined() or @isNull() or @isBoolean()
- isStatement : (o) -> not @properties.length and @base.isStatement o
- isJSXTag : -> @base instanceof JSXTag
- assigns : (name) -> not @properties.length and @base.assigns name
- jumps…