mirror of
https://github.com/jashkenas/coffeescript.git
synced 2026-05-03 03:00:14 -04:00
Previously, the parser created `Literal` nodes for many things. This resulted in information loss. Instead of being able to check the node type, we had to use regexes to tell the different types of `Literal`s apart. That was a bit like parsing literals twice: Once in the lexer, and once (or more) in the compiler. It also caused problems, such as `` `this` `` and `this` being indistinguishable (fixes #2009). Instead returning `new Literal` in the grammar, subtypes of it are now returned instead, such as `NumberLiteral`, `StringLiteral` and `IdentifierLiteral`. `new Literal` by itself is only used to represent code chunks that fit no category. (While mentioning `NumberLiteral`, there's also `InfinityLiteral` now, which is a subtype of `NumberLiteral`.) `StringWithInterpolations` has been added as a subtype of `Parens`, and `RegexWithInterpolations` as a subtype of `Call`. This makes it easier for other programs to make use of CoffeeScript's "AST" (nodes). For example, it is now possible to distinguish between `"a #{b} c"` and `"a " + b + " c"`. Fixes #4192. `SuperCall` has been added as a subtype of `Call`. Note, though, that some information is still lost, especially in the lexer. For example, there is no way to distinguish a heredoc from a regular string, or a heregex without interpolations from a regular regex. Binary and octal number literals are indistinguishable from hexadecimal literals. After the new subtypes were added, they were taken advantage of, removing most regexes in nodes.coffee. `SIMPLENUM` (which matches non-hex integers) had to be kept, though, because such numbers need special handling in JavaScript (for example in `1..toString()`). An especially nice hack to get rid of was using `new String()` for the token value for reserved identifiers (to be able to set a property on them which could survive through the parser). Now it's a good old regular string. In range literals, slices, splices and for loop steps when number literals are involved, CoffeeScript can do some optimizations, such as precomputing the value of, say, `5 - 3` (outputting `2` instead of `5 - 3` literally). As a side bonus, this now also works with hexadecimal number literals, such as `0x02`. Finally, this also improves the output of `coffee --nodes`: # Before: $ bin/coffee -ne 'while true "#{a}" break' Block While Value Bool Block Value Parens Block Op + Value """" Value Parens Block Value "a" "break" # After: $ bin/coffee -ne 'while true "#{a}" break' Block While Value BooleanLiteral: true Block Value StringWithInterpolations Block Op + Value StringLiteral: "" Value Parens Block Value IdentifierLiteral: a StatementLiteral: break
343 lines
12 KiB
CoffeeScript
343 lines
12 KiB
CoffeeScript
# CoffeeScript can be used both on the server, as a command-line compiler based
|
|
# on Node.js/V8, or to run CoffeeScript directly in the browser. This module
|
|
# contains the main entry functions for tokenizing, parsing, and compiling
|
|
# source CoffeeScript into JavaScript.
|
|
|
|
fs = require 'fs'
|
|
vm = require 'vm'
|
|
path = require 'path'
|
|
{Lexer} = require './lexer'
|
|
{parser} = require './parser'
|
|
helpers = require './helpers'
|
|
SourceMap = require './sourcemap'
|
|
|
|
# The current CoffeeScript version number.
|
|
exports.VERSION = '1.10.0'
|
|
|
|
exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
|
|
|
|
# Expose helpers for testing.
|
|
exports.helpers = helpers
|
|
|
|
# Function that allows for btoa in both nodejs and the browser.
|
|
base64encode = (src) -> switch
|
|
when typeof Buffer is 'function'
|
|
new Buffer(src).toString('base64')
|
|
when typeof btoa is 'function'
|
|
btoa(src)
|
|
else
|
|
throw new Error('Unable to base64 encode inline sourcemap.')
|
|
|
|
# Function wrapper to add source file information to SyntaxErrors thrown by the
|
|
# lexer/parser/compiler.
|
|
withPrettyErrors = (fn) ->
|
|
(code, options = {}) ->
|
|
try
|
|
fn.call @, code, options
|
|
catch err
|
|
throw err if typeof code isnt 'string' # Support `CoffeeScript.nodes(tokens)`.
|
|
throw helpers.updateSyntaxError err, code, options.filename
|
|
|
|
# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
|
|
#
|
|
# If `options.sourceMap` is specified, then `options.filename` must also be specified. All
|
|
# options that can be passed to `SourceMap#generate` may also be passed here.
|
|
#
|
|
# This returns a javascript string, unless `options.sourceMap` is passed,
|
|
# in which case this returns a `{js, v3SourceMap, sourceMap}`
|
|
# object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic
|
|
# lookups.
|
|
exports.compile = compile = withPrettyErrors (code, options) ->
|
|
{merge, extend} = helpers
|
|
options = extend {}, options
|
|
generateSourceMap = options.sourceMap or options.inlineMap
|
|
|
|
if generateSourceMap
|
|
map = new SourceMap
|
|
|
|
tokens = lexer.tokenize code, options
|
|
|
|
# Pass a list of referenced variables, so that generated variables won't get
|
|
# the same name.
|
|
options.referencedVars = (
|
|
token[1] for token in tokens when token.variable
|
|
)
|
|
|
|
fragments = parser.parse(tokens).compileToFragments options
|
|
|
|
currentLine = 0
|
|
currentLine += 1 if options.header
|
|
currentLine += 1 if options.shiftLine
|
|
currentColumn = 0
|
|
js = ""
|
|
for fragment in fragments
|
|
# Update the sourcemap with data from each fragment
|
|
if generateSourceMap
|
|
# Do not include empty, whitespace, or semicolon-only fragments.
|
|
if fragment.locationData and not /^[;\s]*$/.test fragment.code
|
|
map.add(
|
|
[fragment.locationData.first_line, fragment.locationData.first_column]
|
|
[currentLine, currentColumn]
|
|
{noReplace: true})
|
|
newLines = helpers.count fragment.code, "\n"
|
|
currentLine += newLines
|
|
if newLines
|
|
currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1)
|
|
else
|
|
currentColumn += fragment.code.length
|
|
|
|
# Copy the code from each fragment into the final JavaScript.
|
|
js += fragment.code
|
|
|
|
if options.header
|
|
header = "Generated by CoffeeScript #{@VERSION}"
|
|
js = "// #{header}\n#{js}"
|
|
|
|
if generateSourceMap
|
|
v3SourceMap = map.generate(options, code)
|
|
|
|
if options.inlineMap
|
|
sourceMapDataURI = "//# sourceMappingURL=data:application/json;base64,#{base64encode v3SourceMap}"
|
|
sourceURL = "//# sourceURL=#{options.filename ? 'coffeescript'}"
|
|
js = "#{js}\n#{sourceMapDataURI}\n#{sourceURL}"
|
|
|
|
if options.sourceMap
|
|
answer = {js}
|
|
answer.sourceMap = map
|
|
answer.v3SourceMap = v3SourceMap
|
|
answer
|
|
else
|
|
js
|
|
|
|
# Tokenize a string of CoffeeScript code, and return the array of tokens.
|
|
exports.tokens = withPrettyErrors (code, options) ->
|
|
lexer.tokenize code, options
|
|
|
|
# Parse a string of CoffeeScript code or an array of lexed tokens, and
|
|
# return the AST. You can then compile it by calling `.compile()` on the root,
|
|
# or traverse it by using `.traverseChildren()` with a callback.
|
|
exports.nodes = withPrettyErrors (source, options) ->
|
|
if typeof source is 'string'
|
|
parser.parse lexer.tokenize source, options
|
|
else
|
|
parser.parse source
|
|
|
|
# Compile and execute a string of CoffeeScript (on the server), correctly
|
|
# setting `__filename`, `__dirname`, and relative `require()`.
|
|
exports.run = (code, options = {}) ->
|
|
mainModule = require.main
|
|
|
|
# Set the filename.
|
|
mainModule.filename = process.argv[1] =
|
|
if options.filename then fs.realpathSync(options.filename) else '.'
|
|
|
|
# Clear the module cache.
|
|
mainModule.moduleCache and= {}
|
|
|
|
# Assign paths for node_modules loading
|
|
dir = if options.filename
|
|
path.dirname fs.realpathSync options.filename
|
|
else
|
|
fs.realpathSync '.'
|
|
mainModule.paths = require('module')._nodeModulePaths dir
|
|
|
|
# Compile.
|
|
if not helpers.isCoffee(mainModule.filename) or require.extensions
|
|
answer = compile code, options
|
|
code = answer.js ? answer
|
|
|
|
mainModule._compile code, mainModule.filename
|
|
|
|
# Compile and evaluate a string of CoffeeScript (in a Node.js-like environment).
|
|
# The CoffeeScript REPL uses this to run the input.
|
|
exports.eval = (code, options = {}) ->
|
|
return unless code = code.trim()
|
|
createContext = vm.Script.createContext ? vm.createContext
|
|
|
|
isContext = vm.isContext ? (ctx) ->
|
|
options.sandbox instanceof createContext().constructor
|
|
|
|
if createContext
|
|
if options.sandbox?
|
|
if isContext options.sandbox
|
|
sandbox = options.sandbox
|
|
else
|
|
sandbox = createContext()
|
|
sandbox[k] = v for own k, v of options.sandbox
|
|
sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
|
|
else
|
|
sandbox = global
|
|
sandbox.__filename = options.filename || 'eval'
|
|
sandbox.__dirname = path.dirname sandbox.__filename
|
|
# define module/require only if they chose not to specify their own
|
|
unless sandbox isnt global or sandbox.module or sandbox.require
|
|
Module = require 'module'
|
|
sandbox.module = _module = new Module(options.modulename || 'eval')
|
|
sandbox.require = _require = (path) -> Module._load path, _module, true
|
|
_module.filename = sandbox.__filename
|
|
for r in Object.getOwnPropertyNames require when r not in ['paths', 'arguments', 'caller']
|
|
_require[r] = require[r]
|
|
# use the same hack node currently uses for their own REPL
|
|
_require.paths = _module.paths = Module._nodeModulePaths process.cwd()
|
|
_require.resolve = (request) -> Module._resolveFilename request, _module
|
|
o = {}
|
|
o[k] = v for own k, v of options
|
|
o.bare = on # ensure return value
|
|
js = compile code, o
|
|
if sandbox is global
|
|
vm.runInThisContext js
|
|
else
|
|
vm.runInContext js, sandbox
|
|
|
|
exports.register = -> require './register'
|
|
|
|
# Throw error with deprecation warning when depending upon implicit `require.extensions` registration
|
|
if require.extensions
|
|
for ext in @FILE_EXTENSIONS then do (ext) ->
|
|
require.extensions[ext] ?= ->
|
|
throw new Error """
|
|
Use CoffeeScript.register() or require the coffee-script/register module to require #{ext} files.
|
|
"""
|
|
|
|
exports._compileFile = (filename, sourceMap = no, inlineMap = no) ->
|
|
raw = fs.readFileSync filename, 'utf8'
|
|
stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw
|
|
|
|
try
|
|
answer = compile(stripped, {filename, sourceMap, inlineMap, literate: helpers.isLiterate filename})
|
|
catch err
|
|
# As the filename and code of a dynamically loaded file will be different
|
|
# from the original file compiled with CoffeeScript.run, add that
|
|
# information to error so it can be pretty-printed later.
|
|
throw helpers.updateSyntaxError err, stripped, filename
|
|
|
|
answer
|
|
|
|
# Instantiate a Lexer for our use here.
|
|
lexer = new Lexer
|
|
|
|
# The real Lexer produces a generic stream of tokens. This object provides a
|
|
# thin wrapper around it, compatible with the Jison API. We can then pass it
|
|
# directly as a "Jison lexer".
|
|
parser.lexer =
|
|
lex: ->
|
|
token = parser.tokens[@pos++]
|
|
if token
|
|
[tag, @yytext, @yylloc] = token
|
|
parser.errorToken = token.origin or token
|
|
@yylineno = @yylloc.first_line
|
|
else
|
|
tag = ''
|
|
|
|
tag
|
|
setInput: (tokens) ->
|
|
parser.tokens = tokens
|
|
@pos = 0
|
|
upcomingInput: ->
|
|
""
|
|
# Make all the AST nodes visible to the parser.
|
|
parser.yy = require './nodes'
|
|
|
|
# Override Jison's default error handling function.
|
|
parser.yy.parseError = (message, {token}) ->
|
|
# Disregard Jison's message, it contains redundant line numer information.
|
|
# Disregard the token, we take its value directly from the lexer in case
|
|
# the error is caused by a generated token which might refer to its origin.
|
|
{errorToken, tokens} = parser
|
|
[errorTag, errorText, errorLoc] = errorToken
|
|
|
|
errorText = switch
|
|
when errorToken is tokens[tokens.length - 1]
|
|
'end of input'
|
|
when errorTag in ['INDENT', 'OUTDENT']
|
|
'indentation'
|
|
when errorTag in ['IDENTIFIER', 'NUMBER', 'INFINITY', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START']
|
|
errorTag.replace(/_START$/, '').toLowerCase()
|
|
else
|
|
helpers.nameWhitespaceCharacter errorText
|
|
|
|
# The second argument has a `loc` property, which should have the location
|
|
# data for this token. Unfortunately, Jison seems to send an outdated `loc`
|
|
# (from the previous token), so we take the location information directly
|
|
# from the lexer.
|
|
helpers.throwSyntaxError "unexpected #{errorText}", errorLoc
|
|
|
|
# Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js
|
|
# Modified to handle sourceMap
|
|
formatSourcePosition = (frame, getSourceMapping) ->
|
|
fileName = undefined
|
|
fileLocation = ''
|
|
|
|
if frame.isNative()
|
|
fileLocation = "native"
|
|
else
|
|
if frame.isEval()
|
|
fileName = frame.getScriptNameOrSourceURL()
|
|
fileLocation = "#{frame.getEvalOrigin()}, " unless fileName
|
|
else
|
|
fileName = frame.getFileName()
|
|
|
|
fileName or= "<anonymous>"
|
|
|
|
line = frame.getLineNumber()
|
|
column = frame.getColumnNumber()
|
|
|
|
# Check for a sourceMap position
|
|
source = getSourceMapping fileName, line, column
|
|
fileLocation =
|
|
if source
|
|
"#{fileName}:#{source[0]}:#{source[1]}"
|
|
else
|
|
"#{fileName}:#{line}:#{column}"
|
|
|
|
functionName = frame.getFunctionName()
|
|
isConstructor = frame.isConstructor()
|
|
isMethodCall = not (frame.isToplevel() or isConstructor)
|
|
|
|
if isMethodCall
|
|
methodName = frame.getMethodName()
|
|
typeName = frame.getTypeName()
|
|
|
|
if functionName
|
|
tp = as = ''
|
|
if typeName and functionName.indexOf typeName
|
|
tp = "#{typeName}."
|
|
if methodName and functionName.indexOf(".#{methodName}") isnt functionName.length - methodName.length - 1
|
|
as = " [as #{methodName}]"
|
|
|
|
"#{tp}#{functionName}#{as} (#{fileLocation})"
|
|
else
|
|
"#{typeName}.#{methodName or '<anonymous>'} (#{fileLocation})"
|
|
else if isConstructor
|
|
"new #{functionName or '<anonymous>'} (#{fileLocation})"
|
|
else if functionName
|
|
"#{functionName} (#{fileLocation})"
|
|
else
|
|
fileLocation
|
|
|
|
# Map of filenames -> sourceMap object.
|
|
sourceMaps = {}
|
|
|
|
# Generates the source map for a coffee file and stores it in the local cache variable.
|
|
getSourceMap = (filename) ->
|
|
return sourceMaps[filename] if sourceMaps[filename]
|
|
return unless path?.extname(filename) in exports.FILE_EXTENSIONS
|
|
answer = exports._compileFile filename, true
|
|
sourceMaps[filename] = answer.sourceMap
|
|
|
|
# Based on [michaelficarra/CoffeeScriptRedux](http://goo.gl/ZTx1p)
|
|
# NodeJS / V8 have no support for transforming positions in stack traces using
|
|
# sourceMap, so we must monkey-patch Error to display CoffeeScript source
|
|
# positions.
|
|
Error.prepareStackTrace = (err, stack) ->
|
|
getSourceMapping = (filename, line, column) ->
|
|
sourceMap = getSourceMap filename
|
|
answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap
|
|
if answer then [answer[0] + 1, answer[1] + 1] else null
|
|
|
|
frames = for frame in stack
|
|
break if frame.getFunction() is exports.run
|
|
" at #{formatSourcePosition frame, getSourceMapping}"
|
|
|
|
"#{err.toString()}\n#{frames.join '\n'}\n"
|