-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Python: Modernize the Signature Mismatch query #20217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
5ba5007
4212d1b
067c98d
f429b90
2bbf24b
502ea82
900a5cd
0a83c11
6587ad4
125c653
318d1cd
9fa630f
71dec0b
eb246f6
f9e094d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
|
|
||
| # Base class method | ||
| def runsource(self, source, filename="<input>", symbol="single"): | ||
| ... # Definition | ||
| class Base: | ||
| def runsource(self, source, filename="<input>"): | ||
| ... | ||
|
|
||
|
|
||
| # Extend base class method | ||
| def runsource(self, source): | ||
| ... # Definition | ||
| class Sub(Base): | ||
| def runsource(self, source): # BAD: Does not match the signature of overridden method. | ||
| ... | ||
|
|
||
| def run(obj: Base): | ||
| obj.runsource("source", filename="foo.txt") |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,23 +13,266 @@ | |||||||||||||
| */ | ||||||||||||||
|
|
||||||||||||||
| import python | ||||||||||||||
| import Expressions.CallArgs | ||||||||||||||
| import semmle.python.dataflow.new.DataFlow | ||||||||||||||
| import semmle.python.dataflow.new.internal.DataFlowDispatch | ||||||||||||||
| import codeql.util.Option | ||||||||||||||
|
|
||||||||||||||
| from FunctionValue base, PythonFunctionValue derived | ||||||||||||||
| where | ||||||||||||||
| not exists(base.getACall()) and | ||||||||||||||
| not exists(FunctionValue a_derived | | ||||||||||||||
| a_derived.overrides(base) and | ||||||||||||||
| exists(a_derived.getACall()) | ||||||||||||||
| /** Holds if `base` is overriden by `sub` */ | ||||||||||||||
| predicate overrides(Function base, Function sub) { | ||||||||||||||
| base.getName() = sub.getName() and | ||||||||||||||
| base.getScope() = getADirectSuperclass+(sub.getScope()) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Constructs a string to pluralize `str` depending on `num`. */ | ||||||||||||||
| bindingset[num, str] | ||||||||||||||
| string plural(int num, string str) { | ||||||||||||||
| num = 1 and result = "1 " + str | ||||||||||||||
| or | ||||||||||||||
| num != 1 and result = num.toString() + " " + str + "s" | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Describes the minimum number of arguments `func` can accept, using "at least" if it may accept more. */ | ||||||||||||||
| string describeMin(Function func) { | ||||||||||||||
| exists(string descr | descr = plural(func.getMinPositionalArguments(), "positional argument") | | ||||||||||||||
| if func.getMinPositionalArguments() = func.getMaxPositionalArguments() | ||||||||||||||
| then result = descr | ||||||||||||||
| else result = "at least " + descr | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Described the maximum number of arguments `func` can accept, using "at most" if it may accept fewer, and "arbitrarily many" if it has a vararg. */ | ||||||||||||||
| string describeMax(Function func) { | ||||||||||||||
| if func.hasVarArg() | ||||||||||||||
| then result = "arbitrarily many positional arguments" | ||||||||||||||
| else | ||||||||||||||
| exists(string descr | descr = plural(func.getMaxPositionalArguments(), "positional argument") | | ||||||||||||||
| if func.getMinPositionalArguments() = func.getMaxPositionalArguments() | ||||||||||||||
| then result = descr | ||||||||||||||
| else result = "at most " + descr | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Describes the minumum number of arguments `func` can accept, without repeating "positional arguments". */ | ||||||||||||||
|
||||||||||||||
| string describeMinShort(Function func) { | ||||||||||||||
| exists(string descr | descr = func.getMinPositionalArguments().toString() | | ||||||||||||||
| if func.getMinPositionalArguments() = func.getMaxPositionalArguments() | ||||||||||||||
| then result = descr | ||||||||||||||
| else result = "at least " + descr | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Describes the maximum number of arguments `func` can accept, without repeating "positional arguments". */ | ||||||||||||||
| string describeMaxShort(Function func) { | ||||||||||||||
| if func.hasVarArg() | ||||||||||||||
| then result = "arbitrarily many" | ||||||||||||||
| else | ||||||||||||||
| exists(string descr | descr = func.getMaxPositionalArguments().toString() | | ||||||||||||||
| if func.getMinPositionalArguments() = func.getMaxPositionalArguments() | ||||||||||||||
| then result = descr | ||||||||||||||
| else result = "at most " + descr | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Describe an upper bound on the number of arguments `func` may accept, without specifying "at most". */ | ||||||||||||||
| string describeMaxBound(Function func) { | ||||||||||||||
| if func.hasVarArg() | ||||||||||||||
| then result = "arbitrarily many" | ||||||||||||||
| else result = func.getMaxPositionalArguments().toString() | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Holds if no way to call `base` would be valid for `sub`. The `msg` applies to the `sub method. */ | ||||||||||||||
| predicate strongSignatureMismatch(Function base, Function sub, string msg) { | ||||||||||||||
| overrides(base, sub) and | ||||||||||||||
| ( | ||||||||||||||
| sub.getMinPositionalArguments() > base.getMaxPositionalArguments() and | ||||||||||||||
| msg = | ||||||||||||||
| "requires " + describeMin(sub) + ", whereas overridden $@ requires " + describeMaxShort(base) + | ||||||||||||||
| "." | ||||||||||||||
| or | ||||||||||||||
| sub.getMaxPositionalArguments() < base.getMinPositionalArguments() and | ||||||||||||||
| msg = | ||||||||||||||
| "requires " + describeMax(sub) + ", whereas overridden $@ requires " + describeMinShort(base) + | ||||||||||||||
| "." | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** Holds if there may be some ways to call `base` that would not be valid for `sub`. The `msg` applies to the `sub` method. */ | ||||||||||||||
| predicate weakSignatureMismatch(Function base, Function sub, string msg) { | ||||||||||||||
| overrides(base, sub) and | ||||||||||||||
| ( | ||||||||||||||
| sub.getMinPositionalArguments() > base.getMinPositionalArguments() and | ||||||||||||||
| msg = | ||||||||||||||
| "requires " + describeMin(sub) + ", whereas overridden $@ may be called with " + | ||||||||||||||
| base.getMinPositionalArguments().toString() + "." | ||||||||||||||
| or | ||||||||||||||
| sub.getMaxPositionalArguments() < base.getMaxPositionalArguments() and | ||||||||||||||
| msg = | ||||||||||||||
| "requires " + describeMax(sub) + ", whereas overridden $@ may be called with " + | ||||||||||||||
| describeMaxBound(base) + "." | ||||||||||||||
| or | ||||||||||||||
| sub.getMinPositionalArguments() <= base.getMinPositionalArguments() and | ||||||||||||||
| sub.getMaxPositionalArguments() >= base.getMaxPositionalArguments() and | ||||||||||||||
| exists(string arg | | ||||||||||||||
| // TODO: positional-only args not considered | ||||||||||||||
| // e.g. `def foo(x, y, /, z):` has x,y as positional only args, should not be considered as possible kw args | ||||||||||||||
| // However, this likely does not create FPs, as we require a 'witness' call to generate an alert. | ||||||||||||||
|
||||||||||||||
| // However, this likely does not create FPs, as we require a 'witness' call to generate an alert. | |
| // TODO: Limitation: positional-only arguments (introduced in Python 3.8 with the `/` syntax) are not considered here. | |
| // For example, in `def foo(x, y, /, z):`, `x` and `y` are positional-only and should not be considered as possible keyword arguments. | |
| // This may affect the accuracy of signature mismatch detection for functions using positional-only parameters. | |
| // See: https://docs.python.org/3/glossary.html#term-positional-only-parameter | |
| // However, this likely does not create false positives, as we require a 'witness' call to generate an alert. |
Fixed
Show fixed
Hide fixed
joefarebrother marked this conversation as resolved.
Show resolved
Hide resolved
Fixed
Show fixed
Hide fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a pretty bad join in this predicate (which is why the DCA results look kinda terrible). You should be able to fix it by adding this helper predicate and referencing it in all of the appropriate places:
pragma[nomagic]
private File getFunctionFile(Function f) { result = f.getLocation().getFile() }| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| category: minorAnalysis | ||
| --- | ||
| * The `py/inheritance/signature-mismatch` query has been modernized. It produces more precise results and more descriptive alert messages. | ||
| * The `py/inheritance/incorrect-overriding-signature` query has been deprecated. Its results have been consolidated into the `py/inheritance/signature-mismatch` query. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| | om_test.py:32:5:32:35 | Function Derived.grossly_wrong1 | Overriding method 'grossly_wrong1' has signature mismatch with $@. | om_test.py:12:5:12:41 | Function Base.grossly_wrong1 | overridden method | | ||
| | om_test.py:35:5:35:47 | Function Derived.grossly_wrong2 | Overriding method 'grossly_wrong2' has signature mismatch with $@. | om_test.py:15:5:15:41 | Function Base.grossly_wrong2 | overridden method | | ||
| | om_test.py:32:5:32:35 | Function grossly_wrong1 | This method requires 2 positional arguments, whereas overridden $@ requires 3. | om_test.py:12:5:12:41 | Function grossly_wrong1 | Base.grossly_wrong1 | file://:0:0:0:0 | (none) | This call | | ||
| | om_test.py:35:5:35:47 | Function grossly_wrong2 | This method requires 4 positional arguments, whereas overridden $@ requires 3. | om_test.py:15:5:15:41 | Function grossly_wrong2 | Base.grossly_wrong2 | file://:0:0:0:0 | (none) | This call | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,13 @@ | ||
| | test.py:30:5:30:26 | Function Derived.meth3 | Overriding method 'meth3' has signature mismatch with $@. | test.py:11:5:11:20 | Function Base.meth3 | overridden method | | ||
| | test.py:24:5:24:26 | Function meth1 | This method requires 2 positional arguments, whereas overridden $@ requires 1. $@ correctly calls the base method, but does not match the signature of the overriding method. | test.py:5:5:5:20 | Function meth1 | Base.meth1 | test.py:15:9:15:20 | Attribute() | This call | | ||
| | test.py:27:5:27:20 | Function meth2 | This method requires 1 positional argument, whereas overridden $@ requires 2. $@ correctly calls the base method, but does not match the signature of the overriding method. | test.py:8:5:8:26 | Function meth2 | Base.meth2 | test.py:18:9:18:21 | Attribute() | This call | | ||
| | test.py:30:5:30:26 | Function meth3 | This method requires 2 positional arguments, whereas overridden $@ requires 1. | test.py:11:5:11:20 | Function meth3 | Base.meth3 | file://:0:0:0:0 | (none) | This call | | ||
| | test.py:69:5:69:24 | Function meth | This method requires 2 positional arguments, whereas overridden $@ requires 1. | test.py:64:5:64:19 | Function meth | BlameBase.meth | file://:0:0:0:0 | (none) | This call | | ||
| | test.py:74:5:74:24 | Function meth | This method requires 2 positional arguments, whereas overridden $@ requires 1. | test.py:64:5:64:19 | Function meth | BlameBase.meth | file://:0:0:0:0 | (none) | This call | | ||
| | test.py:125:5:125:20 | Function meth1 | This method requires 1 positional argument, whereas overridden $@ may be called with 2. $@ correctly calls the base method, but does not match the signature of the overriding method. | test.py:82:5:82:25 | Function meth1 | Base2.meth1 | test.py:110:9:110:23 | Attribute() | This call | | ||
| | test.py:131:5:131:31 | Function meth4 | This method requires at least 3 positional arguments, whereas overridden $@ requires at most 2. | test.py:88:5:88:25 | Function meth4 | Base2.meth4 | file://:0:0:0:0 | (none) | This call | | ||
| | test.py:133:5:133:28 | Function meth5 | This method requires at most 3 positional arguments, whereas overridden $@ requires at least 4. | test.py:90:5:90:34 | Function meth5 | Base2.meth5 | file://:0:0:0:0 | (none) | This call | | ||
| | test.py:135:5:135:23 | Function meth6 | This method requires 2 positional arguments, whereas overridden $@ may be called with arbitrarily many. $@ correctly calls the base method, but does not match the signature of the overriding method. | test.py:92:5:92:28 | Function meth6 | Base2.meth6 | test.py:113:9:113:27 | Attribute() | This call | | ||
| | test.py:137:5:137:28 | Function meth7 | This method requires at least 2 positional arguments, whereas overridden $@ may be called with 1. $@ correctly calls the base method, but does not match the signature of the overriding method. | test.py:94:5:94:25 | Function meth7 | Base2.meth7 | test.py:114:9:114:20 | Attribute() | This call | | ||
| | test.py:139:5:139:26 | Function meth8 | This method does not accept keyword argument `y`, which overridden $@ does. $@ correctly calls the base method, but does not match the signature of the overriding method. | test.py:96:5:96:26 | Function meth8 | Base2.meth8 | test.py:115:9:115:25 | Attribute() | This call | | ||
| | test.py:147:5:147:21 | Function meth12 | This method does not accept arbitrary keyword arguments, which overridden $@ does. $@ correctly calls the base method, but does not match the signature of the overriding method. | test.py:104:5:104:31 | Function meth12 | Base2.meth12 | test.py:119:9:119:24 | Attribute() | This call | | ||
| | test.py:149:5:149:27 | Function meth13 | This method does not accept keyword argument `x`, which overridden $@ does. $@ correctly calls the base method, but does not match the signature of the overriding method. | test.py:106:5:106:27 | Function meth13 | Base2.meth13 | test.py:120:9:120:24 | Attribute() | This call | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| Functions/SignatureOverriddenMethod.ql | ||
| query: Functions/SignatureOverriddenMethod.ql | ||
| postprocess: utils/test/InlineExpectationsTestQuery.ql |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| | test.py:19:9:19:31 | Attribute() | Keyword argument 'spam' is not a supported parameter name of $@. | test.py:5:5:5:20 | Function meth1 | method Base.meth1 | | ||
| | test.py:112:9:112:23 | Attribute() | Keyword argument 'x' is not a supported parameter name of $@. | test.py:86:5:86:20 | Function meth3 | method Base2.meth3 | |
Uh oh!
There was an error while loading. Please reload this page.