mirror of
https://github.com/jashkenas/coffeescript.git
synced 2026-01-14 17:27:59 -05:00
If you passed an array of tokens (as opposed to a string of code) to `CoffeeScript.nodes`, its attempts to prettify error messages would break. Now it does not attempt to prettify error messages in that case anymore (because it is not possible to prettify the errors without a string of code). The repl was affected by the above bug. Fixes #3887.
323 lines
11 KiB
CoffeeScript
323 lines
11 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.9.2'
|
|
|
|
exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
|
|
|
|
# Expose helpers for testing.
|
|
exports.helpers = helpers
|
|
|
|
# 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
|
|
|
|
if options.sourceMap
|
|
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 options.sourceMap
|
|
if fragment.locationData
|
|
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 options.sourceMap
|
|
answer = {js}
|
|
answer.sourceMap = map
|
|
answer.v3SourceMap = map.generate(options, code)
|
|
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
|
|
_require[r] = require[r] for r in Object.getOwnPropertyNames require when r isnt 'paths'
|
|
# 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
|
|
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) ->
|
|
raw = fs.readFileSync filename, 'utf8'
|
|
stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw
|
|
|
|
try
|
|
answer = compile(stripped, {filename, sourceMap, 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', '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"
|