From fa61f6f3dfc2b535f2fdfb3acb4c468d30b55d3e Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 5 Mar 2026 16:56:03 +0000 Subject: [PATCH] Python: Model `@typing.overload` in method resolution Adds `hasOverloadDecorator` as a predicate on functions. It looks for decorators called `overload` or `something.overload` (usually `typing.overload` or `t.overload`). These are then filtered out in the predicates that (approximate) resolving methods according to the MRO. As the test introduced in the previous commit shows, this removes the spurious resolutions we had before. --- .../new/internal/DataFlowDispatch.qll | 20 ++++++++++++++++++- .../dataflow/calls-overload/test.py | 8 ++++---- 2 files changed, 23 insertions(+), 5 deletions(-) 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 d4444c6795b..9ef775f7e3f 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/test.py b/python/ql/test/library-tests/dataflow/calls-overload/test.py index db0e4841936..bb385b9de38 100644 --- a/python/ql/test/library-tests/dataflow/calls-overload/test.py +++ b/python/ql/test/library-tests/dataflow/calls-overload/test.py @@ -11,8 +11,8 @@ class OverloadedInit: def __init__(self, x, y=None): pass -OverloadedInit(1) # $ init=OverloadedInit.__init__:11 SPURIOUS: init=OverloadedInit.__init__:6 init=OverloadedInit.__init__:9 -OverloadedInit("a", "b") # $ init=OverloadedInit.__init__:11 SPURIOUS: init=OverloadedInit.__init__:6 init=OverloadedInit.__init__:9 +OverloadedInit(1) # $ init=OverloadedInit.__init__:11 +OverloadedInit("a", "b") # $ init=OverloadedInit.__init__:11 from typing import overload @@ -28,8 +28,8 @@ class OverloadedInitFromImport: def __init__(self, x, y=None): pass -OverloadedInitFromImport(1) # $ init=OverloadedInitFromImport.__init__:28 SPURIOUS: init=OverloadedInitFromImport.__init__:23 init=OverloadedInitFromImport.__init__:26 -OverloadedInitFromImport("a", "b") # $ init=OverloadedInitFromImport.__init__:28 SPURIOUS: init=OverloadedInitFromImport.__init__:23 init=OverloadedInitFromImport.__init__:26 +OverloadedInitFromImport(1) # $ init=OverloadedInitFromImport.__init__:28 +OverloadedInitFromImport("a", "b") # $ init=OverloadedInitFromImport.__init__:28 class NoOverloads: