From 4d8cd032987f49b84484d194305ef7f71f3434cc Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 5 Mar 2016 20:59:39 +0100 Subject: [PATCH] Unify, simplify and fixup assignment errors - Show the same type of error message for compound assignment as for `=` assignment when the LHS is invalid. - Show the same type of error message when trying to assign to a CoffeeScript keyword as when trying to assign to a JavaScript keyword. - Now longer treat `&& =` as `&&=`. The same goes for `and=`, `||=` and `or=`. - Unify the error message to: ` '' can't be assigned`. --- lib/coffee-script/lexer.js | 55 ++++++++++++------ lib/coffee-script/nodes.js | 60 ++++++++++--------- src/lexer.coffee | 40 +++++++++---- src/nodes.coffee | 35 ++++++----- test/error_messages.coffee | 116 ++++++++++++++++++++++++++++++++++--- 5 files changed, 228 insertions(+), 78 deletions(-) diff --git a/lib/coffee-script/lexer.js b/lib/coffee-script/lexer.js index ee11bf1f..e1b34ed5 100644 --- a/lib/coffee-script/lexer.js +++ b/lib/coffee-script/lexer.js @@ -1,7 +1,8 @@ // Generated by CoffeeScript 1.10.0 (function() { - var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARE, COMPOUND_ASSIGN, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INVALID_ESCAPE, INVERSES, JSTOKEN, JS_FORBIDDEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, LOGIC, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, key, locationDataToString, ref, ref1, repeat, starts, throwSyntaxError, - 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; }; + var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARE, COMPOUND_ASSIGN, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INVALID_ESCAPE, INVERSES, JSTOKEN, JS_FORBIDDEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, LOGIC, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isUnassignable, key, locationDataToString, ref, ref1, repeat, starts, throwSyntaxError, + 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; }, + slice = [].slice; ref = require('./rewriter'), Rewriter = ref.Rewriter, INVERSES = ref.INVERSES; @@ -495,7 +496,7 @@ }; Lexer.prototype.literalToken = function() { - var match, prev, ref2, ref3, ref4, ref5, ref6, tag, token, value; + var match, message, origin, prev, ref2, ref3, ref4, ref5, ref6, skipToken, tag, token, value; if (match = OPERATOR.exec(this.chunk)) { value = match[0]; if (CODE.test(value)) { @@ -506,16 +507,22 @@ } tag = value; ref2 = this.tokens, prev = ref2[ref2.length - 1]; - if (value === '=' && prev) { - if (prev.variable && (ref3 = prev[1], indexOf.call(JS_FORBIDDEN, ref3) >= 0)) { - if (prev.origin) { - prev = prev.origin; - } - this.error("reserved word '" + prev[1] + "' can't be assigned", prev[2]); - } - if ((ref4 = prev[1]) === '||' || ref4 === '&&') { + if (prev && indexOf.call(['='].concat(slice.call(COMPOUND_ASSIGN)), value) >= 0) { + skipToken = false; + if (value === '=' && ((ref3 = prev[1]) === '||' || ref3 === '&&') && !prev.spaced) { prev[0] = 'COMPOUND_ASSIGN'; prev[1] += '='; + prev = this.tokens[this.tokens.length - 2]; + skipToken = true; + } + if (prev && prev.variable) { + origin = (ref4 = prev.origin) != null ? ref4 : prev; + message = isUnassignable(prev[1], origin[1]); + if (message) { + this.error(message, origin[2]); + } + } + if (skipToken) { return value.length; } } @@ -886,6 +893,24 @@ })(); + isUnassignable = function(name, displayName) { + if (displayName == null) { + displayName = name; + } + switch (false) { + case indexOf.call(slice.call(JS_KEYWORDS).concat(slice.call(COFFEE_KEYWORDS)), name) < 0: + return "keyword '" + displayName + "' can't be assigned"; + case indexOf.call(STRICT_PROSCRIBED, name) < 0: + return "'" + displayName + "' can't be assigned"; + case indexOf.call(RESERVED, name) < 0: + return "reserved word '" + displayName + "' can't be assigned"; + default: + return false; + } + }; + + exports.isUnassignable = isUnassignable; + JS_KEYWORDS = ['true', 'false', 'null', 'this', 'new', 'delete', 'typeof', 'in', 'instanceof', 'return', 'throw', 'break', 'continue', 'debugger', 'yield', 'if', 'else', 'switch', 'for', 'while', 'do', 'try', 'catch', 'finally', 'class', 'extends', 'super']; COFFEE_KEYWORDS = ['undefined', 'then', 'unless', 'until', 'loop', 'of', 'by', 'when']; @@ -915,15 +940,11 @@ RESERVED = ['case', 'default', 'function', 'var', 'void', 'with', 'const', 'let', 'enum', 'export', 'import', 'native', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'static']; - STRICT_PROSCRIBED = ['arguments', 'eval', 'yield*']; + STRICT_PROSCRIBED = ['arguments', 'eval']; JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED).concat(STRICT_PROSCRIBED); - exports.RESERVED = RESERVED.concat(JS_KEYWORDS).concat(COFFEE_KEYWORDS).concat(STRICT_PROSCRIBED); - - exports.STRICT_PROSCRIBED = STRICT_PROSCRIBED; - - exports.JS_FORBIDDEN = JS_FORBIDDEN; + exports.JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED).concat(STRICT_PROSCRIBED); BOM = 65279; diff --git a/lib/coffee-script/nodes.js b/lib/coffee-script/nodes.js index 4c694e4e..7515adf3 100644 --- a/lib/coffee-script/nodes.js +++ b/lib/coffee-script/nodes.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.10.0 (function() { - var Access, Arr, Assign, Base, Block, BooleanLiteral, Call, Class, Code, CodeFragment, Comment, Existence, Expansion, Extends, For, IdentifierLiteral, If, In, Index, InfinityLiteral, JS_FORBIDDEN, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, Literal, NEGATE, NO, NullLiteral, NumberLiteral, Obj, Op, Param, Parens, PassthroughLiteral, RESERVED, Range, RegexLiteral, RegexWithInterpolations, Return, SIMPLENUM, STRICT_PROSCRIBED, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, SuperCall, Switch, TAB, THIS, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addLocationDataFn, compact, del, ends, extend, flatten, fragmentsToText, isComplexOrAssignable, isLiteralArguments, isLiteralThis, locationDataToString, merge, multident, ref1, ref2, some, starts, throwSyntaxError, unfoldSoak, utility, + var Access, Arr, Assign, Base, Block, BooleanLiteral, Call, Class, Code, CodeFragment, Comment, Existence, Expansion, Extends, For, IdentifierLiteral, If, In, Index, InfinityLiteral, JS_FORBIDDEN, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, Literal, NEGATE, NO, NullLiteral, NumberLiteral, Obj, Op, Param, Parens, PassthroughLiteral, Range, RegexLiteral, RegexWithInterpolations, Return, SIMPLENUM, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, SuperCall, Switch, TAB, THIS, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addLocationDataFn, compact, del, ends, extend, flatten, fragmentsToText, isComplexOrAssignable, isLiteralArguments, isLiteralThis, isUnassignable, locationDataToString, merge, multident, ref1, ref2, some, starts, throwSyntaxError, unfoldSoak, utility, extend1 = 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; }, 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; }, @@ -10,7 +10,7 @@ Scope = require('./scope').Scope; - ref1 = require('./lexer'), RESERVED = ref1.RESERVED, STRICT_PROSCRIBED = ref1.STRICT_PROSCRIBED, JS_FORBIDDEN = ref1.JS_FORBIDDEN; + ref1 = require('./lexer'), isUnassignable = ref1.isUnassignable, JS_FORBIDDEN = ref1.JS_FORBIDDEN; ref2 = require('./helpers'), compact = ref2.compact, flatten = ref2.flatten, extend = ref2.extend, merge = ref2.merge, del = ref2.del, starts = ref2.starts, ends = ref2.ends, some = ref2.some, addLocationDataFn = ref2.addLocationDataFn, locationDataToString = ref2.locationDataToString, throwSyntaxError = ref2.throwSyntaxError; @@ -608,10 +608,7 @@ return IdentifierLiteral.__super__.constructor.apply(this, arguments); } - IdentifierLiteral.prototype.isAssignable = function() { - var ref3; - return ref3 = this.value, indexOf.call(RESERVED, ref3) < 0; - }; + IdentifierLiteral.prototype.isAssignable = YES; return IdentifierLiteral; @@ -1580,7 +1577,7 @@ Class.prototype.defaultClassVariableName = '_Class'; Class.prototype.determineName = function() { - var name, node, ref3, tail; + var message, name, node, ref3, tail; if (!this.variable) { return this.defaultClassVariableName; } @@ -1590,6 +1587,12 @@ return this.defaultClassVariableName; } name = node.value; + if (!tail) { + message = isUnassignable(name); + if (message) { + this.variable.error(message); + } + } if (indexOf.call(JS_FORBIDDEN, name) >= 0) { return "_" + name; } else { @@ -1762,7 +1765,6 @@ extend1(Assign, superClass1); function Assign(variable1, value1, context, options) { - var forbidden, name, ref3; this.variable = variable1; this.value = value1; this.context = context; @@ -1770,10 +1772,6 @@ options = {}; } this.param = options.param, this.subpattern = options.subpattern, this.operatorToken = options.operatorToken; - forbidden = (ref3 = (name = this.variable.unwrapAll().value), indexOf.call(STRICT_PROSCRIBED, ref3) >= 0); - if (forbidden && this.context !== 'object') { - this.variable.error("variable name may not be \"" + name + "\""); - } } Assign.prototype.children = ['variable', 'value']; @@ -1823,7 +1821,7 @@ if (!this.context) { varBase = this.variable.unwrapAll(); if (!varBase.isAssignable()) { - this.variable.error("\"" + (this.variable.compile(o)) + "\" cannot be assigned"); + this.variable.error("'" + (this.variable.compile(o)) + "' can't be assigned"); } if (!(typeof varBase.hasProperties === "function" ? varBase.hasProperties() : void 0)) { if (this.param) { @@ -1854,7 +1852,7 @@ }; Assign.prototype.compilePatternMatch = function(o) { - var acc, assigns, code, defaultValue, expandedIdx, fragments, i, idx, isObject, ivar, j, len1, name, obj, objects, olen, ref, ref3, ref4, ref5, ref6, ref7, rest, top, val, value, vvar, vvarText; + var acc, assigns, code, defaultValue, expandedIdx, fragments, i, idx, isObject, ivar, j, len1, message, name, obj, objects, olen, ref, ref3, ref4, ref5, ref6, rest, top, val, value, vvar, vvarText; top = o.level === LEVEL_TOP; value = this.value; objects = this.variable.base.objects; @@ -1889,8 +1887,9 @@ acc = idx.unwrap() instanceof IdentifierLiteral; value = new Value(value); value.properties.push(new (acc ? Access : Index)(idx)); - if (ref5 = obj.unwrap().value, indexOf.call(RESERVED, ref5) >= 0) { - obj.error("assignment to a reserved word: " + (obj.compile(o))); + message = isUnassignable(obj.unwrap().value); + if (message) { + obj.error(message); } if (defaultValue) { value = new Op('?', value, defaultValue); @@ -1945,7 +1944,7 @@ } defaultValue = null; if (obj instanceof Assign && obj.context === 'object') { - ref6 = obj, (ref7 = ref6.variable, idx = ref7.base), obj = ref6.value; + ref5 = obj, (ref6 = ref5.variable, idx = ref6.base), obj = ref5.value; if (obj instanceof Assign) { defaultValue = obj.value; obj = obj.variable; @@ -1964,8 +1963,11 @@ val = new Op('?', val, defaultValue); } } - if ((name != null) && indexOf.call(RESERVED, name) >= 0) { - obj.error("assignment to a reserved word: " + (obj.compile(o))); + if (name != null) { + message = isUnassignable(name); + if (message) { + obj.error(message); + } } assigns.push(new Assign(obj, val, null, { param: this.param, @@ -2223,12 +2225,13 @@ extend1(Param, superClass1); function Param(name1, value1, splat) { - var name, ref3, token; + var message, token; this.name = name1; this.value = value1; this.splat = splat; - if (ref3 = (name = this.name.unwrapAll().value), indexOf.call(STRICT_PROSCRIBED, ref3) >= 0) { - this.name.error("parameter name \"" + name + "\" is not allowed"); + message = isUnassignable(this.name.unwrapAll().value); + if (message) { + this.name.error(message); } if (this.name instanceof Obj && this.name.generated) { token = this.name.objects[0].operatorToken; @@ -2611,7 +2614,7 @@ }; Op.prototype.compileNode = function(o) { - var answer, isChain, lhs, ref3, ref4, rhs; + var answer, isChain, lhs, message, ref3, rhs; isChain = this.isChainable() && this.first.isChainable(); if (!isChain) { this.first.front = this.front; @@ -2619,8 +2622,11 @@ if (this.operator === 'delete' && o.scope.check(this.first.unwrapAll().value)) { this.error('delete operand may not be argument or var'); } - if (((ref3 = this.operator) === '--' || ref3 === '++') && (ref4 = this.first.unwrapAll().value, indexOf.call(STRICT_PROSCRIBED, ref4) >= 0)) { - this.error("cannot increment/decrement \"" + (this.first.unwrapAll().value) + "\""); + if ((ref3 = this.operator) === '--' || ref3 === '++') { + message = isUnassignable(this.first.unwrapAll().value); + if (message) { + this.first.error(message); + } } if (this.isYield()) { return this.compileYield(o); @@ -2858,12 +2864,12 @@ }; Try.prototype.compileNode = function(o) { - var catchPart, ensurePart, generatedErrorVariableName, placeholder, tryPart; + var catchPart, ensurePart, generatedErrorVariableName, message, placeholder, tryPart; o.indent += TAB; tryPart = this.attempt.compileToFragments(o, LEVEL_TOP); catchPart = this.recovery ? (generatedErrorVariableName = o.scope.freeVariable('error', { reserve: false - }), placeholder = new IdentifierLiteral(generatedErrorVariableName), this.errorVariable ? this.recovery.unshift(new Assign(this.errorVariable, placeholder)) : void 0, [].concat(this.makeCode(" catch ("), placeholder.compileToFragments(o), this.makeCode(") {\n"), this.recovery.compileToFragments(o, LEVEL_TOP), this.makeCode("\n" + this.tab + "}"))) : !(this.ensure || this.recovery) ? (generatedErrorVariableName = o.scope.freeVariable('error', { + }), placeholder = new IdentifierLiteral(generatedErrorVariableName), this.errorVariable ? (message = isUnassignable(this.errorVariable.unwrapAll().value), message ? this.errorVariable.error(message) : void 0, this.recovery.unshift(new Assign(this.errorVariable, placeholder))) : void 0, [].concat(this.makeCode(" catch ("), placeholder.compileToFragments(o), this.makeCode(") {\n"), this.recovery.compileToFragments(o, LEVEL_TOP), this.makeCode("\n" + this.tab + "}"))) : !(this.ensure || this.recovery) ? (generatedErrorVariableName = o.scope.freeVariable('error', { reserve: false }), [this.makeCode(" catch (" + generatedErrorVariableName + ") {}")]) : []; ensurePart = this.ensure ? [].concat(this.makeCode(" finally {\n"), this.ensure.compileToFragments(o, LEVEL_TOP), this.makeCode("\n" + this.tab + "}")) : []; diff --git a/src/lexer.coffee b/src/lexer.coffee index 5307fa98..4422cf95 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -411,14 +411,20 @@ exports.Lexer = class Lexer value = @chunk.charAt 0 tag = value [..., prev] = @tokens - if value is '=' and prev - if prev.variable and prev[1] in JS_FORBIDDEN - prev = prev.origin if prev.origin - @error "reserved word '#{prev[1]}' can't be assigned", prev[2] - if prev[1] in ['||', '&&'] + + if prev and value in ['=', COMPOUND_ASSIGN...] + skipToken = false + if value is '=' and prev[1] in ['||', '&&'] and not prev.spaced prev[0] = 'COMPOUND_ASSIGN' prev[1] += '=' - return value.length + prev = @tokens[@tokens.length - 2] + skipToken = true + if prev and prev.variable + origin = prev.origin ? prev + message = isUnassignable prev[1], origin[1] + @error message, origin[2] if message + return value.length if skipToken + if value is ';' @seenFor = no tag = 'TERMINATOR' @@ -744,6 +750,21 @@ exports.Lexer = class Lexer {first_line, first_column, last_column: first_column + (options.length ? 1) - 1} throwSyntaxError message, location +# Helper functions +# ---------------- + +isUnassignable = (name, displayName = name) -> switch + when name in [JS_KEYWORDS..., COFFEE_KEYWORDS...] + "keyword '#{displayName}' can't be assigned" + when name in STRICT_PROSCRIBED + "'#{displayName}' can't be assigned" + when name in RESERVED + "reserved word '#{displayName}' can't be assigned" + else + false + +exports.isUnassignable = isUnassignable + # Constants # --------- @@ -782,15 +803,12 @@ RESERVED = [ 'protected', 'public', 'static' ] -STRICT_PROSCRIBED = ['arguments', 'eval', 'yield*'] +STRICT_PROSCRIBED = ['arguments', 'eval'] # The superset of both JavaScript keywords and reserved words, none of which may # be used as identifiers or properties. JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED).concat(STRICT_PROSCRIBED) - -exports.RESERVED = RESERVED.concat(JS_KEYWORDS).concat(COFFEE_KEYWORDS).concat(STRICT_PROSCRIBED) -exports.STRICT_PROSCRIBED = STRICT_PROSCRIBED -exports.JS_FORBIDDEN = JS_FORBIDDEN +exports.JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED).concat(STRICT_PROSCRIBED) # The character code of the nasty Microsoft madness otherwise known as the BOM. BOM = 65279 diff --git a/src/nodes.coffee b/src/nodes.coffee index 87819613..6f291e42 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -6,7 +6,7 @@ Error.stackTraceLimit = Infinity {Scope} = require './scope' -{RESERVED, STRICT_PROSCRIBED, JS_FORBIDDEN} = require './lexer' +{isUnassignable, JS_FORBIDDEN} = require './lexer' # Import the helpers we plan to use. {compact, flatten, extend, merge, del, starts, ends, some, @@ -411,7 +411,7 @@ exports.RegexLiteral = class RegexLiteral extends Literal exports.PassthroughLiteral = class PassthroughLiteral extends Literal exports.IdentifierLiteral = class IdentifierLiteral extends Literal - isAssignable: -> @value not in RESERVED + isAssignable: YES exports.StatementLiteral = class StatementLiteral extends Literal isStatement: YES @@ -1081,6 +1081,9 @@ exports.Class = class Class extends Base @variable.base return @defaultClassVariableName unless node instanceof IdentifierLiteral name = node.value + unless tail + message = isUnassignable name + @variable.error message if message if name in JS_FORBIDDEN then "_#{name}" else name # For all `this`-references and bound functions in the class definition, @@ -1215,9 +1218,6 @@ exports.Class = class Class extends Base exports.Assign = class Assign extends Base constructor: (@variable, @value, @context, options = {}) -> {@param, @subpattern, @operatorToken} = options - forbidden = (name = @variable.unwrapAll().value) in STRICT_PROSCRIBED - if forbidden and @context isnt 'object' - @variable.error "variable name may not be \"#{name}\"" children: ['variable', 'value'] @@ -1254,7 +1254,7 @@ exports.Assign = class Assign extends Base unless @context varBase = @variable.unwrapAll() unless varBase.isAssignable() - @variable.error "\"#{@variable.compile o}\" cannot be assigned" + @variable.error "'#{@variable.compile o}' can't be assigned" unless varBase.hasProperties?() if @param o.scope.add varBase.value, 'var' @@ -1309,8 +1309,8 @@ exports.Assign = class Assign extends Base acc = idx.unwrap() instanceof IdentifierLiteral value = new Value value value.properties.push new (if acc then Access else Index) idx - if obj.unwrap().value in RESERVED - obj.error "assignment to a reserved word: #{obj.compile o}" + message = isUnassignable obj.unwrap().value + obj.error message if message value = new Op '?', value, defaultValue if defaultValue return new Assign(obj, value, null, param: @param).compileToFragments o, LEVEL_TOP vvar = value.compileToFragments o, LEVEL_LIST @@ -1369,8 +1369,9 @@ exports.Assign = class Assign extends Base acc = idx.unwrap() instanceof IdentifierLiteral val = new Value new Literal(vvarText), [new (if acc then Access else Index) idx] val = new Op '?', val, defaultValue if defaultValue - if name? and name in RESERVED - obj.error "assignment to a reserved word: #{obj.compile o}" + if name? + message = isUnassignable name + obj.error message if message assigns.push new Assign(obj, val, null, param: @param, subpattern: yes).compileToFragments o, LEVEL_LIST assigns.push vvar unless top or @subpattern fragments = @joinFragmentArrays assigns, ', ' @@ -1527,8 +1528,8 @@ exports.Code = class Code extends Base # as well as be a splat, gathering up a group of parameters into an array. exports.Param = class Param extends Base constructor: (@name, @value, @splat) -> - if (name = @name.unwrapAll().value) in STRICT_PROSCRIBED - @name.error "parameter name \"#{name}\" is not allowed" + message = isUnassignable @name.unwrapAll().value + @name.error message if message if @name instanceof Obj and @name.generated token = @name.objects[0].operatorToken token.error "unexpected #{token.value}" @@ -1817,8 +1818,9 @@ exports.Op = class Op extends Base @first.front = @front unless isChain if @operator is 'delete' and o.scope.check(@first.unwrapAll().value) @error 'delete operand may not be argument or var' - if @operator in ['--', '++'] and @first.unwrapAll().value in STRICT_PROSCRIBED - @error "cannot increment/decrement \"#{@first.unwrapAll().value}\"" + if @operator in ['--', '++'] + message = isUnassignable @first.unwrapAll().value + @first.error message if message return @compileYield o if @isYield() return @compileUnary o if @isUnary() return @compileChain o if isChain @@ -1969,7 +1971,10 @@ exports.Try = class Try extends Base catchPart = if @recovery generatedErrorVariableName = o.scope.freeVariable 'error', reserve: no placeholder = new IdentifierLiteral generatedErrorVariableName - @recovery.unshift new Assign @errorVariable, placeholder if @errorVariable + if @errorVariable + message = isUnassignable @errorVariable.unwrapAll().value + @errorVariable.error message if message + @recovery.unshift new Assign @errorVariable, placeholder [].concat @makeCode(" catch ("), placeholder.compileToFragments(o), @makeCode(") {\n"), @recovery.compileToFragments(o, LEVEL_TOP), @makeCode("\n#{@tab}}") else unless @ensure or @recovery diff --git a/test/error_messages.coffee b/test/error_messages.coffee index 0e55bf87..8782f8ec 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -36,7 +36,7 @@ test "compiler error formatting", -> evil = (foo, eval, bar) -> ''', ''' - [stdin]:1:14: error: parameter name "eval" is not allowed + [stdin]:1:14: error: 'eval' can't be assigned evil = (foo, eval, bar) -> ^^^^ ''' @@ -656,22 +656,88 @@ test "reserved words", -> case ^^^^ ''' + assertErrorFormat ''' + case = 1 + ''', ''' + [stdin]:1:1: error: reserved word 'case' + case = 1 + ^^^^ + ''' assertErrorFormat ''' for = 1 ''', ''' - [stdin]:1:1: error: reserved word 'for' can't be assigned + [stdin]:1:1: error: keyword 'for' can't be assigned for = 1 ^^^ ''' + assertErrorFormat ''' + unless = 1 + ''', ''' + [stdin]:1:1: error: keyword 'unless' can't be assigned + unless = 1 + ^^^^^^ + ''' + assertErrorFormat ''' + for += 1 + ''', ''' + [stdin]:1:1: error: keyword 'for' can't be assigned + for += 1 + ^^^ + ''' + assertErrorFormat ''' + for &&= 1 + ''', ''' + [stdin]:1:1: error: keyword 'for' can't be assigned + for &&= 1 + ^^^ + ''' + # Make sure token look-behind doesn't go out of range. + assertErrorFormat ''' + &&= 1 + ''', ''' + [stdin]:1:1: error: unexpected &&= + &&= 1 + ^^^ + ''' # #2306: Show unaliased name in error messages. assertErrorFormat ''' on = 1 ''', ''' - [stdin]:1:1: error: reserved word 'on' can't be assigned + [stdin]:1:1: error: keyword 'on' can't be assigned on = 1 ^^ ''' +test "strict mode errors", -> + assertErrorFormat ''' + eval = 1 + ''', ''' + [stdin]:1:1: error: 'eval' can't be assigned + eval = 1 + ^^^^ + ''' + assertErrorFormat ''' + class eval + ''', ''' + [stdin]:1:7: error: 'eval' can't be assigned + class eval + ^^^^ + ''' + assertErrorFormat ''' + arguments++ + ''', ''' + [stdin]:1:1: error: 'arguments' can't be assigned + arguments++ + ^^^^^^^^^ + ''' + assertErrorFormat ''' + --arguments + ''', ''' + [stdin]:1:3: error: 'arguments' can't be assigned + --arguments + ^^^^^^^^^ + ''' + test "invalid numbers", -> assertErrorFormat ''' 0X0 @@ -832,35 +898,35 @@ test "#4130: unassignable in destructured param", -> }) -> console.log "Oh hello!" ''', ''' - [stdin]:2:12: error: assignment to a reserved word: null + [stdin]:2:12: error: keyword 'null' can't be assigned @param : null ^^^^ ''' assertErrorFormat ''' ({a: null}) -> ''', ''' - [stdin]:1:6: error: assignment to a reserved word: null + [stdin]:1:6: error: keyword 'null' can't be assigned ({a: null}) -> ^^^^ ''' assertErrorFormat ''' ({a: 1}) -> ''', ''' - [stdin]:1:6: error: "1" cannot be assigned + [stdin]:1:6: error: '1' can't be assigned ({a: 1}) -> ^ ''' assertErrorFormat ''' ({1}) -> ''', ''' - [stdin]:1:3: error: "1" cannot be assigned + [stdin]:1:3: error: '1' can't be assigned ({1}) -> ^ ''' assertErrorFormat ''' ({a: true = 1}) -> ''', ''' - [stdin]:1:6: error: reserved word 'true' can't be assigned + [stdin]:1:6: error: keyword 'true' can't be assigned ({a: true = 1}) -> ^^^^ ''' @@ -889,3 +955,37 @@ test "#4097: `yield return` as an expression", -> -> (yield return) ^^^^^^^^^^^^ ''' + +test "`&&=` and `||=` with a space in-between", -> + assertErrorFormat ''' + a = 0 + a && = 1 + ''', ''' + [stdin]:2:6: error: unexpected = + a && = 1 + ^ + ''' + assertErrorFormat ''' + a = 0 + a and = 1 + ''', ''' + [stdin]:2:7: error: unexpected = + a and = 1 + ^ + ''' + assertErrorFormat ''' + a = 0 + a || = 1 + ''', ''' + [stdin]:2:6: error: unexpected = + a || = 1 + ^ + ''' + assertErrorFormat ''' + a = 0 + a or = 1 + ''', ''' + [stdin]:2:6: error: unexpected = + a or = 1 + ^ + '''