From 1666716c3168f70e9eb6c997e0fff5f2b7815fc0 Mon Sep 17 00:00:00 2001 From: Troels Nielsen Date: Sun, 24 Feb 2013 19:09:01 +0100 Subject: [PATCH] Improve the handling of implicit object and implicit call combinations by handling them together. --- lib/coffee-script/nodes.js | 53 ++--- lib/coffee-script/repl.js | 2 +- lib/coffee-script/rewriter.js | 344 ++++++++++++++++++++------------ src/nodes.coffee | 28 +-- src/rewriter.coffee | 336 +++++++++++++++++++++---------- test/classes.coffee | 11 + test/control_flow.coffee | 1 - test/function_invocation.coffee | 113 +++++++++++ test/objects.coffee | 116 ++++++++++- 9 files changed, 700 insertions(+), 304 deletions(-) diff --git a/lib/coffee-script/nodes.js b/lib/coffee-script/nodes.js index 34576355..891739cc 100644 --- a/lib/coffee-script/nodes.js +++ b/lib/coffee-script/nodes.js @@ -878,33 +878,6 @@ return ifn; }; - Call.prototype.filterImplicitObjects = function(list) { - var node, nodes, obj, prop, properties, _i, _j, _len, _len1, _ref2; - nodes = []; - for (_i = 0, _len = list.length; _i < _len; _i++) { - node = list[_i]; - if (!((typeof node.isObject === "function" ? node.isObject() : void 0) && node.base.generated)) { - nodes.push(node); - continue; - } - obj = null; - _ref2 = node.base.properties; - for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { - prop = _ref2[_j]; - if (prop instanceof Assign || prop instanceof Comment) { - if (!obj) { - nodes.push(obj = new Obj(properties = [], true)); - } - properties.push(prop); - } else { - nodes.push(prop); - obj = null; - } - } - } - return nodes; - }; - Call.prototype.compileNode = function(o) { var arg, args, code, _ref2; if ((_ref2 = this.variable) != null) { @@ -913,16 +886,16 @@ if (code = Splat.compileSplattedArray(o, this.args, true)) { return this.compileSplat(o, code); } - args = this.filterImplicitObjects(this.args); args = ((function() { - var _i, _len, _results; + var _i, _len, _ref3, _results; + _ref3 = this.args; _results = []; - for (_i = 0, _len = args.length; _i < _len; _i++) { - arg = args[_i]; + for (_i = 0, _len = _ref3.length; _i < _len; _i++) { + arg = _ref3[_i]; _results.push(arg.compile(o, LEVEL_LIST)); } return _results; - })()).join(', '); + }).call(this)).join(', '); if (this.isSuper) { return this.superReference(o) + (".call(" + (this.superThis(o)) + (args && ', ' + args) + ")"); } else { @@ -1240,27 +1213,25 @@ Arr.prototype.children = ['objects']; - Arr.prototype.filterImplicitObjects = Call.prototype.filterImplicitObjects; - Arr.prototype.compileNode = function(o) { - var code, obj, objs; + var code, obj; if (!this.objects.length) { return '[]'; } o.indent += TAB; - objs = this.filterImplicitObjects(this.objects); - if (code = Splat.compileSplattedArray(o, objs)) { + if (code = Splat.compileSplattedArray(o, this.objects)) { return code; } code = ((function() { - var _i, _len, _results; + var _i, _len, _ref2, _results; + _ref2 = this.objects; _results = []; - for (_i = 0, _len = objs.length; _i < _len; _i++) { - obj = objs[_i]; + for (_i = 0, _len = _ref2.length; _i < _len; _i++) { + obj = _ref2[_i]; _results.push(obj.compile(o, LEVEL_LIST)); } return _results; - })()).join(', '); + }).call(this)).join(', '); if (code.indexOf('\n') >= 0) { return "[\n" + o.indent + code + "\n" + this.tab + "]"; } else { diff --git a/lib/coffee-script/repl.js b/lib/coffee-script/repl.js index 1f0ccfc0..27876937 100644 --- a/lib/coffee-script/repl.js +++ b/lib/coffee-script/repl.js @@ -16,7 +16,7 @@ var js; input = input.replace(/\uFF00/g, '\n'); input = input.replace(/(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, '$1$2$3'); - if (/^(\()?\s*(\n\))?$/.test(input)) { + if (/^(\s*|\(\s*\))$/.test(input)) { return cb(null); } try { diff --git a/lib/coffee-script/rewriter.js b/lib/coffee-script/rewriter.js index 07ebcc82..72673ca3 100644 --- a/lib/coffee-script/rewriter.js +++ b/lib/coffee-script/rewriter.js @@ -1,9 +1,16 @@ // Generated by CoffeeScript 1.5.0 (function() { - var BALANCED_PAIRS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_BLOCK, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, IMPLICIT_UNSPACED_CALL, INVERSES, LINEBREAKS, SINGLE_CLOSERS, SINGLE_LINERS, left, rite, _i, _len, _ref, + var BALANCED_PAIRS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_BLOCK, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, IMPLICIT_UNSPACED_CALL, INVERSES, LINEBREAKS, SINGLE_CLOSERS, SINGLE_LINERS, generate, left, rite, _i, _len, _ref, __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; + generate = function(tag, value) { + var tok; + tok = [tag, value]; + tok.generated = true; + return tok; + }; + exports.Rewriter = (function() { function Rewriter() {} @@ -16,8 +23,7 @@ this.closeOpenIndexes(); this.addImplicitIndentation(); this.tagPostfixConditionals(); - this.addImplicitBraces(); - this.addImplicitParentheses(); + this.addImplicitBracesAndParens(); this.addLocationDataToGeneratedTokens(); return this.tokens; }; @@ -112,140 +118,237 @@ }); }; - Rewriter.prototype.addImplicitBraces = function() { - var action, condition, sameLine, stack, start, startIndent, startIndex, startsLine; - stack = []; - start = null; - startsLine = null; - sameLine = true; - startIndent = 0; - startIndex = 0; - condition = function(token, i) { - var one, tag, three, two, _ref, _ref1; - _ref = this.tokens.slice(i + 1, +(i + 3) + 1 || 9e9), one = _ref[0], two = _ref[1], three = _ref[2]; - if ('HERECOMMENT' === (one != null ? one[0] : void 0)) { + Rewriter.prototype.matchTags = function() { + var fuzz, i, j, pattern, _i, _ref, _ref1; + i = arguments[0], pattern = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + fuzz = 0; + for (j = _i = 0, _ref = pattern.length; 0 <= _ref ? _i < _ref : _i > _ref; j = 0 <= _ref ? ++_i : --_i) { + while (this.tag(i + j + fuzz) === 'HERECOMMENT') { + fuzz += 2; + } + if (pattern[j] == null) { + continue; + } + if (typeof pattern[j] === 'string') { + pattern[j] = [pattern[j]]; + } + if (_ref1 = this.tag(i + j + fuzz), __indexOf.call(pattern[j], _ref1) < 0) { return false; } - tag = token[0]; - if (__indexOf.call(LINEBREAKS, tag) >= 0) { - sameLine = false; - } - return (((tag === 'TERMINATOR' || tag === 'OUTDENT') || (__indexOf.call(IMPLICIT_END, tag) >= 0 && sameLine && !(i - startIndex === 1))) && ((!startsLine && this.tag(i - 1) !== ',') || !((two != null ? two[0] : void 0) === ':' || (one != null ? one[0] : void 0) === '@' && (three != null ? three[0] : void 0) === ':'))) || (tag === ',' && one && ((_ref1 = one[0]) !== 'IDENTIFIER' && _ref1 !== 'NUMBER' && _ref1 !== 'STRING' && _ref1 !== '@' && _ref1 !== 'TERMINATOR' && _ref1 !== 'OUTDENT')); - }; - action = function(token, i) { - var tok; - tok = this.generate('}', '}'); - return this.tokens.splice(i, 0, tok); - }; - return this.scanTokens(function(token, i, tokens) { - var ago, idx, prevTag, tag, tok, value, _ref, _ref1; - if (_ref = (tag = token[0]), __indexOf.call(EXPRESSION_START, _ref) >= 0) { - stack.push([(tag === 'INDENT' && this.tag(i - 1) === '{' ? '{' : tag), i]); - return 1; - } - if (__indexOf.call(EXPRESSION_END, tag) >= 0) { - start = stack.pop(); - return 1; - } - if (!(tag === ':' && ((ago = this.tag(i - 2)) === ':' || ((_ref1 = stack[stack.length - 1]) != null ? _ref1[0] : void 0) !== '{'))) { - return 1; - } - sameLine = true; - startIndex = i + 1; - stack.push(['{']); - idx = ago === '@' ? i - 2 : i - 1; - while (this.tag(idx - 2) === 'HERECOMMENT') { - idx -= 2; - } - prevTag = this.tag(idx - 1); - startsLine = !prevTag || (__indexOf.call(LINEBREAKS, prevTag) >= 0); - value = new String('{'); - value.generated = true; - tok = this.generate('{', value); - tokens.splice(idx, 0, tok); - this.detectEnd(i + 2, condition, action); - return 2; - }); + } + return true; }; - Rewriter.prototype.addImplicitParentheses = function() { - var action, callIndex, condition, noCall, seenControl, seenSingle; - noCall = seenSingle = seenControl = false; - callIndex = null; - condition = function(token, i) { - var post, tag, _ref, _ref1; - tag = token[0]; - if (!seenSingle && token.fromThen) { - return true; + Rewriter.prototype.looksObjectish = function(j) { + return this.matchTags(j, '@', null, ':') || this.matchTags(j, null, ':'); + }; + + Rewriter.prototype.findTagsBackwards = function(i, tags) { + var backStack, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; + backStack = []; + while (i >= 0 && (backStack.length || (_ref2 = this.tag(i), __indexOf.call(tags, _ref2) < 0) && ((_ref3 = this.tag(i), __indexOf.call(EXPRESSION_START, _ref3) < 0) || this.tokens[i].generated) && (_ref4 = this.tag(i), __indexOf.call(LINEBREAKS, _ref4) < 0))) { + if (_ref = this.tag(i), __indexOf.call(EXPRESSION_END, _ref) >= 0) { + backStack.push(this.tag(i)); } - if (tag === 'IF' || tag === 'ELSE' || tag === 'CATCH' || tag === '->' || tag === '=>' || tag === 'CLASS') { - seenSingle = true; + if ((_ref1 = this.tag(i), __indexOf.call(EXPRESSION_START, _ref1) >= 0) && backStack.length) { + backStack.pop(); } - if (tag === 'IF' || tag === 'ELSE' || tag === 'SWITCH' || tag === 'TRY' || tag === '=') { - seenControl = true; - } - if ((tag === '.' || tag === '?.' || tag === '::') && this.tag(i - 1) === 'OUTDENT') { - return true; - } - return !token.generated && this.tag(i - 1) !== ',' && (__indexOf.call(IMPLICIT_END, tag) >= 0 || (tag === 'INDENT' && !seenControl)) && (tag !== 'INDENT' || (((_ref = this.tag(i - 2)) !== 'CLASS' && _ref !== 'EXTENDS') && (_ref1 = this.tag(i - 1), __indexOf.call(IMPLICIT_BLOCK, _ref1) < 0) && !(callIndex === i - 1 && (post = this.tokens[i + 1]) && post.generated && post[0] === '{'))); - }; - action = function(token, i) { - return this.tokens.splice(i, 0, this.generate('CALL_END', ')')); - }; + i -= 1; + } + return _ref5 = this.tag(i), __indexOf.call(tags, _ref5) >= 0; + }; + + Rewriter.prototype.addImplicitBracesAndParens = function() { + var stack; + stack = []; return this.scanTokens(function(token, i, tokens) { - var callObject, current, next, prev, tag, _ref, _ref1, _ref2; + var endImplicitCall, endImplicitObject, forward, inImplicit, inImplicitCall, inImplicitControl, inImplicitObject, nextTag, prevTag, s, sameLine, stackIdx, stackTag, stackTop, startIdx, startImplicitCall, startImplicitObject, startsLine, tag, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6; tag = token[0]; - if (tag === 'CLASS' || tag === 'IF' || tag === 'FOR' || tag === 'WHILE') { - noCall = true; + prevTag = (i > 0 ? tokens[i - 1] : [])[0]; + nextTag = (i < tokens.length - 1 ? tokens[i + 1] : [])[0]; + stackTop = function() { + return stack[stack.length - 1]; + }; + startIdx = i; + forward = function(n) { + return i - startIdx + n; + }; + inImplicit = function() { + var _ref, _ref1; + return (_ref = stackTop()) != null ? (_ref1 = _ref[2]) != null ? _ref1.ours : void 0 : void 0; + }; + inImplicitCall = function() { + var _ref; + return inImplicit() && ((_ref = stackTop()) != null ? _ref[0] : void 0) === '('; + }; + inImplicitObject = function() { + var _ref; + return inImplicit() && ((_ref = stackTop()) != null ? _ref[0] : void 0) === '{'; + }; + inImplicitControl = function() { + var _ref; + return inImplicit && ((_ref = stackTop()) != null ? _ref[0] : void 0) === 'CONTROL'; + }; + startImplicitCall = function(j) { + var idx; + idx = j != null ? j : i; + stack.push([ + '(', idx, { + ours: true + } + ]); + tokens.splice(idx, 0, generate('CALL_START', '(')); + if (j == null) { + return i += 1; + } + }; + endImplicitCall = function() { + stack.pop(); + tokens.splice(i, 0, generate('CALL_END', ')')); + return i += 1; + }; + startImplicitObject = function(j, startsLine) { + var idx; + if (startsLine == null) { + startsLine = true; + } + idx = j != null ? j : i; + stack.push([ + '{', idx, { + sameLine: true, + startsLine: startsLine, + ours: true + } + ]); + tokens.splice(idx, 0, generate('{', generate(new String('{')))); + if (j == null) { + return i += 1; + } + }; + endImplicitObject = function() { + stack.pop(); + tokens.splice(i, 0, generate('}', '}')); + return i += 1; + }; + if (inImplicitCall() && (tag === 'IF' || tag === 'CLASS' || tag === 'SWITCH' || tag === 'CATCH')) { + stack.push([ + 'CONTROL', i, { + ours: true + } + ]); + return forward(1); } - _ref = tokens.slice(i - 1, +(i + 1) + 1 || 9e9), prev = _ref[0], current = _ref[1], next = _ref[2]; - callObject = !noCall && tag === 'INDENT' && next && next.generated && next[0] === '{' && prev && (_ref1 = prev[0], __indexOf.call(IMPLICIT_FUNC, _ref1) >= 0); - seenSingle = false; - seenControl = false; - if (__indexOf.call(LINEBREAKS, tag) >= 0) { - noCall = false; + if (tag === 'INDENT' && inImplicit()) { + if (prevTag !== '=>' && prevTag !== '->' && prevTag !== '[' && prevTag !== '(' && prevTag !== ',' && prevTag !== '{' && prevTag !== 'TRY' && prevTag !== 'ELSE' && prevTag !== '=') { + while (inImplicitCall()) { + endImplicitCall(); + } + } + if (inImplicitControl()) { + stack.pop(); + } + stack.push([tag, i]); + return forward(1); } - if (prev && !prev.spaced && tag === '?') { - token.call = true; + if (__indexOf.call(EXPRESSION_START, tag) >= 0) { + stack.push([tag, i]); + return forward(1); } - if (token.fromThen) { - return 1; + if (__indexOf.call(EXPRESSION_END, tag) >= 0) { + while (inImplicit()) { + if (inImplicitCall()) { + endImplicitCall(); + } else if (inImplicitObject()) { + endImplicitObject(); + } else { + stack.pop(); + } + } + stack.pop(); } - if (!(callObject || (prev != null ? prev.spaced : void 0) && (prev.call || (_ref2 = prev[0], __indexOf.call(IMPLICIT_FUNC, _ref2) >= 0)) && (__indexOf.call(IMPLICIT_CALL, tag) >= 0 || !(token.spaced || token.newLine) && __indexOf.call(IMPLICIT_UNSPACED_CALL, tag) >= 0))) { - return 1; + 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); } - callIndex = i; - tokens.splice(i, 0, this.generate('CALL_START', '(', token[2])); - this.detectEnd(i + 1, condition, action); - if (prev[0] === '?') { - prev[0] = 'FUNC_EXIST'; + if (this.matchTags(i, IMPLICIT_FUNC, 'INDENT') && ((_ref2 = stackTop()) != null ? _ref2[0] : void 0) !== '[' && !this.findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH', 'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL'])) { + startImplicitCall(i + 1); + stack.push(['INDENT', i + 2]); + return forward(3); } - return 2; + if (tag === ':') { + if (this.tag(i - 2) === '@') { + s = i - 2; + } else { + s = i - 1; + } + while (this.tag(s - 2) === 'HERECOMMENT') { + s -= 2; + } + startsLine = s === 0 || (_ref3 = this.tag(s - 1), __indexOf.call(LINEBREAKS, _ref3) >= 0) || tokens[s - 1].newLine; + if (stackTop()) { + _ref4 = stackTop(), stackTag = _ref4[0], stackIdx = _ref4[1]; + if ((stackTag === '{' || stackTag === 'INDENT' && this.tag(stackIdx - 1) === '{') && (startsLine || this.tag(s - 1) === ',' || this.tag(s - 1) === '{')) { + return forward(1); + } + } + startImplicitObject(s, !!startsLine); + return forward(2); + } + if (prevTag === 'OUTDENT' && inImplicitCall() && (tag === '.' || tag === '?.' || tag === '::')) { + endImplicitCall(); + return forward(1); + } + if (inImplicitObject() && __indexOf.call(LINEBREAKS, tag) >= 0) { + stackTop()[2].sameLine = false; + } + if (__indexOf.call(IMPLICIT_END, tag) >= 0) { + while (inImplicit()) { + _ref5 = stackTop(), stackTag = _ref5[0], stackIdx = _ref5[1], (_ref6 = _ref5[2], sameLine = _ref6.sameLine, startsLine = _ref6.startsLine); + if (inImplicitCall() && prevTag !== ',') { + endImplicitCall(); + } else if (inImplicitObject() && sameLine && !startsLine) { + endImplicitObject(); + } else if (inImplicitObject() && tag === 'TERMINATOR' && prevTag !== ',' && !(startsLine && this.looksObjectish(i + 1))) { + endImplicitObject(); + } else { + break; + } + } + } + if (tag === ',' && !this.looksObjectish(i + 1) && inImplicitObject() && (nextTag !== 'TERMINATOR' || !this.looksObjectish(i + 2))) { + if (nextTag === 'OUTDENT') { + i += 1; + } + while (inImplicitObject()) { + endImplicitObject(); + } + } + return forward(1); }); }; Rewriter.prototype.addLocationDataToGeneratedTokens = function() { return this.scanTokens(function(token, i, tokens) { - var prevToken, tag; - tag = token[0]; - if ((token.generated || token.explicit) && (!token[2])) { - if (i > 0) { - prevToken = tokens[i - 1]; - token[2] = { - first_line: prevToken[2].last_line, - first_column: prevToken[2].last_column, - last_line: prevToken[2].last_line, - last_column: prevToken[2].last_column - }; - } else { - token[2] = { - first_line: 0, - first_column: 0, - last_line: 0, - last_column: 0 - }; - } + var last_column, last_line, _ref, _ref1, _ref2; + if (token[2]) { + return 1; } + if (!(token.generated || token.explicit)) { + return 1; + } + _ref2 = (_ref = (_ref1 = tokens[i - 1]) != null ? _ref1[2] : void 0) != null ? _ref : { + last_line: 0, + last_column: 0 + }, last_line = _ref2.last_line, last_column = _ref2.last_column; + token[2] = { + first_line: last_line, + first_column: last_column, + last_line: last_line, + last_column: last_column + }; return 1; }); }; @@ -330,12 +433,7 @@ return [indent, outdent]; }; - Rewriter.prototype.generate = function(tag, value) { - var tok; - tok = [tag, value]; - tok.generated = true; - return tok; - }; + Rewriter.prototype.generate = generate; Rewriter.prototype.tag = function(i) { var _ref; diff --git a/src/nodes.coffee b/src/nodes.coffee index 760061cb..9b51b786 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -566,31 +566,12 @@ exports.Call = class Call extends Base ifn = unfoldSoak o, call, 'variable' ifn - # Walk through the objects in the arguments, moving over simple values. - # This allows syntax like `call a: b, c` into `call({a: b}, c);` - filterImplicitObjects: (list) -> - nodes = [] - for node in list - unless node.isObject?() and node.base.generated - nodes.push node - continue - obj = null - for prop in node.base.properties - if prop instanceof Assign or prop instanceof Comment - nodes.push obj = new Obj properties = [], true if not obj - properties.push prop - else - nodes.push prop - obj = null - nodes - # Compile a vanilla function call. compileNode: (o) -> @variable?.front = @front if code = Splat.compileSplattedArray o, @args, true return @compileSplat o, code - args = @filterImplicitObjects @args - args = (arg.compile o, LEVEL_LIST for arg in args).join ', ' + args = (arg.compile o, LEVEL_LIST for arg in @args).join ', ' if @isSuper @superReference(o) + ".call(#{@superThis(o)}#{ args and ', ' + args })" else @@ -840,14 +821,11 @@ exports.Arr = class Arr extends Base children: ['objects'] - filterImplicitObjects: Call::filterImplicitObjects - compileNode: (o) -> return '[]' unless @objects.length o.indent += TAB - objs = @filterImplicitObjects @objects - return code if code = Splat.compileSplattedArray o, objs - code = (obj.compile o, LEVEL_LIST for obj in objs).join ', ' + return code if code = Splat.compileSplattedArray o, @objects + code = (obj.compile o, LEVEL_LIST for obj in @objects).join ', ' if code.indexOf('\n') >= 0 "[\n#{o.indent}#{code}\n#{@tab}]" else diff --git a/src/rewriter.coffee b/src/rewriter.coffee index 8ea0fcd4..6328bc89 100644 --- a/src/rewriter.coffee +++ b/src/rewriter.coffee @@ -5,6 +5,12 @@ # shorthand into the unambiguous long form, add implicit indentation and # parentheses, and generally clean things up. +# Create a generated token: one that exists due to a use of implicit syntax. +generate = (tag, value) -> + tok = [tag, value] + tok.generated = yes + tok + # The **Rewriter** class is used by the [Lexer](lexer.html), directly against # its internal array of tokens. class exports.Rewriter @@ -24,8 +30,7 @@ class exports.Rewriter @closeOpenIndexes() @addImplicitIndentation() @tagPostfixConditionals() - @addImplicitBraces() - @addImplicitParentheses() + @addImplicitBracesAndParens() @addLocationDataToGeneratedTokens() @tokens @@ -71,7 +76,6 @@ class exports.Rewriter # its paired close. We have the mis-nested outdent case included here for # calls that close on the same line, just before their outdent. closeOpenCalls: -> - condition = (token, i) -> token[0] in [')', 'CALL_END'] or token[0] is 'OUTDENT' and @tag(i - 1) is ')' @@ -86,7 +90,6 @@ class exports.Rewriter # The lexer has tagged the opening parenthesis of an indexing operation call. # Match it with its paired close. closeOpenIndexes: -> - condition = (token, i) -> token[0] in [']', 'INDEX_END'] @@ -97,121 +100,237 @@ class exports.Rewriter @detectEnd i + 1, condition, action if token[0] is 'INDEX_START' 1 - # Object literals may be written with implicit braces, for simple cases. - # Insert the missing braces here, so that the parser doesn't have to. - addImplicitBraces: -> + # 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...) -> + 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 - stack = [] - start = null - startsLine = null - sameLine = yes - startIndent = 0 - startIndex = 0 + # yes iff standing in front of something looking like + # @: or :, skipping over 'HERECOMMENT's + looksObjectish: (j) -> + @matchTags(j, '@', null, ':') or @matchTags(j, null, ':') - condition = (token, i) -> - [one, two, three] = @tokens[i + 1 .. i + 3] - return no if 'HERECOMMENT' is one?[0] - [tag] = token - sameLine = no if tag in LINEBREAKS - return ( - (tag in ['TERMINATOR', 'OUTDENT'] or - (tag in IMPLICIT_END and sameLine and not (i - startIndex is 1))) and - ((!startsLine and @tag(i - 1) isnt ',') or - not (two?[0] is ':' or one?[0] is '@' and three?[0] is ':'))) or - (tag is ',' and one and - one[0] not in ['IDENTIFIER', 'NUMBER', 'STRING', '@', 'TERMINATOR', 'OUTDENT'] - ) + # yes iff 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) -> + backStack = [] + while i >= 0 and (backStack.length or + @tag(i) not in tags and + (@tag(i) not in EXPRESSION_START or @tokens[i].generated) and + @tag(i) not in LINEBREAKS) + backStack.push @tag(i) if @tag(i) in EXPRESSION_END + backStack.pop() if @tag(i) in EXPRESSION_START and backStack.length + i -= 1 + @tag(i) in tags - action = (token, i) -> - tok = @generate '}', '}' - @tokens.splice i, 0, tok + # Look for signs of implicit calls and objects in the token stream and + # add them. + addImplicitBracesAndParens: -> + # Track current balancing depth (both implicit and explicit) on stack. + stack = [] @scanTokens (token, i, tokens) -> - if (tag = token[0]) in EXPRESSION_START - stack.push [(if tag is 'INDENT' and @tag(i - 1) is '{' then '{' else tag), i] - return 1 + [tag] = token + [prevTag] = if i > 0 then tokens[i - 1] else [] + [nextTag] = if i < tokens.length - 1 then tokens[i + 1] else [] + stackTop = -> stack[stack.length - 1] + startIdx = i + + # Helper function, used for keeping track of the number of tokens consumed + # and spliced, when returning for getting a new token. + forward = (n) -> i - startIdx + n + + # Helper functions + inImplicit = -> stackTop()?[2]?.ours + inImplicitCall = -> inImplicit() and stackTop()?[0] is '(' + inImplicitObject = -> inImplicit() and stackTop()?[0] is '{' + # Unclosed control statement inside implicit parens (like + # class declaration or if-conditionals) + inImplicitControl = -> inImplicit and stackTop()?[0] is 'CONTROL' + + startImplicitCall = (j) -> + idx = j ? i + stack.push ['(', idx, ours: yes] + tokens.splice idx, 0, generate 'CALL_START', '(' + i += 1 if not j? + + endImplicitCall = -> + stack.pop() + tokens.splice i, 0, generate 'CALL_END', ')' + i += 1 + + startImplicitObject = (j, startsLine = yes) -> + idx = j ? i + stack.push ['{', idx, sameLine: yes, startsLine: startsLine, ours: yes] + tokens.splice idx, 0, generate '{', generate(new String('{')) + i += 1 if not j? + + endImplicitObject = -> + stack.pop() + tokens.splice i, 0, generate '}', '}' + i += 1 + + # Don't end an implicit call on next indent if any of these are in an argument + if inImplicitCall() and tag in ['IF', 'CLASS', 'SWITCH', 'CATCH'] + stack.push ['CONTROL', i, ours: true] + return forward(1) + + if tag is 'INDENT' and inImplicit() + # An INDENT closes an implicit call unless + # 1. We have seen a CONTROL argument on the line. + # 2. The last token before the indent is part of the list below + if prevTag not in ['=>', '->', '[', '(', ',', '{', 'TRY', 'ELSE', '='] + endImplicitCall() while inImplicitCall() + stack.pop() if inImplicitControl() + stack.push [tag, i] + return forward(1) + + # Straightforward start of explicit expression + if tag in EXPRESSION_START + stack.push [tag, i] + return forward(1) + + # Close all implicit expressions inside of explicitly closed expressions. if tag in EXPRESSION_END - start = stack.pop() - return 1 - return 1 unless tag is ':' and - ((ago = @tag i - 2) is ':' or stack[stack.length - 1]?[0] isnt '{') - sameLine = yes - startIndex = i + 1 - stack.push ['{'] - idx = if ago is '@' then i - 2 else i - 1 - idx -= 2 while @tag(idx - 2) is 'HERECOMMENT' - prevTag = @tag(idx - 1) - startsLine = not prevTag or (prevTag in LINEBREAKS) - value = new String('{') - value.generated = yes - tok = @generate '{', value - tokens.splice idx, 0, tok - @detectEnd i + 2, condition, action - 2 + while inImplicit() + if inImplicitCall() + endImplicitCall() + else if inImplicitObject() + endImplicitObject() + else + stack.pop() + stack.pop() - # Methods may be optionally called without parentheses, for simple cases. - # Insert the implicit parentheses here, so that the parser doesn't have to - # deal with them. - addImplicitParentheses: -> + # Recognize standard implicit calls like + # f a, f() b, f? c, h[0] d etc. + 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 + not tokens[i + 1]?.spaced and not tokens[i + 1]?.newLine) + tag = token[0] = 'FUNC_EXIST' if tag is '?' + startImplicitCall i + 1 + return forward(2) - noCall = seenSingle = seenControl = no - callIndex = null + # Implicit call taking an implicit indented object as first argument. + # f + # a: b + # c: d + # and + # f + # 1 + # a: b + # b: c + # Don't accept implicit calls of this type, when on the same line + # as the control strucutures below as that may misinterpret constructs like: + # if f + # a: 1 + # as + # if f(a: 1) + # which is probably always unintended. + # Furthermore don't allow this in literal arrays, as + # that creates grammatical ambiguities. + if @matchTags(i, IMPLICIT_FUNC, 'INDENT') and + stackTop()?[0] isnt '[' and + not @findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH', + 'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL']) + startImplicitCall i + 1 + stack.push ['INDENT', i + 2] + return forward(3) - condition = (token, i) -> - [tag] = token - return yes if not seenSingle and token.fromThen - seenSingle = yes if tag in ['IF', 'ELSE', 'CATCH', '->', '=>', 'CLASS'] - seenControl = yes if tag in ['IF', 'ELSE', 'SWITCH', 'TRY', '='] - return yes if tag in ['.', '?.', '::'] and @tag(i - 1) is 'OUTDENT' - not token.generated and @tag(i - 1) isnt ',' and (tag in IMPLICIT_END or - (tag is 'INDENT' and not seenControl)) and - (tag isnt 'INDENT' or - (@tag(i - 2) not in ['CLASS', 'EXTENDS'] and @tag(i - 1) not in IMPLICIT_BLOCK and - not (callIndex is i - 1 and (post = @tokens[i + 1]) and post.generated and post[0] is '{'))) + # 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 -= 2 while @tag(s - 2) is 'HERECOMMENT' - action = (token, i) -> - @tokens.splice i, 0, @generate 'CALL_END', ')' + startsLine = s is 0 or @tag(s - 1) in LINEBREAKS or tokens[s - 1].newLine + # Are we just continuing an already declared object? + if stackTop() + [stackTag, stackIdx] = stackTop() + if (stackTag is '{' or stackTag is 'INDENT' and @tag(stackIdx - 1) is '{') and + (startsLine or @tag(s - 1) is ',' or @tag(s - 1) is '{') + return forward(1) - @scanTokens (token, i, tokens) -> - tag = token[0] - noCall = yes if tag in ['CLASS', 'IF', 'FOR', 'WHILE'] - [prev, current, next] = tokens[i - 1 .. i + 1] - callObject = not noCall and tag is 'INDENT' and - next and next.generated and next[0] is '{' and - prev and prev[0] in IMPLICIT_FUNC - seenSingle = no - seenControl = no - noCall = no if tag in LINEBREAKS - token.call = yes if prev and not prev.spaced and tag is '?' - return 1 if token.fromThen - return 1 unless callObject or - prev?.spaced and (prev.call or prev[0] in IMPLICIT_FUNC) and - (tag in IMPLICIT_CALL or not (token.spaced or token.newLine) and tag in IMPLICIT_UNSPACED_CALL) - callIndex = i - tokens.splice i, 0, @generate 'CALL_START', '(', token[2] - @detectEnd i + 1, condition, action - prev[0] = 'FUNC_EXIST' if prev[0] is '?' - 2 + startImplicitObject(s, !!startsLine) + return forward(2) + + # End implicit calls when chaining method calls + # like e.g.: + # f -> + # a + # .g b, -> + # c + # .h a + if prevTag is 'OUTDENT' and inImplicitCall() and tag in ['.', '?.', '::'] + endImplicitCall() + return forward(1) + + stackTop()[2].sameLine = no if inImplicitObject() and tag in LINEBREAKS + + if tag in IMPLICIT_END + while inImplicit() + [stackTag, stackIdx, {sameLine, startsLine}] = stackTop() + # Close implicit calls when reached end of argument list + if inImplicitCall() and prevTag isnt ',' + endImplicitCall() + # Close implicit objects such as: + # return a: 1, b: 2 unless true + else if inImplicitObject() and sameLine and not startsLine + 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 + # the continuation of an object. + else if inImplicitObject() and tag is 'TERMINATOR' and prevTag isnt ',' and + not (startsLine and @looksObjectish(i + 1)) + endImplicitObject() + else + break + + # Close implicit object if comma is the last character + # and what comes after doesn't look like it belongs. + # This is used for trailing commas and calls, like: + # x = + # a: b, + # c: d, + # e = 2 + # + # and + # + # f a, b: c, d: e, f, g: h: i, j + if tag is ',' and not @looksObjectish(i + 1) and inImplicitObject() and + (nextTag isnt 'TERMINATOR' or not @looksObjectish(i + 2)) + # When nextTag is OUTDENT the comma is insignificant and + # should just be ignored so embed it in the implicit object. + # + # When it isn't the comma go on to play a role in a call or + # array further up the stack, so give it a chance. + if nextTag is 'OUTDENT' + i += 1 + while inImplicitObject() + endImplicitObject() + return forward(1) # Add location data to all tokens generated by the rewriter. addLocationDataToGeneratedTokens: -> @scanTokens (token, i, tokens) -> - tag = token[0] - if (token.generated or token.explicit) and (not token[2]) - if i > 0 - prevToken = tokens[i-1] - token[2] = - first_line: prevToken[2].last_line - first_column: prevToken[2].last_column - last_line: prevToken[2].last_line - last_column: prevToken[2].last_column - else - token[2] = - first_line: 0 - first_column: 0 - last_line: 0 - last_column: 0 - return 1 + return 1 if token[2] + return 1 unless token.generated or token.explicit + {last_line, last_column} = tokens[i - 1]?[2] ? last_line: 0, last_column: 0 + token[2] = + first_line: last_line + first_column: last_column + last_line: last_line + last_column: last_column + 1 # Because our grammar is LALR(1), it can't handle some single-line # expressions that lack ending delimiters. The **Rewriter** adds the implicit @@ -276,11 +395,7 @@ class exports.Rewriter indent.explicit = outdent.explicit = yes if not implicit [indent, outdent] - # Create a generated token: one that exists due to a use of implicit syntax. - generate: (tag, value) -> - tok = [tag, value] - tok.generated = yes - tok + generate: generate # Look up a tag by token index. tag: (i) -> @tokens[i]?[0] @@ -330,7 +445,8 @@ IMPLICIT_UNSPACED_CALL = ['+', '-'] IMPLICIT_BLOCK = ['->', '=>', '{', '[', ','] # Tokens that always mark the end of an implicit call for single-liners. -IMPLICIT_END = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY', 'LOOP', 'TERMINATOR'] +IMPLICIT_END = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY', + 'LOOP', 'TERMINATOR'] # Single-line flavors of block expressions that have unclosed endings. # The grammar can't disambiguate them, so we insert the implicit indentation. diff --git a/test/classes.coffee b/test/classes.coffee index a2771498..4826b3b6 100644 --- a/test/classes.coffee +++ b/test/classes.coffee @@ -696,3 +696,14 @@ test "#2359: constructors should not return an explicit value", -> return bar: 7 baz() """ + +test "#2319: fn class n extends o.p [INDENT] x = 123", -> + first = -> + + base = onebase: -> + + first class OneKeeper extends base.onebase + one = 1 + one: -> one + + eq new OneKeeper().one(), 1 \ No newline at end of file diff --git a/test/control_flow.coffee b/test/control_flow.coffee index 2aa288d0..f2b04eaa 100644 --- a/test/control_flow.coffee +++ b/test/control_flow.coffee @@ -422,7 +422,6 @@ test "Issue #997. Switch doesn't fallthrough.", -> test "Throw should be usable as an expression.", -> - try false or throw 'up' throw new Error 'failed' diff --git a/test/function_invocation.coffee b/test/function_invocation.coffee index 0cdc27ce..559f2a9f 100644 --- a/test/function_invocation.coffee +++ b/test/function_invocation.coffee @@ -558,3 +558,116 @@ test "#2617: implicit call before unrelated implicit object", -> result = if pass 1 one: 1 eq result.one, 1 + +test "#2292, b: f (z),(x)", -> + f = (x, y) -> y + one = 1 + two = 2 + o = b: f (one),(two) + eq o.b, 2 + +test "#2297, Different behaviors on interpreting literal", -> + foo = (x, y) -> y + bar = + baz: foo 100, on + + eq bar.baz, on + + qux = (x) -> x + quux = qux + corge: foo 100, true + + eq quux.corge, on + + xyzzy = + e: 1 + f: foo + a: 1 + b: 2 + , + one: 1 + two: 2 + three: 3 + g: + a: 1 + b: 2 + c: foo 2, + one: 1 + two: 2 + three: 3 + d: 3 + four: 4 + h: foo one: 1, two: 2, three: three: three: 3, + 2 + + eq xyzzy.f.two, 2 + eq xyzzy.g.c.three, 3 + eq xyzzy.four, 4 + eq xyzzy.h, 2 + + thud = foo + 1 + one: 1 + two: 2 + three: 3 + 2 + 3 + eq thud.two, 2 + +test "#2715, Chained implicit calls", -> + first = (x) -> x + second = (x, y) -> y + + foo = first first + one: 1 + eq foo.one, 1 + + bar = first second + one: 1, 2 + eq bar, 2 + + baz = first second + one: 1, + 2 + eq baz, 2 + + qux = first second + 1 + 2 + 3 + 4 + eq qux, 2 + + +test "More implicit calls", -> + first = (x) -> x + second = (x, y) -> y + + foo = no + if (first + yes) + foo = yes + eq foo, yes + + foo = no + if not first + no + foo = yes + eq foo, no + +test "Implicit calls and new", -> + first = (x) -> x + foo = (@x) -> + bar = first new foo first 1 + eq bar.x, 1 + + third = (x, y, z) -> z + baz = first new foo + new + foo third + one: 1 + two: 2 + 1 + three: 3 + 2 + eq baz.x.x.three, 3 \ No newline at end of file diff --git a/test/objects.coffee b/test/objects.coffee index f56e48c6..6e26b6f1 100644 --- a/test/objects.coffee +++ b/test/objects.coffee @@ -222,6 +222,118 @@ test "#1436: `for` etc. work as normal property names", -> obj.for = 'foo' of obj eq yes, obj.hasOwnProperty 'for' +test "#2706, Un-bracketed object as argument causes inconsistent behavior", -> + foo = (x, y) -> y + bar = baz: yes + + eq yes, foo x: 1, bar.baz + +test "#2608, Allow inline objects in arguments to be followed by more arguments", -> + foo = (x, y) -> y + + eq yes, foo x: 1, y: 2, yes + +test "#2308, a: b = c:1", -> + foo = a: b = c: yes + eq b.c, yes + eq foo.a.c, yes + +test "#2317, a: b c: 1", -> + foo = (x) -> x + bar = a: foo c: yes + eq bar.a.c, yes + +test "#1896, a: func b, {c: d}", -> + first = (x) -> x + second = (x, y) -> y + third = (x, y, z) -> z + + one = 1 + two = 2 + three = 3 + four = 4 + + foo = a: second one, {c: two} + eq foo.a.c, two + + bar = a: second one, c: two + eq bar.a.c, two + + baz = a: second one, {c: two}, e: first first h: three + eq baz.a.c, two + + qux = a: third one, {c: two}, e: first first h: three + eq qux.a.e.h, three + + quux = a: third one, {c: two}, e: first(three), h: four + eq quux.a.e, three + eq quux.a.h, four + + corge = a: third one, {c: two}, e: second three, h: four + eq corge.a.e.h, four + +test "Implicit objects, functions and arrays", -> + first = (x) -> x + second = (x, y) -> y + + foo = [ + 1 + one: 1 + two: 2 + three: 3 + more: + four: 4 + five: 5, six: 6 + 2, 3, 4 + 5] + eq foo[2], 2 + eq foo[1].more.six, 6 + + bar = [ + 1 + first first first second 1, + one: 1, twoandthree: twoandthree: two: 2, three: 3 + 2, + 2 + one: 1 + two: 2 + three: first second -> + no + , -> + 3 + 3 + 4] + eq bar[2], 2 + eq bar[1].twoandthree.twoandthree.two, 2 + eq bar[3].three(), 3 + eq bar[4], 3 + +test "#2549, Brace-less Object Literal as a Second Operand on a New Line", -> + foo = no or + one: 1 + two: 2 + three: 3 + eq foo.one, 1 + + bar = yes and one: 1 + eq bar.one, 1 + + baz = null ? + one: 1 + two: 2 + eq baz.two, 2 + +test "#1865, syntax regression 1.1.3", -> + foo = (x, y) -> y + + bar = a: foo (->), + c: yes + eq bar.a.c, yes + + baz = a: foo (->), c: yes + eq baz.a.c, yes + + test "#1322: implicit call against implicit object with block comments", -> ((obj, arg) -> eq obj.x * obj.y, 6 @@ -273,7 +385,5 @@ test "#1961, #1974, regression with compound assigning to an implicit object", - test "#2207: Immediate implicit closes don't close implicit objects", -> func = -> key: for i in [1, 2, 3] then i - + eq func().key.join(' '), '1 2 3' - - \ No newline at end of file