1
0
Fork 0
mirror of https://github.com/jashkenas/coffeescript.git synced 2022-11-09 12:23:24 -05:00

[CS2] Fix #4631: Expansion that becomes rest parameter causes runtime error (#4634)

* expansion-rest bug fix

* tests; improved implicit call recognition with dots on the left in the `rewriter`

* whitespace cleanup

* more tests
This commit is contained in:
zdenko 2017-08-17 22:13:52 +02:00 committed by Geoffrey Booth
parent eff160eeb7
commit aef54aeaf7
7 changed files with 264 additions and 7 deletions

View file

@ -280,7 +280,7 @@
stack = [];
start = null;
return this.scanTokens(function(token, i, tokens) {
var endImplicitCall, endImplicitObject, forward, implicitObjectContinues, inImplicit, inImplicitCall, inImplicitControl, inImplicitObject, isImplicit, isImplicitCall, isImplicitObject, k, newLine, nextTag, nextToken, offset, prevTag, prevToken, ref, s, sameLine, stackIdx, stackItem, stackTag, stackTop, startIdx, startImplicitCall, startImplicitObject, startsLine, tag;
var endImplicitCall, endImplicitObject, forward, implicitObjectContinues, inImplicit, inImplicitCall, inImplicitControl, inImplicitObject, isImplicit, isImplicitCall, isImplicitObject, k, newLine, nextTag, nextToken, offset, prevTag, prevToken, ref, ref1, s, sameLine, stackIdx, stackItem, stackTag, stackTop, startIdx, startImplicitCall, startImplicitObject, startsLine, tag;
[tag] = token;
[prevTag] = prevToken = i > 0 ? tokens[i - 1] : [];
[nextTag] = nextToken = i < tokens.length - 1 ? tokens[i + 1] : [];
@ -422,7 +422,7 @@
// Recognize standard implicit calls like
// f a, f() b, f? c, h[0] d etc.
// Added support for spread dots on the left side: f ...a
if ((indexOf.call(IMPLICIT_FUNC, tag) >= 0 && token.spaced || tag === '?' && i > 0 && !tokens[i - 1].spaced) && (indexOf.call(IMPLICIT_CALL, nextTag) >= 0 || nextTag === '...' || indexOf.call(IMPLICIT_UNSPACED_CALL, nextTag) >= 0 && !nextToken.spaced && !nextToken.newLine)) {
if ((indexOf.call(IMPLICIT_FUNC, tag) >= 0 && token.spaced || tag === '?' && i > 0 && !tokens[i - 1].spaced) && (indexOf.call(IMPLICIT_CALL, nextTag) >= 0 || (nextTag === '...' && (ref = this.tag(i + 2), indexOf.call(IMPLICIT_CALL, ref) >= 0) && !this.findTagsBackwards(i, ['INDEX_START', '['])) || indexOf.call(IMPLICIT_UNSPACED_CALL, nextTag) >= 0 && !nextToken.spaced && !nextToken.newLine)) {
if (tag === '?') {
tag = token[0] = 'FUNC_EXIST';
}
@ -456,9 +456,9 @@
if (tag === ':') {
// Go back to the (implicit) start of the object.
s = (function() {
var ref;
var ref1;
switch (false) {
case ref = this.tag(i - 1), indexOf.call(EXPRESSION_END, ref) < 0:
case ref1 = this.tag(i - 1), indexOf.call(EXPRESSION_END, ref1) < 0:
return start[1];
case this.tag(i - 2) !== '@':
return i - 2;
@ -466,7 +466,7 @@
return i - 1;
}
}).call(this);
startsLine = s === 0 || (ref = this.tag(s - 1), indexOf.call(LINEBREAKS, ref) >= 0) || tokens[s - 1].newLine;
startsLine = s === 0 || (ref1 = this.tag(s - 1), indexOf.call(LINEBREAKS, ref1) >= 0) || tokens[s - 1].newLine;
// Are we just continuing an already declared object?
if (stackTop()) {
[stackTag, stackIdx] = stackTop();

View file

@ -267,7 +267,8 @@ exports.Rewriter = class Rewriter
# Added support for spread dots on the left side: f ...a
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 is '...' or
(nextTag in IMPLICIT_CALL or
(nextTag is '...' and @tag(i + 2) in IMPLICIT_CALL and not @findTagsBackwards(i, ['INDEX_START', '['])) or
nextTag in IMPLICIT_UNSPACED_CALL and
not nextToken.spaced and not nextToken.newLine)
tag = token[0] = 'FUNC_EXIST' if tag is '?'

View file

@ -185,6 +185,12 @@ test "destructuring assignment with splats", ->
arrayEq [b,c,d], y
eq e, z
# Should not trigger implicit call, e.g. rest ... => rest(...)
[x,y ...,z] = [a,b,c,d,e]
eq a, x
arrayEq [b,c,d], y
eq e, z
test "deep destructuring assignment with splats", ->
a={}; b={}; c={}; d={}; e={}; f={}; g={}; h={}; i={}
[u, [v, w..., x], y..., z] = [a, [b, c, d, e], f, g, h, i]
@ -229,6 +235,11 @@ test "destructuring assignment with objects and splats", ->
eq a, y
arrayEq [b,c,d], z
# Should not trigger implicit call, e.g. rest ... => rest(...)
{a: b: [y, z ...]} = obj
eq a, y
arrayEq [b,c,d], z
test "destructuring assignment against an expression", ->
a={}; b={}
[y, z] = if true then [a, b] else [b, a]
@ -263,6 +274,15 @@ test "destructuring assignment with splats and default values", ->
eq b, 1
deepEqual d, {}
# Should not trigger implicit call, e.g. rest ... => rest(...)
{
a: {b} = c
d ...
} = obj
eq b, 1
deepEqual d, {}
test "destructuring assignment with splat with default value", ->
obj = {}
c = {val: 1}
@ -276,6 +296,18 @@ test "destructuring assignment with multiple splats in different objects", ->
deepEqual a, val: 1
deepEqual b, val: 2
# Should not trigger implicit call, e.g. rest ... => rest(...)
{
a: {
a ...
}
b: {
b ...
}
} = obj
deepEqual a, val: 1
deepEqual b, val: 2
test "destructuring assignment with dynamic keys and splats", ->
i = 0
foo = -> ++i
@ -299,6 +331,15 @@ test "destructuring assignment with objects and splats: Babel tests", ->
n = { x, y, z... }
deepEqual n, { x: 1, y: 2, a: 3, b: 4 }
# Should not trigger implicit call, e.g. rest ... => rest(...)
{ x, y, z ... } = { x: 1, y: 2, a: 3, b: 4 }
eq x, 1
eq y, 2
deepEqual z, { a: 3, b: 4 }
n = { x, y, z ... }
deepEqual n, { x: 1, y: 2, a: 3, b: 4 }
test "deep destructuring assignment with objects: ES2015", ->
a1={}; b1={}; c1={}; d1={}
obj = {
@ -320,6 +361,13 @@ test "deep destructuring assignment with objects: ES2015", ->
eq bb, b1
eq r2.b2, obj.b2
# Should not trigger implicit call, e.g. rest ... => rest(...)
{a: w, b: {c: {d: {b1: bb, r1 ...}}}, r2 ...} = obj
eq r1.e, c1
eq r2.b, undefined
eq bb, b1
eq r2.b2, obj.b2
test "deep destructuring assignment with defaults: ES2015", ->
obj =
b: { c: 1, baz: 'qux' }
@ -343,16 +391,55 @@ test "deep destructuring assignment with defaults: ES2015", ->
eq hello, 'world'
deepEqual h, some: 'prop'
# Should not trigger implicit call, e.g. rest ... => rest(...)
{
a ...
b: {
c,
d ...
}
e: {
f: hello
g: {
h ...
} = i
} = j
} = obj
deepEqual a, foo: 'bar'
eq c, 1
deepEqual d, baz: 'qux'
eq hello, 'world'
deepEqual h, some: 'prop'
test "object spread properties: ES2015", ->
obj = {a: 1, b: 2, c: 3, d: 4, e: 5}
obj2 = {obj..., c:9}
eq obj2.c, 9
eq obj.a, obj2.a
# Should not trigger implicit call, e.g. rest ... => rest(...)
obj2 = {
obj ...
c:9
}
eq obj2.c, 9
eq obj.a, obj2.a
obj2 = {obj..., a: 8, c: 9, obj...}
eq obj2.c, 3
eq obj.a, obj2.a
# Should not trigger implicit call, e.g. rest ... => rest(...)
obj2 = {
obj ...
a: 8
c: 9
obj ...
}
eq obj2.c, 3
eq obj.a, obj2.a
obj3 = {obj..., b: 7, g: {obj2..., c: 1}}
eq obj3.g.c, 1
eq obj3.b, 7
@ -370,10 +457,42 @@ test "object spread properties: ES2015", ->
eq obj4.f.g, 5
deepEqual obj4.f, obj.c.f
# Should not trigger implicit call, e.g. rest ... => rest(...)
(({
a
b
r ...
}) ->
eq 1, a
deepEqual r, {c: 3, d: 44, e: 55}
) {
obj2 ...
d: 44
e: 55
}
# Should not trigger implicit call, e.g. rest ... => rest(...)
obj4 = {
a: 10
obj.c ...
}
eq obj4.a, 10
eq obj4.d, 3
eq obj4.f.g, 5
deepEqual obj4.f, obj.c.f
obj5 = {obj..., ((k) -> {b: k})(99)...}
eq obj5.b, 99
deepEqual obj5.c, obj.c
# Should not trigger implicit call, e.g. rest ... => rest(...)
obj5 = {
obj ...
((k) -> {b: k})(99) ...
}
eq obj5.b, 99
deepEqual obj5.c, obj.c
fn = -> {c: {d: 33, e: 44, f: {g: 55}}}
obj6 = {obj..., fn()...}
eq obj6.c.d, 33
@ -382,7 +501,16 @@ test "object spread properties: ES2015", ->
obj7 = {obj..., fn()..., {c: {d: 55, e: 66, f: {77}}}...}
eq obj7.c.d, 55
deepEqual obj6.c, {d: 33, e: 44, f: {g: 55}}
# Should not trigger implicit call, e.g. rest ... => rest(...)
obj7 = {
obj ...
fn() ...
{c: {d: 55, e: 66, f: {77}}} ...
}
eq obj7.c.d, 55
deepEqual obj6.c, {d: 33, e: 44, f: {g: 55}}
obj =
a:
b:

View file

@ -347,12 +347,26 @@ test "passing splats to functions", ->
arrayEq [2..6], others
eq 7, last
# Should not trigger implicit call, e.g. rest ... => rest(...)
arrayEq [0..4], id id [0..4] ...
fn = (a, b, c ..., d) -> [a, b, c, d]
range = [0..3]
[first, second, others, last] = fn range ..., 4, [5 ... 8] ...
eq 0, first
eq 1, second
arrayEq [2..6], others
eq 7, last
test "splat variables are local to the function", ->
outer = "x"
clobber = (avar, outer...) -> outer
clobber "foo", "bar"
eq "x", outer
test "Issue 4631: left and right spread dots with preceding space", ->
a = []
f = (a) -> a
eq yes, (f ...a) is (f ... a) is (f a...) is (f a ...) is f(a...) is f(...a) is f(a ...) is f(... a)
test "Issue 894: Splatting against constructor-chained functions.", ->
@ -387,6 +401,16 @@ test "splats with super() within classes.", ->
super nums...
ok (new Child).meth().join(' ') is '3 2 1'
# Should not trigger implicit call, e.g. rest ... => rest(...)
class Parent
meth: (args ...) ->
args
class Child extends Parent
meth: ->
nums = [3, 2, 1]
super nums ...
ok (new Child).meth().join(' ') is '3 2 1'
test "#1011: passing a splat to a method of a number", ->
eq '1011', 11.toString [2]...
@ -394,12 +418,21 @@ test "#1011: passing a splat to a method of a number", ->
eq '1011', 69.0.toString [4]...
eq '1011', (131.0).toString [5]...
# Should not trigger implicit call, e.g. rest ... => rest(...)
eq '1011', 11.toString [2] ...
eq '1011', (31).toString [3] ...
eq '1011', 69.0.toString [4] ...
eq '1011', (131.0).toString [5] ...
test "splats and the `new` operator: functions that return `null` should construct their instance", ->
args = []
child = new (constructor = -> null) args...
ok child instanceof constructor
# Should not trigger implicit call, e.g. rest ... => rest(...)
child = new (constructor = -> null) args ...
ok child instanceof constructor
test "splats and the `new` operator: functions that return functions should construct their return value", ->
args = []
fn = ->

View file

@ -110,6 +110,12 @@ test "splats", ->
arrayEq [0, 1], (((splat..., _, _1) -> splat) 0, 1, 2, 3)
arrayEq [2], (((_, _1, splat..., _2) -> splat) 0, 1, 2, 3)
# Should not trigger implicit call, e.g. rest ... => rest(...)
arrayEq [0, 1, 2], (((splat ...) -> splat) 0, 1, 2)
arrayEq [2, 3], (((_, _1, splat ...) -> splat) 0, 1, 2, 3)
arrayEq [0, 1], (((splat ..., _, _1) -> splat) 0, 1, 2, 3)
arrayEq [2], (((_, _1, splat ..., _2) -> splat) 0, 1, 2, 3)
test "destructured splatted parameters", ->
arr = [0,1,2]
splatArray = ([a...]) -> a
@ -117,6 +123,10 @@ test "destructured splatted parameters", ->
arrayEq splatArray(arr), arr
arrayEq splatArrayRest(arr,0,1,2), arr
# Should not trigger implicit call, e.g. rest ... => rest(...)
splatArray = ([a ...]) -> a
splatArrayRest = ([a ...],b ...) -> arrayEq(a,b); b
test "@-parameters: automatically assign an argument's value to a property of the context", ->
nonce = {}
@ -127,10 +137,18 @@ test "@-parameters: automatically assign an argument's value to a property of th
((splat..., @prop) ->).apply context = {}, [0, 0, nonce]
eq nonce, context.prop
# Should not trigger implicit call, e.g. rest ... => rest(...)
((splat ..., @prop) ->).apply context = {}, [0, 0, nonce]
eq nonce, context.prop
# Allow the argument itself to be a splat
((@prop...) ->).call context = {}, 0, nonce, 0
eq nonce, context.prop[1]
# Should not trigger implicit call, e.g. rest ... => rest(...)
((@prop ...) ->).call context = {}, 0, nonce, 0
eq nonce, context.prop[1]
# The argument should not be able to be referenced normally
code = '((@prop) -> prop).call {}'
doesNotThrow -> CoffeeScript.compile code
@ -149,12 +167,26 @@ test "@-parameters and splats with constructors", ->
eq a, obj.first
eq b, obj.last
# Should not trigger implicit call, e.g. rest ... => rest(...)
class Klass
constructor: (@first, splat ..., @last) ->
obj = new Klass a, 0, 0, b
eq a, obj.first
eq b, obj.last
test "destructuring in function definition", ->
(([{a: [b], c}]...) ->
eq 1, b
eq 2, c
) {a: [1], c: 2}
# Should not trigger implicit call, e.g. rest ... => rest(...)
(([{a: [b], c}] ...) ->
eq 1, b
eq 2, c
) {a: [1], c: 2}
context = {}
(([{a: [b, c = 2], @d, e = 4}]...) ->
eq 1, b
@ -198,6 +230,17 @@ test "rest element destructuring in function definition", ->
deepEqual r, {c: 3, d: 4, e: 5}
) {a:1, b:2, c:3, d:4, e:5}, 9
# Should not trigger implicit call, e.g. rest ... => rest(...)
(({
a: p
b
r ...
}, q) ->
eq p, 1
eq q, 9
deepEqual r, {c: 3, d: 4, e: 5}
) {a:1, b:2, c:3, d:4, e:5}, 9
a1={}; b1={}; c1={}; d1={}
obj1 = {
a: a1
@ -256,6 +299,10 @@ test "#4005: `([a = {}]..., b) ->` weirdness", ->
fn = ([a = {}]..., b) -> [a, b]
deepEqual fn(5), [{}, 5]
# Should not trigger implicit call, e.g. rest ... => rest(...)
fn = ([a = {}] ..., b) -> [a, b]
deepEqual fn(5), [{}, 5]
test "default values", ->
nonceA = {}
nonceB = {}
@ -299,6 +346,14 @@ test "default values with splatted arguments", ->
eq 1, withSplats(1,1,1)
eq 2, withSplats(1,1,1,1)
# Should not trigger implicit call, e.g. rest ... => rest(...)
withSplats = (a = 2, b ..., c = 3, d = 5) -> a * (b.length + 1) * c * d
eq 30, withSplats()
eq 15, withSplats(1)
eq 5, withSplats(1,1)
eq 1, withSplats(1,1,1)
eq 2, withSplats(1,1,1,1)
test "#156: parameter lists with expansion", ->
expandArguments = (first, ..., lastButOne, last) ->
eq 1, first
@ -328,6 +383,12 @@ test "variable definitions and splat", ->
eq 0, a
eq 0, b
# Should not trigger implicit call, e.g. rest ... => rest(...)
f = (a, middle ..., b) -> [a, middle, b]
arrayEq [1, [2, 3, 4], 5], f 1, 2, 3, 4, 5
eq 0, a
eq 0, b
test "default values with function calls", ->
doesNotThrow -> CoffeeScript.compile "(x = f()) ->"
@ -354,6 +415,12 @@ test "reserved keyword at-splat", ->
eq 1, a
eq 2, b
# Should not trigger implicit call, e.g. rest ... => rest(...)
f = (@case ...) -> @case
[a, b] = f(1, 2)
eq 1, a
eq 2, b
test "#1574: Destructuring and a parameter named _arg", ->
f = ({a, b}, _arg, _arg1) -> [a, b, _arg, _arg1]
arrayEq [1, 2, 3, 4], f a: 1, b: 2, 3, 4

View file

@ -28,6 +28,19 @@ test "basic exclusive ranges", ->
arrayEq [], [0...0]
arrayEq [], [-1...-1]
# Should not trigger implicit call, e.g. rest ... => rest(...)
arrayEq [1, 2, 3] , [1 ... 4]
arrayEq [0, 1, 2] , [0 ... 3]
arrayEq [0, 1] , [0 ... 2]
arrayEq [0] , [0 ... 1]
arrayEq [-1] , [-1 ... 0]
arrayEq [-1, 0] , [-1 ... 1]
arrayEq [-1, 0, 1], [-1 ... 2]
arrayEq [], [1 ... 1]
arrayEq [], [0 ... 0]
arrayEq [], [-1 ... -1]
test "downward ranges", ->
arrayEq shared, [9..0].reverse()
arrayEq [5, 4, 3, 2] , [5..2]
@ -66,6 +79,9 @@ test "ranges with expressions as endpoints", ->
arrayEq [2, 3, 4, 5, 6], [(a+1)..2*b]
arrayEq [2, 3, 4, 5] , [(a+1)...2*b]
# Should not trigger implicit call, e.g. rest ... => rest(...)
arrayEq [2, 3, 4, 5] , [(a+1) ... 2*b]
test "large ranges are generated with looping constructs", ->
down = [99..0]
eq 100, (len = down.length)

View file

@ -64,6 +64,18 @@ test "#1722: operator precedence in unbounded slice compilation", ->
test "#2349: inclusive slicing to numeric strings", ->
arrayEq [0, 1], [0..10][.."1"]
test "#4631: slicing with space before and/or after the dots", ->
a = (s) -> s
b = [4, 5, 6]
c = [7, 8, 9]
arrayEq [2, 3, 4], shared[2 ... 5]
arrayEq [3, 4, 5], shared[3... 6]
arrayEq [4, 5, 6], shared[4 ...7]
arrayEq shared[(a b...)...(a c...)] , shared[(a ...b)...(a ...c)]
arrayEq shared[(a b...) ... (a c...)], shared[(a ...b) ... (a ...c)]
arrayEq shared[(a b...)... (a c...)] , shared[(a ...b)... (a ...c)]
arrayEq shared[(a b...) ...(a c...)] , shared[(a ...b) ...(a ...c)]
# Splicing