Merge pull request #3840 from lydell/dynakeys

Fix #3597: Allow interpolations in object keys
This commit is contained in:
Jeremy Ashkenas
2015-02-10 10:43:26 -05:00
16 changed files with 658 additions and 293 deletions

View File

@@ -264,7 +264,18 @@
token = arg.token;
errorToken = parser.errorToken, tokens = parser.tokens;
errorTag = errorToken[0], errorText = errorToken[1], errorLoc = errorToken[2];
errorText = errorToken === tokens[tokens.length - 1] ? 'end of input' : errorTag === 'INDENT' || errorTag === 'OUTDENT' ? 'indentation' : helpers.nameWhitespaceCharacter(errorText);
errorText = (function() {
switch (false) {
case errorToken !== tokens[tokens.length - 1]:
return 'end of input';
case errorTag !== 'INDENT' && errorTag !== 'OUTDENT':
return 'indentation';
case errorTag !== 'IDENTIFIER' && errorTag !== 'NUMBER' && errorTag !== 'STRING' && errorTag !== 'STRING_START' && errorTag !== 'REGEX' && errorTag !== 'REGEX_START':
return errorTag.replace(/_START$/, '').toLowerCase();
default:
return helpers.nameWhitespaceCharacter(errorText);
}
})();
return helpers.throwSyntaxError("unexpected " + errorText, errorLoc);
};

View File

@@ -63,16 +63,26 @@
AlphaNumeric: [
o('NUMBER', function() {
return new Literal($1);
}), o('STRING', function() {
}), o('String')
],
String: [
o('STRING', function() {
return new Literal($1);
}), o('STRING_START Body STRING_END', function() {
return new Parens($2);
})
],
Regex: [
o('REGEX', function() {
return new Literal($1);
}), o('REGEX_START Invocation REGEX_END', function() {
return $2;
})
],
Literal: [
o('AlphaNumeric'), o('JS', function() {
return new Literal($1);
}), o('REGEX', function() {
return new Literal($1);
}), o('DEBUGGER', function() {
}), o('Regex'), o('DEBUGGER', function() {
return new Literal($1);
}), o('UNDEFINED', function() {
return new Undefined;

View File

@@ -297,7 +297,7 @@
};
Lexer.prototype.regexToken = function() {
var body, closed, end, errorToken, flags, index, match, prev, ref2, ref3, ref4, regex, rparen, tokens;
var body, closed, end, flags, index, match, origin, prev, ref2, ref3, ref4, regex, tokens;
switch (false) {
case !(match = REGEX_ILLEGAL.exec(this.chunk)):
this.error("regular expressions cannot begin with " + match[2], {
@@ -315,7 +315,7 @@
index = regex.length;
ref2 = this.tokens, prev = ref2[ref2.length - 1];
if (prev) {
if (prev.spaced && (ref3 = prev[0], indexOf.call(CALLABLE, ref3) >= 0) && !prev.stringEnd && !prev.regexEnd) {
if (prev.spaced && (ref3 = prev[0], indexOf.call(CALLABLE, ref3) >= 0)) {
if (!closed || POSSIBLY_DIVISION.test(regex)) {
return 0;
}
@@ -332,7 +332,7 @@
}
flags = REGEX_FLAGS.exec(this.chunk.slice(index))[0];
end = index + flags.length;
errorToken = this.makeToken('REGEX', this.chunk.slice(0, end), 0, end);
origin = this.makeToken('REGEX', null, 0, end);
switch (false) {
case !!VALID_FLAGS.test(flags):
this.error("invalid regular expression flags " + flags, {
@@ -346,11 +346,12 @@
}
this.token('REGEX', "" + (this.makeDelimitedLiteral(body, {
delimiter: '/'
})) + flags, 0, end, errorToken);
})) + flags, 0, end, origin);
break;
default:
this.token('REGEX_START', '(', 0, 0, origin);
this.token('IDENTIFIER', 'RegExp', 0, 0);
this.token('CALL_START', '(', 0, 0, errorToken);
this.token('CALL_START', '(', 0, 0);
this.mergeInterpolationTokens(tokens, {
delimiter: '"',
double: true
@@ -359,8 +360,8 @@
this.token(',', ',', index, 0);
this.token('STRING', '"' + flags + '"', index, flags.length);
}
rparen = this.token(')', ')', end, 0);
rparen.regexEnd = true;
this.token(')', ')', end, 0);
this.token('REGEX_END', ')', end, 0);
}
return end;
};
@@ -522,7 +523,7 @@
} else if (indexOf.call(LOGIC, value) >= 0 || value === '?' && (prev != null ? prev.spaced : void 0)) {
tag = 'LOGIC';
} else if (prev && !prev.spaced) {
if (value === '(' && (ref5 = prev[0], indexOf.call(CALLABLE, ref5) >= 0) && !prev.stringEnd && !prev.regexEnd) {
if (value === '(' && (ref5 = prev[0], indexOf.call(CALLABLE, ref5) >= 0)) {
if (prev[0] === '?') {
prev[0] = 'FUNC_EXIST';
}
@@ -633,6 +634,9 @@
firstToken = tokens[0], lastToken = tokens[tokens.length - 1];
firstToken[2].first_column -= delimiter.length;
lastToken[2].last_column += delimiter.length;
if (lastToken[1].length === 0) {
lastToken[2].last_column -= 1;
}
return {
tokens: tokens,
index: offsetInChunk + delimiter.length
@@ -640,18 +644,9 @@
};
Lexer.prototype.mergeInterpolationTokens = function(tokens, options, fn) {
var converted, errorToken, firstEmptyStringIndex, firstIndex, firstToken, i, interpolated, j, lastToken, len, locationToken, plusToken, ref2, ref3, rparen, tag, token, tokensToPush, value;
if (interpolated = tokens.length > 1) {
firstToken = tokens[0];
errorToken = [
'', 'interpolation', {
first_line: firstToken[2].last_line,
first_column: firstToken[2].last_column,
last_line: firstToken[2].last_line,
last_column: firstToken[2].last_column + 1
}
];
this.token('(', '(', 0, 0, errorToken);
var converted, firstEmptyStringIndex, firstIndex, i, j, lastToken, len, locationToken, lparen, plusToken, ref2, rparen, tag, token, tokensToPush, value;
if (tokens.length > 1) {
lparen = this.token('STRING_START', '(', 0, 0);
}
firstIndex = this.tokens.length;
for (i = j = 0, len = tokens.length; j < len; i = ++j) {
@@ -693,16 +688,23 @@
}
(ref2 = this.tokens).push.apply(ref2, tokensToPush);
}
if (interpolated) {
ref3 = this.tokens, lastToken = ref3[ref3.length - 1];
rparen = this.token(')', ')');
rparen[2] = {
if (lparen) {
lastToken = tokens[tokens.length - 1];
lparen.origin = [
'STRING', null, {
first_line: lparen[2].first_line,
first_column: lparen[2].first_column,
last_line: lastToken[2].last_line,
last_column: lastToken[2].last_column
}
];
rparen = this.token('STRING_END', ')');
return rparen[2] = {
first_line: lastToken[2].last_line,
first_column: lastToken[2].last_column + 1,
first_column: lastToken[2].last_column,
last_line: lastToken[2].last_line,
last_column: lastToken[2].last_column + 1
last_column: lastToken[2].last_column
};
return rparen.stringEnd = true;
}
};
@@ -987,7 +989,7 @@
CALLABLE = ['IDENTIFIER', ')', ']', '?', '@', 'THIS', 'SUPER'];
INDEXABLE = CALLABLE.concat(['NUMBER', 'STRING', 'REGEX', 'BOOL', 'NULL', 'UNDEFINED', '}', '::']);
INDEXABLE = CALLABLE.concat(['NUMBER', 'STRING', 'STRING_END', 'REGEX', 'REGEX_END', 'BOOL', 'NULL', 'UNDEFINED', '}', '::']);
NOT_REGEX = INDEXABLE.concat(['++', '--']);

View File

@@ -1267,11 +1267,8 @@
Obj.prototype.children = ['properties'];
Obj.prototype.compileNode = function(o) {
var answer, i, idt, indent, j, join, k, lastNoncom, len1, len2, node, prop, props;
var answer, dynamicIndex, hasDynamic, i, idt, indent, j, join, k, key, l, lastNoncom, len1, len2, len3, node, oref, prop, props, ref3, value;
props = this.properties;
if (!props.length) {
return [this.makeCode(this.front ? '({})' : '{}')];
}
if (this.generated) {
for (j = 0, len1 = props.length; j < len1; j++) {
node = props[j];
@@ -1280,13 +1277,34 @@
}
}
}
for (dynamicIndex = k = 0, len2 = props.length; k < len2; dynamicIndex = ++k) {
prop = props[dynamicIndex];
if ((prop.variable || prop).base instanceof Parens) {
break;
}
}
hasDynamic = dynamicIndex < props.length;
idt = o.indent += TAB;
lastNoncom = this.lastNonComment(this.properties);
answer = [];
for (i = k = 0, len2 = props.length; k < len2; i = ++k) {
if (hasDynamic) {
oref = o.scope.freeVariable('obj');
answer.push(this.makeCode("(\n" + idt + oref + " = "));
}
answer.push(this.makeCode("{" + (props.length === 0 || dynamicIndex === 0 ? '}' : '\n')));
for (i = l = 0, len3 = props.length; l < len3; i = ++l) {
prop = props[i];
join = i === props.length - 1 ? '' : prop === lastNoncom || prop instanceof Comment ? '\n' : ',\n';
if (i === dynamicIndex) {
if (i !== 0) {
answer.push(this.makeCode("\n" + idt + "}"));
}
answer.push(this.makeCode(',\n'));
}
join = i === props.length - 1 || i === dynamicIndex - 1 ? '' : prop === lastNoncom || prop instanceof Comment ? '\n' : ',\n';
indent = prop instanceof Comment ? '' : idt;
if (hasDynamic && i < dynamicIndex) {
indent += TAB;
}
if (prop instanceof Assign && prop.variable instanceof Value && prop.variable.hasProperties()) {
prop.variable.error('Invalid object key');
}
@@ -1294,10 +1312,20 @@
prop = new Assign(prop.properties[0].name, prop, 'object');
}
if (!(prop instanceof Comment)) {
if (!(prop instanceof Assign)) {
prop = new Assign(prop, prop, 'object');
if (i < dynamicIndex) {
if (!(prop instanceof Assign)) {
prop = new Assign(prop, prop, 'object');
}
(prop.variable.base || prop.variable).asKey = true;
} else {
if (prop instanceof Assign) {
key = prop.variable;
value = prop.value;
} else {
ref3 = prop.base.cache(o), key = ref3[0], value = ref3[1];
}
prop = new Assign(new Value(new Literal(oref), [new Access(key)]), value);
}
(prop.variable.base || prop.variable).asKey = true;
}
if (indent) {
answer.push(this.makeCode(indent));
@@ -1307,9 +1335,14 @@
answer.push(this.makeCode(join));
}
}
answer.unshift(this.makeCode("{" + (props.length && '\n')));
answer.push(this.makeCode((props.length && '\n' + this.tab) + "}"));
if (this.front) {
if (hasDynamic) {
answer.push(this.makeCode(",\n" + idt + oref + "\n" + this.tab + ")"));
} else {
if (props.length !== 0) {
answer.push(this.makeCode("\n" + this.tab + "}"));
}
}
if (this.front && !hasDynamic) {
return this.wrapInBraces(answer);
} else {
return answer;
@@ -1447,7 +1480,7 @@
};
Class.prototype.addProperties = function(node, name, o) {
var assign, base, exprs, func, props;
var acc, assign, base, exprs, func, props;
props = node.base.properties.slice(0);
exprs = (function() {
var results;
@@ -1474,7 +1507,8 @@
if (assign.variable["this"]) {
func["static"] = true;
} else {
assign.variable = new Value(new Literal(name), [new Access(new Literal('prototype')), new Access(base)]);
acc = base.isComplex() ? new Index(base) : new Access(base);
assign.variable = new Value(new Literal(name), [new Access(new Literal('prototype')), acc]);
if (func instanceof Code && func.bound) {
this.boundFuncs.push(base);
func.bound = false;

File diff suppressed because one or more lines are too long

View File

@@ -108,7 +108,7 @@
});
};
Rewriter.prototype.matchTags = function() {
Rewriter.prototype.indexOfTag = function() {
var fuzz, i, j, k, pattern, ref, ref1;
i = arguments[0], pattern = 2 <= arguments.length ? slice.call(arguments, 1) : [];
fuzz = 0;
@@ -123,14 +123,31 @@
pattern[j] = [pattern[j]];
}
if (ref1 = this.tag(i + j + fuzz), indexOf.call(pattern[j], ref1) < 0) {
return false;
return -1;
}
}
return true;
return i + j + fuzz - 1;
};
Rewriter.prototype.looksObjectish = function(j) {
return this.matchTags(j, '@', null, ':') || this.matchTags(j, null, ':');
var end, index;
if (this.indexOfTag(j, '@', null, ':') > -1 || this.indexOfTag(j, null, ':') > -1) {
return true;
}
index = this.indexOfTag(j, EXPRESSION_START);
if (index > -1) {
end = null;
this.detectEnd(index + 1, (function(token) {
var ref;
return ref = token[0], indexOf.call(EXPRESSION_END, ref) >= 0;
}), (function(token, i) {
return end = i;
}));
if (this.tag(end + 1) === ':') {
return true;
}
}
return false;
};
Rewriter.prototype.findTagsBackwards = function(i, tags) {
@@ -149,8 +166,9 @@
};
Rewriter.prototype.addImplicitBracesAndParens = function() {
var stack;
var stack, start;
stack = [];
start = null;
return this.scanTokens(function(token, i, tokens) {
var endImplicitCall, endImplicitObject, forward, inImplicit, inImplicitCall, inImplicitControl, inImplicitObject, newLine, nextTag, offset, prevTag, prevToken, ref, ref1, ref2, ref3, ref4, ref5, s, sameLine, stackIdx, stackTag, stackTop, startIdx, startImplicitCall, startImplicitObject, startsLine, tag;
tag = token[0];
@@ -255,26 +273,32 @@
stack.pop();
}
}
stack.pop();
start = stack.pop();
}
if ((indexOf.call(IMPLICIT_FUNC, tag) >= 0 && token.spaced && !token.stringEnd && !token.regexEnd || tag === '?' && i > 0 && !tokens[i - 1].spaced) && (indexOf.call(IMPLICIT_CALL, nextTag) >= 0 || indexOf.call(IMPLICIT_UNSPACED_CALL, nextTag) >= 0 && !((ref = tokens[i + 1]) != null ? ref.spaced : void 0) && !((ref1 = tokens[i + 1]) != null ? ref1.newLine : void 0))) {
if ((indexOf.call(IMPLICIT_FUNC, tag) >= 0 && token.spaced || tag === '?' && i > 0 && !tokens[i - 1].spaced) && (indexOf.call(IMPLICIT_CALL, nextTag) >= 0 || indexOf.call(IMPLICIT_UNSPACED_CALL, nextTag) >= 0 && !((ref = tokens[i + 1]) != null ? ref.spaced : void 0) && !((ref1 = tokens[i + 1]) != null ? ref1.newLine : void 0))) {
if (tag === '?') {
tag = token[0] = 'FUNC_EXIST';
}
startImplicitCall(i + 1);
return forward(2);
}
if (indexOf.call(IMPLICIT_FUNC, tag) >= 0 && !token.stringEnd && !token.regexEnd && this.matchTags(i + 1, 'INDENT', null, ':') && !this.findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH', 'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL'])) {
if (indexOf.call(IMPLICIT_FUNC, tag) >= 0 && this.indexOfTag(i + 1, 'INDENT', null, ':') > -1 && !this.findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH', 'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL'])) {
startImplicitCall(i + 1);
stack.push(['INDENT', i + 2]);
return forward(3);
}
if (tag === ':') {
if (this.tag(i - 2) === '@') {
s = i - 2;
} else {
s = i - 1;
}
s = (function() {
var ref2;
switch (false) {
case ref2 = this.tag(i - 1), indexOf.call(EXPRESSION_END, ref2) < 0:
return start[1];
case this.tag(i - 2) !== '@':
return i - 2;
default:
return i - 1;
}
}).call(this);
while (this.tag(s - 2) === 'HERECOMMENT') {
s -= 2;
}
@@ -298,8 +322,8 @@
ref4 = stackTop(), stackTag = ref4[0], stackIdx = ref4[1], (ref5 = ref4[2], sameLine = ref5.sameLine, startsLine = ref5.startsLine);
if (inImplicitCall() && prevTag !== ',') {
endImplicitCall();
} else if (inImplicitObject() && !this.insideForDeclaration && sameLine && tag !== 'TERMINATOR' && prevTag !== ':' && endImplicitObject()) {
} else if (inImplicitObject() && !this.insideForDeclaration && sameLine && tag !== 'TERMINATOR' && prevTag !== ':') {
endImplicitObject();
} else if (inImplicitObject() && tag === 'TERMINATOR' && prevTag !== ',' && !(startsLine && this.looksObjectish(i + 1))) {
endImplicitObject();
} else {
@@ -440,7 +464,7 @@
})();
BALANCED_PAIRS = [['(', ')'], ['[', ']'], ['{', '}'], ['INDENT', 'OUTDENT'], ['CALL_START', 'CALL_END'], ['PARAM_START', 'PARAM_END'], ['INDEX_START', 'INDEX_END']];
BALANCED_PAIRS = [['(', ')'], ['[', ']'], ['{', '}'], ['INDENT', 'OUTDENT'], ['CALL_START', 'CALL_END'], ['PARAM_START', 'PARAM_END'], ['INDEX_START', 'INDEX_END'], ['STRING_START', 'STRING_END'], ['REGEX_START', 'REGEX_END']];
exports.INVERSES = INVERSES = {};
@@ -458,7 +482,7 @@
IMPLICIT_FUNC = ['IDENTIFIER', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END', '@', 'THIS'];
IMPLICIT_CALL = ['IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START', 'CLASS', 'IF', 'TRY', 'SWITCH', 'THIS', 'BOOL', 'NULL', 'UNDEFINED', 'UNARY', 'YIELD', 'UNARY_MATH', 'SUPER', 'THROW', '@', '->', '=>', '[', '(', '{', '--', '++'];
IMPLICIT_CALL = ['IDENTIFIER', 'NUMBER', 'STRING', 'STRING_START', 'JS', 'REGEX', 'REGEX_START', 'NEW', 'PARAM_START', 'CLASS', 'IF', 'TRY', 'SWITCH', 'THIS', 'BOOL', 'NULL', 'UNDEFINED', 'UNARY', 'YIELD', 'UNARY_MATH', 'SUPER', 'THROW', '@', '->', '=>', '[', '(', '{', '--', '++'];
IMPLICIT_UNSPACED_CALL = ['+', '-'];

View File

@@ -225,12 +225,15 @@ parser.yy.parseError = (message, {token}) ->
{errorToken, tokens} = parser
[errorTag, errorText, errorLoc] = errorToken
errorText = if errorToken is tokens[tokens.length - 1]
'end of input'
else if errorTag in ['INDENT', 'OUTDENT']
'indentation'
else
helpers.nameWhitespaceCharacter errorText
errorText = switch
when errorToken is tokens[tokens.length - 1]
'end of input'
when errorTag in ['INDENT', 'OUTDENT']
'indentation'
when errorTag in ['IDENTIFIER', 'NUMBER', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START']
errorTag.replace(/_START$/, '').toLowerCase()
else
helpers.nameWhitespaceCharacter errorText
# The second argument has a `loc` property, which should have the location
# data for this token. Unfortunately, Jison seems to send an outdated `loc`

View File

@@ -132,7 +132,17 @@ grammar =
# they can also serve as keys in object literals.
AlphaNumeric: [
o 'NUMBER', -> new Literal $1
o 'String'
]
String: [
o 'STRING', -> new Literal $1
o 'STRING_START Body STRING_END', -> new Parens $2
]
Regex: [
o 'REGEX', -> new Literal $1
o 'REGEX_START Invocation REGEX_END', -> $2
]
# All of our immediate values. Generally these can be passed straight
@@ -140,7 +150,7 @@ grammar =
Literal: [
o 'AlphaNumeric'
o 'JS', -> new Literal $1
o 'REGEX', -> new Literal $1
o 'Regex'
o 'DEBUGGER', -> new Literal $1
o 'UNDEFINED', -> new Undefined
o 'NULL', -> new Null

View File

@@ -267,7 +267,7 @@ exports.Lexer = class Lexer
index = regex.length
[..., prev] = @tokens
if prev
if prev.spaced and prev[0] in CALLABLE and not prev.stringEnd and not prev.regexEnd
if prev.spaced and prev[0] in CALLABLE
return 0 if not closed or POSSIBLY_DIVISION.test regex
else if prev[0] in NOT_REGEX
return 0
@@ -277,22 +277,23 @@ exports.Lexer = class Lexer
[flags] = REGEX_FLAGS.exec @chunk[index..]
end = index + flags.length
errorToken = @makeToken 'REGEX', @chunk[...end], 0, end
origin = @makeToken 'REGEX', null, 0, end
switch
when not VALID_FLAGS.test flags
@error "invalid regular expression flags #{flags}", offset: index, length: flags.length
when regex or tokens.length is 1
body ?= @formatHeregex tokens[0][1]
@token 'REGEX', "#{@makeDelimitedLiteral body, delimiter: '/'}#{flags}", 0, end, errorToken
@token 'REGEX', "#{@makeDelimitedLiteral body, delimiter: '/'}#{flags}", 0, end, origin
else
@token 'REGEX_START', '(', 0, 0, origin
@token 'IDENTIFIER', 'RegExp', 0, 0
@token 'CALL_START', '(', 0, 0, errorToken
@token 'CALL_START', '(', 0, 0
@mergeInterpolationTokens tokens, {delimiter: '"', double: yes}, @formatHeregex
if flags
@token ',', ',', index, 0
@token 'STRING', '"' + flags + '"', index, flags.length
rparen = @token ')', ')', end, 0
rparen.regexEnd = true
@token ')', ')', end, 0
@token 'REGEX_END', ')', end, 0
end
@@ -420,7 +421,7 @@ exports.Lexer = class Lexer
else if value in SHIFT then tag = 'SHIFT'
else if value in LOGIC or value is '?' and prev?.spaced then tag = 'LOGIC'
else if prev and not prev.spaced
if value is '(' and prev[0] in CALLABLE and not prev.stringEnd and not prev.regexEnd
if value is '(' and prev[0] in CALLABLE
prev[0] = 'FUNC_EXIST' if prev[0] is '?'
tag = 'CALL_START'
else if value is '[' and prev[0] in INDEXABLE
@@ -471,8 +472,8 @@ exports.Lexer = class Lexer
# If it encounters an interpolation, this method will recursively create a new
# Lexer and tokenize until the `{` of `#{` is balanced with a `}`.
#
# - `regex` matches the contents of a token (but not `end`, and not `#{` if
# interpolations are desired).
# - `regex` matches the contents of a token (but not `delimiter`, and not
# `#{` if interpolations are desired).
# - `delimiter` is the delimiter of the token. Examples are `'`, `"`, `'''`,
# `"""` and `///`.
#
@@ -525,6 +526,7 @@ exports.Lexer = class Lexer
[firstToken, ..., lastToken] = tokens
firstToken[2].first_column -= delimiter.length
lastToken[2].last_column += delimiter.length
lastToken[2].last_column -= 1 if lastToken[1].length is 0
{tokens, index: offsetInChunk + delimiter.length}
@@ -533,15 +535,8 @@ exports.Lexer = class Lexer
# of 'NEOSTRING's are converted using `fn` and turned into strings using
# `options` first.
mergeInterpolationTokens: (tokens, options, fn) ->
if interpolated = tokens.length > 1
[firstToken] = tokens
errorToken = ['', 'interpolation',
first_line: firstToken[2].last_line
first_column: firstToken[2].last_column
last_line: firstToken[2].last_line
last_column: firstToken[2].last_column + 1
]
@token '(', '(', 0, 0, errorToken
if tokens.length > 1
lparen = @token 'STRING_START', '(', 0, 0
firstIndex = @tokens.length
for token, i in tokens
@@ -583,16 +578,20 @@ exports.Lexer = class Lexer
last_column: locationToken[2].first_column
@tokens.push tokensToPush...
if interpolated
[..., lastToken] = @tokens
rparen = @token ')', ')'
rparen[2] = {
first_line: lastToken[2].last_line
first_column: lastToken[2].last_column + 1
if lparen
[..., lastToken] = tokens
lparen.origin = ['STRING', null,
first_line: lparen[2].first_line
first_column: lparen[2].first_column
last_line: lastToken[2].last_line
last_column: lastToken[2].last_column + 1
}
rparen.stringEnd = true
last_column: lastToken[2].last_column
]
rparen = @token 'STRING_END', ')'
rparen[2] =
first_line: lastToken[2].last_line
first_column: lastToken[2].last_column
last_line: lastToken[2].last_line
last_column: lastToken[2].last_column
# Pairs up a closing token, ensuring that all listed pairs of tokens are
# correctly balanced throughout the course of the token stream.
@@ -913,7 +912,10 @@ BOOL = ['TRUE', 'FALSE']
# parentheses or bracket following these tokens will be recorded as the start
# of a function invocation or indexing operation.
CALLABLE = ['IDENTIFIER', ')', ']', '?', '@', 'THIS', 'SUPER']
INDEXABLE = CALLABLE.concat ['NUMBER', 'STRING', 'REGEX', 'BOOL', 'NULL', 'UNDEFINED', '}', '::']
INDEXABLE = CALLABLE.concat [
'NUMBER', 'STRING', 'STRING_END', 'REGEX', 'REGEX_END'
'BOOL', 'NULL', 'UNDEFINED', '}', '::'
]
# Tokens which a regular expression will never immediately follow (except spaced
# CALLABLEs in some cases), but which a division operator can.

View File

@@ -925,35 +925,54 @@ exports.Obj = class Obj extends Base
compileNode: (o) ->
props = @properties
return [@makeCode(if @front then '({})' else '{}')] unless props.length
if @generated
for node in props when node instanceof Value
node.error 'cannot have an implicit value in an implicit object'
break for prop, dynamicIndex in props when (prop.variable or prop).base instanceof Parens
hasDynamic = dynamicIndex < props.length
idt = o.indent += TAB
lastNoncom = @lastNonComment @properties
answer = []
if hasDynamic
oref = o.scope.freeVariable 'obj'
answer.push @makeCode "(\n#{idt}#{oref} = "
answer.push @makeCode "{#{if props.length is 0 or dynamicIndex is 0 then '}' else '\n'}"
for prop, i in props
join = if i is props.length - 1
if i is dynamicIndex
answer.push @makeCode "\n#{idt}}" unless i is 0
answer.push @makeCode ',\n'
join = if i is props.length - 1 or i is dynamicIndex - 1
''
else if prop is lastNoncom or prop instanceof Comment
'\n'
else
',\n'
indent = if prop instanceof Comment then '' else idt
indent += TAB if hasDynamic and i < dynamicIndex
if prop instanceof Assign and prop.variable instanceof Value and prop.variable.hasProperties()
prop.variable.error 'Invalid object key'
if prop instanceof Value and prop.this
prop = new Assign prop.properties[0].name, prop, 'object'
if prop not instanceof Comment
if prop not instanceof Assign
prop = new Assign prop, prop, 'object'
(prop.variable.base or prop.variable).asKey = yes
if i < dynamicIndex
if prop not instanceof Assign
prop = new Assign prop, prop, 'object'
(prop.variable.base or prop.variable).asKey = yes
else
if prop instanceof Assign
key = prop.variable
value = prop.value
else
[key, value] = prop.base.cache o
prop = new Assign (new Value (new Literal oref), [new Access key]), value
if indent then answer.push @makeCode indent
answer.push prop.compileToFragments(o, LEVEL_TOP)...
if join then answer.push @makeCode join
answer.unshift @makeCode "{#{ props.length and '\n' }"
answer.push @makeCode "#{ props.length and '\n' + @tab }}"
if @front then @wrapInBraces answer else answer
if hasDynamic
answer.push @makeCode ",\n#{idt}#{oref}\n#{@tab})"
else
answer.push @makeCode "\n#{@tab}}" unless props.length is 0
if @front and not hasDynamic then @wrapInBraces answer else answer
assigns: (name) ->
for prop in @properties when prop.assigns name then return yes
@@ -1057,7 +1076,8 @@ exports.Class = class Class extends Base
if assign.variable.this
func.static = yes
else
assign.variable = new Value(new Literal(name), [(new Access new Literal 'prototype'), new Access base])
acc = if base.isComplex() then new Index base else new Access base
assign.variable = new Value(new Literal(name), [(new Access new Literal 'prototype'), acc])
if func instanceof Code and func.bound
@boundFuncs.push base
func.bound = no

View File

@@ -93,24 +93,31 @@ class exports.Rewriter
@detectEnd i + 1, condition, action if token[0] is 'INDEX_START'
1
# Match tags in token stream starting at i with pattern, skipping HERECOMMENTs
# Pattern may consist of strings (equality), an array of strings (one of)
# or null (wildcard)
matchTags: (i, pattern...) ->
# Match tags in token stream starting at `i` with `pattern`, skipping 'HERECOMMENT's.
# `pattern` may consist of strings (equality), an array of strings (one of)
# or null (wildcard). Returns the index of the match or -1 if no match.
indexOfTag: (i, pattern...) ->
fuzz = 0
for j in [0 ... pattern.length]
fuzz += 2 while @tag(i + j + fuzz) is 'HERECOMMENT'
continue if not pattern[j]?
pattern[j] = [pattern[j]] if typeof pattern[j] is 'string'
return no if @tag(i + j + fuzz) not in pattern[j]
yes
return -1 if @tag(i + j + fuzz) not in pattern[j]
i + j + fuzz - 1
# yes iff standing in front of something looking like
# @<x>: or <x>:, skipping over 'HERECOMMENT's
# Returns `yes` if standing in front of something looking like
# `@<x>:`, `<x>:` or `<EXPRESSION_START><x>...<EXPRESSION_END>:`,
# skipping over 'HERECOMMENT's.
looksObjectish: (j) ->
@matchTags(j, '@', null, ':') or @matchTags(j, null, ':')
return yes if @indexOfTag(j, '@', null, ':') > -1 or @indexOfTag(j, null, ':') > -1
index = @indexOfTag(j, EXPRESSION_START)
if index > -1
end = null
@detectEnd index + 1, ((token) -> token[0] in EXPRESSION_END), ((token, i) -> end = i)
return yes if @tag(end + 1) is ':'
no
# yes iff current line of tokens contain an element of tags on same
# Returns `yes` if current line of tokens contain an element of tags on same
# expression level. Stop searching at LINEBREAKS or explicit start of
# containing balanced expression.
findTagsBackwards: (i, tags) ->
@@ -129,6 +136,7 @@ class exports.Rewriter
addImplicitBracesAndParens: ->
# Track current balancing depth (both implicit and explicit) on stack.
stack = []
start = null
@scanTokens (token, i, tokens) ->
[tag] = token
@@ -205,11 +213,11 @@ class exports.Rewriter
endImplicitObject()
else
stack.pop()
stack.pop()
start = stack.pop()
# Recognize standard implicit calls like
# f a, f() b, f? c, h[0] d etc.
if (tag in IMPLICIT_FUNC and token.spaced and not token.stringEnd and not token.regexEnd or
if (tag in IMPLICIT_FUNC and token.spaced or
tag is '?' and i > 0 and not tokens[i - 1].spaced) and
(nextTag in IMPLICIT_CALL or
nextTag in IMPLICIT_UNSPACED_CALL and
@@ -243,8 +251,8 @@ class exports.Rewriter
# which is probably always unintended.
# Furthermore don't allow this in literal arrays, as
# that creates grammatical ambiguities.
if tag in IMPLICIT_FUNC and not token.stringEnd and not token.regexEnd and
@matchTags(i + 1, 'INDENT', null, ':') and
if tag in IMPLICIT_FUNC and
@indexOfTag(i + 1, 'INDENT', null, ':') > -1 and
not @findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH',
'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL'])
startImplicitCall i + 1
@@ -254,7 +262,10 @@ class exports.Rewriter
# Implicit objects start here
if tag is ':'
# Go back to the (implicit) start of the object
if @tag(i - 2) is '@' then s = i - 2 else s = i - 1
s = switch
when @tag(i - 1) in EXPRESSION_END then start[1]
when @tag(i - 2) is '@' then i - 2
else i - 1
s -= 2 while @tag(s - 2) is 'HERECOMMENT'
# Mark if the value is a for loop
@@ -298,7 +309,7 @@ class exports.Rewriter
# Close implicit objects such as:
# return a: 1, b: 2 unless true
else if inImplicitObject() and not @insideForDeclaration and sameLine and
tag isnt 'TERMINATOR' and prevTag isnt ':' and
tag isnt 'TERMINATOR' and prevTag isnt ':'
endImplicitObject()
# Close implicit objects when at end of line, line didn't end with a comma
# and the implicit object didn't start the line or the next line doesn't look like
@@ -445,6 +456,8 @@ BALANCED_PAIRS = [
['CALL_START', 'CALL_END']
['PARAM_START', 'PARAM_END']
['INDEX_START', 'INDEX_END']
['STRING_START', 'STRING_END']
['REGEX_START', 'REGEX_END']
]
# The inverse mappings of `BALANCED_PAIRS` we're trying to fix up, so we can
@@ -467,9 +480,10 @@ IMPLICIT_FUNC = ['IDENTIFIER', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END', '@
# If preceded by an `IMPLICIT_FUNC`, indicates a function invocation.
IMPLICIT_CALL = [
'IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START', 'CLASS'
'IF', 'TRY', 'SWITCH', 'THIS', 'BOOL', 'NULL', 'UNDEFINED', 'UNARY', 'YIELD'
'UNARY_MATH', 'SUPER', 'THROW', '@', '->', '=>', '[', '(', '{', '--', '++'
'IDENTIFIER', 'NUMBER', 'STRING', 'STRING_START', 'JS', 'REGEX', 'REGEX_START'
'NEW', 'PARAM_START', 'CLASS', 'IF', 'TRY', 'SWITCH', 'THIS', 'BOOL', 'NULL'
'UNDEFINED', 'UNARY', 'YIELD', 'UNARY_MATH', 'SUPER', 'THROW'
'@', '->', '=>', '[', '(', '{', '--', '++'
]
IMPLICIT_UNSPACED_CALL = ['+', '-']

View File

@@ -285,6 +285,13 @@ test "#156: destructuring with expansion", ->
throws (-> CoffeeScript.compile "[..., a, b...] = c"), null, "prohibit expansion and a splat"
throws (-> CoffeeScript.compile "[...] = c"), null, "prohibit lone expansion"
test "destructuring with dynamic keys", ->
{"#{'a'}": a, """#{'b'}""": b, c} = {a: 1, b: 2, c: 3}
eq 1, a
eq 2, b
eq 3, c
throws -> CoffeeScript.compile '{"#{a}"} = b'
# Existential Assignment

View File

@@ -860,6 +860,7 @@ test "dynamic method names and super", ->
class Base
@m: -> 6
m: -> 5
m2: -> 4.5
n: -> 4
A = ->
A extends Base
@@ -877,14 +878,17 @@ test "dynamic method names and super", ->
eq 1, count
m = 'm'
m2 = 'm2'
count = 0
class B extends Base
@[name()] = -> super
@::[m] = -> super
"#{m2}": -> super
b = new B
m = 'n'
m = m2 = 'n'
eq 6, B.m()
eq 5, b.m()
eq 4.5, b.m2()
eq 1, count
class C extends B

View File

@@ -87,12 +87,6 @@ if require?
test "#1096: unexpected generated tokens", ->
# Unexpected interpolation
assertErrorFormat '{"#{key}": val}', '''
[stdin]:1:3: error: unexpected interpolation
{"#{key}": val}
^^
'''
# Implicit ends
assertErrorFormat 'a:, b', '''
[stdin]:1:3: error: unexpected ,
@@ -116,31 +110,79 @@ test "#1096: unexpected generated tokens", ->
a +
^
'''
# Unexpected implicit object
# Unexpected key in implicit object (an implicit object itself is _not_
# unexpected here)
assertErrorFormat '''
for i in [1]:
1
''', '''
[stdin]:1:13: error: unexpected :
[stdin]:1:10: error: unexpected [
for i in [1]:
^
^
'''
# Unexpected regex
assertErrorFormat '{/a/i: val}', '''
[stdin]:1:2: error: unexpected /a/i
[stdin]:1:2: error: unexpected regex
{/a/i: val}
^^^^
'''
assertErrorFormat '{///a///i: val}', '''
[stdin]:1:2: error: unexpected ///a///i
[stdin]:1:2: error: unexpected regex
{///a///i: val}
^^^^^^^^
'''
assertErrorFormat '{///#{a}///i: val}', '''
[stdin]:1:2: error: unexpected ///#{a}///i
[stdin]:1:2: error: unexpected regex
{///#{a}///i: val}
^^^^^^^^^^^
'''
# Unexpected string
assertErrorFormat "a''", '''
[stdin]:1:2: error: unexpected string
a''
^^
'''
assertErrorFormat 'a""', '''
[stdin]:1:2: error: unexpected string
a""
^^
'''
assertErrorFormat "a'b'", '''
[stdin]:1:2: error: unexpected string
a'b'
^^^
'''
assertErrorFormat 'a"b"', '''
[stdin]:1:2: error: unexpected string
a"b"
^^^
'''
assertErrorFormat "a'''b'''", """
[stdin]:1:2: error: unexpected string
a'''b'''
^^^^^^^
"""
assertErrorFormat 'a"""b"""', '''
[stdin]:1:2: error: unexpected string
a"""b"""
^^^^^^^
'''
assertErrorFormat 'a"#{b}"', '''
[stdin]:1:2: error: unexpected string
a"#{b}"
^^^^^^
'''
assertErrorFormat 'a"""#{b}"""', '''
[stdin]:1:2: error: unexpected string
a"""#{b}"""
^^^^^^^^^^
'''
# Unexpected number
assertErrorFormat '"a"0x00Af2', '''
[stdin]:1:4: error: unexpected number
"a"0x00Af2
^^^^^^^
'''
test "#1316: unexpected end of interpolation", ->
assertErrorFormat '''
@@ -336,14 +378,14 @@ test "unexpected token after string", ->
assertErrorFormat '''
'foo'bar
''', '''
[stdin]:1:6: error: unexpected bar
[stdin]:1:6: error: unexpected identifier
'foo'bar
^^^
'''
assertErrorFormat '''
"foo"bar
''', '''
[stdin]:1:6: error: unexpected bar
[stdin]:1:6: error: unexpected identifier
"foo"bar
^^^
'''
@@ -365,11 +407,11 @@ test "unexpected token after string", ->
test "#3348: Location data is wrong in interpolations with leading whitespace", ->
assertErrorFormat '''
"#{ {"#{key}": val} }"
"#{ * }"
''', '''
[stdin]:1:7: error: unexpected interpolation
"#{ {"#{key}": val} }"
^^
[stdin]:1:5: error: unexpected *
"#{ * }"
^
'''
test "octal escapes", ->
@@ -643,4 +685,62 @@ test "invalid numbers", ->
[stdin]:1:1: error: octal literal '010' must be prefixed with '0o'
010
^^^
'''
test "unexpected object keys", ->
assertErrorFormat '''
{[[]]}
''', '''
[stdin]:1:2: error: unexpected [
{[[]]}
^
'''
assertErrorFormat '''
{[[]]: 1}
''', '''
[stdin]:1:2: error: unexpected [
{[[]]: 1}
^
'''
assertErrorFormat '''
[[]]: 1
''', '''
[stdin]:1:1: error: unexpected [
[[]]: 1
^
'''
assertErrorFormat '''
{(a + "b")}
''', '''
[stdin]:1:2: error: unexpected (
{(a + "b")}
^
'''
assertErrorFormat '''
{(a + "b"): 1}
''', '''
[stdin]:1:2: error: unexpected (
{(a + "b"): 1}
^
'''
assertErrorFormat '''
(a + "b"): 1
''', '''
[stdin]:1:1: error: unexpected (
(a + "b"): 1
^
'''
assertErrorFormat '''
a: 1, [[]]: 2
''', '''
[stdin]:1:7: error: unexpected [
a: 1, [[]]: 2
^
'''
assertErrorFormat '''
{a: 1, [[]]: 2}
''', '''
[stdin]:1:8: error: unexpected [
{a: 1, [[]]: 2}
^
'''

View File

@@ -438,15 +438,16 @@ test "#3822: Simple string/regex start/end should include delimiters", ->
test "#3621: Multiline regex and manual `Regex` call with interpolation should
result in the same tokens", ->
tokensA = CoffeeScript.tokens 'RegExp(".*#{a}[0-9]")'
tokensA = CoffeeScript.tokens '(RegExp(".*#{a}[0-9]"))'
tokensB = CoffeeScript.tokens '///.*#{a}[0-9]///'
eq tokensA.length, tokensB.length
for i in [0...tokensA.length] by 1
tokenA = tokensA[i]
tokenB = tokensB[i]
eq tokenA[0], tokenB[0]
eq tokenA[0], tokenB[0] unless tokenB[0] in ['REGEX_START', 'REGEX_END']
eq tokenA[1], tokenB[1]
eq tokenA.origin?[1], tokenB.origin?[1] unless tokenA[0] is 'CALL_START'
unless tokenA[0] is 'STRING_START' or tokenB[0] is 'REGEX_START'
eq tokenA.origin?[1], tokenB.origin?[1]
eq tokenA.stringEnd, tokenB.stringEnd
test "Verify all tokens get a location", ->

View File

@@ -446,3 +446,126 @@ test 'inline implicit object literals within multiline implicit object literals'
b: 0
eq 0, x.b
eq 0, x.a.aa
test "object keys with interpolations", ->
# Simple cases.
a = 'a'
obj = "#{a}": yes
eq obj.a, yes
obj = {"#{a}": yes}
eq obj.a, yes
obj = {"#{a}"}
eq obj.a, 'a'
obj = {"#{5}"}
eq obj[5], '5' # Note that the value is a string, just like the key.
# Commas in implicit object.
obj = "#{'a'}": 1, b: 2
deepEqual obj, {a: 1, b: 2}
obj = a: 1, "#{'b'}": 2
deepEqual obj, {a: 1, b: 2}
obj = "#{'a'}": 1, "#{'b'}": 2
deepEqual obj, {a: 1, b: 2}
# Commas in explicit object.
obj = {"#{'a'}": 1, b: 2}
deepEqual obj, {a: 1, b: 2}
obj = {a: 1, "#{'b'}": 2}
deepEqual obj, {a: 1, b: 2}
obj = {"#{'a'}": 1, "#{'b'}": 2}
deepEqual obj, {a: 1, b: 2}
# Commas after key with interpolation.
obj = {"#{'a'}": yes,}
eq obj.a, yes
obj = {
"#{'a'}": 1,
"#{'b'}": 2,
### herecomment ###
"#{'c'}": 3,
}
deepEqual obj, {a: 1, b: 2, c: 3}
obj =
"#{'a'}": 1,
"#{'b'}": 2,
### herecomment ###
"#{'c'}": 3,
deepEqual obj, {a: 1, b: 2, c: 3}
obj =
"#{'a'}": 1,
"#{'b'}": 2,
### herecomment ###
"#{'c'}": 3, "#{'d'}": 4,
deepEqual obj, {a: 1, b: 2, c: 3, d: 4}
# Key with interpolation mixed with `@prop`.
deepEqual (-> {@a, "#{'b'}": 2}).call(a: 1), {a: 1, b: 2}
# Evaluate only once.
count = 0
b = -> count++; 'b'
obj = {"#{b()}"}
eq obj.b, 'b'
eq count, 1
# Evaluation order.
arr = []
obj =
a: arr.push 1
b: arr.push 2
"#{'c'}": arr.push 3
"#{'d'}": arr.push 4
e: arr.push 5
"#{'f'}": arr.push 6
g: arr.push 7
arrayEq arr, [1..7]
deepEqual obj, {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7}
# Object starting with dynamic key.
obj =
"#{'a'}": 1
b: 2
deepEqual obj, {a: 1, b: 2}
# Comments in implicit object.
obj =
### leading comment ###
"#{'a'}": 1
### middle ###
"#{'b'}": 2
# regular comment
'c': 3
### foo ###
d: 4
"#{'e'}": 5
deepEqual obj, {a: 1, b: 2, c: 3, d: 4, e: 5}
# Comments in explicit object.
obj = {
### leading comment ###
"#{'a'}": 1
### middle ###
"#{'b'}": 2
# regular comment
'c': 3
### foo ###
d: 4
"#{'e'}": 5
}
deepEqual obj, {a: 1, b: 2, c: 3, d: 4, e: 5}
# A more complicated case.
obj = {
"#{'interpolated'}":
"""
#{ '''nested''' }
""": 123: 456
}
deepEqual obj,
interpolated:
nested:
123: 456