diff --git a/lint-budget.json b/lint-budget.json index c5af3aeb..99137eae 100644 --- a/lint-budget.json +++ b/lint-budget.json @@ -78,7 +78,7 @@ "RUF013": 2, "RUF015": 2, "RUF059": 2, - "S101": 203, + "S101": 204, "S110": 4, "S603": 5, "S608": 1, diff --git a/src/renaissance/integrations/python/ast/kinds.py b/src/renaissance/integrations/python/ast/kinds.py index 53135c9b..357db1bf 100644 --- a/src/renaissance/integrations/python/ast/kinds.py +++ b/src/renaissance/integrations/python/ast/kinds.py @@ -70,6 +70,7 @@ "Try": SemanticKind.STATEMENT, "TryStar": SemanticKind.STATEMENT, "Delete": SemanticKind.STATEMENT, + "TypeAlias": SemanticKind.STATEMENT, "Expr": SemanticKind.EXPRESSION, "Await": SemanticKind.EXPRESSION, "Yield": SemanticKind.EXPRESSION, diff --git a/test/python/ast/test_python_protocol_metadata.py b/test/python/ast/test_python_protocol_metadata.py index d70575cd..7819a09b 100644 --- a/test/python/ast/test_python_protocol_metadata.py +++ b/test/python/ast/test_python_protocol_metadata.py @@ -4,11 +4,32 @@ from renaissance.integrations.python.ast.cst_node import PythonCstNode from renaissance.integrations.python.ast.factory import PythonFactory +from renaissance.integrations.python.ast.kinds import PYTHON_KIND_MAP from renaissance.integrations.python.ast.rst_node import PythonRstNode from renaissance.syntax_tree.ast_rewriter import Rewritable from renaissance.syntax_tree.semantic_kind import SemanticKind +def _all_subclasses(base: type) -> list[type]: + """Collect every subclass of base, recursively.""" + found: list[type] = [] + for subclass in base.__subclasses__(): + found.append(subclass) + found.extend(_all_subclasses(subclass)) + return found + + +def test_every_python_statement_type_is_classified() -> None: + """Assert no ast statement type falls through to NODE, so a new Python grammar cannot add one unnoticed.""" + unclassified = sorted( + statement.__name__ + for statement in _all_subclasses(ast.stmt) + if PYTHON_KIND_MAP.get(statement.__name__, SemanticKind.NODE) is SemanticKind.NODE + ) + + assert not unclassified, f"PYTHON_KIND_MAP has no entry for {unclassified}" + + def test_python_nodes_satisfy_rewritable() -> None: """AI: Assert Python CST and RST nodes satisfy Rewritable; the annotations also gate this statically.""" cst: Rewritable = PythonCstNode.load_from_text("f(1)") diff --git a/test/python/ast/test_python_rst_node.py b/test/python/ast/test_python_rst_node.py index 12ec59c2..fd65b88e 100644 --- a/test/python/ast/test_python_rst_node.py +++ b/test/python/ast/test_python_rst_node.py @@ -208,6 +208,18 @@ def test_corner_case(self) -> None: node = factory.create_from_text("class ŻP𭻊鲖ÉØ_ąň𣑗: pass\n") assert_that(node.children[0].semantic_kind is not SemanticKind.NODE, is_(True)) + @pytest.mark.parametrize( + "code", + [ + "type Alias = int\n", + "type Alias[T] = list[T]\n", + ], + ) + def test_type_alias_statement_is_classified(self, code: str) -> None: + """Verify a PEP 695 type alias statement is classified as a statement rather than an unclassified node.""" + node = PythonRstNode.load_from_text(code) + assert_that(node[0].semantic_kind, is_(SemanticKind.STATEMENT)) + @pytest.mark.parametrize( "code, expected_kind", [