Repository: Instagram/LibCST

Issue:
CST node validation is broken for parenthesized MatchAs objects
Simple reproducer:
```py
match x:
    case (1) as z:
        pass
```

```
thread '<unnamed>' panicked at 'conversion failed for MatchValue: PyErr { type: <class 'libcst._nodes.base.CSTValidationError'>, value: CSTValidationError('Cannot have left paren without right paren.'), traceback: Some(<traceback object at 0x7f533bb89340>) }', libcst/src/nodes/statement.rs:2338:35
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Traceback (most recent call last):
  File "/usr/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/home/isidentical/projects/LibCST/libcst/tool.py", line 839, in <module>
    main(os.environ.get("LIBCST_TOOL_COMMAND_NAME", "libcst.tool"), sys.argv[1:])
  File "/home/isidentical/projects/LibCST/libcst/tool.py", line 834, in main
    return lookup.get(args.action or None, _invalid_command)(proc_name, command_args)
  File "/home/isidentical/projects/LibCST/libcst/tool.py", line 278, in _print_tree_impl
    tree = parse_module(
  File "/home/isidentical/projects/LibCST/libcst/_parser/entrypoints.py", line 109, in parse_module
    result = _parse(
  File "/home/isidentical/projects/LibCST/libcst/_parser/entrypoints.py", line 55, in _parse
    return parse(source_str)
pyo3_runtime.PanicException: conversion failed for MatchValue: PyErr { type: <class 'libcst._nodes.base.CSTValidationError'>, value: CSTValidationError('Cannot have left paren without right paren.'), traceback: Some(<traceback object at 0x7f533bb89340>) }
```

Comments:

Patch:
diff --git a/libcst/_nodes/statement.py b/libcst/_nodes/statement.py
index ded7c7c..e4b8d28 100644
--- a/libcst/_nodes/statement.py
+++ b/libcst/_nodes/statement.py
@@ -2687,11 +2687,6 @@ class Match(BaseCompoundStatement):
         if len(self.cases) == 0:
             raise CSTValidationError("A match statement must have at least one case.")
 
-        if self.whitespace_after_match.empty:
-            raise CSTValidationError(
-                "Must have at least one space after a 'match' keyword"
-            )
-
         indent = self.indent
         if indent is not None:
             if len(indent) == 0:
@@ -2848,6 +2843,14 @@ class MatchValue(MatchPattern):
     def lpar(self, value: Sequence[LeftParen]) -> None:
         self.value.lpar = value
 
+    @property
+    def rpar(self) -> Sequence[RightParen]:
+        return self.value.rpar
+
+    @rpar.setter
+    def rpar(self, value: Sequence[RightParen]) -> None:
+        self.value.rpar = value
+
 
 @add_slots
 @dataclass(frozen=True)
@@ -2881,6 +2884,15 @@ class MatchSingleton(MatchPattern):
         # pyre-fixme[41]: Cannot reassign final attribute `lpar`.
         self.value.lpar = value
 
+    @property
+    def rpar(self) -> Sequence[RightParen]:
+        return self.value.rpar
+
+    @rpar.setter
+    def rpar(self, value: Sequence[RightParen]) -> None:
+        # pyre-fixme[41]: Cannot reassign final attribute `rpar`.
+        self.value.rpar = value
+
 
 @add_slots
 @dataclass(frozen=True)


Test patch:
diff --git a/libcst/_nodes/tests/test_match.py b/libcst/_nodes/tests/test_match.py
index edf51d8..a203ffe 100644
--- a/libcst/_nodes/tests/test_match.py
+++ b/libcst/_nodes/tests/test_match.py
@@ -39,6 +39,39 @@ class MatchTest(CSTNodeTest):
                 + '    case "foo": pass\n',
                 "parser": parser,
             },
+            # Parenthesized value
+            {
+                "node": cst.Match(
+                    subject=cst.Name(
+                        value="x",
+                    ),
+                    cases=[
+                        cst.MatchCase(
+                            pattern=cst.MatchAs(
+                                pattern=cst.MatchValue(
+                                    value=cst.Integer(
+                                        value="1",
+                                        lpar=[
+                                            cst.LeftParen(),
+                                        ],
+                                        rpar=[
+                                            cst.RightParen(),
+                                        ],
+                                    ),
+                                ),
+                                name=cst.Name(
+                                    value="z",
+                                ),
+                                whitespace_before_as=cst.SimpleWhitespace(" "),
+                                whitespace_after_as=cst.SimpleWhitespace(" "),
+                            ),
+                            body=cst.SimpleStatementSuite([cst.Pass()]),
+                        ),
+                    ],
+                ),
+                "code": "match x:\n    case (1) as z: pass\n",
+                "parser": parser,
+            },
             # List patterns
             {
                 "node": cst.Match(
@@ -425,6 +458,34 @@ class MatchTest(CSTNodeTest):
                 + "    case None | False | True: pass\n",
                 "parser": None,
             },
+            # Match without whitespace between keyword and the expr
+            {
+                "node": cst.Match(
+                    subject=cst.Name(
+                        "x", lpar=[cst.LeftParen()], rpar=[cst.RightParen()]
+                    ),
+                    cases=[
+                        cst.MatchCase(
+                            pattern=cst.MatchSingleton(
+                                cst.Name(
+                                    "None",
+                                    lpar=[cst.LeftParen()],
+                                    rpar=[cst.RightParen()],
+                                )
+                            ),
+                            body=cst.SimpleStatementSuite((cst.Pass(),)),
+                            whitespace_after_case=cst.SimpleWhitespace(
+                                value="",
+                            ),
+                        ),
+                    ],
+                    whitespace_after_match=cst.SimpleWhitespace(
+                        value="",
+                    ),
+                ),
+                "code": "match(x):\n    case(None): pass\n",
+                "parser": parser,
+            },
         )
     )
     def test_valid(self, **kwargs: Any) -> None: