diff --git a/python/ql/src/Functions/IncorrectlyOverriddenMethod.ql b/python/ql/src/Functions/IncorrectlyOverriddenMethod.ql
index a46a2370c0e..a4e3bd6e0ce 100644
--- a/python/ql/src/Functions/IncorrectlyOverriddenMethod.ql
+++ b/python/ql/src/Functions/IncorrectlyOverriddenMethod.ql
@@ -1,4 +1,5 @@
/**
+ * @deprecated
* @name Mismatch between signature and use of an overriding method
* @description Method has a different signature from the overridden method and, if it were called, would be likely to cause an error.
* @kind problem
diff --git a/python/ql/src/Functions/SignatureOverriddenMethod.py b/python/ql/src/Functions/SignatureOverriddenMethod.py
index 7beddcb9e95..e3522351fc0 100644
--- a/python/ql/src/Functions/SignatureOverriddenMethod.py
+++ b/python/ql/src/Functions/SignatureOverriddenMethod.py
@@ -1,9 +1,12 @@
-# Base class method
-def runsource(self, source, filename="", symbol="single"):
- ... # Definition
+class Base:
+ def runsource(self, source, filename=""):
+ ...
-# Extend base class method
-def runsource(self, source):
- ... # Definition
\ No newline at end of file
+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")
\ No newline at end of file
diff --git a/python/ql/src/Functions/SignatureOverriddenMethod.qhelp b/python/ql/src/Functions/SignatureOverriddenMethod.qhelp
index b7da2678e3d..0575d967d3e 100644
--- a/python/ql/src/Functions/SignatureOverriddenMethod.qhelp
+++ b/python/ql/src/Functions/SignatureOverriddenMethod.qhelp
@@ -5,32 +5,25 @@
-
There are one (or more) legal parameters for an overridden method that are
-not legal for an overriding method. This will cause an error when the overriding
-method is called with a number of parameters that is legal for the overridden method.
-This violates the Liskov substitution principle.
+
When the signature of a method of a base class and a method of a subclass that overrides it don't match, a call to the base class method
+may not be a valid call to the subclass method, and thus raise an exception if an instance of the subclass is passed instead.
+If following the Liskov Substitution Principle, in which an instance of a subclass should be usable in every context as though it were an
+instance of the base class, this behavior breaks the principle.
-
Ensure that the overriding method accepts all the parameters that are legal for
-overridden method.
+
Ensure that the overriding method in the subclass accepts the same parameters as the base method.
-
In this example there is a mismatch between the legal parameters for the base
-class method (self, source, filename, symbol) and the extension method
-(self, source). The extension method can be used to override the base
-method as long as values are not specified for the filename and
-symbol parameters. If the extension method was passed the additional
-parameters accepted by the base method then an error would occur.
+
In the following example, Base.runsource takes an optional filename argument. However, the overriding method
+Sub.runsource does not. This means the run function will fail if passed an instance of Sub.
+
-
The extension method should be updated to support the filename and
-symbol parameters supported by the overridden method.
-
diff --git a/python/ql/src/Functions/SignatureOverriddenMethod.ql b/python/ql/src/Functions/SignatureOverriddenMethod.ql
index 3e3877bc139..15b3fa70640 100644
--- a/python/ql/src/Functions/SignatureOverriddenMethod.ql
+++ b/python/ql/src/Functions/SignatureOverriddenMethod.ql
@@ -13,23 +13,254 @@
*/
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())
- ) and
- not derived.getScope().isSpecialMethod() and
- derived.getName() != "__init__" and
- derived.isNormalMethod() and
- // call to overrides distributed for efficiency
- (
- derived.overrides(base) and derived.minParameters() > base.maxParameters()
- or
- derived.overrides(base) and derived.maxParameters() < base.minParameters()
+/** Holds if `base` is overridden 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
)
-select derived, "Overriding method '" + derived.getName() + "' has signature mismatch with $@.",
- base, "overridden method"
+}
+
+/** 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 minimum 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.
+ arg = base.getAnArg().getName() and
+ not arg = sub.getAnArg().getName() and
+ not exists(sub.getKwarg()) and
+ msg = "does not accept keyword argument `" + arg + "`, which overridden $@ does."
+ )
+ or
+ exists(base.getKwarg()) and
+ not exists(sub.getKwarg()) and
+ msg = "does not accept arbitrary keyword arguments, which overridden $@ does."
+ )
+}
+
+/** Holds if `f` should be ignored for considering signature mismatches. */
+predicate ignore(Function f) {
+ isClassmethod(f)
+ or
+ exists(
+ Function g // other functions with the same name, e.g. @property getters/setters.
+ |
+ g.getScope() = f.getScope() and
+ g.getName() = f.getName() and
+ g != f
+ )
+}
+
+/** Gets a function that `call` may resolve to. */
+Function resolveCall(Call call) {
+ exists(DataFlowCall dfc | call = dfc.getNode().(CallNode).getNode() |
+ result = viableCallable(dfc).(DataFlowFunction).getScope()
+ )
+}
+
+/** Holds if `call` may resolve to either `base` or `sub`, and `base` is overridden by `sub`. */
+predicate callViableForEitherOverride(Function base, Function sub, Call call) {
+ overrides(base, sub) and
+ base = resolveCall(call) and
+ sub = resolveCall(call)
+}
+
+/** Holds if either both `base` and `sub` are static methods, or both are not static methods, and `base` is overridden by `sub`. */
+predicate matchingStatic(Function base, Function sub) {
+ overrides(base, sub) and
+ (
+ isStaticmethod(base) and
+ isStaticmethod(sub)
+ or
+ not isStaticmethod(base) and
+ not isStaticmethod(sub)
+ )
+}
+
+int extraSelfArg(Function func) { if isStaticmethod(func) then result = 0 else result = 1 }
+
+/** Holds if the call `call` matches the signature for `func`. */
+predicate callMatchesSignature(Function func, Call call) {
+ func = resolveCall(call) and
+ (
+ // Each parameter of the function is accounted for in the call
+ forall(Parameter param, int i | param = func.getArg(i) |
+ // self arg
+ i = 0 and not isStaticmethod(func)
+ or
+ // positional arg
+ i - extraSelfArg(func) < call.getPositionalArgumentCount()
+ or
+ // has default
+ exists(param.getDefault())
+ or
+ // keyword arg
+ call.getANamedArgumentName() = param.getName()
+ )
+ or
+ // arbitrary varargs or kwargs
+ exists(call.getStarArg())
+ or
+ exists(call.getKwargs())
+ ) and
+ // No excess parameters
+ call.getPositionalArgumentCount() + extraSelfArg(func) <= func.getMaxPositionalArguments() and
+ (
+ exists(func.getKwarg())
+ or
+ forall(string name | name = call.getANamedArgumentName() | exists(func.getArgByName(name)))
+ )
+}
+
+pragma[nomagic]
+private File getFunctionFile(Function f) { result = f.getLocation().getFile() }
+
+/** Gets a call which matches the signature of `base`, but not of overridden `sub`. */
+Call getASignatureMismatchWitness(Function base, Function sub) {
+ callViableForEitherOverride(base, sub, result) and
+ callMatchesSignature(base, result) and
+ not callMatchesSignature(sub, result)
+}
+
+pragma[inline]
+string preferredFile(File callFile, Function base, Function sub) {
+ if callFile = getFunctionFile(base)
+ then result = " A"
+ else
+ if callFile = getFunctionFile(sub)
+ then result = " B"
+ else result = callFile.getAbsolutePath()
+}
+
+/** Choose a 'witnessing' call that matches the signature of `base` but not of overridden `sub`. */
+Call chooseASignatureMismatchWitness(Function base, Function sub) {
+ exists(getASignatureMismatchWitness(base, sub)) and
+ result =
+ min(Call c |
+ c = getASignatureMismatchWitness(base, sub)
+ |
+ c
+ order by
+ preferredFile(c.getLocation().getFile(), base, sub), c.getLocation().getStartLine(),
+ c.getLocation().getStartColumn()
+ )
+}
+
+module CallOption = LocatableOption;
+
+from Function base, Function sub, string msg, string extraMsg, CallOption::Option call
+where
+ not sub.isSpecialMethod() and
+ sub.getName() != "__init__" and
+ not ignore(sub) and
+ not ignore(base) and
+ matchingStatic(base, sub) and
+ (
+ // If we have a witness, alert for a 'weak' mismatch, but prefer the message for a 'strong' mismatch if that holds.
+ call.asSome() = chooseASignatureMismatchWitness(base, sub) and
+ extraMsg =
+ " $@ correctly calls the base method, but does not match the signature of the overriding method." and
+ (
+ strongSignatureMismatch(base, sub, msg)
+ or
+ not strongSignatureMismatch(base, sub, _) and
+ weakSignatureMismatch(base, sub, msg)
+ )
+ or
+ // With no witness, only alert for 'strong' mismatches.
+ not exists(getASignatureMismatchWitness(base, sub)) and
+ call.isNone() and
+ strongSignatureMismatch(base, sub, msg) and
+ extraMsg = ""
+ )
+select sub, "This method " + msg + extraMsg, base, base.getQualifiedName(), call, "This call"
diff --git a/python/ql/src/change-notes/2025-08-19-signature-mismatch.md b/python/ql/src/change-notes/2025-08-19-signature-mismatch.md
new file mode 100644
index 00000000000..60c3efa32eb
--- /dev/null
+++ b/python/ql/src/change-notes/2025-08-19-signature-mismatch.md
@@ -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.
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Functions/general/SignatureOverriddenMethod.expected b/python/ql/test/query-tests/Functions/general/SignatureOverriddenMethod.expected
index 81c45ab8abe..21b11212075 100644
--- a/python/ql/test/query-tests/Functions/general/SignatureOverriddenMethod.expected
+++ b/python/ql/test/query-tests/Functions/general/SignatureOverriddenMethod.expected
@@ -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 |
diff --git a/python/ql/test/query-tests/Functions/overriding/SignatureOverriddenMethod.expected b/python/ql/test/query-tests/Functions/overriding/SignatureOverriddenMethod.expected
index bcb9363a55a..07756ee4600 100644
--- a/python/ql/test/query-tests/Functions/overriding/SignatureOverriddenMethod.expected
+++ b/python/ql/test/query-tests/Functions/overriding/SignatureOverriddenMethod.expected
@@ -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 |
diff --git a/python/ql/test/query-tests/Functions/overriding/SignatureOverriddenMethod.qlref b/python/ql/test/query-tests/Functions/overriding/SignatureOverriddenMethod.qlref
index a306477b3b4..5470a05e0e4 100644
--- a/python/ql/test/query-tests/Functions/overriding/SignatureOverriddenMethod.qlref
+++ b/python/ql/test/query-tests/Functions/overriding/SignatureOverriddenMethod.qlref
@@ -1 +1,2 @@
-Functions/SignatureOverriddenMethod.ql
+query: Functions/SignatureOverriddenMethod.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
diff --git a/python/ql/test/query-tests/Functions/overriding/WrongNameForArgumentInCall.expected b/python/ql/test/query-tests/Functions/overriding/WrongNameForArgumentInCall.expected
index d2fc2ef6784..0cadf5e5fbf 100644
--- a/python/ql/test/query-tests/Functions/overriding/WrongNameForArgumentInCall.expected
+++ b/python/ql/test/query-tests/Functions/overriding/WrongNameForArgumentInCall.expected
@@ -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 |
diff --git a/python/ql/test/query-tests/Functions/overriding/test.py b/python/ql/test/query-tests/Functions/overriding/test.py
index c4c7caaa1aa..63ee50e820c 100644
--- a/python/ql/test/query-tests/Functions/overriding/test.py
+++ b/python/ql/test/query-tests/Functions/overriding/test.py
@@ -21,13 +21,13 @@ class Base(object):
class Derived(Base):
- def meth1(self, spam):
+ def meth1(self, spam): # $Alert[py/inheritance/signature-mismatch] # Has 1 more arg, base called in Base.foo
pass
- def meth2(self):
+ def meth2(self): # $Alert[py/inheritance/signature-mismatch] # Has 1 fewer arg, base called in Base.foo
pass
- def meth3(self, eggs): #Incorrectly overridden and not called.
+ def meth3(self, eggs): # $Alert[py/inheritance/signature-mismatch] # Has 1 more arg. Method is not called.
pass
def bar(self):
@@ -66,13 +66,84 @@ class BlameBase(object):
class Correct1(BlameBase):
- def meth(self, arg):
+ def meth(self, arg): # $Alert[py/inheritance/signature-mismatch] # Has 1 more arg. The incorrect-overridden-method query would alert for the base method in this case.
pass
class Correct2(BlameBase):
- def meth(self, arg):
+ def meth(self, arg): # $Alert[py/inheritance/signature-mismatch] # Has 1 more arg
pass
c = Correct2()
c.meth("hi")
+
+class Base2:
+
+ def meth1(self, x=1): pass
+
+ def meth2(self, x=1): pass
+
+ def meth3(self): pass
+
+ def meth4(self, x=1): pass
+
+ def meth5(self, x, y, z, w=1): pass
+
+ def meth6(self, x, *ys): pass
+
+ def meth7(self, *ys): pass
+
+ def meth8(self, x, y): pass
+
+ def meth9(self, x, y): pass
+
+ def meth10(self, x, *, y=3): pass
+
+ def meth11(self, x, y): pass
+
+ def meth12(self, **kwargs): pass
+
+ def meth13(self, /, x): pass
+
+ def call_some(self):
+ self.meth1()
+ self.meth1(x=2)
+ self.meth3()
+ self.meth3(x=2)
+ self.meth6(2, 3, 4)
+ self.meth7()
+ self.meth8(1,y=3)
+ self.meth9(1,2)
+ self.meth10(1,y=3)
+ self.meth11(1,y=3)
+ self.meth12(x=2)
+ self.meth13(x=2)
+
+
+class Derrived2(Base2):
+
+ def meth1(self): pass # $Alert[py/inheritance/signature-mismatch] # Weak mismatch (base may be called with 2 args. only alert if mismatching call exists)
+
+ def meth2(self): pass # No alert (weak mismatch, but not called)
+
+ def meth3(self, x=1): pass # No alert (no mismatch - all base calls are valid for sub)
+
+ def meth4(self, x, y, z=1): pass # $Alert[py/inheritance/signature-mismatch] # sub min > base max (strong mismatch)
+
+ def meth5(self, x, y=1): pass # $Alert[py/inheritance/signature-mismatch]
+
+ def meth6(self, x): pass # $Alert[py/inheritance/signature-mismatch] # weak mismatch (base may be called with 3+ args)
+
+ def meth7(self, x, *ys): pass # $Alert[py/inheritance/signature-mismatch] # weak mismatch (base may be called with 1 arg only)
+
+ def meth8(self, x, z): pass # $Alert[py/inheritance/signature-mismatch] # weak mismatch (base may be called with arg named y)
+
+ def meth9(self, x, z): pass # No alert (never called with wrong keyword arg)
+
+ def meth10(self, x, **kwargs): pass # No alert (y is kw-only arg in base, calls that use it are valid for sub)
+
+ def meth11(self, x, z, **kwargs): pass # $MISSING:Alert[py/inheritance/signature-mismatch] # call using y kw-arg is invalid due to not specifying z, but this is not detected. Likely a fairly niche situation.
+
+ def meth12(self): pass # $Alert[py/inheritance/signature-mismatch] # call including extra kwarg invalid
+
+ def meth13(self, /, y): pass # $Alert[py/inheritance/signature-mismatch] # weak mismatch (base may be called with arg named x), however meth13 is incorrectly detected as having 2 minimum positional arguments, whereas x is kw-only; resulting in the witness call not being detected as a valid call to Base2.meth13.