Change how error messages are shown
Instead of throwing the syntax errors with their source file location and needing to then catch them and call a `prettyErrorMessage` function in order to get the formatted error message, now syntax errors know how to pretty-print themselves (their `toString` method gets overridden). An intermediate `catch` & re-`throw` is needed at the level of `CoffeeScript.compile` and friends. But the benefit of this approach is that now libraries that use the `CoffeeScript` object directly don't need to bother catching the possible compilation errors and calling a special function in order to get the nice error messages; they can just print the error itself (or let it bubble up) and the error will know how to pretty-print itself.
This commit is contained in:
parent
51c625205b
commit
3f9cdcf1fa
|
@ -1,6 +1,6 @@
|
|||
// Generated by CoffeeScript 1.6.3
|
||||
(function() {
|
||||
var Lexer, Module, SourceMap, child_process, compile, compileFile, ext, fileExtensions, findExtension, fork, formatSourcePosition, fs, getSourceMap, helpers, lexer, loadFile, parser, path, sourceMaps, vm, _i, _len,
|
||||
var Lexer, Module, SourceMap, child_process, compile, compileFile, ext, fileExtensions, findExtension, fork, formatSourcePosition, fs, getSourceMap, helpers, lexer, loadFile, parser, path, sourceMaps, vm, withPrettyErrors, _i, _len,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
|
||||
|
@ -26,11 +26,25 @@
|
|||
|
||||
exports.helpers = helpers;
|
||||
|
||||
exports.compile = compile = function(code, options) {
|
||||
withPrettyErrors = function(fn) {
|
||||
return function(code, options) {
|
||||
var err;
|
||||
if (options == null) {
|
||||
options = {};
|
||||
}
|
||||
try {
|
||||
return fn.call(this, code, options);
|
||||
} catch (_error) {
|
||||
err = _error;
|
||||
err.code || (err.code = code);
|
||||
err.filename || (err.filename = options.filename);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
exports.compile = compile = withPrettyErrors(function(code, options) {
|
||||
var answer, currentColumn, currentLine, fragment, fragments, header, js, map, merge, newLines, _i, _len;
|
||||
if (options == null) {
|
||||
options = {};
|
||||
}
|
||||
merge = helpers.merge;
|
||||
if (options.sourceMap) {
|
||||
map = new SourceMap;
|
||||
|
@ -73,19 +87,19 @@
|
|||
} else {
|
||||
return js;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
exports.tokens = function(code, options) {
|
||||
exports.tokens = withPrettyErrors(function(code, options) {
|
||||
return lexer.tokenize(code, options);
|
||||
};
|
||||
});
|
||||
|
||||
exports.nodes = function(source, options) {
|
||||
exports.nodes = withPrettyErrors(function(source, options) {
|
||||
if (typeof source === 'string') {
|
||||
return parser.parse(lexer.tokenize(source, options));
|
||||
} else {
|
||||
return parser.parse(source);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
exports.run = function(code, options) {
|
||||
var answer, mainModule, _ref;
|
||||
|
@ -178,8 +192,8 @@
|
|||
});
|
||||
} catch (_error) {
|
||||
err = _error;
|
||||
err.filename = filename;
|
||||
err.code = stripped;
|
||||
err.filename || (err.filename = filename);
|
||||
err.code || (err.code = stripped);
|
||||
throw err;
|
||||
}
|
||||
return answer;
|
||||
|
|
|
@ -150,7 +150,7 @@
|
|||
};
|
||||
|
||||
compileScript = function(file, input, base) {
|
||||
var compiled, err, message, o, options, t, task, useColors;
|
||||
var compiled, err, message, o, options, t, task;
|
||||
if (base == null) {
|
||||
base = null;
|
||||
}
|
||||
|
@ -195,8 +195,7 @@
|
|||
if (CoffeeScript.listeners('failure').length) {
|
||||
return;
|
||||
}
|
||||
useColors = process.stdout.isTTY && !process.env.NODE_DISABLE_COLORS;
|
||||
message = helpers.prettyErrorMessage(err, file || '[stdin]', input, useColors);
|
||||
message = err.stack || ("" + err);
|
||||
if (o.watch) {
|
||||
return printLine(message + '\x07');
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Generated by CoffeeScript 1.6.3
|
||||
(function() {
|
||||
var buildLocationData, extend, flatten, last, repeat, _ref;
|
||||
var buildLocationData, extend, flatten, last, repeat, syntaxErrorToString, _ref;
|
||||
|
||||
exports.starts = function(string, literal, start) {
|
||||
return literal === string.substr(start, literal.length);
|
||||
|
@ -188,38 +188,41 @@
|
|||
|
||||
exports.throwSyntaxError = function(message, location) {
|
||||
var error;
|
||||
if (location.last_line == null) {
|
||||
location.last_line = location.first_line;
|
||||
}
|
||||
if (location.last_column == null) {
|
||||
location.last_column = location.first_column;
|
||||
}
|
||||
error = new SyntaxError(message);
|
||||
error.location = location;
|
||||
error.toString = syntaxErrorToString;
|
||||
delete error.stack;
|
||||
throw error;
|
||||
};
|
||||
|
||||
exports.prettyErrorMessage = function(error, filename, code, useColors) {
|
||||
var codeLine, colorize, end, first_column, first_line, last_column, last_line, marker, message, start, _ref1;
|
||||
if (!error.location) {
|
||||
return error.stack || ("" + error);
|
||||
syntaxErrorToString = function() {
|
||||
var codeLine, colorize, colorsEnabled, end, filename, first_column, first_line, last_column, last_line, marker, start, _ref1, _ref2;
|
||||
if (!(this.code && this.location)) {
|
||||
return Error.prototype.toString.call(this);
|
||||
}
|
||||
filename = error.filename || filename;
|
||||
code = error.code || code;
|
||||
_ref1 = error.location, first_line = _ref1.first_line, first_column = _ref1.first_column, last_line = _ref1.last_line, last_column = _ref1.last_column;
|
||||
codeLine = code.split('\n')[first_line];
|
||||
_ref1 = this.location, first_line = _ref1.first_line, first_column = _ref1.first_column, last_line = _ref1.last_line, last_column = _ref1.last_column;
|
||||
if (last_line == null) {
|
||||
last_line = first_line;
|
||||
}
|
||||
if (last_column == null) {
|
||||
last_column = first_column;
|
||||
}
|
||||
filename = this.filename || '[stdin]';
|
||||
codeLine = this.code.split('\n')[first_line];
|
||||
start = first_column;
|
||||
end = first_line === last_line ? last_column + 1 : codeLine.length;
|
||||
marker = repeat(' ', start) + repeat('^', end - start);
|
||||
if (useColors) {
|
||||
if (typeof process !== "undefined" && process !== null) {
|
||||
colorsEnabled = process.stdout.isTTY && !process.env.NODE_DISABLE_COLORS;
|
||||
}
|
||||
if ((_ref2 = this.colorful) != null ? _ref2 : colorsEnabled) {
|
||||
colorize = function(str) {
|
||||
return "\x1B[1;31m" + str + "\x1B[0m";
|
||||
};
|
||||
codeLine = codeLine.slice(0, start) + colorize(codeLine.slice(start, end)) + codeLine.slice(end);
|
||||
marker = colorize(marker);
|
||||
}
|
||||
message = "" + filename + ":" + (first_line + 1) + ":" + (first_column + 1) + ": error: " + error.message + "\n" + codeLine + "\n" + marker;
|
||||
return message;
|
||||
return "" + filename + ":" + (first_line + 1) + ":" + (first_column + 1) + ": error: " + this.message + "\n" + codeLine + "\n" + marker;
|
||||
};
|
||||
|
||||
}).call(this);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Generated by CoffeeScript 1.6.3
|
||||
(function() {
|
||||
var CoffeeScript, addHistory, addMultilineHandler, fs, merge, nodeREPL, path, prettyErrorMessage, replDefaults, vm, _ref;
|
||||
var CoffeeScript, addHistory, addMultilineHandler, fs, merge, nodeREPL, path, replDefaults, vm;
|
||||
|
||||
fs = require('fs');
|
||||
|
||||
|
@ -12,17 +12,17 @@
|
|||
|
||||
CoffeeScript = require('./coffee-script');
|
||||
|
||||
_ref = require('./helpers'), merge = _ref.merge, prettyErrorMessage = _ref.prettyErrorMessage;
|
||||
merge = require('./helpers').merge;
|
||||
|
||||
replDefaults = {
|
||||
prompt: 'coffee> ',
|
||||
historyFile: process.env.HOME ? path.join(process.env.HOME, '.coffee_history') : void 0,
|
||||
historyMaxInputSize: 10240,
|
||||
"eval": function(input, context, filename, cb) {
|
||||
var Assign, Block, Literal, Value, ast, err, js, _ref1;
|
||||
var Assign, Block, Literal, Value, ast, err, js, _ref;
|
||||
input = input.replace(/\uFF00/g, '\n');
|
||||
input = input.replace(/^\(([\s\S]*)\n\)$/m, '$1');
|
||||
_ref1 = require('./nodes'), Block = _ref1.Block, Assign = _ref1.Assign, Value = _ref1.Value, Literal = _ref1.Literal;
|
||||
_ref = require('./nodes'), Block = _ref.Block, Assign = _ref.Assign, Value = _ref.Value, Literal = _ref.Literal;
|
||||
try {
|
||||
ast = CoffeeScript.nodes(input);
|
||||
ast = new Block([new Assign(new Value(new Literal('_')), ast, '=')]);
|
||||
|
@ -33,7 +33,8 @@
|
|||
return cb(null, vm.runInContext(js, context, filename));
|
||||
} catch (_error) {
|
||||
err = _error;
|
||||
return cb(prettyErrorMessage(err, filename, input, true));
|
||||
err.code || (err.code = input);
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -132,13 +133,13 @@
|
|||
|
||||
module.exports = {
|
||||
start: function(opts) {
|
||||
var build, major, minor, repl, _ref1;
|
||||
var build, major, minor, repl, _ref;
|
||||
if (opts == null) {
|
||||
opts = {};
|
||||
}
|
||||
_ref1 = process.versions.node.split('.').map(function(n) {
|
||||
_ref = process.versions.node.split('.').map(function(n) {
|
||||
return parseInt(n);
|
||||
}), major = _ref1[0], minor = _ref1[1], build = _ref1[2];
|
||||
}), major = _ref[0], minor = _ref[1], build = _ref[2];
|
||||
if (major === 0 && minor < 8) {
|
||||
console.warn("Node 0.8.0+ required for CoffeeScript REPL");
|
||||
process.exit(1);
|
||||
|
|
|
@ -20,6 +20,17 @@ fileExtensions = ['.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
|
||||
err.code or= code
|
||||
err.filename or= options.filename
|
||||
throw err
|
||||
|
||||
# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
|
||||
#
|
||||
# If `options.sourceMap` is specified, then `options.filename` must also be specified. All
|
||||
|
@ -29,7 +40,7 @@ exports.helpers = helpers
|
|||
# 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 = (code, options = {}) ->
|
||||
exports.compile = compile = withPrettyErrors (code, options) ->
|
||||
{merge} = helpers
|
||||
|
||||
if options.sourceMap
|
||||
|
@ -70,13 +81,13 @@ exports.compile = compile = (code, options = {}) ->
|
|||
js
|
||||
|
||||
# Tokenize a string of CoffeeScript code, and return the array of tokens.
|
||||
exports.tokens = (code, options) ->
|
||||
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 = (source, options) ->
|
||||
exports.nodes = withPrettyErrors (source, options) ->
|
||||
if typeof source is 'string'
|
||||
parser.parse lexer.tokenize source, options
|
||||
else
|
||||
|
@ -150,8 +161,8 @@ compileFile = (filename, sourceMap) ->
|
|||
# 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.
|
||||
err.filename = filename
|
||||
err.code = stripped
|
||||
err.filename or= filename
|
||||
err.code or= stripped
|
||||
throw err
|
||||
|
||||
answer
|
||||
|
|
|
@ -141,8 +141,7 @@ compileScript = (file, input, base=null) ->
|
|||
catch err
|
||||
CoffeeScript.emit 'failure', err, task
|
||||
return if CoffeeScript.listeners('failure').length
|
||||
useColors = process.stdout.isTTY and not process.env.NODE_DISABLE_COLORS
|
||||
message = helpers.prettyErrorMessage err, file or '[stdin]', input, useColors
|
||||
message = err.stack or "#{err}"
|
||||
if o.watch
|
||||
printLine message + '\x07'
|
||||
else
|
||||
|
|
|
@ -134,44 +134,45 @@ exports.isCoffee = (file) -> /\.((lit)?coffee|coffee\.md)$/.test file
|
|||
# Determine if a filename represents a Literate CoffeeScript file.
|
||||
exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file
|
||||
|
||||
# Throws a SyntaxError with a source file location data attached to it in a
|
||||
# property called `location`.
|
||||
# Throws a SyntaxError from a given location.
|
||||
# The error's `toString` will return an error message following the "standard"
|
||||
# format <filename>:<line>:<col>: <message> plus the line with the error and a
|
||||
# marker showing where the error is.
|
||||
exports.throwSyntaxError = (message, location) ->
|
||||
location.last_line ?= location.first_line
|
||||
location.last_column ?= location.first_column
|
||||
error = new SyntaxError message
|
||||
error.location = location
|
||||
error.toString = syntaxErrorToString
|
||||
|
||||
# Prevent compiler error from showing the compiler's stacktrace.
|
||||
delete error.stack
|
||||
|
||||
throw error
|
||||
|
||||
# Creates a nice error message like, following the "standard" format
|
||||
# <filename>:<line>:<col>: <message> plus the line with the error and a marker
|
||||
# showing where the error is.
|
||||
exports.prettyErrorMessage = (error, filename, code, useColors) ->
|
||||
return error.stack or "#{error}" unless error.location
|
||||
syntaxErrorToString = ->
|
||||
return Error::toString.call @ unless @code and @location
|
||||
|
||||
# Prefer original source file information stored in the error if present.
|
||||
filename = error.filename or filename
|
||||
code = error.code or code
|
||||
{first_line, first_column, last_line, last_column} = @location
|
||||
last_line ?= first_line
|
||||
last_column ?= first_column
|
||||
|
||||
{first_line, first_column, last_line, last_column} = error.location
|
||||
codeLine = code.split('\n')[first_line]
|
||||
filename = @filename or '[stdin]'
|
||||
codeLine = @code.split('\n')[first_line]
|
||||
start = first_column
|
||||
# Show only the first line on multi-line errors.
|
||||
end = if first_line is last_line then last_column + 1 else codeLine.length
|
||||
marker = repeat(' ', start) + repeat('^', end - start)
|
||||
|
||||
if useColors
|
||||
# Check to see if we're running on a color-enabled TTY.
|
||||
if process?
|
||||
colorsEnabled = process.stdout.isTTY and not process.env.NODE_DISABLE_COLORS
|
||||
|
||||
if @colorful ? colorsEnabled
|
||||
colorize = (str) -> "\x1B[1;31m#{str}\x1B[0m"
|
||||
codeLine = codeLine[...start] + colorize(codeLine[start...end]) + codeLine[end..]
|
||||
marker = colorize marker
|
||||
|
||||
message = """
|
||||
#{filename}:#{first_line + 1}:#{first_column + 1}: error: #{error.message}
|
||||
"""
|
||||
#{filename}:#{first_line + 1}:#{first_column + 1}: error: #{@message}
|
||||
#{codeLine}
|
||||
#{marker}
|
||||
"""
|
||||
|
||||
# Uncomment to add stacktrace.
|
||||
#message += "\n#{error.stack}"
|
||||
|
||||
message
|
||||
|
|
|
@ -3,7 +3,7 @@ path = require 'path'
|
|||
vm = require 'vm'
|
||||
nodeREPL = require 'repl'
|
||||
CoffeeScript = require './coffee-script'
|
||||
{merge, prettyErrorMessage} = require './helpers'
|
||||
{merge} = require './helpers'
|
||||
|
||||
replDefaults =
|
||||
prompt: 'coffee> ',
|
||||
|
@ -29,7 +29,9 @@ replDefaults =
|
|||
js = ast.compile bare: yes, locals: Object.keys(context)
|
||||
cb null, vm.runInContext(js, context, filename)
|
||||
catch err
|
||||
cb prettyErrorMessage(err, filename, input, yes)
|
||||
# AST's `compile` does not add source code information to syntax errors.
|
||||
err.code or= input
|
||||
cb err
|
||||
|
||||
addMultilineHandler = (repl) ->
|
||||
{rli, inputStream, outputStream} = repl
|
||||
|
|
|
@ -4,12 +4,10 @@
|
|||
# Ensure that errors of different kinds (lexer, parser and compiler) are shown
|
||||
# in a consistent way.
|
||||
|
||||
{prettyErrorMessage} = CoffeeScript.helpers
|
||||
|
||||
assertErrorFormat = (code, expectedErrorFormat) ->
|
||||
throws (-> CoffeeScript.run code), (err) ->
|
||||
message = prettyErrorMessage err, 'test.coffee', code
|
||||
eq expectedErrorFormat, message
|
||||
err.colorful = no
|
||||
eq expectedErrorFormat, "#{err}"
|
||||
yes
|
||||
|
||||
test "lexer errors formating", ->
|
||||
|
@ -18,7 +16,7 @@ test "lexer errors formating", ->
|
|||
insideOutObject = }{
|
||||
''',
|
||||
'''
|
||||
test.coffee:2:19: error: unmatched }
|
||||
[stdin]:2:19: error: unmatched }
|
||||
insideOutObject = }{
|
||||
^
|
||||
'''
|
||||
|
@ -28,7 +26,7 @@ test "parser error formating", ->
|
|||
foo in bar or in baz
|
||||
''',
|
||||
'''
|
||||
test.coffee:1:15: error: unexpected RELATION
|
||||
[stdin]:1:15: error: unexpected RELATION
|
||||
foo in bar or in baz
|
||||
^^
|
||||
'''
|
||||
|
@ -38,7 +36,7 @@ test "compiler error formatting", ->
|
|||
evil = (foo, eval, bar) ->
|
||||
''',
|
||||
'''
|
||||
test.coffee:1:14: error: parameter name "eval" is not allowed
|
||||
[stdin]:1:14: error: parameter name "eval" is not allowed
|
||||
evil = (foo, eval, bar) ->
|
||||
^^^^
|
||||
'''
|
||||
|
|
Loading…
Reference in New Issue