From 962374aec1a0f238ff3c40817d8cff9e9672809b Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Tue, 4 Apr 2017 17:03:11 -0700 Subject: [PATCH] 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