| 1867 | } |
| 1868 | |
| 1869 | void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { |
| 1870 | if (!spec->call_node_types || !spec->call_node_types[0]) { |
| 1871 | return; |
| 1872 | } |
| 1873 | |
| 1874 | if (cbm_kind_in_set(node, spec->call_node_types)) { |
| 1875 | char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language); |
| 1876 | // Keyword-filter callees, but keep builtins we mint a node for (len, str, |
| 1877 | // ...) so the LSP-resolved builtin call still forms a CALLS edge. |
| 1878 | if (callee && callee[0] && |
| 1879 | (!cbm_is_keyword(callee, ctx->language) || |
| 1880 | cbm_is_resolvable_builtin(callee, ctx->language))) { |
| 1881 | CBMCall call = {0}; |
| 1882 | call.callee_name = callee; |
| 1883 | call.enclosing_func_qn = state->enclosing_func_qn; |
| 1884 | call.loop_depth = state->loop_depth; // enclosing loop nesting at this call |
| 1885 | call.branch_depth = state->branch_depth; // enclosing branch nesting at this call |
| 1886 | call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET; |
| 1887 | // Perl-only: flag arrow/method calls ($obj->m / Class->m). The |
| 1888 | // generic short-name resolver cannot place a method without a known |
| 1889 | // receiver type, so the call-resolution pass suppresses those edges. |
| 1890 | // Default false for every other language (struct is zero-init). |
| 1891 | if (ctx->language == CBM_LANG_PERL && |
| 1892 | strcmp(ts_node_type(node), "method_call_expression") == 0) { |
| 1893 | call.is_method = true; |
| 1894 | } |
| 1895 | // TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent |
| 1896 | // as the Perl flag above). Flag a member call x.foo() whose receiver |
| 1897 | // is NOT `this`/`super`. When the TS-LSP cannot resolve the receiver |
| 1898 | // type, the call-resolution pass suppresses weak short-name matches |
| 1899 | // for these (so `re.test()` cannot fabricate an edge to a project |
| 1900 | // `test`). this/super receivers stay unflagged — their target is the |
| 1901 | // enclosing class, where a namespace-proximity weak match is usually |
| 1902 | // right. Bare calls (helper()) and new_expression have no member |
| 1903 | // receiver, so they keep is_method=false (struct is zero-init). |
| 1904 | if ((ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TYPESCRIPT || |
| 1905 | ctx->language == CBM_LANG_TSX) && |
| 1906 | strcmp(ts_node_type(node), "call_expression") == 0) { |
| 1907 | TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function")); |
| 1908 | if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "member_expression") == 0) { |
| 1909 | TSNode obj = ts_node_child_by_field_name(fn, TS_FIELD("object")); |
| 1910 | if (!ts_node_is_null(obj)) { |
| 1911 | const char *ok = ts_node_type(obj); |
| 1912 | if (strcmp(ok, "this") != 0 && strcmp(ok, "super") != 0) { |
| 1913 | call.is_method = true; |
| 1914 | } |
| 1915 | } |
| 1916 | } |
| 1917 | } |
| 1918 | |
| 1919 | TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); |
| 1920 | if (!ts_node_is_null(args)) { |
| 1921 | call.first_string_arg = extract_url_or_topic_arg(ctx, args); |
| 1922 | if (call.first_string_arg && call.first_string_arg[0] == '/') { |
| 1923 | call.second_arg_name = extract_handler_arg(ctx, args); |
| 1924 | } |
| 1925 | extract_call_args(ctx, args, &call); |
| 1926 | } |
no test coverage detected