diff --git a/python/ql/lib/change-notes/2026-03-05-ignore-type-overloaded-methods-during-resolution.md b/python/ql/lib/change-notes/2026-03-05-ignore-type-overloaded-methods-during-resolution.md new file mode 100644 index 000000000000..001b46a0adb5 --- /dev/null +++ b/python/ql/lib/change-notes/2026-03-05-ignore-type-overloaded-methods-during-resolution.md @@ -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. diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll index d4444c6795bf..9ef775f7e3ff 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll @@ -304,6 +304,22 @@ 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. + */ +overlay[local] +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 +865,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 +908,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 diff --git a/python/ql/test/library-tests/dataflow/calls-overload/OverloadCallTest.expected b/python/ql/test/library-tests/dataflow/calls-overload/OverloadCallTest.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/dataflow/calls-overload/OverloadCallTest.ql b/python/ql/test/library-tests/dataflow/calls-overload/OverloadCallTest.ql new file mode 100644 index 000000000000..6050f56c0ba9 --- /dev/null +++ b/python/ql/test/library-tests/dataflow/calls-overload/OverloadCallTest.ql @@ -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 diff --git a/python/ql/test/library-tests/dataflow/calls-overload/test.py b/python/ql/test/library-tests/dataflow/calls-overload/test.py new file mode 100644 index 000000000000..bb385b9de38e --- /dev/null +++ b/python/ql/test/library-tests/dataflow/calls-overload/test.py @@ -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