From caa3310714e9081ed6ce6acbf2f097f47b7e6bb5 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 16 Mar 2026 22:06:27 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=204=20=E2=80=94=20known=20limitat?= =?UTF-8?q?ion=20tests,=20match=20arm=20fix,=20final=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Rust match_arm pattern extraction: unwrap match_pattern to get tuple_struct_pattern inside (tree-sitter-rust wraps in match_pattern node) - Add first-writer-wins regression test for match arm scope leakage - Add 5 documented skip tests for known limitations: - TS destructured for-of (tuple destructuring) - Python tuple unpacking in for-loops - TS instanceof narrowing (block-level scoping) - Rust for with .iter() (method call iterable) - Ruby block parameters (closure param inference) Final: 238 passed, 5 skipped (documented limitations), tsc clean --- .../core/ingestion/type-extractors/rust.ts | 8 +++- gitnexus/test/unit/type-env.test.ts | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/gitnexus/src/core/ingestion/type-extractors/rust.ts b/gitnexus/src/core/ingestion/type-extractors/rust.ts index 71f869342..067bcc750 100644 --- a/gitnexus/src/core/ingestion/type-extractors/rust.ts +++ b/gitnexus/src/core/ingestion/type-extractors/rust.ts @@ -222,9 +222,13 @@ const extractPatternBinding: PatternBindingExtractor = ( patternNode = node.childForFieldName('pattern'); valueNode = node.childForFieldName('value'); } else if (node.type === 'match_arm') { - // match_arm → pattern is in the 'pattern' field + // match_arm → pattern field is match_pattern wrapping the actual pattern + const matchPatternNode = node.childForFieldName('pattern'); + // Unwrap match_pattern to get the tuple_struct_pattern inside + patternNode = matchPatternNode?.type === 'match_pattern' + ? matchPatternNode.firstNamedChild + : matchPatternNode; // source variable is in the parent match_expression's 'value' field - patternNode = node.childForFieldName('pattern'); const matchExpr = node.parent?.parent; // match_arm → match_block → match_expression if (matchExpr?.type === 'match_expression') { valueNode = matchExpr.childForFieldName('value'); diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index 7e1056e41..67390e8be 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -2927,4 +2927,43 @@ class Foo { expect(flatGet(env, 'user')).toBeUndefined(); }); }); + + describe('known limitations (documented skip tests)', () => { + it.skip('TS destructured for-of: for (const [k, v] of entries) — requires tuple destructuring', () => { + // extractVarName returns undefined for array_pattern + }); + + it.skip('Python tuple unpacking: for key, value in dict.items() — requires pattern_list', () => { + // left is pattern_list, not identifier + }); + + it.skip('TS instanceof narrowing: if (x instanceof User) { x.save() } — needs block-level scoping', () => { + // Narrows existing variable, does not introduce new one + }); + + it.skip('Rust for with .iter(): for user in users.iter() — needs method call iterable resolution', () => { + // Iterable is a call_expression, not an identifier + }); + + it.skip('Ruby block parameter: users.each { |user| } — closure param inference, different feature', () => { + // Not a for-loop; requires block/closure parameter inference + }); + }); + + describe('match arm scoping — first-writer-wins regression', () => { + it('Rust: first match arm binding wins, later arms do not overwrite', () => { + const tree = parse(` +fn process(opt: Option) { + match opt { + Some(user) => user.save(), + None => {}, + } +} + `, Rust); + const { env } = buildTypeEnv(tree, 'rust'); + // user should be typed from the first arm (Some unwrap) + // Known limitation: binding leaks across arms (first-writer-wins) + expect(flatGet(env, 'user')).toBe('User'); + }); + }); });