Repository: psf/black

Issue:
Black 23.7.0 thinks accessing an attribute named `type` inside brackets in a switch statement is invalid syntax
<!--
Please make sure that the bug is not already fixed either in newer versions or the
current development version. To confirm this, you have three options:

1. Update Black's version if a newer release exists: `pip install -U black`
2. Use the online formatter at <https://black.vercel.app/?version=main>, which will use
   the latest main branch.
3. Or run _Black_ on your machine:
   - create a new virtualenv (make sure it's the same Python version);
   - clone this repository;
   - run `pip install -e .[d]`;
   - run `pip install -r test_requirements.txt`
   - make sure it's sane by running `python -m pytest`; and
   - run `black` like you did last time.
-->

**Describe the bug**

When black encounters access of a `.type` attribute inside brackets, inside a switch statement, it thinks that it is invalid syntax, even though python 3.11 executes it just fine.

**To Reproduce**

<!--
Minimal steps to reproduce the behavior with source code and Black's configuration.
-->

For example, take this code:

```python
class SomeClass:
    type = ""


match (SomeClass.type):
    case _:
        print("Hello there ...")
```

And run it with these arguments:

```sh
$ pipx run --python python3.11 --spec=black==23.7.0 black --target-version py311 --diff file.py
```

The resulting error is:

> error: cannot format file.py: Cannot parse: 5:0: match (SomeClass.type):

**Expected behavior**

It should leave the file unchanged or format it, as black 23.3.0 leaves it unchanged, and it is indeed valid syntax:

```console
$ python3.11 --version
Python 3.11.4
$ python3.11 file.py
Hello there ...
$ pipx run --python python3.11 --spec=black==23.3.0 black --target-version py311 --diff file.py
⚠️  black is already on your PATH and installed at /home/iwana/.local/bin/black. Downloading and running anyway.
All done! ✨ 🍰 ✨
1 file would be left unchanged.
```


**Environment**

<!-- Please complete the following information: -->

- Black's version: 23.3.0
- OS and Python version: Linux/Python 3.11.4



Discussion:
Thanks, cc @isidentical as this seems to be an interaction with the match-case support you added. I'll take a look too when I get a chance.
In case it's helpful, I hit the same error in a slightly different way, which leads to a much more confusing error message, since the parser fails on the line after the `match` keyword rather than the line containing the `match` keyword itself. Here's an example case and its corresponding error message:

```py
class Foo:
    type = "Foo"


foo = Foo()
bar = Foo()

match (foo.type, bar.type):
    case _:
        pass
```

Black's output:

```
error: cannot format blacktest.py: Cannot parse: 9:0:     case _:

Oh no! 💥 💔 💥
1 file failed to reformat.
```

Changing the attribute name to something other than `type` avoids the issue, as does assigning the match argument tuple to a variable before the `match` statement.
Hello, apparently I hit exactly the same bug
```python
from collections import namedtuple
from typing import Any


class AAA:
    def do_xxx(
        self: Any,
    ):
        # Custom modules
        Foo = namedtuple("Foo", "a,b,type")
        foo0 = Foo(0, 1, 1)
        foo = Foo(0,1,1)
        match (foo.a, foo.type):
            case (foo0.a, foo0.b):
                return
            case _:
                raise ValueError("still rejected")
```
```
error: cannot format black_minimal_example.py: Cannot parse: 13:8:         match (foo.a, foo.type):

Oh no! 💥 💔 💥
1 file failed to reformat.
```
Applying this change:
```diff
-        match (foo.a, foo.type):
+        match (foo.a, foo.b):
```
makes black works again.

It’s working in black `23.3.0`, but not with `23.9.0`

Resolving patch:
diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py
--- a/src/blib2to3/pgen2/parse.py
+++ b/src/blib2to3/pgen2/parse.py
@@ -211,6 +211,7 @@ def __init__(self, grammar: Grammar, convert: Optional[Convert] = None) -> None:
         # See note in docstring above. TL;DR this is ignored.
         self.convert = convert or lam_sub
         self.is_backtracking = False
+        self.last_token: Optional[int] = None
 
     def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None:
         """Prepare for parsing.
@@ -236,6 +237,7 @@ def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None:
         self.rootnode: Optional[NL] = None
         self.used_names: Set[str] = set()
         self.proxy = proxy
+        self.last_token = None
 
     def addtoken(self, type: int, value: str, context: Context) -> bool:
         """Add a token; return True iff this is the end of the program."""
@@ -317,6 +319,7 @@ def _addtoken(self, ilabel: int, type: int, value: str, context: Context) -> boo
                         dfa, state, node = self.stack[-1]
                         states, first = dfa
                     # Done with this token
+                    self.last_token = type
                     return False
 
             else:
@@ -343,9 +346,23 @@ def classify(self, type: int, value: str, context: Context) -> List[int]:
                 return [self.grammar.keywords[value]]
             elif value in self.grammar.soft_keywords:
                 assert type in self.grammar.tokens
+                # Current soft keywords (match, case, type) can only appear at the
+                # beginning of a statement. So as a shortcut, don't try to treat them
+                # like keywords in any other context.
+                # ('_' is also a soft keyword in the real grammar, but for our grammar
+                # it's just an expression, so we don't need to treat it specially.)
+                if self.last_token not in (
+                    None,
+                    token.INDENT,
+                    token.DEDENT,
+                    token.NEWLINE,
+                    token.SEMI,
+                    token.COLON,
+                ):
+                    return [self.grammar.tokens[type]]
                 return [
-                    self.grammar.soft_keywords[value],
                     self.grammar.tokens[type],
+                    self.grammar.soft_keywords[value],
                 ]
 
         ilabel = self.grammar.tokens.get(type)


Test patch:
diff --git a/tests/data/cases/pattern_matching_complex.py b/tests/data/cases/pattern_matching_complex.py
--- a/tests/data/cases/pattern_matching_complex.py
+++ b/tests/data/cases/pattern_matching_complex.py
@@ -143,3 +143,7 @@
         y = 1
     case []:
         y = 2
+# issue 3790
+match (X.type, Y):
+    case _:
+        pass
diff --git a/tests/data/cases/type_aliases.py b/tests/data/cases/type_aliases.py
--- a/tests/data/cases/type_aliases.py
+++ b/tests/data/cases/type_aliases.py
@@ -5,6 +5,8 @@
 type Alias[T]=lambda: T
 type And[T]=T and T
 type IfElse[T]=T if T else T
+type One = int; type Another = str
+class X: type InClass = int
 
 type = aliased
 print(type(42))
@@ -16,6 +18,13 @@
 type Alias[T] = lambda: T
 type And[T] = T and T
 type IfElse[T] = T if T else T
+type One = int
+type Another = str
+
+
+class X:
+    type InClass = int
+
 
 type = aliased
 print(type(42))