Files
coffeescript/test/functions.coffee
Geoffrey Booth cbea7b5d1c [CS2] Fix handling of parameters that are complex (#4430)
* Add failing test per #4406

* If a parameter is a function call, define it in an expression within the function body

* Remove the space between `function` and `*` for generator functions, to follow usual ES idiom

* We can collapse `isCall` into `isComplex`

* Don’t need existence check here

* Correct destructured parameter default evaluation order with an incrementing variable (or more generally any complicated parameter that isComplex)

* Try to pull complex parameters out of the parameter list if their order of execution matters; but don’t pull _all_ complex parameters out of the parameter list, so that we don’t lose parameter default values

* Add lots of comments about node special properties

* Err on the side of caution in deciding whether a complex parameter is allowable in a function parameter list rather than the function body (there are lots more detections we could add to find additional “safe” parameters)

* Follow the ES and CS2 convention of assigning parameter default values only when undefined, not when null or undefined

* Along with arrays and empty objects, also let values whose bases are not complex be allowed in the function parameter list (like `obj.prop`)

* Better way to check for undefined parameters when declaring them in a function body

* Once we’ve put a complex parameter in the function body, all following complex parameters go into the function body; no need to create lots of exceptions of when to choose whether to put a complex param in the body

* Rename `isComplex` to `shouldCache` for clarity
2017-02-01 06:54:42 -08:00

355 lines
8.8 KiB
CoffeeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Function Literals
# -----------------
# TODO: add indexing and method invocation tests: (->)[0], (->).call()
# * Function Definition
# * Bound Function Definition
# * Parameter List Features
# * Splat Parameters
# * Context (@) Parameters
# * Parameter Destructuring
# * Default Parameters
# Function Definition
x = 1
y = {}
y.x = -> 3
ok x is 1
ok typeof(y.x) is 'function'
ok y.x instanceof Function
ok y.x() is 3
# The empty function should not cause a syntax error.
->
() ->
# Multiple nested function declarations mixed with implicit calls should not
# cause a syntax error.
(one) -> (two) -> three four, (five) -> six seven, eight, (nine) ->
# with multiple single-line functions on the same line.
func = (x) -> (x) -> (x) -> x
ok func(1)(2)(3) is 3
# Make incorrect indentation safe.
func = ->
obj = {
key: 10
}
obj.key - 5
eq func(), 5
# Ensure that functions with the same name don't clash with helper functions.
del = -> 5
ok del() is 5
# Bound Function Definition
obj =
bound: ->
(=> this)()
unbound: ->
(-> this)()
nested: ->
(=>
(=>
(=> this)()
)()
)()
eq obj, obj.bound()
ok obj isnt obj.unbound()
eq obj, obj.nested()
test "even more fancy bound functions", ->
obj =
one: ->
do =>
return this.two()
two: ->
do =>
do =>
do =>
return this.three
three: 3
eq obj.one(), 3
test "arguments in bound functions inherit from parent function", ->
# The `arguments` object in an ES arrow function refers to the `arguments`
# of the parent scope, just like `this`. In the CoffeeScript 1.x
# implementation of `=>`, the `arguments` object referred to the arguments
# of the arrow function; but per the ES2015 spec, `arguments` should refer
# to the parent.
arrayEq ((a...) -> a)([1, 2, 3]), ((a...) => a)([1, 2, 3])
parent = (a, b, c) ->
(bound = =>
[arguments[0], arguments[1], arguments[2]]
)()
arrayEq [1, 2, 3], parent(1, 2, 3)
test "self-referencing functions", ->
changeMe = ->
changeMe = 2
changeMe()
eq changeMe, 2
# Parameter List Features
test "splats", ->
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
splatArrayRest = ([a...],b...) -> arrayEq(a,b); b
arrayEq splatArray(arr), arr
arrayEq splatArrayRest(arr,0,1,2), arr
test "@-parameters: automatically assign an argument's value to a property of the context", ->
nonce = {}
((@prop) ->).call context = {}, nonce
eq nonce, context.prop
# allow splats along side the special argument
((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]
# the argument should not be able to be referenced normally
code = '((@prop) -> prop).call {}'
doesNotThrow -> CoffeeScript.compile code
throws (-> CoffeeScript.run code), ReferenceError
code = '((@prop) -> _at_prop).call {}'
doesNotThrow -> CoffeeScript.compile code
throws (-> CoffeeScript.run code), ReferenceError
test "@-parameters and splats with constructors", ->
a = {}
b = {}
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}
context = {}
(([{a: [b, c = 2], @d, e = 4}]...) ->
eq 1, b
eq 2, c
eq @d, 3
eq context.d, 3
eq e, 4
).call context, {a: [1], d: 3}
(({a: aa = 1, b: bb = 2}) ->
eq 5, aa
eq 2, bb
) {a: 5}
ajax = (url, {
async = true,
beforeSend = (->),
cache = true,
method = 'get',
data = {}
}) ->
{url, async, beforeSend, cache, method, data}
fn = ->
deepEqual ajax('/home', beforeSend: fn, cache: null, method: 'post'), {
url: '/home', async: true, beforeSend: fn, cache: true, method: 'post', data: {}
}
test "#4005: `([a = {}]..., b) ->` weirdness", ->
fn = ([a = {}]..., b) -> [a, b]
deepEqual fn(5), [{}, 5]
test "default values", ->
nonceA = {}
nonceB = {}
a = (_,_1,arg=nonceA) -> arg
eq nonceA, a()
eq nonceA, a(0)
eq nonceB, a(0,0,nonceB)
eq nonceA, a(0,0,undefined)
eq null, a(0,0,null) # Per ES2015, `null` doesnt trigger a parameter default value
eq false , a(0,0,false)
eq nonceB, a(undefined,undefined,nonceB,undefined)
b = (_,arg=nonceA,_1,_2) -> arg
eq nonceA, b()
eq nonceA, b(0)
eq nonceB, b(0,nonceB)
eq nonceA, b(0,undefined)
eq null, b(0,null)
eq false , b(0,false)
eq nonceB, b(undefined,nonceB,undefined)
c = (arg=nonceA,_,_1) -> arg
eq nonceA, c()
eq 0, c(0)
eq nonceB, c(nonceB)
eq nonceA, c(undefined)
eq null, c(null)
eq false , c(false)
eq nonceB, c(nonceB,undefined,undefined)
test "default values with @-parameters", ->
a = {}
b = {}
obj = f: (q = a, @p = b) -> q
eq a, obj.f()
eq b, obj.p
test "default values with splatted arguments", ->
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
eq 4, lastButOne
last
eq 5, expandArguments 1, 2, 3, 4, 5
throws (-> CoffeeScript.compile "(..., a, b...) ->"), null, "prohibit expansion and a splat"
throws (-> CoffeeScript.compile "(...) ->"), null, "prohibit lone expansion"
test "#156: parameter lists with expansion in array destructuring", ->
expandArray = (..., [..., last]) ->
last
eq 3, expandArray 1, 2, 3, [1, 2, 3]
test "#3502: variable definitions and expansion", ->
a = b = 0
f = (a, ..., b) -> [a, b]
arrayEq [1, 5], f 1, 2, 3, 4, 5
eq 0, a
eq 0, b
test "variable definitions and splat", ->
a = b = 0
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()) ->"
test "arguments vs parameters", ->
doesNotThrow -> CoffeeScript.compile "f(x) ->"
f = (g) -> g()
eq 5, f (x) -> 5
test "reserved keyword as parameters", ->
f = (_case, @case) -> [_case, @case]
[a, b] = f(1, 2)
eq 1, a
eq 2, b
f = (@case, _case...) -> [@case, _case...]
[a, b, c] = f(1, 2, 3)
eq 1, a
eq 2, b
eq 3, c
test "reserved keyword at-splat", ->
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
test "#1844: bound functions in nested comprehensions causing empty var statements", ->
a = ((=>) for a in [0] for b in [0])
eq 1, a.length
test "#1859: inline function bodies shouldn't modify prior postfix ifs", ->
list = [1, 2, 3]
ok true if list.some (x) -> x is 2
test "#2258: allow whitespace-style parameter lists in function definitions", ->
func = (
a, b, c
) -> c
eq func(1, 2, 3), 3
func = (
a
b
c
) -> b
eq func(1, 2, 3), 2
test "#2621: fancy destructuring in parameter lists", ->
func = ({ prop1: { key1 }, prop2: { key2, key3: [a, b, c] } }) ->
eq(key2, 'key2')
eq(a, 'a')
func({prop1: {key1: 'key1'}, prop2: {key2: 'key2', key3: ['a', 'b', 'c']}})
test "#1435 Indented property access", ->
rec = -> rec: rec
eq 1, do ->
rec()
.rec ->
rec()
.rec ->
rec.rec()
.rec()
1
test "#1038 Optimize trailing return statements", ->
compile = (code) -> CoffeeScript.compile(code, bare: yes).trim().replace(/\s+/g, " ")
eq "(function() {});", compile("->")
eq "(function() {});", compile("-> return")
eq "(function() { return void 0; });", compile("-> undefined")
eq "(function() { return void 0; });", compile("-> return undefined")
eq "(function() { foo(); });", compile("""
->
foo()
return
""")
test "#4406 Destructured parameter default evaluation order with incrementing variable", ->
i = 0
f = ({ a = ++i }, b = ++i) -> [a, b]
arrayEq f({}), [1, 2]
test "#4406 Destructured parameter default evaluation order with generator function", ->
current = 0
next = -> ++current
foo = ({ a = next() }, b = next()) -> [ a, b ]
arrayEq foo({}), [1, 2]