From 4b284f6687920734c42792233b03ea0402e7cd5a Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 15 Jun 2010 00:54:02 -0400 Subject: [PATCH] first draft at ticket #437 ... automatic quoting of reserved words and keywords. --- lib/lexer.js | 48 +++++++++++++++++++++++++++------------ lib/nodes.js | 9 ++++---- src/lexer.coffee | 17 +++++++++++--- src/nodes.coffee | 7 +++--- test/test_classes.coffee | 9 +++++++- test/test_literals.coffee | 7 ++++++ 6 files changed, 72 insertions(+), 25 deletions(-) diff --git a/lib/lexer.js b/lib/lexer.js index 25a2398e..91561487 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -128,22 +128,32 @@ // referenced as property names here, so you can still do `jQuery.is()` even // though `is` means `===` otherwise. Lexer.prototype.identifierToken = function() { - var forcedIdentifier, id, tag; + var close_index, forcedIdentifier, id, tag; if (!(id = this.match(IDENTIFIER, 1))) { return false; } + this.i += id.length; forcedIdentifier = this.tagAccessor() || this.match(ASSIGNED, 1); tag = 'IDENTIFIER'; if (include(JS_KEYWORDS, id) || (!forcedIdentifier && include(COFFEE_KEYWORDS, id))) { tag = id.toUpperCase(); } - if (include(RESERVED, id)) { - this.identifierError(id); - } if (tag === 'WHEN' && include(LINE_BREAK, this.tag())) { tag = 'LEADING_WHEN'; } - this.i += id.length; + if (include(JS_FORBIDDEN, id)) { + if (forcedIdentifier) { + tag = 'STRING'; + id = ("'" + id + "'"); + if (forcedIdentifier === 'accessor') { + close_index = true; + this.tokens.pop(); + this.token('INDEX_START', '['); + } + } else if (include(RESERVED, id)) { + this.identifierError(id); + } + } if (!(forcedIdentifier)) { if (include(COFFEE_ALIASES, id)) { tag = (id = CONVERSIONS[id]); @@ -153,6 +163,9 @@ } } this.token(tag, id); + if (close_index) { + this.token(']', ']'); + } return true; }; // Matches numbers, including decimals, hex, and exponential notation. @@ -419,21 +432,28 @@ // if it's a special kind of accessor. Return `true` if any type of accessor // is the previous token. Lexer.prototype.tagAccessor = function() { - var prev; + var accessor, prev; if ((!(prev = this.prev())) || (prev && prev.spaced)) { return false; } - if (prev[1] === '::') { - return this.tag(1, 'PROTOTYPE_ACCESS'); - } else if (prev[1] === '.' && !(this.value(2) === '.')) { - if (this.tag(2) === '?') { - this.tag(1, 'SOAK_ACCESS'); - return this.tokens.splice(-2, 1); + accessor = (function() { + if (prev[1] === '::') { + return this.tag(1, 'PROTOTYPE_ACCESS'); + } else if (prev[1] === '.' && !(this.value(2) === '.')) { + if (this.tag(2) === '?') { + this.tag(1, 'SOAK_ACCESS'); + return this.tokens.splice(-2, 1); + } else { + return this.tag(1, 'PROPERTY_ACCESS'); + } } else { - return this.tag(1, 'PROPERTY_ACCESS'); + return prev[0] === '@'; } + }).call(this); + if (accessor) { + return 'accessor'; } else { - return prev[0] === '@'; + return false; } }; // Sanitize a heredoc or herecomment by escaping internal double quotes and diff --git a/lib/nodes.js b/lib/nodes.js index 1a7d5b97..4cffe2e4 100644 --- a/lib/nodes.js +++ b/lib/nodes.js @@ -651,7 +651,7 @@ exports.AccessorNode = (function() { AccessorNode = function(name, tag) { this.name = name; - this.prototype = tag === 'prototype'; + this.prototype = tag === 'prototype' ? '.prototype' : ''; this.soakNode = tag === 'soak'; return this; }; @@ -659,10 +659,11 @@ AccessorNode.prototype.type = 'AccessorNode'; AccessorNode.prototype.children = ['name']; AccessorNode.prototype.compileNode = function(o) { - var protoPart; + var name, namePart; + name = this.name.compile(o); o.chainRoot.wrapped = o.chainRoot.wrapped || this.soakNode; - protoPart = this.prototype ? 'prototype.' : ''; - return "." + protoPart + (this.name.compile(o)); + namePart = name.match(IS_STRING) ? ("[" + name + "]") : ("." + name); + return this.prototype + namePart; }; return AccessorNode; })(); diff --git a/src/lexer.coffee b/src/lexer.coffee index 4f99d6ac..d790bec5 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -90,16 +90,26 @@ exports.Lexer: class Lexer # though `is` means `===` otherwise. identifierToken: -> return false unless id: @match IDENTIFIER, 1 + @i: + id.length forcedIdentifier: @tagAccessor() or @match ASSIGNED, 1 tag: 'IDENTIFIER' tag: id.toUpperCase() if include(JS_KEYWORDS, id) or (not forcedIdentifier and include(COFFEE_KEYWORDS, id)) - @identifierError id if include RESERVED, id tag: 'LEADING_WHEN' if tag is 'WHEN' and include LINE_BREAK, @tag() - @i: + id.length + if include(JS_FORBIDDEN, id) + if forcedIdentifier + tag: 'STRING' + id: "'$id'" + if forcedIdentifier is 'accessor' + close_index: true + @tokens.pop() + @token 'INDEX_START', '[' + else if include(RESERVED, id) + @identifierError id unless forcedIdentifier tag: id: CONVERSIONS[id] if include COFFEE_ALIASES, id return @tagHalfAssignment tag if @prev() and @prev()[0] is 'ASSIGN' and include HALF_ASSIGNMENTS, tag @token tag, id + @token ']', ']' if close_index true # Matches numbers, including decimals, hex, and exponential notation. @@ -289,7 +299,7 @@ exports.Lexer: class Lexer # is the previous token. tagAccessor: -> return false if (not prev: @prev()) or (prev and prev.spaced) - if prev[1] is '::' + accessor: if prev[1] is '::' @tag 1, 'PROTOTYPE_ACCESS' else if prev[1] is '.' and not (@value(2) is '.') if @tag(2) is '?' @@ -299,6 +309,7 @@ exports.Lexer: class Lexer @tag 1, 'PROPERTY_ACCESS' else prev[0] is '@' + if accessor then 'accessor' else false # Sanitize a heredoc or herecomment by escaping internal double quotes and # erasing all external indentation on the left-hand side. diff --git a/src/nodes.coffee b/src/nodes.coffee index fd0846c3..d4b0fe74 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -474,13 +474,14 @@ exports.AccessorNode: class AccessorNode extends BaseNode constructor: (name, tag) -> @name: name - @prototype: tag is 'prototype' + @prototype: if tag is 'prototype' then '.prototype' else '' @soakNode: tag is 'soak' compileNode: (o) -> + name: @name.compile o o.chainRoot.wrapped: or @soakNode - protoPart: if @prototype then 'prototype.' else '' - ".$protoPart${@name.compile(o)}" + namePart: if name.match(IS_STRING) then "[$name]" else ".$name" + @prototype + namePart #### IndexNode diff --git a/test/test_classes.coffee b/test/test_classes.coffee index 16150dc0..b0bb4c3b 100644 --- a/test/test_classes.coffee +++ b/test/test_classes.coffee @@ -117,4 +117,11 @@ class Hive.Bee extends Hive constructor: (name) -> super name maya: new Hive.Bee 'Maya' -ok maya.name is 'Maya' \ No newline at end of file +ok maya.name is 'Maya' + + +# Class with JS-keyword properties. +class Class + class: 'class' + +ok (new Class()).class is 'class' \ No newline at end of file diff --git a/test/test_literals.coffee b/test/test_literals.coffee index 960d5f51..17158c6f 100644 --- a/test/test_literals.coffee +++ b/test/test_literals.coffee @@ -107,3 +107,10 @@ result: [['a'] ok result[0][0] is 'a' ok result[1]['b'] is 'c' + + +# Object literals should be able to include keywords. +obj: {class: 'hot'} +obj.function: 'dog' + +ok obj.class + obj.function is 'hotdog'