Python: Add basic support for *args

This commit is contained in:
Rasmus Wriedt Larsen
2022-09-12 16:45:57 +02:00
parent b6314dd19d
commit db921ac036
3 changed files with 48 additions and 4 deletions

View File

@@ -42,6 +42,13 @@ newtype TParameterPosition =
TSelfParameterPosition() or
TPositionalParameterPosition(int pos) { pos = any(Parameter p).getPosition() } or
TKeywordParameterPosition(string name) { name = any(Parameter p).getName() } or
TStarArgsParameterPosition(int pos) {
// since `.getPosition` does not work for `*args`, we need *args parameter positions
// at index 1 larger than the largest positional parameter position (and 0 must be
// included as well). This is a bit of an over-approximation.
pos = 0 or
pos = any(Parameter p).getPosition() + 1
} or
TDictSplatParameterPosition()
/** A parameter position. */
@@ -55,6 +62,9 @@ class ParameterPosition extends TParameterPosition {
/** Holds if this position represents a keyword parameter named `name`. */
predicate isKeyword(string name) { this = TKeywordParameterPosition(name) }
/** Holds if this position represents a `*args` parameter at (0-based) `index`. */
predicate isStarArgs(int index) { this = TStarArgsParameterPosition(index) }
/** Holds if this position represents a `**kwargs` parameter. */
predicate isDictSplat() { this = TDictSplatParameterPosition() }
@@ -66,6 +76,8 @@ class ParameterPosition extends TParameterPosition {
or
exists(string name | this.isKeyword(name) and result = "keyword " + name)
or
exists(int index | this.isStarArgs(index) and result = "*args at " + index)
or
this.isDictSplat() and result = "**"
}
}
@@ -75,6 +87,7 @@ newtype TArgumentPosition =
TSelfArgumentPosition() or
TPositionalArgumentPosition(int pos) { exists(any(CallNode c).getArg(pos)) } or
TKeywordArgumentPosition(string name) { exists(any(CallNode c).getArgByName(name)) } or
TStarArgsArgumentPosition(int pos) { exists(Call c | c.getPositionalArg(pos) instanceof Starred) } or
TDictSplatArgumentPosition()
/** An argument position. */
@@ -88,6 +101,9 @@ class ArgumentPosition extends TArgumentPosition {
/** Holds if this position represents a keyword argument named `name`. */
predicate isKeyword(string name) { this = TKeywordArgumentPosition(name) }
/** Holds if this position represents a `*args` argument at (0-based) `index`. */
predicate isStarArgs(int index) { this = TStarArgsArgumentPosition(index) }
/** Holds if this position represents a `**kwargs` argument. */
predicate isDictSplat() { this = TDictSplatArgumentPosition() }
@@ -99,6 +115,8 @@ class ArgumentPosition extends TArgumentPosition {
or
exists(string name | this.isKeyword(name) and result = "keyword " + name)
or
exists(int index | this.isStarArgs(index) and result = "*args at " + index)
or
this.isDictSplat() and result = "**"
}
}
@@ -112,6 +130,8 @@ predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) {
or
exists(string name | ppos.isKeyword(name) and apos.isKeyword(name))
or
exists(int index | ppos.isStarArgs(index) and apos.isStarArgs(index))
or
ppos.isDictSplat() and apos.isDictSplat()
}
@@ -198,6 +218,22 @@ abstract class DataFlowFunction extends DataFlowCallable, TFunction {
or
exists(string name | ppos.isKeyword(name) | result.getParameter() = func.getArgByName(name))
or
exists(int index |
ppos.isStarArgs(index) and
result.getParameter() = func.getVararg()
|
// a `*args` parameter comes after the last positional parameter. We need to take
// self parameter into account, so for
// `def func(foo, bar, *args)` it should be index 2 (1 + max-index == 1 + 1)
// `class A: def func(self, foo, bar, *args)` it should be index 2 (1 + max-index - 1 == 1 + 2 - 1)
index =
1 + max(int positionalIndex | exists(func.getArg(positionalIndex)) | positionalIndex) -
this.positionalOffset()
or
// no positional argument
not exists(func.getArg(_)) and index = 0
)
or
ppos.isDictSplat() and result.getParameter() = func.getKwarg()
or
ppos.isDictSplat() and result = TSynthDictSplatParameterNode(this)
@@ -912,6 +948,12 @@ private predicate normalCallArg(CallNode call, Node arg, ArgumentPosition apos)
arg.asCfgNode() = call.getArgByName(name)
)
or
exists(int index |
apos.isStarArgs(index) and
arg.asCfgNode() = call.getStarArg() and
call.getStarArg().getNode() = call.getNode().getPositionalArg(index).(Starred).getValue()
)
or
apos.isDictSplat() and
(
arg.asCfgNode() = call.getKwargs()

View File

@@ -6,6 +6,8 @@ import semmle.python.dataflow.new.internal.DataFlowImplConsistency::Consistency
// `python/ql/consistency-queries`. For for now it resides here.
private class MyConsistencyConfiguration extends ConsistencyConfiguration {
override predicate argHasPostUpdateExclude(ArgumentNode n) {
exists(ArgumentPosition apos | n.argumentOf(_, apos) and apos.isStarArgs(_))
or
exists(ArgumentPosition apos | n.argumentOf(_, apos) and apos.isDictSplat())
}

View File

@@ -214,8 +214,8 @@ def test_only_starargs():
args = (arg2, "safe")
starargs_only(arg1, *args) # $ MISSING: arg1 arg2
args = (arg1, arg2, "safe")
starargs_only(*args) # $ MISSING: arg1 arg2
args = (arg1, arg2, "safe") # $ arg1 arg2 func=starargs_only
starargs_only(*args)
def starargs_mixed(a, *args):
@@ -227,8 +227,8 @@ def starargs_mixed(a, *args):
def test_stararg_mixed():
starargs_mixed(arg1, arg2, "safe") # $ arg1 MISSING: arg2
args = (arg2, "safe")
starargs_mixed(arg1, *args) # $ arg1 MISSING: arg2
args = (arg2, "safe") # $ arg2 func=starargs_mixed
starargs_mixed(arg1, *args) # $ arg1
args = (arg1, arg2, "safe")
starargs_mixed(*args) # $ MISSING: arg1 arg2