Skip to content

Commit 2400732

Browse files
authored
Fix: function-redefined not triggered for functions with leading underscores (close #9894) (#10606)
1 parent 4ada741 commit 2400732

File tree

8 files changed

+117
-12
lines changed

8 files changed

+117
-12
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix false negative where function-redefined (E0102) was not reported for functions with a leading underscore.
2+
3+
Closes #9894

pylint/checkers/base/basic_error_checker.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,71 @@ def x(self, value): self._x = value
9595
return False
9696

9797

98+
def _extract_register_target(dec: nodes.NodeNG) -> nodes.NodeNG | None:
99+
"""
100+
If decorator `dec` looks like `@func.register(...)` or `@func.register`,
101+
return the `func` target node (Name or Attribute). Otherwise return None.
102+
"""
103+
if isinstance(dec, nodes.Call):
104+
func_part = dec.func
105+
if isinstance(func_part, nodes.Attribute) and func_part.attrname == "register":
106+
return func_part.expr
107+
return None
108+
109+
if isinstance(dec, nodes.Attribute) and dec.attrname == "register":
110+
return dec.expr
111+
112+
return None
113+
114+
115+
def _inferred_has_singledispatchmethod(target: nodes.NodeNG) -> bool:
116+
"""
117+
Infer `target` and return True if the inferred object has a
118+
@singledispatchmethod decorator.
119+
"""
120+
inferred = utils.safe_infer(target)
121+
if not inferred:
122+
return False
123+
124+
if isinstance(inferred, (nodes.FunctionDef, nodes.AsyncFunctionDef)):
125+
decorators = inferred.decorators
126+
if isinstance(decorators, nodes.Decorators):
127+
for dec in decorators.nodes:
128+
inferred_dec = utils.safe_infer(dec)
129+
if (
130+
inferred_dec
131+
and inferred_dec.qname() == "functools.singledispatchmethod"
132+
):
133+
return True
134+
135+
return False
136+
137+
138+
def _is_singledispatchmethod_registration(node: nodes.FunctionDef) -> bool:
139+
"""
140+
Return True if `node` is a function decorated like:
141+
142+
@func.register(...)
143+
def _(…): ...
144+
145+
where `func` is a singledispatchmethod (i.e. its base was decorated
146+
with @singledispatchmethod).
147+
"""
148+
decorators = node.decorators
149+
if not decorators:
150+
return False
151+
152+
for dec in decorators.nodes:
153+
target = _extract_register_target(dec)
154+
if target is None:
155+
continue
156+
157+
if _inferred_has_singledispatchmethod(target):
158+
return True
159+
160+
return False
161+
162+
98163
class BasicErrorChecker(_BasicChecker):
99164
msgs = {
100165
"E0100": (
@@ -536,6 +601,9 @@ def _check_redefinition(
536601
):
537602
return
538603

604+
if _is_singledispatchmethod_registration(node):
605+
return
606+
539607
# Skip typing.overload() functions.
540608
if utils.is_overload_stub(node):
541609
return
@@ -572,9 +640,6 @@ def _check_redefinition(
572640
):
573641
return
574642

575-
dummy_variables_rgx = self.linter.config.dummy_variables_rgx
576-
if dummy_variables_rgx and dummy_variables_rgx.match(node.name):
577-
return
578643
self.add_message(
579644
"function-redefined",
580645
node=node,

tests/functional/f/function_redefined.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def dummy_func():
8080
"""First dummy function"""
8181
pass
8282

83-
def dummy_func():
83+
def dummy_func2():
8484
"""Second dummy function, don't emit function-redefined message
8585
because of the dummy name"""
8686
pass
@@ -94,7 +94,7 @@ def math(): # [function-redefined]
9494
pass
9595

9696
import math as _
97-
def _():
97+
def fun():
9898
pass
9999

100100
# pylint: disable=too-few-public-methods
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
function-redefined:18:4:18:15:AAAA.method2:method already defined line 15:UNDEFINED
2-
function-redefined:21:0:21:10:AAAA:class already defined line 8:UNDEFINED
3-
function-redefined:35:0:35:9:func2:function already defined line 32:UNDEFINED
4-
redefined-outer-name:37:4:37:16:func2:Redefining name '__revision__' from outer scope (line 7):UNDEFINED
5-
function-redefined:54:4:54:23:exclusive_func2:function already defined line 48:UNDEFINED
6-
function-redefined:89:0:89:8:ceil:function already defined line 88:UNDEFINED
7-
function-redefined:93:0:93:8:math:function already defined line 92:UNDEFINED
1+
function-redefined:18:4:18:15:AAAA.method2:method already defined line 15:UNDEFINED
2+
function-redefined:21:0:21:10:AAAA:class already defined line 8:UNDEFINED
3+
function-redefined:35:0:35:9:func2:function already defined line 32:UNDEFINED
4+
redefined-outer-name:37:4:37:16:func2:Redefining name '__revision__' from outer scope (line 7):UNDEFINED
5+
function-redefined:54:4:54:23:exclusive_func2:function already defined line 48:UNDEFINED
6+
function-redefined:89:0:89:8:ceil:function already defined line 88:UNDEFINED
7+
function-redefined:93:0:93:8:math:function already defined line 92:UNDEFINED
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# pylint: disable=missing-module-docstring, missing-function-docstring
2+
def _my_func():
3+
pass
4+
5+
def _my_func(): # [function-redefined]
6+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
function-redefined:5:0:5:12:_my_func:function already defined line 2:UNDEFINED
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# pylint: disable=missing-module-docstring, missing-class-docstring, missing-function-docstring, too-few-public-methods
2+
3+
from functools import singledispatch, singledispatchmethod
4+
5+
# --- singledispatch function case ---
6+
@singledispatch
7+
def process(value):
8+
return f"default handler for {value!r}"
9+
10+
@process.register(int)
11+
def _(value):
12+
return f"int handler for {value}"
13+
14+
@process.register(str)
15+
def _(value):
16+
return f"str handler for {value}"
17+
18+
# --- singledispatchmethod case ---
19+
class Handler:
20+
@singledispatchmethod
21+
def handle(self, value):
22+
return f"default method handler for {value!r}"
23+
24+
@handle.register(int)
25+
def _(self, value):
26+
return f"int method handler for {value}"
27+
28+
@handle.register(str)
29+
def _(self, value):
30+
return f"str method handler for {value}"

tests/functional/s/singledispatch/singledispatch_function_redefined.txt

Whitespace-only changes.

0 commit comments

Comments
 (0)