From 25091fb2a0138d62bcedfa5083aea348392c5984 Mon Sep 17 00:00:00 2001 From: Demian Ferreiro Date: Mon, 25 Feb 2013 14:41:34 -0300 Subject: [PATCH] Improved lexer error messages --- Cakefile | 2 +- lib/coffee-script/command.js | 16 +++++++++---- lib/coffee-script/error.js | 44 ++++++++++++++++++++++++++++++++++++ lib/coffee-script/helpers.js | 4 ++++ lib/coffee-script/lexer.js | 6 +++-- src/command.coffee | 31 ++++++++++++++++--------- src/error.coffee | 27 ++++++++++++++++++++++ src/helpers.coffee | 11 +++++---- src/lexer.coffee | 7 +++--- test/formatting.coffee | 2 +- 10 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 lib/coffee-script/error.js create mode 100644 src/error.coffee diff --git a/Cakefile b/Cakefile index 0afea0a5..577fd4a7 100644 --- a/Cakefile +++ b/Cakefile @@ -91,7 +91,7 @@ task 'build:ultraviolet', 'build and install the Ultraviolet syntax highlighter' task 'build:browser', 'rebuild the merged script for inclusion in the browser', -> code = '' - for name in ['helpers', 'rewriter', 'lexer', 'parser', 'scope', 'nodes', 'coffee-script', 'browser'] + for name in ['helpers', 'error', 'rewriter', 'lexer', 'parser', 'scope', 'nodes', 'coffee-script', 'browser'] code += """ require['./#{name}'] = new function() { var exports = this; diff --git a/lib/coffee-script/command.js b/lib/coffee-script/command.js index 4941505b..dc8954f1 100644 --- a/lib/coffee-script/command.js +++ b/lib/coffee-script/command.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.5.0 (function() { - var BANNER, CoffeeScript, EventEmitter, SWITCHES, coffee_exts, compileJoin, compileOptions, compilePath, compileScript, compileStdio, exec, exists, forkNode, fs, helpers, hidden, joinTimeout, lint, notSources, optionParser, optparse, opts, outputPath, parseOptions, path, printLine, printTokens, printWarn, removeSource, sourceCode, sources, spawn, timeLog, unwatchDir, usage, version, wait, watch, watchDir, watchers, writeJs, _ref, + var BANNER, CoffeeScript, CompilerError, EventEmitter, SWITCHES, coffee_exts, compileJoin, compileOptions, compilePath, compileScript, compileStdio, exec, exists, forkNode, fs, helpers, hidden, joinTimeout, lint, notSources, optionParser, optparse, opts, outputPath, parseOptions, path, printLine, printTokens, printWarn, removeSource, sourceCode, sources, spawn, timeLog, unwatchDir, usage, version, wait, watch, watchDir, watchers, writeJs, _ref, __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; }; fs = require('fs'); @@ -13,6 +13,8 @@ CoffeeScript = require('./coffee-script'); + CompilerError = require('./error').CompilerError; + _ref = require('child_process'), spawn = _ref.spawn, exec = _ref.exec; EventEmitter = require('events').EventEmitter; @@ -159,7 +161,7 @@ }; compileScript = function(file, input, base) { - var o, options, t, task; + var message, o, options, t, task; o = opts; options = compileOptions(file); try { @@ -194,11 +196,15 @@ if (CoffeeScript.listeners('failure').length) { return; } + message = err instanceof CompilerError ? err.prettyMessage(file || '[stdin]', input) : err.stack || ("ERROR: " + err); if (o.watch) { - return printLine(err.message + '\x07'); + if (o.watch) { + return printLine(message + '\x07'); + } + } else { + printWarn(message); + return process.exit(1); } - printWarn(err instanceof Error && err.stack || ("ERROR: " + err)); - return process.exit(1); } }; diff --git a/lib/coffee-script/error.js b/lib/coffee-script/error.js new file mode 100644 index 00000000..bf2602e0 --- /dev/null +++ b/lib/coffee-script/error.js @@ -0,0 +1,44 @@ +// Generated by CoffeeScript 1.5.0 +(function() { + var CompilerError, repeat, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + + repeat = require('./helpers').repeat; + + exports.CompilerError = CompilerError = (function(_super) { + + __extends(CompilerError, _super); + + CompilerError.prototype.name = 'CompilerError'; + + function CompilerError(message, startLine, startColumn, endLine, endColumn) { + this.message = message; + this.startLine = startLine; + this.startColumn = startColumn; + this.endLine = endLine != null ? endLine : this.startLine; + this.endColumn = endColumn != null ? endColumn : this.startColumn; + if (typeof Error.captureStackTrace === "function") { + Error.captureStackTrace(this, CompilerError); + } + } + + CompilerError.prototype.prettyMessage = function(fileName, code) { + var errorLength, errorLine, marker, message; + message = "" + fileName + ":" + this.startLine + ":" + this.startColumn + ": " + this.message; + if (this.startLine === this.endLine) { + errorLine = code.split('\n')[this.startLine - 1]; + errorLength = this.endColumn - this.startColumn + 1; + marker = (repeat(' ', this.startColumn - 1)) + (repeat('^', errorLength)); + message += "\n" + errorLine + "\n" + marker; + } else { + void 0; + } + return message; + }; + + return CompilerError; + + })(Error); + +}).call(this); diff --git a/lib/coffee-script/helpers.js b/lib/coffee-script/helpers.js index 045c5b42..d166e8af 100644 --- a/lib/coffee-script/helpers.js +++ b/lib/coffee-script/helpers.js @@ -12,6 +12,10 @@ return literal === string.substr(string.length - len - (back || 0), len); }; + exports.repeat = function(string, n) { + return (Array(n + 1)).join(string); + }; + exports.compact = function(array) { var item, _i, _len, _results; _results = []; diff --git a/lib/coffee-script/lexer.js b/lib/coffee-script/lexer.js index 6a7b5430..f1e5d464 100644 --- a/lib/coffee-script/lexer.js +++ b/lib/coffee-script/lexer.js @@ -1,12 +1,14 @@ // Generated by CoffeeScript 1.5.0 (function() { - var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARE, COMPOUND_ASSIGN, HEREDOC, HEREDOC_ILLEGAL, HEREDOC_INDENT, HEREGEX, HEREGEX_OMIT, IDENTIFIER, INDEXABLE, INVERSES, JSTOKEN, JS_FORBIDDEN, JS_KEYWORDS, LINE_BREAK, LINE_CONTINUER, LITERATE, LOGIC, Lexer, MATH, MULTILINER, MULTI_DENT, NOT_REGEX, NOT_SPACED_REGEX, NUMBER, OPERATOR, REGEX, RELATION, RESERVED, Rewriter, SHIFT, SIMPLESTR, STRICT_PROSCRIBED, TRAILING_SPACES, UNARY, WHITESPACE, compact, count, key, last, locationDataToString, starts, _ref, _ref1, + var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARE, COMPOUND_ASSIGN, CompilerError, HEREDOC, HEREDOC_ILLEGAL, HEREDOC_INDENT, HEREGEX, HEREGEX_OMIT, IDENTIFIER, INDEXABLE, INVERSES, JSTOKEN, JS_FORBIDDEN, JS_KEYWORDS, LINE_BREAK, LINE_CONTINUER, LITERATE, LOGIC, Lexer, MATH, MULTILINER, MULTI_DENT, NOT_REGEX, NOT_SPACED_REGEX, NUMBER, OPERATOR, REGEX, RELATION, RESERVED, Rewriter, SHIFT, SIMPLESTR, STRICT_PROSCRIBED, TRAILING_SPACES, UNARY, WHITESPACE, compact, count, key, last, locationDataToString, starts, _ref, _ref1, __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; }; _ref = require('./rewriter'), Rewriter = _ref.Rewriter, INVERSES = _ref.INVERSES; _ref1 = require('./helpers'), count = _ref1.count, starts = _ref1.starts, compact = _ref1.compact, last = _ref1.last, locationDataToString = _ref1.locationDataToString; + CompilerError = require('./error').CompilerError; + exports.Lexer = Lexer = (function() { function Lexer() {} @@ -786,7 +788,7 @@ }; Lexer.prototype.error = function(message) { - throw SyntaxError("" + message + " on line " + (this.chunkLine + 1)); + throw new CompilerError(message, this.chunkLine + 1, this.chunkColumn + 1); }; return Lexer; diff --git a/src/command.coffee b/src/command.coffee index 0b8496ce..1ca16b1f 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -5,15 +5,16 @@ # interactive REPL. # External dependencies. -fs = require 'fs' -path = require 'path' -helpers = require './helpers' -optparse = require './optparse' -CoffeeScript = require './coffee-script' -{spawn, exec} = require 'child_process' -{EventEmitter} = require 'events' +fs = require 'fs' +path = require 'path' +helpers = require './helpers' +optparse = require './optparse' +CoffeeScript = require './coffee-script' +{CompilerError} = require './error' +{spawn, exec} = require 'child_process' +{EventEmitter} = require 'events' -exists = fs.exists or path.exists +exists = fs.exists or path.exists # Allow CoffeeScript to emit Node.js events. helpers.extend CoffeeScript, new EventEmitter @@ -138,9 +139,17 @@ compileScript = (file, input, base) -> catch err CoffeeScript.emit 'failure', err, task return if CoffeeScript.listeners('failure').length - return printLine err.message + '\x07' if o.watch - printWarn err instanceof Error and err.stack or "ERROR: #{err}" - process.exit 1 + + message = if err instanceof CompilerError + err.prettyMessage file or '[stdin]', input + else + err.stack or "ERROR: #{err}" + + if o.watch + printLine message + '\x07' if o.watch + else + printWarn message + process.exit 1 # Attach the appropriate listeners to compile scripts incoming over **stdin**, # and write them back to **stdout**. diff --git a/src/error.coffee b/src/error.coffee new file mode 100644 index 00000000..1d131606 --- /dev/null +++ b/src/error.coffee @@ -0,0 +1,27 @@ +{repeat} = require './helpers' + +# A common error class used throughout the compiler to indicate compilation +# errors at a given location in the source code. +exports.CompilerError = class CompilerError extends Error + name: 'CompilerError' + + constructor: (@message, @startLine, @startColumn, + @endLine = @startLine, @endColumn = @startColumn) -> + # Add a stack trace in V8. + Error.captureStackTrace? @, CompilerError + + # Creates a nice error message like, following the "standard" format + # ::: plus the line with the error and a marker + # showing where the error is. + # TODO: tests + prettyMessage: (fileName, code) -> + message = "#{fileName}:#{@startLine}:#{@startColumn}: #{@message}" + if @startLine is @endLine + errorLine = code.split('\n')[@startLine - 1] + errorLength = @endColumn - @startColumn + 1 + marker = (repeat ' ', @startColumn - 1) + (repeat '^', errorLength) + message += "\n#{errorLine}\n#{marker}" + else + # TODO: How do we show multi-line errors? + undefined + message diff --git a/src/helpers.coffee b/src/helpers.coffee index 85d73cec..d1f90485 100644 --- a/src/helpers.coffee +++ b/src/helpers.coffee @@ -11,6 +11,10 @@ exports.ends = (string, literal, back) -> len = literal.length literal is string.substr string.length - len - (back or 0), len +# Repeat a string `n` times. +exports.repeat = (string, n) -> + (Array n + 1).join string + # Trim out all falsy values from an array. exports.compact = (array) -> item for item in array when item @@ -71,8 +75,9 @@ buildLocationData = (first, last) -> last_line: last.last_line last_column: last.last_column -# This returns a function which takes an object as a parameter, and if that object is an AST node, -# updates that object's locationData. The object is returned either way. +# This returns a function which takes an object as a parameter, and if that +# object is an AST node, updates that object's locationData. +# The object is returned either way. exports.addLocationDataFn = (first, last) -> (obj) -> if ((typeof obj) is 'object') and (!!obj['updateLocationDataIfMissing']) @@ -91,5 +96,3 @@ exports.locationDataToString = (obj) -> "#{locationData.last_line + 1}:#{locationData.last_column + 1}" else "No location data" - - diff --git a/src/lexer.coffee b/src/lexer.coffee index 81c2834b..e4e06f63 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -13,6 +13,7 @@ # Import the helpers we need. {count, starts, compact, last, locationDataToString} = require './helpers' +{CompilerError} = require './error' # The Lexer Class # --------------- @@ -41,7 +42,7 @@ exports.Lexer = class Lexer @outdebt = 0 # The under-outdentation at the current level. @indents = [] # The stack of all current indentation levels. @ends = [] # The stack for pairing up tokens. - @tokens = [] # Stream of parsed tokens in the form `['TYPE', value, line]`. + @tokens = [] # Stream of parsed tokens in the form `['TYPE', value, location data]`. @chunkLine = opts.line or 0 # The start line for the current @chunk. @@ -693,11 +694,11 @@ exports.Lexer = class Lexer body = body.replace /// #{quote} ///g, '\\$&' quote + @escapeLines(body, heredoc) + quote - # Throws a syntax error on the current `@line`. + # Throws a compiler error on the current position. error: (message) -> # TODO: Are there some cases we could improve the error line number by # passing the offset in the chunk where the error happened? - throw SyntaxError "#{message} on line #{ @chunkLine + 1 }" + throw new CompilerError message, @chunkLine + 1, @chunkColumn + 1 # Constants # --------- diff --git a/test/formatting.coffee b/test/formatting.coffee index 6f52aa26..99cc15f4 100644 --- a/test/formatting.coffee +++ b/test/formatting.coffee @@ -143,4 +143,4 @@ test "#1299: Disallow token misnesting", -> ''' ok no catch e - eq 'unmatched ] on line 2', e.message + eq 'unmatched ]', e.message