Skip to content

Commit

Permalink
resolve: statically reject duplicate keyword args in a call (google#72)
Browse files Browse the repository at this point in the history
This complements the dynamic checks, which is already implemented in setArgs and in UnpackArgs.

Updates bazelbuild/starlark#21
  • Loading branch information
adonovan authored Dec 14, 2018
1 parent 5846440 commit 2c65f9e
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 13 deletions.
5 changes: 5 additions & 0 deletions doc/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,11 @@ Once the parameters have been successfully bound to the arguments
supplied by the call, the sequence of statements that comprise the
function body is executed.

It is a static error if a function call has two named arguments of the
same name, such as `f(x=1, x=2)`. A call that provides a `**kwargs`
argument may yet have two values for the same name, such as
`f(x=1, **dict(x=2))`. This results in a dynamic error.

A function call completes normally after the execution of either a
`return` statement, or of the last statement in the function body.
The result of the function call is the value of the return statement's
Expand Down
16 changes: 12 additions & 4 deletions resolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,8 @@ func (r *resolver) expr(e syntax.Expr) {

case *syntax.CallExpr:
r.expr(e.Fn)
var seenVarargs, seenKwargs, seenNamed bool
var seenVarargs, seenKwargs bool
var seenName map[string]bool
for _, arg := range e.Args {
pos, _ := arg.Span()
if unop, ok := arg.(*syntax.UnaryExpr); ok && unop.Op == syntax.STARSTAR {
Expand All @@ -692,16 +693,23 @@ func (r *resolver) expr(e syntax.Expr) {
if seenKwargs {
r.errorf(pos, "argument may not follow **kwargs")
}
// ignore binop.X
x := binop.X.(*syntax.Ident)
if seenName[x.Name] {
r.errorf(x.NamePos, "keyword argument %s repeated", x.Name)
} else {
if seenName == nil {
seenName = make(map[string]bool)
}
seenName[x.Name] = true
}
r.expr(binop.Y)
seenNamed = true
} else {
// positional argument
if seenVarargs {
r.errorf(pos, "argument may not follow *args")
} else if seenKwargs {
r.errorf(pos, "argument may not follow **kwargs")
} else if seenNamed {
} else if len(seenName) > 0 {
r.errorf(pos, "positional argument may not follow named")
}
r.expr(arg)
Expand Down
5 changes: 5 additions & 0 deletions resolve/testdata/resolve.star
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,8 @@ M = 2 # ok (legacy)
# universal predeclared name
U = 1 # ok
U = 1 # ok (legacy)

---
# https://github.com/bazelbuild/starlark/starlark/issues/21
def f(**kwargs): pass
f(a=1, a=1) ### `keyword argument a repeated`
12 changes: 6 additions & 6 deletions starlark/testdata/dict.star
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ small = dict([("a", 0), ("b", 1), ("c", 2)])
small.update([("d", 4), ("e", 5), ("f", 6), ("g", 7), ("h", 8), ("i", 9), ("j", 10), ("k", 11)])
assert.eq(small.keys(), ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"])

# duplicate keys are not permitted in dictionary expressions (see b/35698444).
# Duplicate keys are not permitted in dictionary expressions (see b/35698444).
# (Nor in keyword args to function calls---checked by resolver.)
assert.fails(lambda: {"aa": 1, "bb": 2, "cc": 3, "bb": 4}, 'duplicate key: "bb"')
# nor in keyword args to dict built-in
assert.fails(lambda: dict(a=1, a=1), 'dict: duplicate keyword arg: "a"')
# check that even with many positional args, keyword collisions are detected
assert.fails(lambda: dict({'b': 3}, a=4, a=5), 'dict: duplicate keyword arg: "a"')
assert.fails(lambda: dict({'a': 2, 'b': 3}, a=4, a=5), 'dict: duplicate keyword arg: "a"')

# Check that even with many positional args, keyword collisions are detected.
assert.fails(lambda: dict({'b': 3}, a=4, **dict(a=5)), 'dict: duplicate keyword arg: "a"')
assert.fails(lambda: dict({'a': 2, 'b': 3}, a=4, **dict(a=5)), 'dict: duplicate keyword arg: "a"')
# positional/keyword arg key collisions are ok
assert.eq(dict((['a', 2], ), a=4), {'a': 4})
assert.eq(dict((['a', 2], ['a', 3]), a=4), {'a': 4})
Expand Down
8 changes: 5 additions & 3 deletions starlark/testdata/function.star
Original file line number Diff line number Diff line change
Expand Up @@ -175,22 +175,24 @@ assert.fails(lambda: f(
mm = 100), 'multiple values for keyword argument "mm"')

---
# Regression test for github.com/google/starlark-go/issues/21.
# Regression test for github.com/google/starlark-go/issues/21,
# which concerns dynamic checks.
# Related: https://github.com/bazelbuild/starlark/issues/21,
# which concerns static checks.

load("assert.star", "assert")

def f(*args, **kwargs):
return args, kwargs

assert.eq(f(x=1, y=2), ((), {"x": 1, "y": 2}))
assert.fails(lambda: f(x=1, x=2), 'multiple values for keyword argument "x"')
assert.fails(lambda: f(x=1, **dict(x=2)), 'multiple values for keyword argument "x"')

def g(x, y):
return x, y

assert.eq(g(1, y=2), (1, 2))
assert.fails(lambda: g(1, y=2, y=3), 'multiple values for keyword argument "y"')
assert.fails(lambda: g(1, y=2, **{'y': 3}), 'multiple values for keyword argument "y"')

---
# Regression test for a bug in CALL_VAR_KW.
Expand Down

0 comments on commit 2c65f9e

Please sign in to comment.