Skip to content

Commit ba634af

Browse files
authored
Merge pull request github#3362 from RasmusWL/python-keyword-only-args
Python: properly support keyword only arguments
2 parents b5c8f22 + 513c297 commit ba634af

32 files changed

+2453
-30
lines changed

python/ql/src/semmle/python/AstGenerated.qll

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,13 +1310,13 @@ library class AliasList_ extends @py_alias_list {
13101310

13111311
/** INTERNAL: See the class `Arguments` for further information. */
13121312
library class Arguments_ extends @py_arguments {
1313-
/** Gets the keyword default values of this parameters definition. */
1313+
/** Gets the keyword-only default values of this parameters definition. */
13141314
ExprList getKwDefaults() { py_expr_lists(result, this, 0) }
13151315

1316-
/** Gets the nth keyword default value of this parameters definition. */
1316+
/** Gets the nth keyword-only default value of this parameters definition. */
13171317
Expr getKwDefault(int index) { result = this.getKwDefaults().getItem(index) }
13181318

1319-
/** Gets a keyword default value of this parameters definition. */
1319+
/** Gets a keyword-only default value of this parameters definition. */
13201320
Expr getAKwDefault() { result = this.getKwDefaults().getAnItem() }
13211321

13221322
/** Gets the default values of this parameters definition. */
@@ -1343,13 +1343,13 @@ library class Arguments_ extends @py_arguments {
13431343
/** Gets the **kwarg annotation of this parameters definition. */
13441344
Expr getKwargannotation() { py_exprs(result, _, this, 4) }
13451345

1346-
/** Gets the kw_annotations of this parameters definition. */
1346+
/** Gets the keyword-only annotations of this parameters definition. */
13471347
ExprList getKwAnnotations() { py_expr_lists(result, this, 5) }
13481348

1349-
/** Gets the nth kw_annotation of this parameters definition. */
1349+
/** Gets the nth keyword-only annotation of this parameters definition. */
13501350
Expr getKwAnnotation(int index) { result = this.getKwAnnotations().getItem(index) }
13511351

1352-
/** Gets a kw_annotation of this parameters definition. */
1352+
/** Gets a keyword-only annotation of this parameters definition. */
13531353
Expr getAKwAnnotation() { result = this.getKwAnnotations().getAnItem() }
13541354

13551355
ArgumentsParent getParent() { py_arguments(this, result) }

python/ql/src/semmle/python/Function.qll

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ class Function extends Function_, Scope, AstNode {
4949
string getArgName(int index) { result = this.getArg(index).(Name).getId() }
5050

5151
Parameter getArgByName(string name) {
52-
result = this.getAnArg() and
52+
(
53+
result = this.getAnArg()
54+
or
55+
result = this.getAKeywordOnlyArg()
56+
) and
5357
result.(Name).getId() = name
5458
}
5559

@@ -90,7 +94,7 @@ class Function extends Function_, Scope, AstNode {
9094
int getPositionalParameterCount() { result = count(this.getAnArg()) }
9195

9296
/** Gets the number of keyword-only parameters */
93-
int getKeywordOnlyParameterCount() { result = count(this.getAKwonlyarg()) }
97+
int getKeywordOnlyParameterCount() { result = count(this.getAKeywordOnlyArg()) }
9498

9599
/** Whether this function accepts a variable number of arguments. That is, whether it has a starred (*arg) parameter. */
96100
predicate hasVarArg() { exists(this.getVararg()) }
@@ -102,6 +106,7 @@ class Function extends Function_, Scope, AstNode {
102106
result = this.getAStmt() or
103107
result = this.getAnArg() or
104108
result = this.getVararg() or
109+
result = this.getAKeywordOnlyArg() or
105110
result = this.getKwarg()
106111
}
107112

@@ -185,6 +190,8 @@ class Parameter extends Parameter_ {
185190
f.getVararg() = this
186191
or
187192
f.getKwarg() = this
193+
or
194+
f.getAKeywordOnlyArg() = this
188195
)
189196
}
190197

@@ -202,19 +209,31 @@ class Parameter extends Parameter_ {
202209

203210
/** Gets the expression for the default value of this parameter */
204211
Expr getDefault() {
205-
exists(Function f, int n, int c, int d, Arguments args | args = f.getDefinition().getArgs() |
206-
f.getArg(n) = this and
207-
c = count(f.getAnArg()) and
208-
d = count(args.getADefault()) and
209-
result = args.getDefault(d - c + n)
212+
exists(Function f, int i, Arguments args | args = f.getDefinition().getArgs() |
213+
// positional (normal)
214+
f.getArg(i) = this and
215+
result = args.getDefault(i)
216+
)
217+
or
218+
exists(Function f, int i, Arguments args | args = f.getDefinition().getArgs() |
219+
// keyword-only
220+
f.getKeywordOnlyArg(i) = this and
221+
result = args.getKwDefault(i)
210222
)
211223
}
212224

213225
/** Gets the annotation expression of this parameter */
214226
Expr getAnnotation() {
215-
exists(Function f, int n, Arguments args | args = f.getDefinition().getArgs() |
216-
f.getArg(n) = this and
217-
result = args.getAnnotation(n)
227+
exists(Function f, int i, Arguments args | args = f.getDefinition().getArgs() |
228+
// positional (normal)
229+
f.getArg(i) = this and
230+
result = args.getAnnotation(i)
231+
)
232+
or
233+
exists(Function f, int i, Arguments args | args = f.getDefinition().getArgs() |
234+
// keyword-only
235+
f.getKeywordOnlyArg(i) = this and
236+
result = args.getKwAnnotation(i)
218237
)
219238
or
220239
exists(Function f, Arguments args | args = f.getDefinition().getArgs() |
@@ -228,7 +247,10 @@ class Parameter extends Parameter_ {
228247

229248
Variable getVariable() { result.getAnAccess() = this.asName() }
230249

231-
/** Gets the position of this parameter */
250+
/**
251+
* Gets the position of this parameter (if any).
252+
* No result if this is a "varargs", "kwargs", or keyword-only parameter.
253+
*/
232254
int getPosition() { exists(Function f | f.getArg(result) = this) }
233255

234256
/** Gets the name of this parameter */
@@ -243,13 +265,13 @@ class Parameter extends Parameter_ {
243265
}
244266

245267
/**
246-
* Holds if this parameter is a 'varargs' parameter.
268+
* Holds if this parameter is a "varargs" parameter.
247269
* The `varargs` in `f(a, b, *varargs)`.
248270
*/
249271
predicate isVarargs() { exists(Function func | func.getVararg() = this) }
250272

251273
/**
252-
* Holds if this parameter is a 'kwargs' parameter.
274+
* Holds if this parameter is a "kwargs" parameter.
253275
* The `kwargs` in `f(a, b, **kwargs)`.
254276
*/
255277
predicate isKwargs() { exists(Function func | func.getKwarg() = this) }
@@ -258,7 +280,8 @@ class Parameter extends Parameter_ {
258280
/** An expression that generates a callable object, either a function expression or a lambda */
259281
abstract class CallableExpr extends Expr {
260282
/**
261-
* Gets the parameters of this callable.
283+
* Gets The default values and annotations (type-hints) for the arguments of this callable.
284+
*
262285
* This predicate is called getArgs(), rather than getParameters() for compatibility with Python's AST module.
263286
*/
264287
abstract Arguments getArgs();
@@ -295,7 +318,7 @@ class FunctionExpr extends FunctionExpr_, CallableExpr {
295318
override Arguments getArgs() { result = FunctionExpr_.super.getArgs() }
296319
}
297320

298-
/** A lambda expression, such as lambda x:x*x */
321+
/** A lambda expression, such as `lambda x: x+1` */
299322
class Lambda extends Lambda_, CallableExpr {
300323
/** Gets the expression to the right of the colon in this lambda expression */
301324
Expr getExpression() {
@@ -314,13 +337,36 @@ class Lambda extends Lambda_, CallableExpr {
314337
override Arguments getArgs() { result = Lambda_.super.getArgs() }
315338
}
316339

317-
/** The arguments in a function definition */
340+
/**
341+
* The default values and annotations (type hints) for the arguments in a function definition.
342+
*
343+
* Annotations (PEP 3107) is a general mechanism for providing annotations for a function,
344+
* that is generally only used for type hints today (PEP 484).
345+
*/
318346
class Arguments extends Arguments_ {
347+
319348
Expr getASubExpression() {
349+
result = this.getADefault() or
320350
result = this.getAKwDefault() or
351+
//
321352
result = this.getAnAnnotation() or
322-
result = this.getKwargannotation() or
323353
result = this.getVarargannotation() or
324-
result = this.getADefault()
354+
result = this.getAKwAnnotation() or
355+
result = this.getKwargannotation()
325356
}
357+
358+
// The following 4 methods are overwritten to provide better QLdoc. Since the
359+
// Arguments_ is auto-generated, we can't change the poor auto-generated docs there :(
360+
361+
/** Gets the default value for the `index`'th positional parameter. */
362+
override Expr getDefault(int index) { result = super.getDefault(index) }
363+
364+
/** Gets the default value for the `index`'th keyword-only parameter. */
365+
override Expr getKwDefault(int index) { result = super.getKwDefault(index) }
366+
367+
/** Gets the annotation for the `index`'th positional parameter. */
368+
override Expr getAnnotation(int index) { result = super.getAnnotation(index) }
369+
370+
/** Gets the annotation for the `index`'th keyword-only parameter. */
371+
override Expr getKwAnnotation(int index) { result = super.getKwAnnotation(index) }
326372
}

python/ql/src/semmle/python/objects/ObjectAPI.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ class CallableValue extends Value {
438438
exists(int n |
439439
call.getArg(n) = result and
440440
this.getParameter(n + offset).getId() = name
441+
// TODO: and not positional only argument (Python 3.8+)
441442
)
442443
or
443444
called instanceof BoundMethodObjectInternal and

python/ql/src/semmlecode.python.dbscheme

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55
* by adding rules to dbscheme.template
66
*/
77

8+
/* This is a dummy line to alter the dbscheme, so we can make a database upgrade
9+
* without actually changing any of the dbscheme predicates. It contains a date
10+
* to allow for such updates in the future as well.
11+
*
12+
* 2020-07-02
13+
*
14+
* DO NOT remove this comment carelessly, since it can revert the dbscheme back to a
15+
* previously seen state (matching a previously seen SHA), which would make the upgrade
16+
* mechanism not work properly.
17+
*/
18+
819
/*
920
* External artifacts
1021
*/
@@ -126,7 +137,7 @@ containerparent(int parent: @container ref,
126137
unique int child: @container ref);
127138

128139
@sourceline = @file | @py_Module | @xmllocatable;
129-
140+
130141
numlines(int element_id: @sourceline ref,
131142
int num_lines: int ref,
132143
int num_code: int ref,
@@ -922,7 +933,7 @@ ext_rettype(int funcid : @py_object ref,
922933
ext_proptype(int propid : @py_object ref,
923934
int typeid : @py_object ref);
924935

925-
ext_argreturn(int funcid : @py_object ref,
936+
ext_argreturn(int funcid : @py_object ref,
926937
int arg : int ref);
927938

928939
py_special_objects(unique int obj : @py_cobject ref,
@@ -935,15 +946,15 @@ py_decorated_object(int object : @py_object ref,
935946

936947
@py_source_element = @py_ast_node | @container;
937948

938-
/* XML Files */
949+
/* XML Files */
939950

940951
xmlEncoding (unique int id: @file ref, varchar(900) encoding: string ref);
941952

942953
xmlDTDs (unique int id: @xmldtd,
943954
varchar(900) root: string ref,
944955
varchar(900) publicId: string ref,
945956
varchar(900) systemId: string ref,
946-
int fileid: @file ref);
957+
int fileid: @file ref);
947958

948959
xmlElements (unique int id: @xmlelement,
949960
varchar(900) name: string ref,
@@ -958,7 +969,7 @@ xmlAttrs (unique int id: @xmlattribute,
958969
int idx: int ref,
959970
int fileid: @file ref);
960971

961-
xmlNs (int id: @xmlnamespace,
972+
xmlNs (int id: @xmlnamespace,
962973
varchar(900) prefixName: string ref,
963974
varchar(900) URI: string ref,
964975
int fileid: @file ref);
@@ -970,7 +981,7 @@ xmlHasNs (int elementId: @xmlnamespaceable ref,
970981
xmlComments (unique int id: @xmlcomment,
971982
varchar(3600) text: string ref,
972983
int parentid: @xmlparent ref,
973-
int fileid: @file ref);
984+
int fileid: @file ref);
974985

975986
xmlChars (unique int id: @xmlcharacters,
976987
varchar(3600) text: string ref,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
| test.py:4:1:11:2 | Function func | test.py:5:5:5:12 | pos_only |
2+
| test.py:4:1:11:2 | Function func | test.py:7:5:7:10 | normal |
3+
| test.py:4:1:11:2 | Function func | test.py:8:6:8:9 | args |
4+
| test.py:4:1:11:2 | Function func | test.py:9:5:9:16 | keyword_only |
5+
| test.py:4:1:11:2 | Function func | test.py:10:7:10:12 | kwargs |
6+
| test.py:4:1:11:2 | Function func | test.py:12:5:12:41 | ExprStmt |
7+
| test.py:4:1:11:2 | Function func | test.py:13:5:13:15 | ExprStmt |
8+
| test.py:4:1:11:2 | Function func | test.py:14:5:14:17 | ExprStmt |
9+
| test.py:23:1:31:2 | Function func2 | test.py:24:5:24:11 | pos_req |
10+
| test.py:23:1:31:2 | Function func2 | test.py:25:5:25:17 | pos_w_default |
11+
| test.py:23:1:31:2 | Function func2 | test.py:26:5:26:18 | pos_w_default2 |
12+
| test.py:23:1:31:2 | Function func2 | test.py:28:5:28:15 | keyword_req |
13+
| test.py:23:1:31:2 | Function func2 | test.py:29:5:29:21 | keyword_w_default |
14+
| test.py:23:1:31:2 | Function func2 | test.py:30:5:30:20 | keyword_also_req |
15+
| test.py:23:1:31:2 | Function func2 | test.py:32:5:32:18 | ExprStmt |
16+
| test.py:23:1:31:2 | Function func2 | test.py:33:5:40:5 | ExprStmt |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import python
2+
3+
from Function f
4+
select f, f.getAChildNode()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
| test.py:4:1:11:2 | Function func | 0 | test.py:5:5:5:12 | Parameter |
2+
| test.py:4:1:11:2 | Function func | 1 | test.py:7:5:7:10 | Parameter |
3+
| test.py:23:1:31:2 | Function func2 | 0 | test.py:24:5:24:11 | Parameter |
4+
| test.py:23:1:31:2 | Function func2 | 1 | test.py:25:5:25:17 | Parameter |
5+
| test.py:23:1:31:2 | Function func2 | 2 | test.py:26:5:26:18 | Parameter |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import python
2+
3+
from Function f, int i
4+
select f, i, f.getArg(i)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
| test.py:4:1:11:2 | Function func | keyword_only | test.py:9:5:9:16 | Parameter |
2+
| test.py:4:1:11:2 | Function func | normal | test.py:7:5:7:10 | Parameter |
3+
| test.py:4:1:11:2 | Function func | pos_only | test.py:5:5:5:12 | Parameter |
4+
| test.py:23:1:31:2 | Function func2 | keyword_also_req | test.py:30:5:30:20 | Parameter |
5+
| test.py:23:1:31:2 | Function func2 | keyword_req | test.py:28:5:28:15 | Parameter |
6+
| test.py:23:1:31:2 | Function func2 | keyword_w_default | test.py:29:5:29:21 | Parameter |
7+
| test.py:23:1:31:2 | Function func2 | pos_req | test.py:24:5:24:11 | Parameter |
8+
| test.py:23:1:31:2 | Function func2 | pos_w_default | test.py:25:5:25:17 | Parameter |
9+
| test.py:23:1:31:2 | Function func2 | pos_w_default2 | test.py:26:5:26:18 | Parameter |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import python
2+
3+
from Function f, string name
4+
select f, name, f.getArgByName(name)

0 commit comments

Comments
 (0)