Repository: PyCQA/pyflakes (python)

Issue:
`typing.Literal` is not detected when renamed at import time
I just encountered a false-positive regression reported by PyFlakes 2.2.0, but not 2.1.1: If the *Literal* class from `typing` or `typing_extensions` is renamed at import time, PyFlakes looses track of it and treats any string argument as forward-reference that then cannot be resolved – causing it to emit a *F821 undefined name 'none'* error through flake8.

Minimum example:

```py
import typing as ty
from typing_extensions import Literal as ty_Literal
		
@ty.overload
def request(
		decoder: ty_Literal["none"] = "none",
) -> None:
	...
```

Pull request:
Detect typing module attributes with 'import typing as <name>'
Fixes #561

See https://github.com/pallets/click/pull/1896#discussion_r632645323 for use case.

Patch:
diff --git a/README.rst b/README.rst
index c456d25..47ea7ed 100644
--- a/README.rst
+++ b/README.rst
@@ -39,8 +39,7 @@ Design Principles
 Pyflakes makes a simple promise: it will never complain about style,
 and it will try very, very hard to never emit false positives.
 
-Pyflakes is also faster than Pylint_
-or Pychecker_. This is
+Pyflakes is also faster than Pylint_. This is
 largely because Pyflakes only examines the syntax tree of each file
 individually. As a consequence, Pyflakes is more limited in the
 types of things it can check.
@@ -79,7 +78,6 @@ All changes should include tests and pass flake8_.
 .. _Pylint: https://www.pylint.org/
 .. _flake8: https://pypi.org/project/flake8/
 .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
-.. _Pychecker: http://pychecker.sourceforge.net/
 .. _`rebase your changes`: https://git-scm.com/book/en/v2/Git-Branching-Rebasing
 .. _`GitHub pull request`: https://github.com/PyCQA/pyflakes/pulls
 
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 48d3841..45c7a4a 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -275,7 +275,8 @@ def iter_child_nodes(node, omit=None, _fields_order=_FieldsOrder()):
             yield field
         elif isinstance(field, list):
             for item in field:
-                yield item
+                if isinstance(item, ast.AST):
+                    yield item
 
 
 def convert_to_value(item):
@@ -691,6 +692,8 @@ def getNodeName(node):
         return node.id
     if hasattr(node, 'name'):   # an ExceptHandler node
         return node.name
+    if hasattr(node, 'rest'):   # a MatchMapping node
+        return node.rest
 
 
 TYPING_MODULES = frozenset(('typing', 'typing_extensions'))
@@ -717,6 +720,16 @@ def _is_typing_helper(node, is_name_match_fn, scope_stack):
 
         return False
 
+    def _module_scope_is_typing(name):
+        for scope in reversed(scope_stack):
+            if name in scope:
+                return (
+                    isinstance(scope[name], Importation) and
+                    scope[name].fullName in TYPING_MODULES
+                )
+
+        return False
+
     return (
         (
             isinstance(node, ast.Name) and
@@ -724,7 +737,7 @@ def _is_typing_helper(node, is_name_match_fn, scope_stack):
         ) or (
             isinstance(node, ast.Attribute) and
             isinstance(node.value, ast.Name) and
-            node.value.id in TYPING_MODULES and
+            _module_scope_is_typing(node.value.id) and
             is_name_match_fn(node.attr)
         )
     )
@@ -868,7 +881,6 @@ class Checker(object):
 
     nodeDepth = 0
     offset = None
-    traceTree = False
     _in_annotation = AnnotationState.NONE
     _in_deferred = False
 
@@ -1393,8 +1405,6 @@ class Checker(object):
         if self.offset and getattr(node, 'lineno', None) is not None:
             node.lineno += self.offset[0]
             node.col_offset += self.offset[1]
-        if self.traceTree:
-            print('  ' * self.nodeDepth + node.__class__.__name__)
         if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or
                                         self.isDocstring(node)):
             self.futuresAllowed = False
@@ -1406,8 +1416,6 @@ class Checker(object):
             handler(node)
         finally:
             self.nodeDepth -= 1
-        if self.traceTree:
-            print('  ' * self.nodeDepth + 'end ' + node.__class__.__name__)
 
     _getDoctestExamples = doctest.DocTestParser().get_examples
 
@@ -2383,3 +2391,12 @@ class Checker(object):
             left = right
 
         self.handleChildren(node)
+
+    MATCH = MATCH_CASE = MATCHCLASS = MATCHOR = MATCHSEQUENCE = handleChildren
+    MATCHSINGLETON = MATCHVALUE = handleChildren
+
+    def _match_target(self, node):
+        self.handleNodeStore(node)
+        self.handleChildren(node)
+
+    MATCHAS = MATCHMAPPING = MATCHSTAR = _match_target
diff --git a/tox.ini b/tox.ini
index c88831a..0396f02 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 [tox]
 skip_missing_interpreters = True
 envlist =
-    py27,py34,py35,py36,py37,py38,pypy,pypy3
+    py27,py35,py36,py37,py38,py39,py310,pypy,pypy3
 
 [testenv]
 deps = flake8==3.6.0


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 39ba139..8c078ae 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,7 +8,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "pypy2", "pypy3"]
+        python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10-dev", "pypy2", "pypy3"]
         os: [ubuntu-latest]
         # Include py2 + minimum py3 + maximum py3 + pypy + pypy3 on Windows
         include:
diff --git a/pyflakes/test/test_api.py b/pyflakes/test/test_api.py
index d379b3b..2c1cf19 100644
--- a/pyflakes/test/test_api.py
+++ b/pyflakes/test/test_api.py
@@ -441,7 +441,7 @@ def baz():
             evaluate(source)
         except SyntaxError:
             e = sys.exc_info()[1]
-            if not PYPY:
+            if not PYPY and sys.version_info < (3, 10):
                 self.assertTrue(e.text.count('\n') > 1)
         else:
             self.fail()
@@ -449,10 +449,17 @@ def baz():
         with self.makeTempFile(source) as sourcePath:
             if PYPY:
                 message = 'end of file (EOF) while scanning triple-quoted string literal'
+            elif sys.version_info >= (3, 10):
+                message = 'unterminated triple-quoted string literal (detected at line 8)'  # noqa: E501
             else:
                 message = 'invalid syntax'
 
-            column = 8 if sys.version_info >= (3, 8) else 11
+            if sys.version_info >= (3, 10):
+                column = 12
+            elif sys.version_info >= (3, 8):
+                column = 8
+            else:
+                column = 11
             self.assertHasErrors(
                 sourcePath,
                 ["""\
@@ -468,21 +475,25 @@ def baz():
         """
         with self.makeTempFile("def foo(") as sourcePath:
             if PYPY:
-                result = """\
-%s:1:7: parenthesis is never closed
-def foo(
-      ^
-""" % (sourcePath,)
+                msg = 'parenthesis is never closed'
+            elif sys.version_info >= (3, 10):
+                msg = "'(' was never closed"
             else:
-                result = """\
-%s:1:9: unexpected EOF while parsing
-def foo(
-        ^
-""" % (sourcePath,)
+                msg = 'unexpected EOF while parsing'
 
-            self.assertHasErrors(
-                sourcePath,
-                [result])
+            if PYPY:
+                column = 7
+            elif sys.version_info >= (3, 10):
+                column = 8
+            else:
+                column = 9
+
+            spaces = ' ' * (column - 1)
+            expected = '{}:1:{}: {}\ndef foo(\n{}^\n'.format(
+                sourcePath, column, msg, spaces
+            )
+
+            self.assertHasErrors(sourcePath, [expected])
 
     def test_eofSyntaxErrorWithTab(self):
         """
@@ -515,6 +526,8 @@ def foo(bar=baz, bax):
             if ERROR_HAS_LAST_LINE:
                 if PYPY:
                     column = 7
+                elif sys.version_info >= (3, 10):
+                    column = 18
                 elif sys.version_info >= (3, 9):
                     column = 21
                 elif sys.version_info >= (3, 8):
diff --git a/pyflakes/test/test_match.py b/pyflakes/test/test_match.py
new file mode 100644
index 0000000..89826e3
--- /dev/null
+++ b/pyflakes/test/test_match.py
@@ -0,0 +1,83 @@
+from sys import version_info
+
+from pyflakes.test.harness import TestCase, skipIf
+
+
+@skipIf(version_info < (3, 10), "Python >= 3.10 only")
+class TestMatch(TestCase):
+    def test_match_bindings(self):
+        self.flakes('''
+            def f():
+                x = 1
+                match x:
+                    case 1 as y:
+                        print(f'matched as {y}')
+        ''')
+        self.flakes('''
+            def f():
+                x = [1, 2, 3]
+                match x:
+                    case [1, y, 3]:
+                        print(f'matched {y}')
+        ''')
+        self.flakes('''
+            def f():
+                x = {'foo': 1}
+                match x:
+                    case {'foo': y}:
+                        print(f'matched {y}')
+        ''')
+
+    def test_match_pattern_matched_class(self):
+        self.flakes('''
+            from a import B
+
+            match 1:
+                case B(x=1) as y:
+                    print(f'matched {y}')
+        ''')
+        self.flakes('''
+            from a import B
+
+            match 1:
+                case B(a, x=z) as y:
+                    print(f'matched {y} {a} {z}')
+        ''')
+
+    def test_match_placeholder(self):
+        self.flakes('''
+            def f():
+                match 1:
+                    case _:
+                        print('catchall!')
+        ''')
+
+    def test_match_singleton(self):
+        self.flakes('''
+            match 1:
+                case True:
+                    print('true')
+        ''')
+
+    def test_match_or_pattern(self):
+        self.flakes('''
+            match 1:
+                case 1 | 2:
+                    print('one or two')
+        ''')
+
+    def test_match_star(self):
+        self.flakes('''
+            x = [1, 2, 3]
+            match x:
+                case [1, *y]:
+                    print(f'captured: {y}')
+        ''')
+
+    def test_match_double_star(self):
+        self.flakes('''
+            x = {'foo': 'bar', 'baz': 'womp'}
+            match x:
+                case {'foo': k1, **rest}:
+                    print(f'{k1=} {rest=}')
+        ''')
diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py
index f131034..f3b6c24 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -121,6 +121,23 @@ class TestTypeAnnotations(TestCase):
             def f(self, x): return x
         """)
 
+    def test_aliased_import(self):
+        """Detect when typing is imported as another name"""
+        self.flakes("""
+        import typing as t
+
+        @t.overload
+        def f(s):  # type: (None) -> None
+            pass
+
+        @t.overload
+        def f(s):  # type: (int) -> int
+            pass
+
+        def f(s):
+            return s
+        """)
+
     def test_not_a_typing_overload(self):
         """regression test for @typing.overload detection bug in 2.1.0"""
         self.flakes("""
@@ -531,7 +548,7 @@ class TestTypeAnnotations(TestCase):
     def test_type_cast_literal_str_to_str(self):
         # Checks that our handling of quoted type annotations in the first
         # argument to `cast` doesn't cause issues when (only) the _second_
-        # argument is a literal str which looks a bit like a type annoation.
+        # argument is a literal str which looks a bit like a type annotation.
         self.flakes("""
         from typing import cast