|
| 1 | +"""Regression tests for the Zig FunctionExtractor (u14). |
| 2 | +
|
| 3 | +Three confirmed extraction/metadata bugs, each reproduced through the REAL |
| 4 | +extractor (FunctionExtractor.extract()) on a temp .zig file, asserting on the |
| 5 | +emitted `functions` map. |
| 6 | +
|
| 7 | +- [BUG 16] fn-name extraction: a free fn whose return type is a bare named |
| 8 | + identifier (`fn makeWidget(v: i32) Widget { ... }`) is recorded |
| 9 | + under the RETURN-TYPE name (`Widget`) because the identifier loop |
| 10 | + overwrites `name` with the second `identifier` child (the return |
| 11 | + type). The real fn name must be the FIRST identifier. |
| 12 | +- [BUG 26] test classification: `pub fn testConnection() bool {}` is wrongly |
| 13 | + classified `test` because `_classify_function` matches the |
| 14 | + `startswith("test")` prefix. A plain function named testXxx is a |
| 15 | + regular function, not a zig `test "..." {}` block. |
| 16 | +- [BUG 37] struct/enum container methods: methods inside a |
| 17 | + `const Foo = struct { fn method() ... };` container are never |
| 18 | + extracted, because the walker keys on node types tree-sitter-zig |
| 19 | + never emits (`VarDecl`/`container_decl`). The real types are |
| 20 | + `variable_declaration` and `struct_declaration` / `enum_declaration` |
| 21 | + / `union_declaration` / `opaque_declaration`. |
| 22 | +""" |
| 23 | + |
| 24 | +import os |
| 25 | +import sys |
| 26 | +import tempfile |
| 27 | +from pathlib import Path |
| 28 | + |
| 29 | +_CORE_ROOT = Path(__file__).resolve().parents[3] |
| 30 | +sys.path.insert(0, str(_CORE_ROOT)) |
| 31 | + |
| 32 | +from parsers.zig.function_extractor import FunctionExtractor |
| 33 | + |
| 34 | + |
| 35 | +def _extract(src: str) -> dict: |
| 36 | + """Run the real extractor on a single zig source file; return extract() output.""" |
| 37 | + workdir = tempfile.mkdtemp() |
| 38 | + file_path = os.path.join(workdir, "m.zig") |
| 39 | + with open(file_path, "w") as fh: |
| 40 | + fh.write(src) |
| 41 | + scan_results = {"files": [{"path": "m.zig"}]} |
| 42 | + return FunctionExtractor(workdir, scan_results).extract() |
| 43 | + |
| 44 | + |
| 45 | +# --------------------------------------------------------------------------- |
| 46 | +# [BUG 16] fn name must be the first identifier, not the return-type identifier |
| 47 | +# --------------------------------------------------------------------------- |
| 48 | + |
| 49 | +def test_bug16_named_return_type_does_not_shadow_fn_name(): |
| 50 | + """`fn makeWidget(v: i32) Widget {}` must be recorded as makeWidget, not Widget.""" |
| 51 | + src = ( |
| 52 | + "const Widget = struct { x: i32 };\n\n" |
| 53 | + "pub fn makeWidget(v: i32) Widget {\n" |
| 54 | + " return Widget{ .x = v };\n" |
| 55 | + "}\n" |
| 56 | + ) |
| 57 | + out = _extract(src) |
| 58 | + funcs = out["functions"] |
| 59 | + assert "m.zig:makeWidget" in funcs, ( |
| 60 | + f"makeWidget missing; functions keys = {sorted(funcs)}" |
| 61 | + ) |
| 62 | + assert funcs["m.zig:makeWidget"]["name"] == "makeWidget" |
| 63 | + # The return-type identifier must NOT have become a phantom function. |
| 64 | + assert "m.zig:Widget" not in funcs, ( |
| 65 | + f"return-type Widget leaked as a function; keys = {sorted(funcs)}" |
| 66 | + ) |
| 67 | + |
| 68 | + |
| 69 | +def test_bug16_generic_named_return_type_build_T(): |
| 70 | + """Re-confirm the general case: `fn build() T` records build, not T.""" |
| 71 | + src = "fn build() T {\n return undefined;\n}\n" |
| 72 | + out = _extract(src) |
| 73 | + funcs = out["functions"] |
| 74 | + assert "m.zig:build" in funcs, f"build missing; keys = {sorted(funcs)}" |
| 75 | + assert funcs["m.zig:build"]["name"] == "build" |
| 76 | + assert "m.zig:T" not in funcs |
| 77 | + |
| 78 | + |
| 79 | +# --------------------------------------------------------------------------- |
| 80 | +# [BUG 26] a plain fn named testXxx must classify as 'function', not 'test' |
| 81 | +# --------------------------------------------------------------------------- |
| 82 | + |
| 83 | +def test_bug26_fn_named_testconnection_is_function_not_test(): |
| 84 | + """`pub fn testConnection() bool {}` must have unit_type 'function'.""" |
| 85 | + src = "pub fn testConnection() bool {\n return true;\n}\n" |
| 86 | + out = _extract(src) |
| 87 | + funcs = out["functions"] |
| 88 | + assert "m.zig:testConnection" in funcs, f"keys = {sorted(funcs)}" |
| 89 | + assert funcs["m.zig:testConnection"]["unit_type"] == "function", ( |
| 90 | + f"testConnection wrongly classified: " |
| 91 | + f"{funcs['m.zig:testConnection']['unit_type']!r}" |
| 92 | + ) |
| 93 | + |
| 94 | + |
| 95 | +# --------------------------------------------------------------------------- |
| 96 | +# [BUG 37] container methods (struct/enum/union/opaque) must be extracted |
| 97 | +# under the qualified Container.method name. |
| 98 | +# --------------------------------------------------------------------------- |
| 99 | + |
| 100 | +def test_bug37_struct_method_qualified_name_extracted(): |
| 101 | + """`const Foo = struct { fn method() ... };` must yield Foo.method.""" |
| 102 | + src = ( |
| 103 | + "pub fn ordinary() void {}\n\n" |
| 104 | + "const Foo = struct {\n" |
| 105 | + " pub fn method(self: Foo) void {}\n" |
| 106 | + "};\n" |
| 107 | + ) |
| 108 | + out = _extract(src) |
| 109 | + funcs = out["functions"] |
| 110 | + assert "m.zig:Foo.method" in funcs, ( |
| 111 | + f"Foo.method missing; keys = {sorted(funcs)}" |
| 112 | + ) |
| 113 | + info = funcs["m.zig:Foo.method"] |
| 114 | + assert info["qualified_name"] == "Foo.method" |
| 115 | + assert info["class_name"] == "Foo" |
| 116 | + assert info["unit_type"] == "method" |
| 117 | + # The struct itself should be recorded as a class. |
| 118 | + assert "m.zig:Foo" in out["classes"], f"classes = {sorted(out['classes'])}" |
| 119 | + |
| 120 | + |
| 121 | +def test_bug37_enum_method_extracted(): |
| 122 | + """`const E = enum { a, fn em() ... };` must yield E.em.""" |
| 123 | + src = "const E = enum {\n a,\n pub fn em() void {}\n};\n" |
| 124 | + out = _extract(src) |
| 125 | + funcs = out["functions"] |
| 126 | + assert "m.zig:E.em" in funcs, f"keys = {sorted(funcs)}" |
| 127 | + assert funcs["m.zig:E.em"]["class_name"] == "E" |
| 128 | + |
| 129 | + |
| 130 | +def test_bug37_union_method_extracted(): |
| 131 | + """`const U = union(enum) { a: u8, fn um() ... };` must yield U.um.""" |
| 132 | + src = "const U = union(enum) {\n a: u8,\n pub fn um() void {}\n};\n" |
| 133 | + out = _extract(src) |
| 134 | + funcs = out["functions"] |
| 135 | + assert "m.zig:U.um" in funcs, f"keys = {sorted(funcs)}" |
| 136 | + assert funcs["m.zig:U.um"]["class_name"] == "U" |
| 137 | + |
| 138 | + |
| 139 | +def test_bug37_opaque_method_extracted(): |
| 140 | + """`const O = opaque { fn om() ... };` must yield O.om.""" |
| 141 | + src = "const O = opaque {\n pub fn om() void {}\n};\n" |
| 142 | + out = _extract(src) |
| 143 | + funcs = out["functions"] |
| 144 | + assert "m.zig:O.om" in funcs, f"keys = {sorted(funcs)}" |
| 145 | + assert funcs["m.zig:O.om"]["class_name"] == "O" |
0 commit comments