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:
Demian Ferreiro 2013-07-31 08:27:49 -03:00
parent 51c625205b
commit 3f9cdcf1fa
9 changed files with 107 additions and 79 deletions

View File

@ -1,6 +1,6 @@
// Generated by CoffeeScript 1.6.3 // Generated by CoffeeScript 1.6.3
(function() { (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, __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; }; __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.helpers = helpers;
exports.compile = compile = function(code, options) { withPrettyErrors = function(fn) {
var answer, currentColumn, currentLine, fragment, fragments, header, js, map, merge, newLines, _i, _len; return function(code, options) {
var err;
if (options == null) { if (options == null) {
options = {}; 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;
merge = helpers.merge; merge = helpers.merge;
if (options.sourceMap) { if (options.sourceMap) {
map = new SourceMap; map = new SourceMap;
@ -73,19 +87,19 @@
} else { } else {
return js; return js;
} }
}; });
exports.tokens = function(code, options) { exports.tokens = withPrettyErrors(function(code, options) {
return lexer.tokenize(code, options); return lexer.tokenize(code, options);
}; });
exports.nodes = function(source, options) { exports.nodes = withPrettyErrors(function(source, options) {
if (typeof source === 'string') { if (typeof source === 'string') {
return parser.parse(lexer.tokenize(source, options)); return parser.parse(lexer.tokenize(source, options));
} else { } else {
return parser.parse(source); return parser.parse(source);
} }
}; });
exports.run = function(code, options) { exports.run = function(code, options) {
var answer, mainModule, _ref; var answer, mainModule, _ref;
@ -178,8 +192,8 @@
}); });
} catch (_error) { } catch (_error) {
err = _error; err = _error;
err.filename = filename; err.filename || (err.filename = filename);
err.code = stripped; err.code || (err.code = stripped);
throw err; throw err;
} }
return answer; return answer;

View File

@ -150,7 +150,7 @@
}; };
compileScript = function(file, input, base) { 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) { if (base == null) {
base = null; base = null;
} }
@ -195,8 +195,7 @@
if (CoffeeScript.listeners('failure').length) { if (CoffeeScript.listeners('failure').length) {
return; return;
} }
useColors = process.stdout.isTTY && !process.env.NODE_DISABLE_COLORS; message = err.stack || ("" + err);
message = helpers.prettyErrorMessage(err, file || '[stdin]', input, useColors);
if (o.watch) { if (o.watch) {
return printLine(message + '\x07'); return printLine(message + '\x07');
} else { } else {

View File

@ -1,6 +1,6 @@
// Generated by CoffeeScript 1.6.3 // Generated by CoffeeScript 1.6.3
(function() { (function() {
var buildLocationData, extend, flatten, last, repeat, _ref; var buildLocationData, extend, flatten, last, repeat, syntaxErrorToString, _ref;
exports.starts = function(string, literal, start) { exports.starts = function(string, literal, start) {
return literal === string.substr(start, literal.length); return literal === string.substr(start, literal.length);
@ -188,38 +188,41 @@
exports.throwSyntaxError = function(message, location) { exports.throwSyntaxError = function(message, location) {
var error; 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 = new SyntaxError(message);
error.location = location; error.location = location;
error.toString = syntaxErrorToString;
delete error.stack;
throw error; throw error;
}; };
exports.prettyErrorMessage = function(error, filename, code, useColors) { syntaxErrorToString = function() {
var codeLine, colorize, end, first_column, first_line, last_column, last_line, marker, message, start, _ref1; var codeLine, colorize, colorsEnabled, end, filename, first_column, first_line, last_column, last_line, marker, start, _ref1, _ref2;
if (!error.location) { if (!(this.code && this.location)) {
return error.stack || ("" + error); return Error.prototype.toString.call(this);
} }
filename = error.filename || filename; _ref1 = this.location, first_line = _ref1.first_line, first_column = _ref1.first_column, last_line = _ref1.last_line, last_column = _ref1.last_column;
code = error.code || code; if (last_line == null) {
_ref1 = error.location, first_line = _ref1.first_line, first_column = _ref1.first_column, last_line = _ref1.last_line, last_column = _ref1.last_column; last_line = first_line;
codeLine = code.split('\n')[first_line]; }
if (last_column == null) {
last_column = first_column;
}
filename = this.filename || '[stdin]';
codeLine = this.code.split('\n')[first_line];
start = first_column; start = first_column;
end = first_line === last_line ? last_column + 1 : codeLine.length; end = first_line === last_line ? last_column + 1 : codeLine.length;
marker = repeat(' ', start) + repeat('^', end - start); 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) { colorize = function(str) {
return "\x1B[1;31m" + str + "\x1B[0m"; return "\x1B[1;31m" + str + "\x1B[0m";
}; };
codeLine = codeLine.slice(0, start) + colorize(codeLine.slice(start, end)) + codeLine.slice(end); codeLine = codeLine.slice(0, start) + colorize(codeLine.slice(start, end)) + codeLine.slice(end);
marker = colorize(marker); marker = colorize(marker);
} }
message = "" + filename + ":" + (first_line + 1) + ":" + (first_column + 1) + ": error: " + error.message + "\n" + codeLine + "\n" + marker; return "" + filename + ":" + (first_line + 1) + ":" + (first_column + 1) + ": error: " + this.message + "\n" + codeLine + "\n" + marker;
return message;
}; };
}).call(this); }).call(this);

View File

@ -1,6 +1,6 @@
// Generated by CoffeeScript 1.6.3 // Generated by CoffeeScript 1.6.3
(function() { (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'); fs = require('fs');
@ -12,17 +12,17 @@
CoffeeScript = require('./coffee-script'); CoffeeScript = require('./coffee-script');
_ref = require('./helpers'), merge = _ref.merge, prettyErrorMessage = _ref.prettyErrorMessage; merge = require('./helpers').merge;
replDefaults = { replDefaults = {
prompt: 'coffee> ', prompt: 'coffee> ',
historyFile: process.env.HOME ? path.join(process.env.HOME, '.coffee_history') : void 0, historyFile: process.env.HOME ? path.join(process.env.HOME, '.coffee_history') : void 0,
historyMaxInputSize: 10240, historyMaxInputSize: 10240,
"eval": function(input, context, filename, cb) { "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(/\uFF00/g, '\n');
input = input.replace(/^\(([\s\S]*)\n\)$/m, '$1'); 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 { try {
ast = CoffeeScript.nodes(input); ast = CoffeeScript.nodes(input);
ast = new Block([new Assign(new Value(new Literal('_')), ast, '=')]); ast = new Block([new Assign(new Value(new Literal('_')), ast, '=')]);
@ -33,7 +33,8 @@
return cb(null, vm.runInContext(js, context, filename)); return cb(null, vm.runInContext(js, context, filename));
} catch (_error) { } catch (_error) {
err = _error; err = _error;
return cb(prettyErrorMessage(err, filename, input, true)); err.code || (err.code = input);
return cb(err);
} }
} }
}; };
@ -132,13 +133,13 @@
module.exports = { module.exports = {
start: function(opts) { start: function(opts) {
var build, major, minor, repl, _ref1; var build, major, minor, repl, _ref;
if (opts == null) { if (opts == null) {
opts = {}; opts = {};
} }
_ref1 = process.versions.node.split('.').map(function(n) { _ref = process.versions.node.split('.').map(function(n) {
return parseInt(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) { if (major === 0 && minor < 8) {
console.warn("Node 0.8.0+ required for CoffeeScript REPL"); console.warn("Node 0.8.0+ required for CoffeeScript REPL");
process.exit(1); process.exit(1);

View File

@ -20,6 +20,17 @@ fileExtensions = ['.coffee', '.litcoffee', '.coffee.md']
# Expose helpers for testing. # Expose helpers for testing.
exports.helpers = helpers 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. # Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
# #
# If `options.sourceMap` is specified, then `options.filename` must also be specified. All # 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}` # in which case this returns a `{js, v3SourceMap, sourceMap}`
# object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic # object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic
# lookups. # lookups.
exports.compile = compile = (code, options = {}) -> exports.compile = compile = withPrettyErrors (code, options) ->
{merge} = helpers {merge} = helpers
if options.sourceMap if options.sourceMap
@ -70,13 +81,13 @@ exports.compile = compile = (code, options = {}) ->
js js
# Tokenize a string of CoffeeScript code, and return the array of tokens. # 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 lexer.tokenize code, options
# Parse a string of CoffeeScript code or an array of lexed tokens, and # 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, # return the AST. You can then compile it by calling `.compile()` on the root,
# or traverse it by using `.traverseChildren()` with a callback. # or traverse it by using `.traverseChildren()` with a callback.
exports.nodes = (source, options) -> exports.nodes = withPrettyErrors (source, options) ->
if typeof source is 'string' if typeof source is 'string'
parser.parse lexer.tokenize source, options parser.parse lexer.tokenize source, options
else else
@ -150,8 +161,8 @@ compileFile = (filename, sourceMap) ->
# As the filename and code of a dynamically loaded file will be different # As the filename and code of a dynamically loaded file will be different
# from the original file compiled with CoffeeScript.run, add that # from the original file compiled with CoffeeScript.run, add that
# information to error so it can be pretty-printed later. # information to error so it can be pretty-printed later.
err.filename = filename err.filename or= filename
err.code = stripped err.code or= stripped
throw err throw err
answer answer

View File

@ -141,8 +141,7 @@ compileScript = (file, input, base=null) ->
catch err catch err
CoffeeScript.emit 'failure', err, task CoffeeScript.emit 'failure', err, task
return if CoffeeScript.listeners('failure').length return if CoffeeScript.listeners('failure').length
useColors = process.stdout.isTTY and not process.env.NODE_DISABLE_COLORS message = err.stack or "#{err}"
message = helpers.prettyErrorMessage err, file or '[stdin]', input, useColors
if o.watch if o.watch
printLine message + '\x07' printLine message + '\x07'
else else

View File

@ -134,44 +134,45 @@ exports.isCoffee = (file) -> /\.((lit)?coffee|coffee\.md)$/.test file
# Determine if a filename represents a Literate CoffeeScript file. # Determine if a filename represents a Literate CoffeeScript file.
exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file
# Throws a SyntaxError with a source file location data attached to it in a # Throws a SyntaxError from a given location.
# property called `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) -> exports.throwSyntaxError = (message, location) ->
location.last_line ?= location.first_line
location.last_column ?= location.first_column
error = new SyntaxError message error = new SyntaxError message
error.location = location error.location = location
error.toString = syntaxErrorToString
# Prevent compiler error from showing the compiler's stacktrace.
delete error.stack
throw error throw error
# Creates a nice error message like, following the "standard" format syntaxErrorToString = ->
# <filename>:<line>:<col>: <message> plus the line with the error and a marker return Error::toString.call @ unless @code and @location
# showing where the error is.
exports.prettyErrorMessage = (error, filename, code, useColors) ->
return error.stack or "#{error}" unless error.location
# Prefer original source file information stored in the error if present. {first_line, first_column, last_line, last_column} = @location
filename = error.filename or filename last_line ?= first_line
code = error.code or code last_column ?= first_column
{first_line, first_column, last_line, last_column} = error.location filename = @filename or '[stdin]'
codeLine = code.split('\n')[first_line] codeLine = @code.split('\n')[first_line]
start = first_column start = first_column
# Show only the first line on multi-line errors. # Show only the first line on multi-line errors.
end = if first_line is last_line then last_column + 1 else codeLine.length end = if first_line is last_line then last_column + 1 else codeLine.length
marker = repeat(' ', start) + repeat('^', end - start) 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" colorize = (str) -> "\x1B[1;31m#{str}\x1B[0m"
codeLine = codeLine[...start] + colorize(codeLine[start...end]) + codeLine[end..] codeLine = codeLine[...start] + colorize(codeLine[start...end]) + codeLine[end..]
marker = colorize marker marker = colorize marker
message = """ """
#{filename}:#{first_line + 1}:#{first_column + 1}: error: #{error.message} #{filename}:#{first_line + 1}:#{first_column + 1}: error: #{@message}
#{codeLine} #{codeLine}
#{marker} #{marker}
""" """
# Uncomment to add stacktrace.
#message += "\n#{error.stack}"
message

View File

@ -3,7 +3,7 @@ path = require 'path'
vm = require 'vm' vm = require 'vm'
nodeREPL = require 'repl' nodeREPL = require 'repl'
CoffeeScript = require './coffee-script' CoffeeScript = require './coffee-script'
{merge, prettyErrorMessage} = require './helpers' {merge} = require './helpers'
replDefaults = replDefaults =
prompt: 'coffee> ', prompt: 'coffee> ',
@ -29,7 +29,9 @@ replDefaults =
js = ast.compile bare: yes, locals: Object.keys(context) js = ast.compile bare: yes, locals: Object.keys(context)
cb null, vm.runInContext(js, context, filename) cb null, vm.runInContext(js, context, filename)
catch err 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) -> addMultilineHandler = (repl) ->
{rli, inputStream, outputStream} = repl {rli, inputStream, outputStream} = repl

View File

@ -4,12 +4,10 @@
# Ensure that errors of different kinds (lexer, parser and compiler) are shown # Ensure that errors of different kinds (lexer, parser and compiler) are shown
# in a consistent way. # in a consistent way.
{prettyErrorMessage} = CoffeeScript.helpers
assertErrorFormat = (code, expectedErrorFormat) -> assertErrorFormat = (code, expectedErrorFormat) ->
throws (-> CoffeeScript.run code), (err) -> throws (-> CoffeeScript.run code), (err) ->
message = prettyErrorMessage err, 'test.coffee', code err.colorful = no
eq expectedErrorFormat, message eq expectedErrorFormat, "#{err}"
yes yes
test "lexer errors formating", -> test "lexer errors formating", ->
@ -18,7 +16,7 @@ test "lexer errors formating", ->
insideOutObject = }{ insideOutObject = }{
''', ''',
''' '''
test.coffee:2:19: error: unmatched } [stdin]:2:19: error: unmatched }
insideOutObject = }{ insideOutObject = }{
^ ^
''' '''
@ -28,7 +26,7 @@ test "parser error formating", ->
foo in bar or in baz 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 foo in bar or in baz
^^ ^^
''' '''
@ -38,7 +36,7 @@ test "compiler error formatting", ->
evil = (foo, eval, bar) -> 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) -> evil = (foo, eval, bar) ->
^^^^ ^^^^
''' '''