Repository: PyCQA/pyflakes

Issue:
F811 (redefinition of unused variable) incorrectly triggered with @overload decorator
This bug was fixed for functions in #320. It still occurs if the function is inside a class and/or has multiple decorators (e.g. @staticmethod, @classmethod):
```python
from typing import overload


class test:
    @overload
    def utf8(value: None) -> None:
        pass

    @overload
    def utf8(value: bytes) -> bytes:
        pass

    @overload
    def utf8(value: str) -> bytes:
        pass

    def utf8(value):
        pass  # actual implementation
```

```
test.py:9:5: F811 redefinition of unused 'utf8' from line 5
test.py:13:5: F811 redefinition of unused 'utf8' from line 9
test.py:17:5: F811 redefinition of unused 'utf8' from line 13
```

```python
from typing import overload


def do_nothing(func):
    return func


@overload
@do_nothing
def utf8(value: None) -> None:
    pass


@overload
@do_nothing
def utf8(value: bytes) -> bytes:
    pass


@overload
@do_nothing
def utf8(value: str) -> bytes:
    pass


def utf8(value):
    pass  # actual implementation
```

```
test.py:14:1: F811 redefinition of unused 'utf8' from line 8
test.py:20:1: F811 redefinition of unused 'utf8' from line 14
test.py:26:1: F811 redefinition of unused 'utf8' from line 20
```

Discussion:
Zac-HD: Hurray!  

This fixes another bug too: in the current released version, using a decorator named `overload` causes an internal error (because pyflakes thinks that it's `typing.overload`, and tries to access a nonexistent scope name).  We first noticed this a while ago in [Hypothesis' build](https://travis-ci.org/HypothesisWorks/hypothesis/jobs/498740860#L783), but it's reproducible by running `pyflakes` on the following file:

```python
overload = lambda f: f  # or `def overload(arg): return arg

@overload
def func(arg): pass

def func(arg): return ...
```

Happily I've confirmed that this change fixes pyflakes for both the minimal case above (which might be worth adding as another test case for this PR?) and for Hypothesis - thanks @asottile 😍 
asottile: @Zac-HD 🎉 though it was probably #423 that fixed that one ~o/
Zac-HD: Yeah, probably... still fixed though and still fixed by you 😁 
asottile: heh only fair -- I broke it in the first place /o\

Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 4c88af2..0e636c1 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -530,14 +530,21 @@ def getNodeName(node):
         return node.name
 
 
-def is_typing_overload(value, scope):
+def is_typing_overload(value, scope_stack):
+    def name_is_typing_overload(name):  # type: (str) -> bool
+        for scope in reversed(scope_stack):
+            if name in scope:
+                return (
+                    isinstance(scope[name], ImportationFrom) and
+                    scope[name].fullName == 'typing.overload'
+                )
+        else:
+            return False
+
     def is_typing_overload_decorator(node):
         return (
             (
-                isinstance(node, ast.Name) and
-                node.id in scope and
-                isinstance(scope[node.id], ImportationFrom) and
-                scope[node.id].fullName == 'typing.overload'
+                isinstance(node, ast.Name) and name_is_typing_overload(node.id)
             ) or (
                 isinstance(node, ast.Attribute) and
                 isinstance(node.value, ast.Name) and
@@ -548,8 +555,10 @@ def is_typing_overload(value, scope):
 
     return (
         isinstance(value.source, ast.FunctionDef) and
-        len(value.source.decorator_list) == 1 and
-        is_typing_overload_decorator(value.source.decorator_list[0])
+        any(
+            is_typing_overload_decorator(dec)
+            for dec in value.source.decorator_list
+        )
     )
 
 
@@ -888,7 +897,7 @@ class Checker(object):
                                 node, value.name, existing.source)
                 elif not existing.used and value.redefines(existing):
                     if value.name != '_' or isinstance(existing, Importation):
-                        if not is_typing_overload(existing, self.scope):
+                        if not is_typing_overload(existing, self.scopeStack):
                             self.report(messages.RedefinedWhileUnused,
                                         node, value.name, existing.source)
 


Test patch:
diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py
index 48635bb..b8876cb 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -39,6 +39,41 @@ class TestTypeAnnotations(TestCase):
             return s
         """)
 
+    def test_overload_with_multiple_decorators(self):
+        self.flakes("""
+            from typing import overload
+            dec = lambda f: f
+
+            @dec
+            @overload
+            def f(x):  # type: (int) -> int
+                pass
+
+            @dec
+            @overload
+            def f(x):  # type: (str) -> str
+                pass
+
+            @dec
+            def f(x): return x
+       """)
+
+    def test_overload_in_class(self):
+        self.flakes("""
+        from typing import overload
+
+        class C:
+            @overload
+            def f(self, x):  # type: (int) -> int
+                pass
+
+            @overload
+            def f(self, x):  # type: (str) -> str
+                pass
+
+            def f(self, x): return x
+        """)
+
     def test_not_a_typing_overload(self):
         """regression test for @typing.overload detection bug in 2.1.0"""
         self.flakes("""