Merge pull request #21419 from github/tausbn/python-improve-overloaded-method-resolution

Python: Improve modelling of overloaded methods
This commit is contained in:
Taus
2026-03-09 16:25:05 +01:00
committed by GitHub
5 changed files with 92 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
- The call graph resolution no longer considers methods marked using [`@typing.overload`](https://typing.python.org/en/latest/spec/overload.html#overloads) as valid targets. This ensures that only the method that contains the actual implementation gets resolved as a target.

View File

@@ -304,6 +304,25 @@ predicate hasContextmanagerDecorator(Function func) {
)
}
/**
* Holds if the function `func` has a `typing.overload` decorator.
* Such functions are type stubs that declare an overload signature but are
* not the actual implementation.
*
* Normally we would want to model this using API graphs for more precision, but since this
* predicate is used in the call graph computation, we have to use a more syntactic approach.
*/
overlay[local]
private predicate hasOverloadDecorator(Function func) {
exists(ControlFlowNode overload |
overload.(NameNode).getId() = "overload" and overload.(NameNode).isGlobal()
or
overload.(AttrNode).getObject("overload").(NameNode).isGlobal()
|
func.getADecorator() = overload.getNode()
)
}
// =============================================================================
// Callables
// =============================================================================
@@ -849,7 +868,8 @@ private Class getNextClassInMro(Class cls) {
*/
Function findFunctionAccordingToMro(Class cls, string name) {
result = cls.getAMethod() and
result.getName() = name
result.getName() = name and
not hasOverloadDecorator(result)
or
not class_has_method(cls, name) and
result = findFunctionAccordingToMro(getNextClassInMro(cls), name)
@@ -891,6 +911,7 @@ Class getNextClassInMroKnownStartingClass(Class cls, Class startingClass) {
Function findFunctionAccordingToMroKnownStartingClass(Class cls, Class startingClass, string name) {
result = cls.getAMethod() and
result.getName() = name and
not hasOverloadDecorator(result) and
cls = getADirectSuperclass*(startingClass)
or
not class_has_method(cls, name) and

View File

@@ -0,0 +1,26 @@
/**
* Test that `@typing.overload` stubs are not resolved as call targets.
*/
import python
import semmle.python.dataflow.new.internal.DataFlowDispatch as DataFlowDispatch
import utils.test.InlineExpectationsTest
module OverloadCallTest implements TestSig {
string getARelevantTag() { result = "init" }
predicate hasActualResult(Location location, string element, string tag, string value) {
exists(location.getFile().getRelativePath()) and
exists(DataFlowDispatch::DataFlowCall call, Function target |
location = call.getLocation() and
element = call.toString() and
DataFlowDispatch::resolveCall(call.getNode(), target, _) and
target.getName() = "__init__"
|
value = target.getQualifiedName() + ":" + target.getLocation().getStartLine().toString() and
tag = "init"
)
}
}
import MakeTest<OverloadCallTest>

View File

@@ -0,0 +1,39 @@
import typing
class OverloadedInit:
@typing.overload
def __init__(self, x: int) -> None: ...
@typing.overload
def __init__(self, x: str, y: str) -> None: ...
def __init__(self, x, y=None):
pass
OverloadedInit(1) # $ init=OverloadedInit.__init__:11
OverloadedInit("a", "b") # $ init=OverloadedInit.__init__:11
from typing import overload
class OverloadedInitFromImport:
@overload
def __init__(self, x: int) -> None: ...
@overload
def __init__(self, x: str, y: str) -> None: ...
def __init__(self, x, y=None):
pass
OverloadedInitFromImport(1) # $ init=OverloadedInitFromImport.__init__:28
OverloadedInitFromImport("a", "b") # $ init=OverloadedInitFromImport.__init__:28
class NoOverloads:
def __init__(self, x):
pass
NoOverloads(1) # $ init=NoOverloads.__init__:36