From 5596dac5e1349aac5fc7f82ab0600dcb7f3bc7cb Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Mon, 3 Apr 2017 22:36:00 -0700 Subject: [PATCH 1/9] Throw an error for ambiguous `get` or `set` function calls or ES5 getter/setter keywords, to warn the user to use parentheses if they intend a function call (or to inform them that `get` or `set` cannot be used as a keyword) --- lib/coffeescript/lexer.js | 6 +- src/lexer.coffee | 2 + src/nodes.coffee | 2 +- test/error_messages.coffee | 116 ++++++++++++++++++++++++++++++++ test/function_invocation.coffee | 40 ++++++++++- 5 files changed, 162 insertions(+), 4 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index ea1479da61..13100f2a96 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -64,7 +64,7 @@ } identifierToken() { - var alias, colon, colonOffset, id, idLength, input, match, poppedToken, prev, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, tag, tagToken; + var alias, colon, colonOffset, id, idLength, input, match, poppedToken, prev, ref10, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, tag, tagToken; if (!(match = IDENTIFIER.exec(this.chunk))) { return 0; } @@ -129,6 +129,8 @@ } else if (tag === 'IDENTIFIER' && this.seenFor && id === 'from' && isForFrom(prev)) { tag = 'FORFROM'; this.seenFor = false; + } else if (tag === 'PROPERTY' && prev && prev.spaced && (ref9 = prev[0], indexOf.call(CALLABLE, ref9) >= 0) && /^[g|s]et$/.test(prev[1])) { + this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); } if (tag === 'IDENTIFIER' && indexOf.call(RESERVED, id) >= 0) { this.error(`reserved word '${id}'`, { @@ -167,7 +169,7 @@ tagToken.origin = [tag, alias, tagToken[2]]; } if (poppedToken) { - ref9 = [poppedToken[2].first_line, poppedToken[2].first_column], tagToken[2].first_line = ref9[0], tagToken[2].first_column = ref9[1]; + ref10 = [poppedToken[2].first_line, poppedToken[2].first_column], tagToken[2].first_line = ref10[0], tagToken[2].first_column = ref10[1]; } if (colon) { colonOffset = input.lastIndexOf(':'); diff --git a/src/lexer.coffee b/src/lexer.coffee index dab265de27..92c59030bc 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -170,6 +170,8 @@ exports.Lexer = class Lexer isForFrom(prev) tag = 'FORFROM' @seenFor = no + else if tag is 'PROPERTY' and prev and prev.spaced and prev[0] in CALLABLE and /^[g|s]et$/.test(prev[1]) + @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] if tag is 'IDENTIFIER' and id in RESERVED @error "reserved word '#{id}'", length: id.length diff --git a/src/nodes.coffee b/src/nodes.coffee index cc2a6dabc7..c14b576cd3 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -764,7 +764,7 @@ exports.Call = class Call extends Base constructor: (@variable, @args = [], @soak) -> super() - @isNew = false + @isNew = no if @variable instanceof Value and @variable.isNotCallable() @variable.error "literal is not a function" diff --git a/test/error_messages.coffee b/test/error_messages.coffee index bbf7a02706..92cb3f124f 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -1340,3 +1340,119 @@ test "new with 'super'", -> class extends A then foo: -> new super() ^^^^^ ''' + +test "getter keyword in object", -> + assertErrorFormat ''' + obj = + get foo: -> + ''', ''' + [stdin]:2:3: error: 'get' cannot be used as a keyword, or as a function call without parentheses + get foo: -> + ^^^ + ''' + +test "setter keyword in object", -> + assertErrorFormat ''' + obj = + set foo: -> + ''', ''' + [stdin]:2:3: error: 'set' cannot be used as a keyword, or as a function call without parentheses + set foo: -> + ^^^ + ''' + +test "getter keyword in inline implicit object", -> + assertErrorFormat 'obj = get foo: ->', ''' + [stdin]:1:7: error: 'get' cannot be used as a keyword, or as a function call without parentheses + obj = get foo: -> + ^^^ + ''' + +test "setter keyword in inline implicit object", -> + assertErrorFormat 'obj = set foo: ->', ''' + [stdin]:1:7: error: 'set' cannot be used as a keyword, or as a function call without parentheses + obj = set foo: -> + ^^^ + ''' + +test "getter keyword in inline explicit object", -> + assertErrorFormat 'obj = {get foo: ->}', ''' + [stdin]:1:8: error: 'get' cannot be used as a keyword, or as a function call without parentheses + obj = {get foo: ->} + ^^^ + ''' + +test "setter keyword in inline explicit object", -> + assertErrorFormat 'obj = {set foo: ->}', ''' + [stdin]:1:8: error: 'set' cannot be used as a keyword, or as a function call without parentheses + obj = {set foo: ->} + ^^^ + ''' + +test "getter keyword in function", -> + assertErrorFormat ''' + f = -> + get foo: -> + ''', ''' + [stdin]:2:3: error: 'get' cannot be used as a keyword, or as a function call without parentheses + get foo: -> + ^^^ + ''' + +test "setter keyword in function", -> + assertErrorFormat ''' + f = -> + set foo: -> + ''', ''' + [stdin]:2:3: error: 'set' cannot be used as a keyword, or as a function call without parentheses + set foo: -> + ^^^ + ''' + +test "getter keyword in inline function", -> + assertErrorFormat 'f = -> get foo: ->', ''' + [stdin]:1:8: error: 'get' cannot be used as a keyword, or as a function call without parentheses + f = -> get foo: -> + ^^^ + ''' + +test "setter keyword in inline function", -> + assertErrorFormat 'f = -> set foo: ->', ''' + [stdin]:1:8: error: 'set' cannot be used as a keyword, or as a function call without parentheses + f = -> set foo: -> + ^^^ + ''' + +test "getter keyword in class", -> + assertErrorFormat ''' + class A + get foo: -> + ''', ''' + [stdin]:2:3: error: 'get' cannot be used as a keyword, or as a function call without parentheses + get foo: -> + ^^^ + ''' + +test "setter keyword in class", -> + assertErrorFormat ''' + class A + set foo: -> + ''', ''' + [stdin]:2:3: error: 'set' cannot be used as a keyword, or as a function call without parentheses + set foo: -> + ^^^ + ''' + +test "getter keyword in inline class", -> + assertErrorFormat 'class A then get foo: ->', ''' + [stdin]:1:14: error: 'get' cannot be used as a keyword, or as a function call without parentheses + class A then get foo: -> + ^^^ + ''' + +test "setter keyword in inline class", -> + assertErrorFormat 'class A then set foo: ->', ''' + [stdin]:1:14: error: 'set' cannot be used as a keyword, or as a function call without parentheses + class A then set foo: -> + ^^^ + ''' diff --git a/test/function_invocation.coffee b/test/function_invocation.coffee index 7baf0b09ac..df57f4e3d1 100644 --- a/test/function_invocation.coffee +++ b/test/function_invocation.coffee @@ -675,7 +675,7 @@ test "Non-callable literals shouldn't compile", -> cantCompile '[1..10][2..9] 2' cantCompile '[1..10][2..9](2)' -test 'implicit invocation with implicit object literal', -> +test "implicit invocation with implicit object literal", -> f = (obj) -> eq 1, obj.a f @@ -706,3 +706,41 @@ test 'implicit invocation with implicit object literal', -> else "#{a}": 1 eq 2, obj.a + +test "get and set can be used as function names when not ambiguous with `get`/`set` keywords", -> + get = (val) -> val + set = (val) -> val + eq 2, get(2) + eq 3, set(3) + + get = ({val}) -> val + set = ({val}) -> val + eq 4, get({val: 4}) + eq 5, set({val: 5}) + +test "get and set can be used as variable and property names", -> + get = 2 + set = 3 + eq 2, get + eq 3, set + + {get} = {get: 4} + {set} = {set: 5} + eq 4, get + eq 5, set + +test "get and set can be used as class method names", -> + class A + get: -> 2 + set: -> 3 + + a = new A() + eq 2, a.get() + eq 3, a.set() + + class B + @get = -> 4 + @set = -> 5 + + eq 4, B.get() + eq 5, B.set() From 725fe8e0187f6657ad3f786ca68af829eea225ad Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Tue, 4 Apr 2017 00:04:50 -0700 Subject: [PATCH 2/9] Code golf --- lib/coffeescript/lexer.js | 2 +- src/lexer.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 13100f2a96..f5be046710 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -129,7 +129,7 @@ } else if (tag === 'IDENTIFIER' && this.seenFor && id === 'from' && isForFrom(prev)) { tag = 'FORFROM'; this.seenFor = false; - } else if (tag === 'PROPERTY' && prev && prev.spaced && (ref9 = prev[0], indexOf.call(CALLABLE, ref9) >= 0) && /^[g|s]et$/.test(prev[1])) { + } else if (tag === 'PROPERTY' && prev && prev.spaced && (ref9 = prev[0], indexOf.call(CALLABLE, ref9) >= 0) && /^[gs]et$/.test(prev[1])) { this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); } if (tag === 'IDENTIFIER' && indexOf.call(RESERVED, id) >= 0) { diff --git a/src/lexer.coffee b/src/lexer.coffee index 92c59030bc..0a63cc5fc8 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -170,7 +170,7 @@ exports.Lexer = class Lexer isForFrom(prev) tag = 'FORFROM' @seenFor = no - else if tag is 'PROPERTY' and prev and prev.spaced and prev[0] in CALLABLE and /^[g|s]et$/.test(prev[1]) + else if tag is 'PROPERTY' and prev and prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] if tag is 'IDENTIFIER' and id in RESERVED From fcf7ddaf45353039269f67bc73ba876168b688d1 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Tue, 4 Apr 2017 16:31:52 -0700 Subject: [PATCH 3/9] Catch get or set keyword before static method --- lib/coffeescript/lexer.js | 15 +++++++++++---- src/lexer.coffee | 9 +++++++-- test/error_messages.coffee | 20 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index f5be046710..d2cd515152 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -64,7 +64,7 @@ } identifierToken() { - var alias, colon, colonOffset, id, idLength, input, match, poppedToken, prev, ref10, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, tag, tagToken; + var alias, colon, colonOffset, id, idLength, input, match, poppedToken, prev, prevprev, ref10, ref11, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, tag, tagToken; if (!(match = IDENTIFIER.exec(this.chunk))) { return 0; } @@ -129,8 +129,15 @@ } else if (tag === 'IDENTIFIER' && this.seenFor && id === 'from' && isForFrom(prev)) { tag = 'FORFROM'; this.seenFor = false; - } else if (tag === 'PROPERTY' && prev && prev.spaced && (ref9 = prev[0], indexOf.call(CALLABLE, ref9) >= 0) && /^[gs]et$/.test(prev[1])) { - this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); + } else if (tag === 'PROPERTY' && prev) { + if (prev.spaced && (ref9 = prev[0], indexOf.call(CALLABLE, ref9) >= 0) && /^[gs]et$/.test(prev[1])) { + this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); + } else { + prevprev = this.tokens[this.tokens.length - 2]; + if (((ref10 = prev[0]) === '@' || ref10 === 'THIS') && prevprev && prevprev.spaced && /^[gs]et$/.test(prevprev[1])) { + this.error(`'${prevprev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prevprev[2]); + } + } } if (tag === 'IDENTIFIER' && indexOf.call(RESERVED, id) >= 0) { this.error(`reserved word '${id}'`, { @@ -169,7 +176,7 @@ tagToken.origin = [tag, alias, tagToken[2]]; } if (poppedToken) { - ref10 = [poppedToken[2].first_line, poppedToken[2].first_column], tagToken[2].first_line = ref10[0], tagToken[2].first_column = ref10[1]; + ref11 = [poppedToken[2].first_line, poppedToken[2].first_column], tagToken[2].first_line = ref11[0], tagToken[2].first_column = ref11[1]; } if (colon) { colonOffset = input.lastIndexOf(':'); diff --git a/src/lexer.coffee b/src/lexer.coffee index 0a63cc5fc8..25da3c4ad0 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -170,8 +170,13 @@ exports.Lexer = class Lexer isForFrom(prev) tag = 'FORFROM' @seenFor = no - else if tag is 'PROPERTY' and prev and prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) - @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] + else if tag is 'PROPERTY' and prev + if prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) + @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] + else + prevprev = @tokens[@tokens.length - 2] + if prev[0] in ['@', 'THIS'] and prevprev and prevprev.spaced and /^[gs]et$/.test(prevprev[1]) + @error "'#{prevprev[1]}' cannot be used as a keyword, or as a function call without parentheses", prevprev[2] if tag is 'IDENTIFIER' and id in RESERVED @error "reserved word '#{id}'", length: id.length diff --git a/test/error_messages.coffee b/test/error_messages.coffee index 92cb3f124f..17d823df6f 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -1456,3 +1456,23 @@ test "setter keyword in inline class", -> class A then set foo: -> ^^^ ''' + +test "getter keyword before static method", -> + assertErrorFormat ''' + class A + get @foo = -> + ''', ''' + [stdin]:2:3: error: 'get' cannot be used as a keyword, or as a function call without parentheses + get @foo = -> + ^^^ + ''' + +test "setter keyword before static method", -> + assertErrorFormat ''' + class A + set @foo = -> + ''', ''' + [stdin]:2:3: error: 'set' cannot be used as a keyword, or as a function call without parentheses + set @foo = -> + ^^^ + ''' From ed4c8286a2fbc67f6dd69d4955db1de554e4eecb Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Tue, 4 Apr 2017 17:00:43 -0700 Subject: [PATCH 4/9] DRY up getting the previous token --- src/lexer.coffee | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lexer.coffee b/src/lexer.coffee index 25da3c4ad0..1239d1b73a 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -132,7 +132,7 @@ exports.Lexer = class Lexer @token 'DEFAULT', id return id.length - [..., prev] = @tokens + prev = @prev() tag = if colon or prev? and @@ -242,6 +242,8 @@ exports.Lexer = class Lexer [quote] = STRING_START.exec(@chunk) || [] return 0 unless quote + prev = @prev() + # If the preceding token is `from` and this is an import or export statement, # properly tag the `from`. if @tokens.length and @value() is 'from' and (@seenImport or @seenExport) @@ -325,7 +327,7 @@ exports.Lexer = class Lexer [regex, body, closed] = match @validateEscapes body, isRegex: yes, offsetInChunk: 1 index = regex.length - [..., prev] = @tokens + prev = @prev() if prev if prev.spaced and prev[0] in CALLABLE return 0 if not closed or POSSIBLY_DIVISION.test regex @@ -450,7 +452,7 @@ exports.Lexer = class Lexer whitespaceToken: -> return 0 unless (match = WHITESPACE.exec @chunk) or (nline = @chunk.charAt(0) is '\n') - [..., prev] = @tokens + prev = @prev() prev[if match then 'spaced' else 'newLine'] = true if prev if match then match[0].length else 0 @@ -478,7 +480,7 @@ exports.Lexer = class Lexer else value = @chunk.charAt 0 tag = value - [..., prev] = @tokens + prev = @prev() if prev and value in ['=', COMPOUND_ASSIGN...] skipToken = false @@ -768,6 +770,11 @@ exports.Lexer = class Lexer [..., token] = @tokens token?[1] + # Get the previous token in the token stream. + prev: -> + [..., token] = @tokens + token if token? + # Are we in the midst of an unfinished expression? unfinished: -> LINE_CONTINUER.test(@chunk) or From 962374aec1a0f238ff3c40817d8cff9e9672809b Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Tue, 4 Apr 2017 17:03:11 -0700 Subject: [PATCH 5/9] Throw an error if get or set are used as keywords before what looks like a function or method with an interpolated/dynamic name --- lib/coffeescript/lexer.js | 60 ++++++++++++++++++++------------- src/lexer.coffee | 10 ++++-- test/error_messages.coffee | 20 +++++++++++ test/function_invocation.coffee | 4 +++ 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index d2cd515152..3bc2d48c8f 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -64,7 +64,7 @@ } identifierToken() { - var alias, colon, colonOffset, id, idLength, input, match, poppedToken, prev, prevprev, ref10, ref11, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, tag, tagToken; + var alias, colon, colonOffset, id, idLength, input, match, poppedToken, prev, prevprev, ref10, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, tag, tagToken; if (!(match = IDENTIFIER.exec(this.chunk))) { return 0; } @@ -98,11 +98,11 @@ this.token('DEFAULT', id); return id.length; } - ref6 = this.tokens, prev = ref6[ref6.length - 1]; - tag = colon || (prev != null) && (((ref7 = prev[0]) === '.' || ref7 === '?.' || ref7 === '::' || ref7 === '?::') || !prev.spaced && prev[0] === '@') ? 'PROPERTY' : 'IDENTIFIER'; + prev = this.prev(); + tag = colon || (prev != null) && (((ref6 = prev[0]) === '.' || ref6 === '?.' || ref6 === '::' || ref6 === '?::') || !prev.spaced && prev[0] === '@') ? 'PROPERTY' : 'IDENTIFIER'; if (tag === 'IDENTIFIER' && (indexOf.call(JS_KEYWORDS, id) >= 0 || indexOf.call(COFFEE_KEYWORDS, id) >= 0) && !(this.exportSpecifierList && indexOf.call(COFFEE_KEYWORDS, id) >= 0)) { tag = id.toUpperCase(); - if (tag === 'WHEN' && (ref8 = this.tag(), indexOf.call(LINE_BREAK, ref8) >= 0)) { + if (tag === 'WHEN' && (ref7 = this.tag(), indexOf.call(LINE_BREAK, ref7) >= 0)) { tag = 'LEADING_WHEN'; } else if (tag === 'FOR') { this.seenFor = true; @@ -130,11 +130,11 @@ tag = 'FORFROM'; this.seenFor = false; } else if (tag === 'PROPERTY' && prev) { - if (prev.spaced && (ref9 = prev[0], indexOf.call(CALLABLE, ref9) >= 0) && /^[gs]et$/.test(prev[1])) { + if (prev.spaced && (ref8 = prev[0], indexOf.call(CALLABLE, ref8) >= 0) && /^[gs]et$/.test(prev[1])) { this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); } else { prevprev = this.tokens[this.tokens.length - 2]; - if (((ref10 = prev[0]) === '@' || ref10 === 'THIS') && prevprev && prevprev.spaced && /^[gs]et$/.test(prevprev[1])) { + if (((ref9 = prev[0]) === '@' || ref9 === 'THIS') && prevprev && prevprev.spaced && /^[gs]et$/.test(prevprev[1])) { this.error(`'${prevprev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prevprev[2]); } } @@ -176,7 +176,7 @@ tagToken.origin = [tag, alias, tagToken[2]]; } if (poppedToken) { - ref11 = [poppedToken[2].first_line, poppedToken[2].first_column], tagToken[2].first_line = ref11[0], tagToken[2].first_column = ref11[1]; + ref10 = [poppedToken[2].first_line, poppedToken[2].first_column], tagToken[2].first_line = ref10[0], tagToken[2].first_column = ref10[1]; } if (colon) { colonOffset = input.lastIndexOf(':'); @@ -232,13 +232,17 @@ } stringToken() { - var $, attempt, delimiter, doc, end, heredoc, i, indent, indentRegex, match, quote, ref2, ref3, regex, token, tokens; + var $, attempt, delimiter, doc, end, heredoc, i, indent, indentRegex, match, prev, quote, ref2, ref3, ref4, regex, token, tokens; quote = (STRING_START.exec(this.chunk) || [])[0]; if (!quote) { return 0; } - if (this.tokens.length && this.value() === 'from' && (this.seenImport || this.seenExport)) { - this.tokens[this.tokens.length - 1][0] = 'FROM'; + prev = this.prev(); + if (prev && this.value() === 'from' && (this.seenImport || this.seenExport)) { + prev[0] = 'FROM'; + } + if (prev && prev.spaced && (ref2 = prev[0], indexOf.call(CALLABLE, ref2) >= 0) && /^[gs]et$/.test(prev[1])) { + this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); } regex = (function() { switch (quote) { @@ -253,7 +257,7 @@ } })(); heredoc = quote.length === 3; - ref2 = this.matchWithInterpolations(regex, quote), tokens = ref2.tokens, end = ref2.index; + ref3 = this.matchWithInterpolations(regex, quote), tokens = ref3.tokens, end = ref3.index; $ = tokens.length - 1; delimiter = quote.charAt(0); if (heredoc) { @@ -271,7 +275,7 @@ })()).join('#{}'); while (match = HEREDOC_INDENT.exec(doc)) { attempt = match[1]; - if (indent === null || (0 < (ref3 = attempt.length) && ref3 < indent.length)) { + if (indent === null || (0 < (ref4 = attempt.length) && ref4 < indent.length)) { indent = attempt; } } @@ -345,7 +349,7 @@ } regexToken() { - var body, closed, end, flags, index, match, origin, prev, ref2, ref3, ref4, regex, tokens; + var body, closed, end, flags, index, match, origin, prev, ref2, ref3, regex, tokens; switch (false) { case !(match = REGEX_ILLEGAL.exec(this.chunk)): this.error(`regular expressions cannot begin with ${match[2]}`, { @@ -362,13 +366,13 @@ offsetInChunk: 1 }); index = regex.length; - ref2 = this.tokens, prev = ref2[ref2.length - 1]; + prev = this.prev(); if (prev) { - if (prev.spaced && (ref3 = prev[0], indexOf.call(CALLABLE, ref3) >= 0)) { + if (prev.spaced && (ref2 = prev[0], indexOf.call(CALLABLE, ref2) >= 0)) { if (!closed || POSSIBLY_DIVISION.test(regex)) { return 0; } - } else if (ref4 = prev[0], indexOf.call(NOT_REGEX, ref4) >= 0) { + } else if (ref3 = prev[0], indexOf.call(NOT_REGEX, ref3) >= 0) { return 0; } } @@ -517,11 +521,11 @@ } whitespaceToken() { - var match, nline, prev, ref2; + var match, nline, prev; if (!((match = WHITESPACE.exec(this.chunk)) || (nline = this.chunk.charAt(0) === '\n'))) { return 0; } - ref2 = this.tokens, prev = ref2[ref2.length - 1]; + prev = this.prev(); if (prev) { prev[match ? 'spaced' : 'newLine'] = true; } @@ -550,7 +554,7 @@ } literalToken() { - var match, message, origin, prev, ref2, ref3, ref4, ref5, ref6, skipToken, tag, token, value; + var match, message, origin, prev, ref2, ref3, ref4, ref5, skipToken, tag, token, value; if (match = OPERATOR.exec(this.chunk)) { value = match[0]; if (CODE.test(value)) { @@ -560,17 +564,17 @@ value = this.chunk.charAt(0); } tag = value; - ref2 = this.tokens, prev = ref2[ref2.length - 1]; + prev = this.prev(); if (prev && indexOf.call(['=', ...COMPOUND_ASSIGN], value) >= 0) { skipToken = false; - if (value === '=' && ((ref3 = prev[1]) === '||' || ref3 === '&&') && !prev.spaced) { + if (value === '=' && ((ref2 = prev[1]) === '||' || ref2 === '&&') && !prev.spaced) { prev[0] = 'COMPOUND_ASSIGN'; prev[1] += '='; prev = this.tokens[this.tokens.length - 2]; skipToken = true; } if (prev && prev[0] !== 'PROPERTY') { - origin = (ref4 = prev.origin) != null ? ref4 : prev; + origin = (ref3 = prev.origin) != null ? ref3 : prev; message = isUnassignable(prev[1], origin[1]); if (message) { this.error(message, origin[2]); @@ -605,12 +609,12 @@ } else if (value === '?' && (prev != null ? prev.spaced : void 0)) { tag = 'BIN?'; } else if (prev && !prev.spaced) { - if (value === '(' && (ref5 = prev[0], indexOf.call(CALLABLE, ref5) >= 0)) { + if (value === '(' && (ref4 = prev[0], indexOf.call(CALLABLE, ref4) >= 0)) { if (prev[0] === '?') { prev[0] = 'FUNC_EXIST'; } tag = 'CALL_START'; - } else if (value === '[' && (ref6 = prev[0], indexOf.call(INDEXABLE, ref6) >= 0)) { + } else if (value === '[' && (ref5 = prev[0], indexOf.call(INDEXABLE, ref5) >= 0)) { tag = 'INDEX_START'; switch (prev[0]) { case '?': @@ -862,6 +866,14 @@ return token != null ? token[1] : void 0; } + prev() { + var ref2, token; + ref2 = this.tokens, token = ref2[ref2.length - 1]; + if (token != null) { + return token; + } + } + unfinished() { var ref2; return LINE_CONTINUER.test(this.chunk) || ((ref2 = this.tag()) === '\\' || ref2 === '.' || ref2 === '?.' || ref2 === '?::' || ref2 === 'UNARY' || ref2 === 'MATH' || ref2 === 'UNARY_MATH' || ref2 === '+' || ref2 === '-' || ref2 === '**' || ref2 === 'SHIFT' || ref2 === 'RELATION' || ref2 === 'COMPARE' || ref2 === '&' || ref2 === '^' || ref2 === '|' || ref2 === '&&' || ref2 === '||' || ref2 === 'BIN?' || ref2 === 'THROW' || ref2 === 'EXTENDS'); diff --git a/src/lexer.coffee b/src/lexer.coffee index 1239d1b73a..3bf22b4b03 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -170,6 +170,9 @@ exports.Lexer = class Lexer isForFrom(prev) tag = 'FORFROM' @seenFor = no + # Throw an error on attempts to use `get` or `set` as keywords, or + # what CoffeeScript would normally interpret as calls to functions named + # `get` or `set`, i.e. `get({foo: function () {}})` else if tag is 'PROPERTY' and prev if prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] @@ -246,8 +249,11 @@ exports.Lexer = class Lexer # If the preceding token is `from` and this is an import or export statement, # properly tag the `from`. - if @tokens.length and @value() is 'from' and (@seenImport or @seenExport) - @tokens[@tokens.length - 1][0] = 'FROM' + if prev and @value() is 'from' and (@seenImport or @seenExport) + prev[0] = 'FROM' + + if prev and prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) + @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] regex = switch quote when "'" then STRING_SINGLE diff --git a/test/error_messages.coffee b/test/error_messages.coffee index 17d823df6f..835b696e34 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -1476,3 +1476,23 @@ test "setter keyword before static method", -> set @foo = -> ^^^ ''' + +test "getter keyword with dynamic property name", -> + assertErrorFormat ''' + class A + get "#{'foo'}": -> + ''', ''' + [stdin]:2:3: error: 'get' cannot be used as a keyword, or as a function call without parentheses + get "#{'foo'}": -> + ^^^ + ''' + +test "setter keyword with dynamic property name", -> + assertErrorFormat ''' + class A + set "#{'foo'}": -> + ''', ''' + [stdin]:2:3: error: 'set' cannot be used as a keyword, or as a function call without parentheses + set "#{'foo'}": -> + ^^^ + ''' diff --git a/test/function_invocation.coffee b/test/function_invocation.coffee index df57f4e3d1..99e1a4b362 100644 --- a/test/function_invocation.coffee +++ b/test/function_invocation.coffee @@ -712,11 +712,15 @@ test "get and set can be used as function names when not ambiguous with `get`/`s set = (val) -> val eq 2, get(2) eq 3, set(3) + eq 'a', get('a') + eq 'b', set('b') get = ({val}) -> val set = ({val}) -> val eq 4, get({val: 4}) eq 5, set({val: 5}) + eq 'c', get({val: 'c'}) + eq 'd', set({val: 'd'}) test "get and set can be used as variable and property names", -> get = 2 From 2d1addf5a4d2ebdb4039aee8d9f751d2c50b50da Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Thu, 6 Apr 2017 00:47:06 -0700 Subject: [PATCH 6/9] Allow `get` or `set` parentheses-less function calls when first argument is a string without a colon (so a plain string, not a property accessor) --- lib/coffeescript/lexer.js | 2 +- src/lexer.coffee | 2 +- test/function_invocation.coffee | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 3bc2d48c8f..0447d38618 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -241,7 +241,7 @@ if (prev && this.value() === 'from' && (this.seenImport || this.seenExport)) { prev[0] = 'FROM'; } - if (prev && prev.spaced && (ref2 = prev[0], indexOf.call(CALLABLE, ref2) >= 0) && /^[gs]et$/.test(prev[1])) { + if (prev && prev.spaced && (ref2 = prev[0], indexOf.call(CALLABLE, ref2) >= 0) && /^[gs]et$/.test(prev[1]) && /^\S*:\s/.test(this.chunk)) { this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); } regex = (function() { diff --git a/src/lexer.coffee b/src/lexer.coffee index 3bf22b4b03..2288f8fe15 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -252,7 +252,7 @@ exports.Lexer = class Lexer if prev and @value() is 'from' and (@seenImport or @seenExport) prev[0] = 'FROM' - if prev and prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) + if prev and prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) and /^\S*:\s/.test(@chunk) @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] regex = switch quote diff --git a/test/function_invocation.coffee b/test/function_invocation.coffee index 99e1a4b362..9bc9b46497 100644 --- a/test/function_invocation.coffee +++ b/test/function_invocation.coffee @@ -712,15 +712,23 @@ test "get and set can be used as function names when not ambiguous with `get`/`s set = (val) -> val eq 2, get(2) eq 3, set(3) + eq 2, get 2 + eq 3, set 3 eq 'a', get('a') eq 'b', set('b') + eq 'a', get 'a' + eq 'b', set 'b' get = ({val}) -> val set = ({val}) -> val eq 4, get({val: 4}) eq 5, set({val: 5}) + eq 4, get {val: 4} + eq 5, set {val: 5} eq 'c', get({val: 'c'}) eq 'd', set({val: 'd'}) + eq 'c', get {val: 'c'} + eq 'd', set {val: 'd'} test "get and set can be used as variable and property names", -> get = 2 From a7447d5bba9b04cb428f20031aa43d47f5f6bb83 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Thu, 6 Apr 2017 09:28:23 -0700 Subject: [PATCH 7/9] Revert "Allow `get` or `set` parentheses-less function calls when first argument is a string without a colon (so a plain string, not a property accessor)" This reverts commit 2d1addf5a4d2ebdb4039aee8d9f751d2c50b50da. --- lib/coffeescript/lexer.js | 2 +- src/lexer.coffee | 2 +- test/function_invocation.coffee | 8 -------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 0447d38618..3bc2d48c8f 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -241,7 +241,7 @@ if (prev && this.value() === 'from' && (this.seenImport || this.seenExport)) { prev[0] = 'FROM'; } - if (prev && prev.spaced && (ref2 = prev[0], indexOf.call(CALLABLE, ref2) >= 0) && /^[gs]et$/.test(prev[1]) && /^\S*:\s/.test(this.chunk)) { + if (prev && prev.spaced && (ref2 = prev[0], indexOf.call(CALLABLE, ref2) >= 0) && /^[gs]et$/.test(prev[1])) { this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); } regex = (function() { diff --git a/src/lexer.coffee b/src/lexer.coffee index 2288f8fe15..3bf22b4b03 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -252,7 +252,7 @@ exports.Lexer = class Lexer if prev and @value() is 'from' and (@seenImport or @seenExport) prev[0] = 'FROM' - if prev and prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) and /^\S*:\s/.test(@chunk) + if prev and prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] regex = switch quote diff --git a/test/function_invocation.coffee b/test/function_invocation.coffee index 9bc9b46497..99e1a4b362 100644 --- a/test/function_invocation.coffee +++ b/test/function_invocation.coffee @@ -712,23 +712,15 @@ test "get and set can be used as function names when not ambiguous with `get`/`s set = (val) -> val eq 2, get(2) eq 3, set(3) - eq 2, get 2 - eq 3, set 3 eq 'a', get('a') eq 'b', set('b') - eq 'a', get 'a' - eq 'b', set 'b' get = ({val}) -> val set = ({val}) -> val eq 4, get({val: 4}) eq 5, set({val: 5}) - eq 4, get {val: 4} - eq 5, set {val: 5} eq 'c', get({val: 'c'}) eq 'd', set({val: 'd'}) - eq 'c', get {val: 'c'} - eq 'd', set {val: 'd'} test "get and set can be used as variable and property names", -> get = 2 From 4d25907155988a443a57861a1edc8ebfd3ecb7af Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Thu, 6 Apr 2017 10:11:29 -0700 Subject: [PATCH 8/9] Optimization --- lib/coffeescript/lexer.js | 6 +----- src/lexer.coffee | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 3bc2d48c8f..b66eea7029 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -867,11 +867,7 @@ } prev() { - var ref2, token; - ref2 = this.tokens, token = ref2[ref2.length - 1]; - if (token != null) { - return token; - } + return this.tokens[this.tokens.length - 1]; } unfinished() { diff --git a/src/lexer.coffee b/src/lexer.coffee index 3bf22b4b03..06021287db 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -778,8 +778,7 @@ exports.Lexer = class Lexer # Get the previous token in the token stream. prev: -> - [..., token] = @tokens - token if token? + @tokens[@tokens.length - 1] # Are we in the midst of an unfinished expression? unfinished: -> From 0576eb3a107a18cd691beee260d2d6951715dc59 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Thu, 6 Apr 2017 15:23:14 -0700 Subject: [PATCH 9/9] No longer throw an error on `get` or `set` function calls to objects with dynamic property names (introduces a way to circumvent our check for trying to avoid the `get` or `set` keywords, but not worth the complications for this tiny edge case) --- lib/coffeescript/lexer.js | 7 ++----- src/lexer.coffee | 6 +----- test/error_messages.coffee | 20 -------------------- 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index a741d908ee..6d8f8f90bb 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -232,7 +232,7 @@ } stringToken() { - var $, attempt, delimiter, doc, end, heredoc, i, indent, indentRegex, match, prev, quote, ref, ref1, regex, token, tokens; + var $, attempt, delimiter, doc, end, heredoc, i, indent, indentRegex, match, prev, quote, ref, regex, token, tokens; [quote] = STRING_START.exec(this.chunk) || []; if (!quote) { return 0; @@ -241,9 +241,6 @@ if (prev && this.value() === 'from' && (this.seenImport || this.seenExport)) { prev[0] = 'FROM'; } - if (prev && prev.spaced && (ref = prev[0], indexOf.call(CALLABLE, ref) >= 0) && /^[gs]et$/.test(prev[1])) { - this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); - } regex = (function() { switch (quote) { case "'": @@ -278,7 +275,7 @@ })()).join('#{}'); while (match = HEREDOC_INDENT.exec(doc)) { attempt = match[1]; - if (indent === null || (0 < (ref1 = attempt.length) && ref1 < indent.length)) { + if (indent === null || (0 < (ref = attempt.length) && ref < indent.length)) { indent = attempt; } } diff --git a/src/lexer.coffee b/src/lexer.coffee index 06021287db..e66f732c3b 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -245,16 +245,12 @@ exports.Lexer = class Lexer [quote] = STRING_START.exec(@chunk) || [] return 0 unless quote - prev = @prev() - # If the preceding token is `from` and this is an import or export statement, # properly tag the `from`. + prev = @prev() if prev and @value() is 'from' and (@seenImport or @seenExport) prev[0] = 'FROM' - if prev and prev.spaced and prev[0] in CALLABLE and /^[gs]et$/.test(prev[1]) - @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] - regex = switch quote when "'" then STRING_SINGLE when '"' then STRING_DOUBLE diff --git a/test/error_messages.coffee b/test/error_messages.coffee index 835b696e34..17d823df6f 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -1476,23 +1476,3 @@ test "setter keyword before static method", -> set @foo = -> ^^^ ''' - -test "getter keyword with dynamic property name", -> - assertErrorFormat ''' - class A - get "#{'foo'}": -> - ''', ''' - [stdin]:2:3: error: 'get' cannot be used as a keyword, or as a function call without parentheses - get "#{'foo'}": -> - ^^^ - ''' - -test "setter keyword with dynamic property name", -> - assertErrorFormat ''' - class A - set "#{'foo'}": -> - ''', ''' - [stdin]:2:3: error: 'set' cannot be used as a keyword, or as a function call without parentheses - set "#{'foo'}": -> - ^^^ - '''