From eb50569bed508b71e1f18d2f2f516fec050f441b Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 31 May 2026 23:45:35 +0200 Subject: [PATCH] fix(extract): capture arrow methods returned from JS/TS factory functions walk_defs stopped descending into a function body once the function itself was extracted, so arrow methods of an object literal returned from a factory (the Zustand actions-slice pattern) were never visited. Let JS/TS/TSX descend into function bodies (as Wolfram already did) and resolve an arrow's name from a 'pair' parent's key, so { addItem: (...) => {...} } is captured as a Function. Anonymous nested arrows have no resolvable name and are skipped, so no noise. Closes #341 --- internal/cbm/extract_defs.c | 32 ++++++++++++++++++++++++++++---- tests/test_extraction.c | 22 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 9884b4afe..7a877ea1b 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -546,16 +546,26 @@ static TSNode find_cpp_template_inner_node(TSNode node, CBMLanguage lang) { return null_node; } -// Try arrow_function name via parent variable_declarator (top-level resolution). +// Try arrow_function name via parent variable_declarator (top-level resolution) +// or object-literal property (`pair` key) — the latter covers factory functions +// that return an object of arrow methods (the Zustand actions-slice pattern, #341). static TSNode resolve_toplevel_arrow_name(TSNode node, const char *kind) { if (strcmp(kind, "arrow_function") != 0) { TSNode null_node = {0}; return null_node; } TSNode parent = ts_node_parent(node); - if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declarator") == 0) { + if (ts_node_is_null(parent)) { + TSNode null_node = {0}; + return null_node; + } + const char *pk = ts_node_type(parent); + if (strcmp(pk, "variable_declarator") == 0) { return ts_node_child_by_field_name(parent, TS_FIELD("name")); } + if (strcmp(pk, "pair") == 0) { + return ts_node_child_by_field_name(parent, TS_FIELD("key")); + } TSNode null_node = {0}; return null_node; } @@ -2113,7 +2123,9 @@ static TSNode resolve_dart_method_name(TSNode child, const char *ck) { return null_node; } -// Arrow function: name on parent variable_declarator/field_definition. +// Arrow function: name on parent variable_declarator/field_definition, or the +// key of an object-literal property — the Zustand "actions returned from a +// factory" pattern, `{ addItem: (...) => {...} }` (#341). static TSNode resolve_arrow_func_name(TSNode child) { TSNode parent = ts_node_parent(child); if (!ts_node_is_null(parent)) { @@ -2124,6 +2136,10 @@ static TSNode resolve_arrow_func_name(TSNode child) { if (strcmp(pk, "public_field_definition") == 0 || strcmp(pk, "variable_declarator") == 0) { return ts_node_child_by_field_name(parent, TS_FIELD("name")); } + if (strcmp(pk, "pair") == 0) { + // Object-literal property `key: () => {...}` → name is the key. + return ts_node_child_by_field_name(parent, TS_FIELD("key")); + } } TSNode null_node = {0}; return null_node; @@ -3581,7 +3597,15 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec, if (cbm_kind_in_set(node, spec->function_node_types)) { if (!is_template_class_node(node, ctx->language)) { extract_func_def(ctx, node, spec); - if (ctx->language != CBM_LANG_WOLFRAM) { + // Most languages stop here. JS/TS (and Wolfram) descend into the + // function body so NESTED named definitions are also captured — + // e.g. arrow methods of an object literal returned from a factory + // (the Zustand actions-slice pattern, #341). Anonymous nested + // arrows have no resolvable name and are skipped. + bool descend_into_func = + (ctx->language == CBM_LANG_WOLFRAM || ctx->language == CBM_LANG_TYPESCRIPT || + ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TSX); + if (!descend_into_func) { continue; } } diff --git a/tests/test_extraction.c b/tests/test_extraction.c index fd1107b3d..4a3a65808 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -99,6 +99,27 @@ TEST(extract_r_dollar_call_issue219) { PASS(); } +/* --- TS: object-literal arrow methods from a factory (Zustand, #341) --- */ +TEST(extract_ts_factory_object_methods_issue341) { + CBMFileResult *r = extract("export function createItemActions(set, get) {\n" + " return {\n" + " addItem: (type, id) => { return 1; },\n" + " moveItem: (id, target) => { return 2; },\n" + " deleteItem: (id) => { return 3; },\n" + " };\n" + "}\n", + CBM_LANG_TYPESCRIPT, "t", "item-actions.ts"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + /* The factory itself + each returned arrow method are Function nodes. */ + ASSERT(has_def_any(r, "createItemActions")); + ASSERT(has_def_any(r, "addItem")); + ASSERT(has_def_any(r, "moveItem")); + ASSERT(has_def_any(r, "deleteItem")); + cbm_free_result(r); + PASS(); +} + /* --- Java --- */ TEST(java_class) { CBMFileResult *r = extract( @@ -2363,6 +2384,7 @@ SUITE(extraction) { /* R box-module imports + member calls */ RUN_TEST(extract_r_box_use_imports_issue218); RUN_TEST(extract_r_dollar_call_issue219); + RUN_TEST(extract_ts_factory_object_methods_issue341); /* OOP */ RUN_TEST(java_class);