diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8f8021a..7ac41fbb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,11 @@ uv run pytest tests/test_parser.py -v CI runs the suite on Python 3.10, 3.11, 3.12 and 3.13 and fails below 65% coverage. +`tests/test_parser_grammar_contract.py` checks every built-in class, function, import, +and call dispatch name against its bundled Tree-sitter grammar. When updating a grammar +or adding dispatch entries, use the grammar's actual named node types. This contract +checks configured dispatch names; language fixtures still verify extraction behavior. + ## Linting and Type Checking ```bash diff --git a/code_review_graph/parser.py b/code_review_graph/parser.py index 1a390c76..2050d544 100644 --- a/code_review_graph/parser.py +++ b/code_review_graph/parser.py @@ -1173,13 +1173,13 @@ def __post_init__(self) -> None: "csharp": [ "class_declaration", "interface_declaration", "enum_declaration", "struct_declaration", - "record_declaration", "record_struct_declaration", + "record_declaration", ], "ruby": ["class", "module"], "r": [], # Classes detected via call pattern-matching, not AST node types "perl": ["package_statement", "class_statement", "role_statement"], "kotlin": ["class_declaration", "object_declaration"], - "swift": ["class_declaration", "struct_declaration", "protocol_declaration"], + "swift": ["class_declaration", "protocol_declaration"], "php": [ "class_declaration", "interface_declaration", "trait_declaration", "enum_declaration", @@ -1197,7 +1197,7 @@ def __post_init__(self) -> None: "luau": ["type_definition"], # Luau type aliases; table-based OOP via constructs handler "objc": [ "class_interface", "class_implementation", - "category_interface", "protocol_declaration", + "protocol_declaration", ], "bash": [], # Shell has no classes # Elixir: `defmodule Name do ... end` is a ``call`` node whose first @@ -1393,7 +1393,7 @@ def __post_init__(self) -> None: "c": ["call_expression"], "cpp": ["call_expression"], "csharp": ["invocation_expression", "object_creation_expression"], - "ruby": ["call", "method_call"], + "ruby": ["call"], "r": ["call"], "perl": [ "function_call_expression", "method_call_expression", @@ -1429,7 +1429,7 @@ def __post_init__(self) -> None: # SuffixExpr); calls are walked explicitly in # _extract_zig_calls_in_subtree from inside function bodies. "zig": [], - "powershell": ["command_expression"], + "powershell": ["command"], "julia": [ "call_expression", "broadcast_call_expression", diff --git a/tests/test_parser_grammar_contract.py b/tests/test_parser_grammar_contract.py new file mode 100644 index 00000000..86b508ee --- /dev/null +++ b/tests/test_parser_grammar_contract.py @@ -0,0 +1,25 @@ +"""Grammar dispatch names must exist in the bundled grammar (issue #986).""" + +import pytest +from tree_sitter_language_pack import get_language + +from code_review_graph.parser import _CALL_TYPES, _CLASS_TYPES, _FUNCTION_TYPES, _IMPORT_TYPES + + +@pytest.mark.parametrize( + ("table", "language", "node_type"), + [ + (table, language, node_type) + for table, mapping in ( + ("classes", _CLASS_TYPES), ("functions", _FUNCTION_TYPES), + ("imports", _IMPORT_TYPES), ("calls", _CALL_TYPES), + ) + for language, names in mapping.items() + for node_type in names + ], +) +def test_dispatch_type_exists_in_grammar(table, language, node_type): + grammar = get_language(language) + assert grammar.id_for_node_kind(node_type, True) is not None, ( + f"{table}[{language!r}] contains nonexistent named node {node_type!r}" + )