From 0509ec6f0b10114fd973a418af97604dab4adf50 Mon Sep 17 00:00:00 2001 From: Taus Date: Mon, 23 Feb 2026 14:43:49 +0000 Subject: [PATCH] Python: Port WrongNumberArgumentsInClassInstantiation.ql Included test changes are trivial `toString` changes. --- ...rongNumberArgumentsInClassInstantiation.ql | 58 +++++++++++++++++-- ...mberArgumentsInClassInstantiation.expected | 26 ++++----- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/python/ql/src/Classes/WrongNumberArgumentsInClassInstantiation.ql b/python/ql/src/Classes/WrongNumberArgumentsInClassInstantiation.ql index 263a1a336a1..17fc597a159 100644 --- a/python/ql/src/Classes/WrongNumberArgumentsInClassInstantiation.ql +++ b/python/ql/src/Classes/WrongNumberArgumentsInClassInstantiation.ql @@ -14,10 +14,60 @@ */ import python -import Expressions.CallArgs -private import LegacyPointsTo +private import semmle.python.dataflow.new.internal.DataFlowDispatch -from Call call, ClassValue cls, string too, string should, int limit, FunctionValue init +/** + * Gets the number of positional arguments in `call`, including elements of any + * literal list passed as `*args`, plus keyword arguments that don't match + * keyword-only parameters (when the function doesn't accept `**kwargs`). + */ +int positional_arg_count(Call call, Class cls, Function init) { + resolveClassCall(call.getAFlowNode(), cls) and + init = DuckTyping::getInit(cls) and + exists(int positional_keywords | + if init.hasKwArg() + then positional_keywords = 0 + else + positional_keywords = + count(Keyword kw | + kw = call.getAKeyword() and + not init.getAKeywordOnlyArg().getId() = kw.getArg() + ) + | + result = + count(call.getAnArg()) + count(call.getStarargs().(List).getAnElt()) + positional_keywords + ) +} + +/** + * Holds if `call` constructs `cls` with too many arguments, where `limit` is the maximum. + */ +predicate too_many_args(Call call, Class cls, int limit) { + exists(Function init | + not init.hasVarArg() and + // Subtract 1 from max to account for `self` parameter + limit = init.getMaxPositionalArguments() - 1 and + limit >= 0 and + positional_arg_count(call, cls, init) > limit + ) +} + +/** + * Holds if `call` constructs `cls` with too few arguments, where `limit` is the minimum. + */ +predicate too_few_args(Call call, Class cls, int limit) { + resolveClassCall(call.getAFlowNode(), cls) and + exists(Function init | + init = DuckTyping::getInit(cls) and + not exists(call.getStarargs()) and + not exists(call.getKwargs()) and + // Subtract 1 from min to account for `self` parameter + limit = init.getMinPositionalArguments() - 1 and + count(call.getAnArg()) + count(call.getAKeyword()) < limit + ) +} + +from Call call, Class cls, string too, string should, int limit, Function init where ( too_many_args(call, cls, limit) and @@ -28,6 +78,6 @@ where too = "too few arguments" and should = "no fewer than " ) and - init = get_function_or_initializer(cls) + init = DuckTyping::getInit(cls) select call, "Call to $@ with " + too + "; should be " + should + limit.toString() + ".", init, init.getQualifiedName() diff --git a/python/ql/test/query-tests/Classes/Arguments/WrongNumberArgumentsInClassInstantiation.expected b/python/ql/test/query-tests/Classes/Arguments/WrongNumberArgumentsInClassInstantiation.expected index f0df1fe58c9..29035025964 100644 --- a/python/ql/test/query-tests/Classes/Arguments/WrongNumberArgumentsInClassInstantiation.expected +++ b/python/ql/test/query-tests/Classes/Arguments/WrongNumberArgumentsInClassInstantiation.expected @@ -1,15 +1,15 @@ -| wrong_arguments.py:37:1:37:4 | F0() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:4:5:4:26 | Function F0.__init__ | F0.__init__ | -| wrong_arguments.py:38:1:38:4 | F1() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:8:5:8:36 | Function F1.__init__ | F1.__init__ | -| wrong_arguments.py:39:1:39:4 | F2() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:12:5:12:30 | Function F2.__init__ | F2.__init__ | -| wrong_arguments.py:40:1:40:4 | F3() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:16:5:16:40 | Function F3.__init__ | F3.__init__ | -| wrong_arguments.py:41:1:41:4 | F4() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:20:5:20:31 | Function F4.__init__ | F4.__init__ | -| wrong_arguments.py:42:1:42:4 | F5() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:24:5:24:42 | Function F5.__init__ | F5.__init__ | -| wrong_arguments.py:43:1:43:5 | F6() | Call to $@ with too few arguments; should be no fewer than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ | -| wrong_arguments.py:44:1:44:7 | F7() | Call to $@ with too few arguments; should be no fewer than 3. | wrong_arguments.py:32:5:32:33 | Function F7.__init__ | F7.__init__ | -| wrong_arguments.py:48:1:48:7 | F0() | Call to $@ with too many arguments; should be no more than 1. | wrong_arguments.py:4:5:4:26 | Function F0.__init__ | F0.__init__ | -| wrong_arguments.py:49:1:49:9 | F1() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:8:5:8:36 | Function F1.__init__ | F1.__init__ | -| wrong_arguments.py:50:1:50:9 | F5() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:24:5:24:42 | Function F5.__init__ | F5.__init__ | -| wrong_arguments.py:51:1:51:9 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ | -| wrong_arguments.py:52:1:52:11 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ | | wrong_arguments.py:85:1:85:12 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ | | wrong_arguments.py:86:1:86:7 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ | +| wrong_arguments.py:37:1:37:4 | F0() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:4:5:4:26 | Function __init__ | F0.__init__ | +| wrong_arguments.py:38:1:38:4 | F1() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:8:5:8:36 | Function __init__ | F1.__init__ | +| wrong_arguments.py:39:1:39:4 | F2() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:12:5:12:30 | Function __init__ | F2.__init__ | +| wrong_arguments.py:40:1:40:4 | F3() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:16:5:16:40 | Function __init__ | F3.__init__ | +| wrong_arguments.py:41:1:41:4 | F4() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:20:5:20:31 | Function __init__ | F4.__init__ | +| wrong_arguments.py:42:1:42:4 | F5() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:24:5:24:42 | Function __init__ | F5.__init__ | +| wrong_arguments.py:43:1:43:5 | F6() | Call to $@ with too few arguments; should be no fewer than 2. | wrong_arguments.py:28:5:28:30 | Function __init__ | F6.__init__ | +| wrong_arguments.py:44:1:44:7 | F7() | Call to $@ with too few arguments; should be no fewer than 3. | wrong_arguments.py:32:5:32:33 | Function __init__ | F7.__init__ | +| wrong_arguments.py:48:1:48:7 | F0() | Call to $@ with too many arguments; should be no more than 1. | wrong_arguments.py:4:5:4:26 | Function __init__ | F0.__init__ | +| wrong_arguments.py:49:1:49:9 | F1() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:8:5:8:36 | Function __init__ | F1.__init__ | +| wrong_arguments.py:50:1:50:9 | F5() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:24:5:24:42 | Function __init__ | F5.__init__ | +| wrong_arguments.py:51:1:51:9 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function __init__ | F6.__init__ | +| wrong_arguments.py:52:1:52:11 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function __init__ | F6.__init__ |