2135 lines
92 KiB
C
2135 lines
92 KiB
C
#include "cbm.h"
|
|
#include "arena.h" // CBMArena, cbm_arena_sprintf
|
|
#include "helpers.h"
|
|
#include "lang_specs.h"
|
|
#include "extract_unified.h"
|
|
#include "foundation/constants.h"
|
|
#include "extract_node_stack.h"
|
|
#include "service_patterns.h" // cbm_service_pattern_route_method (#952)
|
|
#include "tree_sitter/api.h" // TSNode, ts_node_*
|
|
#include <stdint.h> // uint32_t
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
/* Max ancestor depth for Lean type-position check. */
|
|
enum { LEAN_MAX_PARENT_DEPTH = 20 };
|
|
/* Max positional args to scan for URL/string. */
|
|
enum { MAX_POSITIONAL_SCAN = 3 };
|
|
/* Max positional args to scan for handler ref. */
|
|
enum { MAX_HANDLER_SCAN = 4 };
|
|
/* Max string arg length before rejection. */
|
|
enum { MAX_STRING_ARG_LEN = CBM_SZ_512 };
|
|
/* Min printable ASCII (space). */
|
|
enum { MIN_PRINTABLE = 0x20 };
|
|
/* Handler arg scan start index (skip first positional). */
|
|
enum { HANDLER_START_IDX = 1 };
|
|
|
|
/* Look up a module-level string constant by name. */
|
|
static const char *lookup_string_constant(const CBMExtractCtx *ctx, const char *name) {
|
|
if (!name || !name[0]) {
|
|
return NULL;
|
|
}
|
|
const CBMStringConstantMap *map = &ctx->string_constants;
|
|
for (int i = 0; i < map->count; i++) {
|
|
if (strcmp(map->names[i], name) == 0) {
|
|
return map->values[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Check if a node type is a string literal */
|
|
static int is_string_like(const char *kind) {
|
|
return (strcmp(kind, "string") == 0 || strcmp(kind, "string_literal") == 0 ||
|
|
strcmp(kind, "interpreted_string_literal") == 0 ||
|
|
strcmp(kind, "raw_string_literal") == 0 || strcmp(kind, "string_content") == 0);
|
|
}
|
|
|
|
/* Strip surrounding quotes from a string, return arena-allocated copy */
|
|
static const char *strip_quotes(CBMArena *a, const char *text) {
|
|
if (!text || !text[0]) {
|
|
return NULL;
|
|
}
|
|
int len = (int)strlen(text);
|
|
if (len >= CBM_QUOTE_PAIR && (text[0] == '"' || text[0] == '\'')) {
|
|
return cbm_arena_strndup(a, text + CBM_QUOTE_OFFSET, (size_t)(len - CBM_QUOTE_PAIR));
|
|
}
|
|
return text;
|
|
}
|
|
|
|
// Callee suffixes for IRIS Python interop string-dispatch. Kept at file scope
|
|
// (not inside the function) to satisfy cppcheck variableScope.
|
|
static const char *s_py_dispatch_suffixes[] = {".classMethodValue", ".classMethodVoid",
|
|
".classMethodBoolean", ".classMethodObject", NULL};
|
|
|
|
// Per-language callee-suffix dispatch table — returns a NULL-terminated list of
|
|
// method-name suffixes whose calls should be resolved by extracting class+method
|
|
// from the first two string arguments (e.g. IRIS Python interop). Kept here
|
|
// rather than in CBMLangSpec to avoid -Wmissing-field-initializers across 155
|
|
// language rows.
|
|
const char **cbm_string_dispatch_suffixes(CBMLanguage lang) {
|
|
if (lang == CBM_LANG_PYTHON) {
|
|
return s_py_dispatch_suffixes;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Forward declarations
|
|
static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, CBMLanguage lang);
|
|
static char *gotemplate_callee(CBMArena *a, TSNode node, const char *source);
|
|
static const char *strip_and_validate_string_arg(CBMArena *a, char *text);
|
|
|
|
// Lean 4: check if an apply node is inside a type annotation.
|
|
// Strategy: walk up to the nearest declaration boundary; if the apply falls
|
|
// inside that declaration's explicit_binder/implicit_binder, or before the
|
|
// body field, it's a type annotation. We check byte ranges: a call is valid
|
|
// only if it overlaps the body range of the enclosing declaration.
|
|
static bool lean_is_in_type_position(TSNode node) {
|
|
TSNode cur = ts_node_parent(node);
|
|
for (int depth = 0; depth < LEAN_MAX_PARENT_DEPTH; depth++) {
|
|
if (ts_node_is_null(cur)) {
|
|
return false;
|
|
}
|
|
const char *pk = ts_node_type(cur);
|
|
// Inside a binder — definitely type position
|
|
if (strcmp(pk, "explicit_binder") == 0 || strcmp(pk, "implicit_binder") == 0 ||
|
|
strcmp(pk, "instance_binder") == 0) {
|
|
return true;
|
|
}
|
|
// At a declaration boundary: check if apply is inside the body field
|
|
if (strcmp(pk, "def") == 0 || strcmp(pk, "theorem") == 0 || strcmp(pk, "instance") == 0 ||
|
|
strcmp(pk, "abbrev") == 0 || strcmp(pk, "structure") == 0 ||
|
|
strcmp(pk, "inductive") == 0) {
|
|
// Check if apply comes after the type annotation.
|
|
// Strategy: if the node starts after the end of the "type" field, it's in value
|
|
// position. If there's no "type" field, allow the call (no annotation to filter).
|
|
TSNode type_field = ts_node_child_by_field_name(cur, TS_FIELD("type"));
|
|
if (ts_node_is_null(type_field)) {
|
|
return false; // no type annotation → allow call
|
|
}
|
|
uint32_t type_end = ts_node_end_byte(type_field);
|
|
uint32_t node_start = ts_node_start_byte(node);
|
|
// If apply starts after the type annotation ends, it's a value (call)
|
|
if (node_start > type_end) {
|
|
return false;
|
|
}
|
|
return true; // apply is within or before type annotation → type position
|
|
}
|
|
cur = ts_node_parent(cur);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Resolve a selector_expression that may chain through call_expressions.
|
|
* Go pattern: pb.NewFooClient(conn).GetBar → "pb.NewFooClient.GetBar"
|
|
* Without this, cbm_node_text returns full text including args/parens.
|
|
* Iteratively walks the chain: selector → operand(call) → function(selector) → ... */
|
|
static char *resolve_chained_selector(CBMArena *a, TSNode sel, const char *source) {
|
|
TSNode operand = ts_node_child_by_field_name(sel, TS_FIELD("operand"));
|
|
TSNode field = ts_node_child_by_field_name(sel, TS_FIELD("field"));
|
|
if (ts_node_is_null(operand) || ts_node_is_null(field) ||
|
|
strcmp(ts_node_type(operand), "call_expression") != 0) {
|
|
return cbm_node_text(a, sel, source);
|
|
}
|
|
|
|
/* Operand is a call_expression — extract its callee iteratively.
|
|
* Walk: call_expression → function field → if selector_expression, repeat. */
|
|
char *method = cbm_node_text(a, field, source);
|
|
TSNode inner = operand;
|
|
enum { MAX_CHAIN_DEPTH = 4 };
|
|
for (int depth = 0; depth < MAX_CHAIN_DEPTH; depth++) {
|
|
TSNode fn = ts_node_child_by_field_name(inner, TS_FIELD("function"));
|
|
if (ts_node_is_null(fn)) {
|
|
break;
|
|
}
|
|
const char *fnk = ts_node_type(fn);
|
|
if (strcmp(fnk, "selector_expression") == 0) {
|
|
/* Check if this selector also chains through a call */
|
|
TSNode inner_op = ts_node_child_by_field_name(fn, TS_FIELD("operand"));
|
|
if (!ts_node_is_null(inner_op) &&
|
|
strcmp(ts_node_type(inner_op), "call_expression") == 0) {
|
|
inner = inner_op;
|
|
continue;
|
|
}
|
|
}
|
|
/* Reached a non-chained callee — extract its text */
|
|
char *base = cbm_node_text(a, fn, source);
|
|
if (base && method) {
|
|
return cbm_arena_sprintf(a, "%s.%s", base, method);
|
|
}
|
|
return method;
|
|
}
|
|
|
|
/* Fallback: just return the method name */
|
|
return method;
|
|
}
|
|
|
|
// Strip a trailing generic argument list ("<...>" / "[...]") from a type name,
|
|
// returning the bare type identifier. Mutates an arena-owned copy in place.
|
|
static char *strip_generic_args(char *t) {
|
|
if (!t) {
|
|
return NULL;
|
|
}
|
|
char *angle = strchr(t, '<');
|
|
if (angle) {
|
|
*angle = '\0';
|
|
}
|
|
char *brack = strchr(t, '[');
|
|
if (brack) {
|
|
*brack = '\0';
|
|
}
|
|
return t;
|
|
}
|
|
|
|
// Pull the constructed type name out of a constructor/instantiation node:
|
|
// new_expression (TS/JS) -> `constructor`/`type` field or first type child
|
|
// object_creation_expression (Java/C#/PHP) -> `type` field or first type child
|
|
// instance_expression (Scala) -> nested type in the wrapped type/call
|
|
// Returns the bare type name (generic args stripped) or NULL if not a
|
|
// constructor node / no type found. Constructor calls resolve to the class's
|
|
// constructor (or the class node) downstream, producing a CALLS edge.
|
|
static char *extract_constructor_callee(CBMArena *a, TSNode node, const char *source,
|
|
const char *nk) {
|
|
if (strcmp(nk, "new_expression") != 0 && strcmp(nk, "object_creation_expression") != 0 &&
|
|
strcmp(nk, "instance_expression") != 0) {
|
|
return NULL;
|
|
}
|
|
|
|
// Preferred: explicit fields used by the various grammars.
|
|
static const char *type_fields[] = {"constructor", "type", "name", NULL};
|
|
for (const char **f = type_fields; *f; f++) {
|
|
TSNode tn = ts_node_child_by_field_name(node, *f, (uint32_t)strlen(*f));
|
|
if (!ts_node_is_null(tn)) {
|
|
const char *tk = ts_node_type(tn);
|
|
// For a generic_type wrapper, descend to the bare name child.
|
|
if (strcmp(tk, "generic_type") == 0 && ts_node_named_child_count(tn) > 0) {
|
|
tn = ts_node_named_child(tn, 0);
|
|
}
|
|
char *t = strip_generic_args(cbm_node_text(a, tn, source));
|
|
if (t && t[0]) {
|
|
return t;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: first type-like named child (covers grammars that don't expose
|
|
// a field, e.g. Scala's instance_expression wraps the type directly).
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode child = ts_node_named_child(node, i);
|
|
const char *ck = ts_node_type(child);
|
|
if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "identifier") == 0 ||
|
|
strcmp(ck, "qualified_name") == 0 || strcmp(ck, "scoped_type_identifier") == 0 ||
|
|
strcmp(ck, "qualified_identifier") == 0 || strcmp(ck, "name") == 0 ||
|
|
strcmp(ck, "type") == 0 || strcmp(ck, "generic_type") == 0 ||
|
|
strcmp(ck, "simple_type") == 0 || strcmp(ck, "stable_type_identifier") == 0 ||
|
|
strcmp(ck, "user_type") == 0) {
|
|
// Descend through a generic_type wrapper to the bare name.
|
|
if (strcmp(ck, "generic_type") == 0 && ts_node_named_child_count(child) > 0) {
|
|
child = ts_node_named_child(child, 0);
|
|
}
|
|
char *t = strip_generic_args(cbm_node_text(a, child, source));
|
|
if (t && t[0]) {
|
|
return t;
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Try common field-based callee resolution (function, name, method fields).
|
|
static char *extract_callee_from_fields(CBMArena *a, TSNode node, const char *source) {
|
|
// Try "function" field
|
|
TSNode func_node = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
if (!ts_node_is_null(func_node)) {
|
|
const char *fk = ts_node_type(func_node);
|
|
if (strcmp(fk, "selector_expression") == 0) {
|
|
return resolve_chained_selector(a, func_node, source);
|
|
}
|
|
if (strcmp(fk, "identifier") == 0 || strcmp(fk, "simple_identifier") == 0 ||
|
|
strcmp(fk, "attribute") == 0 || strcmp(fk, "member_expression") == 0 ||
|
|
strcmp(fk, "field_expression") == 0 || strcmp(fk, "dot") == 0 ||
|
|
strcmp(fk, "function") == 0 || strcmp(fk, "dotted_identifier") == 0 ||
|
|
strcmp(fk, "member_access_expression") == 0 || strcmp(fk, "scoped_identifier") == 0 ||
|
|
strcmp(fk, "qualified_identifier") == 0 ||
|
|
/* ReScript: call_expression `function` field is a value_identifier
|
|
* (or value_identifier_path for module-qualified calls). */
|
|
strcmp(fk, "value_identifier") == 0 || strcmp(fk, "value_identifier_path") == 0) {
|
|
return cbm_node_text(a, func_node, source);
|
|
}
|
|
// C++ explicit template call f<T>(args): the `function` field is a
|
|
// template_function whose `name` child is the bare callee (identifier
|
|
// "identity" or qualified_identifier "ns::f"). Without this the whole
|
|
// "identity<int>" text would never be produced as a textual callee, so
|
|
// no CALLS edge — and the LSP's lsp_template resolution has nothing to
|
|
// attach to. Return the name child so the join recovers the bare method.
|
|
if (strcmp(fk, "template_function") == 0) {
|
|
TSNode tname = ts_node_child_by_field_name(func_node, TS_FIELD("name"));
|
|
if (!ts_node_is_null(tname)) {
|
|
return cbm_node_text(a, tname, source);
|
|
}
|
|
}
|
|
// R member call: module$fn() — function node is an extract_operator
|
|
// with lhs (object) and rhs (method). Emit "module.fn" so it resolves
|
|
// like other member calls (#219). Previously dropped → no CALLS edge.
|
|
if (strcmp(fk, "extract_operator") == 0) {
|
|
TSNode lhs = ts_node_child_by_field_name(func_node, TS_FIELD("lhs"));
|
|
TSNode rhs = ts_node_child_by_field_name(func_node, TS_FIELD("rhs"));
|
|
if (!ts_node_is_null(rhs)) {
|
|
char *rt = cbm_node_text(a, rhs, source);
|
|
if (!ts_node_is_null(lhs)) {
|
|
char *lt = cbm_node_text(a, lhs, source);
|
|
if (lt && lt[0] && rt && rt[0]) {
|
|
return cbm_arena_sprintf(a, "%s.%s", lt, rt);
|
|
}
|
|
}
|
|
return rt;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try "name" field (Java method_invocation)
|
|
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
|
if (!ts_node_is_null(name_node)) {
|
|
char *name = cbm_node_text(a, name_node, source);
|
|
TSNode obj = ts_node_child_by_field_name(node, TS_FIELD("object"));
|
|
if (!ts_node_is_null(obj) && name) {
|
|
char *obj_text = cbm_node_text(a, obj, source);
|
|
if (obj_text && obj_text[0]) {
|
|
return cbm_arena_sprintf(a, "%s.%s", obj_text, name);
|
|
}
|
|
}
|
|
return name;
|
|
}
|
|
|
|
// Ruby: "method" + "receiver" fields
|
|
TSNode method_node = ts_node_child_by_field_name(node, TS_FIELD("method"));
|
|
if (!ts_node_is_null(method_node)) {
|
|
char *method = cbm_node_text(a, method_node, source);
|
|
TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
|
|
if (!ts_node_is_null(recv) && method) {
|
|
char *recv_text = cbm_node_text(a, recv, source);
|
|
if (recv_text && recv_text[0]) {
|
|
return cbm_arena_sprintf(a, "%s.%s", recv_text, method);
|
|
}
|
|
}
|
|
return method;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// Haskell/OCaml: extract callee from apply/infix nodes.
|
|
static char *extract_fp_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "apply") == 0 || strcmp(nk, "application_expression") == 0 ||
|
|
strcmp(nk, "exp_apply") == 0) {
|
|
if (ts_node_child_count(node) > 0) {
|
|
TSNode callee = ts_node_child(node, 0);
|
|
const char *ck = ts_node_type(callee);
|
|
if (strcmp(ck, "identifier") == 0 || strcmp(ck, "variable") == 0 ||
|
|
strcmp(ck, "constructor") == 0 || strcmp(ck, "value_path") == 0 ||
|
|
/* PureScript: exp_apply's function head is an `exp_name` whose
|
|
* text is the (possibly qualified) function name. */
|
|
strcmp(ck, "exp_name") == 0) {
|
|
return cbm_node_text(a, callee, source);
|
|
}
|
|
/* Curried application `f a b` nests exp_apply/apply — descend the
|
|
* function head to recover the leftmost callee. */
|
|
if (strcmp(ck, "exp_apply") == 0 || strcmp(ck, "apply") == 0 ||
|
|
strcmp(ck, "application_expression") == 0) {
|
|
return extract_fp_callee(a, callee, source, ck);
|
|
}
|
|
}
|
|
}
|
|
if (strcmp(nk, "infix") == 0 || strcmp(nk, "infix_expression") == 0) {
|
|
TSNode op = ts_node_child_by_field_name(node, TS_FIELD("operator"));
|
|
if (!ts_node_is_null(op)) {
|
|
return cbm_node_text(a, op, source);
|
|
}
|
|
enum { INFIX_MIN_CHILDREN = 3, INFIX_OP_IDX = 1 };
|
|
if (ts_node_child_count(node) >= INFIX_MIN_CHILDREN) {
|
|
return cbm_node_text(a, ts_node_child(node, INFIX_OP_IDX), source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Wolfram: extract callee from apply, skipping LHS of set definitions.
|
|
static char *extract_wolfram_callee(CBMArena *a, TSNode node, const char *source) {
|
|
TSNode parent = ts_node_parent(node);
|
|
if (!ts_node_is_null(parent)) {
|
|
const char *pk = ts_node_type(parent);
|
|
if ((strcmp(pk, "set_delayed_top") == 0 || strcmp(pk, "set_top") == 0 ||
|
|
strcmp(pk, "set_delayed") == 0 || strcmp(pk, "set") == 0) &&
|
|
ts_node_named_child_count(parent) > 0 &&
|
|
ts_node_eq(ts_node_named_child(parent, 0), node)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
if (ts_node_named_child_count(node) > 0) {
|
|
TSNode head = ts_node_named_child(node, 0);
|
|
const char *hk = ts_node_type(head);
|
|
if (strcmp(hk, "user_symbol") == 0 || strcmp(hk, "builtin_symbol") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Language-specific callee extraction for FP and niche languages.
|
|
// Swift callee extraction from call/constructor expressions.
|
|
static char *extract_swift_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "call_expression") != 0 && strcmp(nk, "constructor_expression") != 0) {
|
|
return NULL;
|
|
}
|
|
if (ts_node_named_child_count(node) > 0) {
|
|
TSNode callee = ts_node_named_child(node, 0);
|
|
const char *ck = ts_node_type(callee);
|
|
if (strcmp(ck, "simple_identifier") == 0 || strcmp(ck, "navigation_expression") == 0) {
|
|
return cbm_node_text(a, callee, source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// A Perl sub/method name is an identifier: it starts with a letter or '_',
|
|
// contains only [A-Za-z0-9_] plus the '::' package separator, and is never a
|
|
// string/config literal. tree-sitter-perl mis-parses config lines in .cgi /
|
|
// heredoc-heavy files into call-shaped nodes whose "callee" is a dotted config
|
|
// token (e.g. "log4perl.appender.File.utf8"); rejecting non-identifier text
|
|
// here stops those from becoming bogus CALLS edges. Any '.', whitespace, quote,
|
|
// or '/' disqualifies the token.
|
|
static bool perl_is_identifier_callee(const char *name) {
|
|
if (!name || !name[0]) {
|
|
return false;
|
|
}
|
|
unsigned char c0 = (unsigned char)name[0];
|
|
if (!(isalpha(c0) || c0 == '_')) {
|
|
return false;
|
|
}
|
|
for (const char *p = name; *p; p++) {
|
|
unsigned char c = (unsigned char)*p;
|
|
if (isalnum(c) || c == '_') {
|
|
continue;
|
|
}
|
|
if (c == ':') {
|
|
// Only the '::' package separator is allowed: require an adjacent
|
|
// pair, and reject a lone ':', ':::', or a trailing '::'.
|
|
if (p[1] != ':' || p[2] == ':' || p[2] == '\0') {
|
|
return false;
|
|
}
|
|
p++; // consume the second ':'; the loop's p++ moves past the pair
|
|
continue;
|
|
}
|
|
return false; // '.', space, quote, '/', etc. → not a sub/method name
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Callee extraction for scripting languages (Elixir, Perl, PHP, Kotlin, MATLAB).
|
|
static char *extract_scripting_callee(CBMArena *a, TSNode node, const char *source,
|
|
CBMLanguage lang, const char *nk) {
|
|
if (lang == CBM_LANG_ELIXIR && strcmp(nk, "call") == 0 && ts_node_child_count(node) > 0) {
|
|
TSNode first = ts_node_child(node, 0);
|
|
const char *fk = ts_node_type(first);
|
|
if (strcmp(fk, "identifier") == 0 || strcmp(fk, "dot") == 0) {
|
|
return cbm_node_text(a, first, source);
|
|
}
|
|
return NULL;
|
|
}
|
|
if (lang == CBM_LANG_PERL && ts_node_child_count(node) > 0) {
|
|
// Pull the actual sub/method name token rather than blindly taking
|
|
// child(0). Grammar (verified against the vendored parser):
|
|
// method_call_expression : field `method` ($obj->m / Class->m)
|
|
// function_call_expression : field `function` (foo(); name with '.'
|
|
// from a config-string misparse lands here)
|
|
// ambiguous_function_call_expression : field `function`
|
|
// func1op_call_expression : builtin keyword as child(0) (no field)
|
|
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("method"));
|
|
if (ts_node_is_null(name_node)) {
|
|
name_node = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
}
|
|
if (ts_node_is_null(name_node)) {
|
|
name_node = ts_node_child(node, 0);
|
|
}
|
|
char *pn = cbm_node_text(a, name_node, source);
|
|
// Reject anything that is not a bare Perl sub/method identifier (config
|
|
// strings, quoted literals, paths) so no spurious CALLS edge is emitted.
|
|
return perl_is_identifier_callee(pn) ? pn : NULL;
|
|
}
|
|
if (lang == CBM_LANG_PHP) {
|
|
TSNode func_node = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
if (ts_node_is_null(func_node)) {
|
|
func_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
|
}
|
|
char *pn = ts_node_is_null(func_node) ? NULL : cbm_node_text(a, func_node, source);
|
|
return pn;
|
|
}
|
|
if (lang == CBM_LANG_KOTLIN && ts_node_child_count(node) > 0) {
|
|
return cbm_node_text(a, ts_node_child(node, 0), source);
|
|
}
|
|
if (lang == CBM_LANG_MATLAB && strcmp(nk, "command") == 0 && ts_node_child_count(node) > 0) {
|
|
return cbm_node_text(a, ts_node_child(node, 0), source);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// ObjC: extract callee from message_expression selector.
|
|
static char *extract_objc_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "message_expression") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode selector = ts_node_child_by_field_name(node, TS_FIELD("selector"));
|
|
return ts_node_is_null(selector) ? NULL : cbm_node_text(a, selector, source);
|
|
}
|
|
|
|
// Erlang: extract callee from call node's first child.
|
|
static char *extract_erlang_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "call") != 0 || ts_node_child_count(node) == 0) {
|
|
return NULL;
|
|
}
|
|
return cbm_node_text(a, ts_node_child(node, 0), source);
|
|
}
|
|
|
|
// Lisp dialects: a call is a list (`list` / `list_lit`) whose head (first named
|
|
// child) is the function symbol (`symbol` / `sym_lit`). Generic field/first-child
|
|
// extraction misses it because the head is not an `identifier` node.
|
|
static char *extract_lisp_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "list") != 0 && strcmp(nk, "list_lit") != 0) {
|
|
return NULL;
|
|
}
|
|
if (ts_node_named_child_count(node) > 0) {
|
|
TSNode head = ts_node_named_child(node, 0);
|
|
const char *hk = ts_node_type(head);
|
|
if (strcmp(hk, "symbol") == 0 || strcmp(hk, "sym_lit") == 0 ||
|
|
strcmp(hk, "identifier") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// F#: application_expression head is a long_identifier_or_op wrapper, not a bare
|
|
// identifier, so extract_fp_callee's accepted-type list would miss it.
|
|
static char *extract_fsharp_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "application_expression") != 0 || ts_node_named_child_count(node) == 0) {
|
|
return NULL;
|
|
}
|
|
TSNode head = ts_node_named_child(node, 0);
|
|
const char *hk = ts_node_type(head);
|
|
if (strcmp(hk, "long_identifier_or_op") == 0 || strcmp(hk, "long_identifier") == 0 ||
|
|
strcmp(hk, "identifier") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// CSS: a `call_expression` (e.g. `url(...)`, `calc(...)`) carries its callee on a
|
|
// plain `function_name` child rather than a `function`/`name` field, so generic
|
|
// field/first-child resolution misses it.
|
|
static char *extract_css_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "call_expression") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode fn = cbm_find_child_by_kind(node, "function_name");
|
|
return ts_node_is_null(fn) ? NULL : cbm_node_text(a, fn, source);
|
|
}
|
|
|
|
// PowerShell: a `command` node's callee is its `command_name` child.
|
|
static char *extract_powershell_callee(CBMArena *a, TSNode node, const char *source,
|
|
const char *nk) {
|
|
if (strcmp(nk, "command") != 0) {
|
|
return NULL;
|
|
}
|
|
uint32_t n = ts_node_named_child_count(node);
|
|
for (uint32_t i = 0; i < n; i++) {
|
|
TSNode c = ts_node_named_child(node, i);
|
|
if (strcmp(ts_node_type(c), "command_name") == 0) {
|
|
return cbm_node_text(a, c, source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Ada: procedure_call_statement / function_call carry the callee in a `name` field.
|
|
static char *extract_ada_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "procedure_call_statement") != 0 && strcmp(nk, "function_call") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode name = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
|
if (!ts_node_is_null(name)) {
|
|
return cbm_node_text(a, name, source);
|
|
}
|
|
if (ts_node_named_child_count(node) > 0) {
|
|
TSNode head = ts_node_named_child(node, 0);
|
|
const char *hk = ts_node_type(head);
|
|
if (strcmp(hk, "name") == 0 || strcmp(hk, "identifier") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Solidity: a call_expression's callee is on the `function` field, wrapped in an
|
|
// `expression` node (call_expression -> function:expression -> identifier). Descend
|
|
// left-most through expression wrappers until we reach the identifier/member.
|
|
static char *extract_solidity_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "call_expression") != 0 && strcmp(nk, "call") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode head = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
if (ts_node_is_null(head) && ts_node_named_child_count(node) > 0) {
|
|
head = ts_node_named_child(node, 0);
|
|
}
|
|
// Unwrap nested `expression` wrappers down to the callee identifier/member.
|
|
for (int depth = 0; depth < 4 && !ts_node_is_null(head); depth++) {
|
|
const char *hk = ts_node_type(head);
|
|
if (strcmp(hk, "identifier") == 0 || strcmp(hk, "member_expression") == 0 ||
|
|
strcmp(hk, "member_access") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
if (strcmp(hk, "expression") == 0 && ts_node_named_child_count(head) > 0) {
|
|
head = ts_node_named_child(head, 0);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Groovy: function_call's first named child is the callee identifier (the generic
|
|
// first-child fallback misses it because child 0 is anonymous).
|
|
static char *extract_groovy_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "function_call") != 0 && strcmp(nk, "juxt_function_call") != 0) {
|
|
return NULL;
|
|
}
|
|
if (ts_node_named_child_count(node) > 0) {
|
|
TSNode head = ts_node_named_child(node, 0);
|
|
if (!ts_node_is_null(head) && strcmp(ts_node_type(head), "identifier") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// WGSL: callee is nested type_constructor_or_function_call_expression ->
|
|
// type_declaration -> identifier. Descend left-most until an identifier.
|
|
static char *extract_wgsl_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "type_constructor_or_function_call_expression") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode head = node;
|
|
while (ts_node_named_child_count(head) > 0 && strcmp(ts_node_type(head), "identifier") != 0) {
|
|
head = ts_node_named_child(head, 0);
|
|
}
|
|
if (strcmp(ts_node_type(head), "identifier") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Dart: the invocation `selector` (the `(...)` part) follows the callee
|
|
// identifier as a sibling; `new_expression`'s first named child is the type.
|
|
static char *extract_dart_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "selector") == 0) {
|
|
TSNode prev = ts_node_prev_named_sibling(node);
|
|
if (!ts_node_is_null(prev) && strcmp(ts_node_type(prev), "identifier") == 0) {
|
|
return cbm_node_text(a, prev, source);
|
|
}
|
|
return NULL;
|
|
}
|
|
if (strcmp(nk, "new_expression") == 0 && ts_node_named_child_count(node) > 0) {
|
|
TSNode head = ts_node_named_child(node, 0);
|
|
const char *hk = ts_node_type(head);
|
|
if (strcmp(hk, "identifier") == 0 || strcmp(hk, "type_identifier") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// SCSS: an `@include foo;` is an include_statement whose callee is its
|
|
// `identifier` child (the mixin name).
|
|
static char *extract_scss_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "include_statement") == 0) {
|
|
TSNode id = cbm_find_child_by_kind(node, "identifier");
|
|
return ts_node_is_null(id) ? NULL : cbm_node_text(a, id, source);
|
|
}
|
|
/* SCSS @function call `double($x)` is a call_expression whose callee is a
|
|
* `function_name` child (there is no `function` field), so the generic
|
|
* field-based resolver returns NULL and the call is dropped — no CALLS edge
|
|
* to the in-file @function. */
|
|
if (strcmp(nk, "call_expression") == 0) {
|
|
TSNode fn = cbm_find_child_by_kind(node, "function_name");
|
|
if (!ts_node_is_null(fn)) {
|
|
return cbm_node_text(a, fn, source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// SQL: an `invocation` node's callee is nested object_reference > `name` field
|
|
// (the same shape as a create_function's name).
|
|
static char *extract_sql_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "invocation") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode oref = cbm_find_child_by_kind(node, "object_reference");
|
|
if (ts_node_is_null(oref)) {
|
|
return NULL;
|
|
}
|
|
TSNode nm = ts_node_child_by_field_name(oref, TS_FIELD("name"));
|
|
return ts_node_is_null(nm) ? NULL : cbm_node_text(a, nm, source);
|
|
}
|
|
|
|
// COBOL: a `CALL 'HELPER'` is a call_statement whose `x` field is a string
|
|
// literal naming the called program; the callee is that string sans quotes.
|
|
static char *extract_cobol_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "call_statement") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode x = ts_node_child_by_field_name(node, TS_FIELD("x"));
|
|
if (ts_node_is_null(x)) {
|
|
x = cbm_find_child_by_kind(node, "string");
|
|
}
|
|
if (ts_node_is_null(x)) {
|
|
return NULL;
|
|
}
|
|
char *text = cbm_node_text(a, x, source);
|
|
return (char *)strip_and_validate_string_arg(a, text);
|
|
}
|
|
|
|
// Elm: a `function_call_expr` has a `target` field; the callee identifier is
|
|
// target > value_expr > `name` field (value_qid) > lower_case_identifier.
|
|
static char *extract_elm_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "function_call_expr") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode target = ts_node_child_by_field_name(node, TS_FIELD("target"));
|
|
if (ts_node_is_null(target)) {
|
|
return NULL;
|
|
}
|
|
TSNode ve = strcmp(ts_node_type(target), "value_expr") == 0
|
|
? target
|
|
: cbm_find_child_by_kind(target, "value_expr");
|
|
if (ts_node_is_null(ve)) {
|
|
return NULL;
|
|
}
|
|
TSNode qid = ts_node_child_by_field_name(ve, TS_FIELD("name"));
|
|
if (ts_node_is_null(qid)) {
|
|
qid = cbm_find_child_by_kind(ve, "value_qid");
|
|
}
|
|
if (ts_node_is_null(qid)) {
|
|
return NULL;
|
|
}
|
|
TSNode id = cbm_find_child_by_kind(qid, "lower_case_identifier");
|
|
if (ts_node_is_null(id)) {
|
|
// module-qualified call: emit the whole qualified id text
|
|
return cbm_node_text(a, qid, source);
|
|
}
|
|
return cbm_node_text(a, id, source);
|
|
}
|
|
|
|
// Jsonnet: a `functioncall` node's callee is its first `id` child (the called
|
|
// binding name); the generic field path misses it (no `function`/`name` field).
|
|
static char *extract_jsonnet_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "functioncall") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode id = cbm_find_child_by_kind(node, "id");
|
|
return ts_node_is_null(id) ? NULL : cbm_node_text(a, id, source);
|
|
}
|
|
|
|
// Nickel: function application is `applicative` and curries left-associatively:
|
|
// `f x y` parses as `(applicative t1:(applicative t1:f t2:x) t2:y)`. A real call
|
|
// node carries a `t2` (argument) field; a bare value (`applicative
|
|
// (record_operand (atom (ident))))` wraps every expression and has no `t2`, so it
|
|
// is NOT a call. We also skip applicatives whose parent is itself an applicative
|
|
// (the inner partial-application nodes) so a curried call emits exactly one edge,
|
|
// keyed on the leftmost ident reached by descending the `t1` chain.
|
|
// (`infix_expr` is binary operator application, not a call, and is excluded from
|
|
// nickel_call_types.)
|
|
static char *extract_nickel_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "applicative") != 0) {
|
|
return NULL;
|
|
}
|
|
// Not an application unless it has an argument (`t2`).
|
|
if (ts_node_is_null(ts_node_child_by_field_name(node, TS_FIELD("t2")))) {
|
|
return NULL;
|
|
}
|
|
// Emit only at the outermost applicative of a curried chain.
|
|
TSNode parent = ts_node_parent(node);
|
|
if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "applicative") == 0) {
|
|
return NULL;
|
|
}
|
|
enum { NICKEL_APPLY_DEPTH = 8 };
|
|
TSNode cur = node;
|
|
for (int depth = 0; depth < NICKEL_APPLY_DEPTH && !ts_node_is_null(cur); depth++) {
|
|
const char *ck = ts_node_type(cur);
|
|
if (strcmp(ck, "ident") == 0) {
|
|
return cbm_node_text(a, cur, source);
|
|
}
|
|
// Descend the function side: the `t1` field for curried applicatives, or
|
|
// the wrapper's first named child (record_operand -> atom -> ident).
|
|
TSNode next = ts_node_child_by_field_name(cur, TS_FIELD("t1"));
|
|
if (ts_node_is_null(next) && ts_node_named_child_count(cur) > 0) {
|
|
next = ts_node_named_child(cur, 0);
|
|
}
|
|
if (ts_node_is_null(next) || ts_node_eq(next, cur)) {
|
|
break;
|
|
}
|
|
cur = next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Typst: a `call` node's callee is its `item` field (an ident), matching the
|
|
// def-side resolution of `#let greet(name) = ...`.
|
|
static char *extract_typst_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "call") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode item = ts_node_child_by_field_name(node, TS_FIELD("item"));
|
|
return ts_node_is_null(item) ? NULL : cbm_node_text(a, item, source);
|
|
}
|
|
|
|
// Meson: a builtin invocation (`executable(...)`, `dependency(...)`) is a
|
|
// `normal_command` whose `command` field is the called identifier.
|
|
static char *extract_meson_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "normal_command") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode cmd = ts_node_child_by_field_name(node, TS_FIELD("command"));
|
|
return ts_node_is_null(cmd) ? NULL : cbm_node_text(a, cmd, source);
|
|
}
|
|
|
|
// Descend left-most through wrapper nodes to the first identifier-bearing leaf.
|
|
// Used by HDL call nodes whose callee identifier is nested under one or more
|
|
// grammar wrappers (Verilog tf_call -> simple_identifier; SystemVerilog
|
|
// tf_call -> hierarchical_identifier -> simple_identifier).
|
|
static char *first_leaf_identifier(CBMArena *a, TSNode node, const char *source) {
|
|
TSNode cur = node;
|
|
for (int depth = 0; depth < 8 && !ts_node_is_null(cur); depth++) {
|
|
const char *k = ts_node_type(cur);
|
|
if (strcmp(k, "simple_identifier") == 0 || strcmp(k, "identifier") == 0 ||
|
|
strcmp(k, "word") == 0 || strcmp(k, "name") == 0 || strcmp(k, "qid") == 0) {
|
|
char *t = cbm_node_text(a, cur, source);
|
|
return (t && t[0]) ? t : NULL;
|
|
}
|
|
if (ts_node_named_child_count(cur) == 0) {
|
|
return NULL;
|
|
}
|
|
cur = ts_node_named_child(cur, 0);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Verilog / SystemVerilog: a function_subroutine_call wraps
|
|
// subroutine_call -> tf_call -> [hierarchical_identifier ->] simple_identifier.
|
|
// Descend to the first identifier leaf to name the callee.
|
|
static char *extract_hdl_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "function_subroutine_call") != 0 && strcmp(nk, "subroutine_call") != 0 &&
|
|
strcmp(nk, "tf_call") != 0 && strcmp(nk, "system_tf_call") != 0) {
|
|
return NULL;
|
|
}
|
|
return first_leaf_identifier(a, node, source);
|
|
}
|
|
|
|
// VHDL: `add(x, 1)` parses as `(name (library_function) (parenthesis_group ...))`
|
|
// inside a `simple_expression` (the function-call / indexed-name ambiguity). The
|
|
// call_node_types set targets `parenthesis_group`; the callee is its immediately
|
|
// preceding named sibling (a `library_function`/`identifier`/`name` token).
|
|
static char *extract_vhdl_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "parenthesis_group") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode prev = ts_node_prev_named_sibling(node);
|
|
if (ts_node_is_null(prev)) {
|
|
return NULL;
|
|
}
|
|
const char *pk = ts_node_type(prev);
|
|
if (strcmp(pk, "library_function") == 0 || strcmp(pk, "identifier") == 0 ||
|
|
strcmp(pk, "name") == 0 || strcmp(pk, "simple_name") == 0) {
|
|
char *t = cbm_node_text(a, prev, source);
|
|
return (t && t[0]) ? t : NULL;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// NASM: a `call`/`jmp`-style instruction is an `actual_instruction` whose
|
|
// `instruction:` field is the mnemonic word and whose first operand word is the
|
|
// target label. Only treat call/jump mnemonics as calls; everything else (add,
|
|
// mov, ret, ...) is plain data-flow, not a call.
|
|
static char *extract_nasm_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "actual_instruction") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode mnem = ts_node_child_by_field_name(node, TS_FIELD("instruction"));
|
|
if (ts_node_is_null(mnem)) {
|
|
return NULL;
|
|
}
|
|
char *m = cbm_node_text(a, mnem, source);
|
|
if (!m || (strcmp(m, "call") != 0 && strcmp(m, "jmp") != 0 && strcmp(m, "je") != 0 &&
|
|
strcmp(m, "jne") != 0 && strcmp(m, "jz") != 0 && strcmp(m, "jnz") != 0)) {
|
|
return NULL;
|
|
}
|
|
TSNode ops = ts_node_child_by_field_name(node, TS_FIELD("operands"));
|
|
if (ts_node_is_null(ops) || ts_node_named_child_count(ops) == 0) {
|
|
return NULL;
|
|
}
|
|
return first_leaf_identifier(a, ts_node_named_child(ops, 0), source);
|
|
}
|
|
|
|
// LLVM-IR: a `call`/`invoke` is an `instruction_call` whose `callee:` field is a
|
|
// `value -> var -> global_var` chain (e.g. `@inner`). Strip the leading sigil.
|
|
static char *extract_llvm_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "instruction_call") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode callee = ts_node_child_by_field_name(node, TS_FIELD("callee"));
|
|
if (ts_node_is_null(callee)) {
|
|
return NULL;
|
|
}
|
|
char *t = first_leaf_identifier(a, callee, source);
|
|
if (!t) {
|
|
t = cbm_node_text(a, callee, source);
|
|
}
|
|
if (t && (t[0] == '@' || t[0] == '%')) {
|
|
return t + 1;
|
|
}
|
|
return t;
|
|
}
|
|
|
|
// FunC: a `function_application` carries the callee on its `function:` field.
|
|
static char *extract_func_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "function_application") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
return ts_node_is_null(fn) ? NULL : cbm_node_text(a, fn, source);
|
|
}
|
|
|
|
// Nix: an `apply_expression` (`f x`) carries the applied function on its
|
|
// `function:` field. The head is a `variable_expression` whose `name` is the
|
|
// callee identifier; curried application (`f x y`) nests apply_expressions, so
|
|
// descend the `function` chain to the head variable_expression. The generic
|
|
// field resolver does not recognise `variable_expression`, so without this the
|
|
// call to `addOne` would never be captured.
|
|
static char *extract_nix_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "apply_expression") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
for (int depth = 0; depth < 8 && !ts_node_is_null(fn); depth++) {
|
|
const char *fk = ts_node_type(fn);
|
|
if (strcmp(fk, "apply_expression") == 0) {
|
|
fn = ts_node_child_by_field_name(fn, TS_FIELD("function"));
|
|
continue;
|
|
}
|
|
if (strcmp(fk, "variable_expression") == 0) {
|
|
TSNode nm = ts_node_child_by_field_name(fn, TS_FIELD("name"));
|
|
return ts_node_is_null(nm) ? NULL : cbm_node_text(a, nm, source);
|
|
}
|
|
if (strcmp(fk, "identifier") == 0) {
|
|
return cbm_node_text(a, fn, source);
|
|
}
|
|
return NULL;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Agda: function application `f x y` parses as an `expr` whose named children are
|
|
// `atom`s (no dedicated application node). Treat an `expr` with >= 2 atom children
|
|
// as a call whose callee is the head atom's identifier.
|
|
static char *extract_agda_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "expr") != 0 || ts_node_named_child_count(node) < 2) {
|
|
return NULL;
|
|
}
|
|
TSNode head = ts_node_named_child(node, 0);
|
|
if (strcmp(ts_node_type(head), "atom") != 0) {
|
|
return NULL;
|
|
}
|
|
return first_leaf_identifier(a, head, source);
|
|
}
|
|
|
|
// Make: `$(shell ...)` is a `shell_function` node; the callee is the literal
|
|
// `shell` keyword. tree-sitter-make also exposes `function_call` for other
|
|
// builtins ($(wildcard ...), $(patsubst ...)).
|
|
static char *extract_make_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "shell_function") == 0) {
|
|
return cbm_arena_strndup(a, "shell", 5);
|
|
}
|
|
if (strcmp(nk, "function_call") == 0) {
|
|
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
if (ts_node_is_null(fn) && ts_node_named_child_count(node) > 0) {
|
|
fn = ts_node_named_child(node, 0);
|
|
}
|
|
return ts_node_is_null(fn) ? NULL : cbm_node_text(a, fn, source);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Just: a recipe dependency `recipe: dep` is a `dependency` node whose `name:`
|
|
// field is the referenced recipe.
|
|
static char *extract_just_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "dependency") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode name = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
|
if (ts_node_is_null(name) && ts_node_named_child_count(node) > 0) {
|
|
name = ts_node_named_child(node, 0);
|
|
}
|
|
return ts_node_is_null(name) ? NULL : cbm_node_text(a, name, source);
|
|
}
|
|
|
|
// Puppet: `include foo` is an `include_statement`; the callee is the literal
|
|
// `include` keyword (the class/identifier args are resolved as separate refs).
|
|
static char *extract_puppet_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
|
|
if (strcmp(nk, "include_statement") == 0) {
|
|
return cbm_arena_strndup(a, "include", 7);
|
|
}
|
|
if (strcmp(nk, "function_call") == 0) {
|
|
if (ts_node_named_child_count(node) > 0) {
|
|
TSNode head = ts_node_named_child(node, 0);
|
|
if (strcmp(ts_node_type(head), "identifier") == 0) {
|
|
return cbm_node_text(a, head, source);
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *source,
|
|
CBMLanguage lang) {
|
|
const char *nk = ts_node_type(node);
|
|
|
|
/* Python dict-dispatch call `funcs["a"](v)`: the call's `function` field is a
|
|
* subscript whose base is the identifier holding the dispatch table. Emit the
|
|
* base identifier ("funcs") as the textual callee so a CALLS edge exists; the
|
|
* py-LSP resolves it to the real target and joins via `reason` (lsp_resolve.h,
|
|
* lsp_dict_dispatch). Gated to the literal-string-key shape the LSP handles so
|
|
* other subscript calls (arr[i]()) are unaffected. */
|
|
if (lang == CBM_LANG_PYTHON && strcmp(nk, "call") == 0) {
|
|
TSNode fnf = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
if (!ts_node_is_null(fnf) && strcmp(ts_node_type(fnf), "subscript") == 0) {
|
|
TSNode val = ts_node_child_by_field_name(fnf, TS_FIELD("value"));
|
|
TSNode idx = ts_node_child_by_field_name(fnf, TS_FIELD("subscript"));
|
|
if (!ts_node_is_null(val) && !ts_node_is_null(idx) &&
|
|
strcmp(ts_node_type(val), "identifier") == 0 &&
|
|
strcmp(ts_node_type(idx), "string") == 0) {
|
|
return cbm_node_text(a, val, source);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (lang == CBM_LANG_JSONNET) {
|
|
char *c = extract_jsonnet_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
if (lang == CBM_LANG_NICKEL) {
|
|
char *c = extract_nickel_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
if (lang == CBM_LANG_TYPST) {
|
|
char *c = extract_typst_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
if (lang == CBM_LANG_MESON) {
|
|
char *c = extract_meson_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
|
|
if (lang == CBM_LANG_SCSS) {
|
|
char *c = extract_scss_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
if (lang == CBM_LANG_CSS) {
|
|
char *c = extract_css_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
if (lang == CBM_LANG_SQL) {
|
|
char *c = extract_sql_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
if (lang == CBM_LANG_COBOL) {
|
|
char *c = extract_cobol_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
if (lang == CBM_LANG_ELM) {
|
|
char *c = extract_elm_callee(a, node, source, nk);
|
|
return c ? c : extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
|
|
if (lang == CBM_LANG_CLOJURE || lang == CBM_LANG_COMMONLISP || lang == CBM_LANG_SCHEME ||
|
|
lang == CBM_LANG_FENNEL || lang == CBM_LANG_RACKET || lang == CBM_LANG_EMACSLISP) {
|
|
return extract_lisp_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_FSHARP) {
|
|
return extract_fsharp_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_POWERSHELL) {
|
|
return extract_powershell_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_ADA) {
|
|
return extract_ada_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_SOLIDITY) {
|
|
return extract_solidity_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_GROOVY) {
|
|
return extract_groovy_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_WGSL) {
|
|
return extract_wgsl_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_DART) {
|
|
return extract_dart_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_OBJC) {
|
|
return extract_objc_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_ERLANG) {
|
|
return extract_erlang_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_HASKELL || lang == CBM_LANG_OCAML || lang == CBM_LANG_PURESCRIPT) {
|
|
return extract_fp_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_WOLFRAM && strcmp(nk, "apply") == 0) {
|
|
return extract_wolfram_callee(a, node, source);
|
|
}
|
|
if (lang == CBM_LANG_SWIFT) {
|
|
return extract_swift_callee(a, node, source, nk);
|
|
}
|
|
if (lang == CBM_LANG_VERILOG || lang == CBM_LANG_SYSTEMVERILOG) {
|
|
char *c = extract_hdl_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_VHDL) {
|
|
char *c = extract_vhdl_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_NASM) {
|
|
char *c = extract_nasm_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_LLVM_IR) {
|
|
char *c = extract_llvm_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_FUNC) {
|
|
char *c = extract_func_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_AGDA) {
|
|
char *c = extract_agda_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_NIX) {
|
|
char *c = extract_nix_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_MAKEFILE) {
|
|
char *c = extract_make_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_JUST) {
|
|
char *c = extract_just_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
if (lang == CBM_LANG_PUPPET) {
|
|
char *c = extract_puppet_callee(a, node, source, nk);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
|
|
return extract_scripting_callee(a, node, source, lang, nk);
|
|
}
|
|
|
|
// Extract callee name from a call node
|
|
/* #952: compose Laravel group prefixes into a route path. Walks UP from a
|
|
* route-registration call: every enclosing anonymous function that is the
|
|
* argument of a `->group(...)` member call contributes the `prefix('...')`
|
|
* (or `Route::prefix('...')`) found on that group call's receiver chain.
|
|
* Chain methods that don't shape the path (middleware, name, as, domain)
|
|
* are skipped. Outer groups accumulate before inner ones; nested groups
|
|
* compose left-to-right. Returns the composed path (arena) or NULL when no
|
|
* enclosing group carries a prefix. */
|
|
enum { PHP_GROUP_WALK_MAX = 64, PHP_PREFIX_PARTS_MAX = 8 };
|
|
|
|
static const char *php_chain_prefix_arg(CBMArena *a, TSNode call_node, const char *source) {
|
|
/* call_node is a member_call_expression / scoped_call_expression whose
|
|
* name is `prefix`; return its first string argument (unquoted). */
|
|
TSNode args = ts_node_child_by_field_name(call_node, TS_FIELD("arguments"));
|
|
if (ts_node_is_null(args)) {
|
|
return NULL;
|
|
}
|
|
uint32_t nc = ts_node_named_child_count(args);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode arg = ts_node_named_child(args, i);
|
|
TSNode inner = arg;
|
|
if (strcmp(ts_node_type(arg), "argument") == 0 && ts_node_named_child_count(arg) > 0) {
|
|
inner = ts_node_named_child(arg, 0);
|
|
}
|
|
if (strcmp(ts_node_type(inner), "string") == 0 ||
|
|
strcmp(ts_node_type(inner), "encapsed_string") == 0) {
|
|
char *txt = cbm_node_text(a, inner, source);
|
|
if (txt && txt[0]) {
|
|
size_t len = strlen(txt);
|
|
if (len >= 2 && (txt[0] == '\'' || txt[0] == '"')) {
|
|
txt[len - 1] = '\0';
|
|
txt++;
|
|
}
|
|
return txt[0] ? txt : NULL;
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static const char *php_group_prefix_for_call(CBMArena *a, TSNode node, const char *source) {
|
|
const char *parts[PHP_PREFIX_PARTS_MAX];
|
|
int part_count = 0;
|
|
TSNode cur = ts_node_parent(node);
|
|
for (int depth = 0; depth < PHP_GROUP_WALK_MAX && !ts_node_is_null(cur); depth++) {
|
|
if (strcmp(ts_node_type(cur), "anonymous_function") == 0 ||
|
|
strcmp(ts_node_type(cur), "arrow_function") == 0) {
|
|
/* Is this closure the argument of a ->group(...) call? Walk to
|
|
* the enclosing call and check its method name. */
|
|
TSNode p = ts_node_parent(cur);
|
|
while (!ts_node_is_null(p) && strcmp(ts_node_type(p), "member_call_expression") != 0 &&
|
|
strcmp(ts_node_type(p), "scoped_call_expression") != 0) {
|
|
if (strcmp(ts_node_type(p), "statement") == 0 ||
|
|
strcmp(ts_node_type(p), "expression_statement") == 0) {
|
|
break; /* left the argument position */
|
|
}
|
|
p = ts_node_parent(p);
|
|
}
|
|
if (!ts_node_is_null(p) && (strcmp(ts_node_type(p), "member_call_expression") == 0 ||
|
|
strcmp(ts_node_type(p), "scoped_call_expression") == 0)) {
|
|
TSNode gname = ts_node_child_by_field_name(p, TS_FIELD("name"));
|
|
char *gtxt = ts_node_is_null(gname) ? NULL : cbm_node_text(a, gname, source);
|
|
if (gtxt && strcmp(gtxt, "group") == 0) {
|
|
/* Scan the receiver chain for prefix('...'). */
|
|
TSNode recv = ts_node_child_by_field_name(p, TS_FIELD("object"));
|
|
for (int hops = 0; hops < PHP_GROUP_WALK_MAX && !ts_node_is_null(recv);
|
|
hops++) {
|
|
const char *rk = ts_node_type(recv);
|
|
if (strcmp(rk, "member_call_expression") == 0 ||
|
|
strcmp(rk, "scoped_call_expression") == 0) {
|
|
TSNode rname = ts_node_child_by_field_name(recv, TS_FIELD("name"));
|
|
char *rtxt =
|
|
ts_node_is_null(rname) ? NULL : cbm_node_text(a, rname, source);
|
|
if (rtxt && strcmp(rtxt, "prefix") == 0) {
|
|
const char *pf = php_chain_prefix_arg(a, recv, source);
|
|
if (pf && part_count < PHP_PREFIX_PARTS_MAX) {
|
|
parts[part_count++] = pf; /* inner-first */
|
|
}
|
|
break;
|
|
}
|
|
recv = ts_node_child_by_field_name(recv, TS_FIELD("object"));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
cur = ts_node_parent(cur);
|
|
}
|
|
if (part_count == 0) {
|
|
return NULL;
|
|
}
|
|
/* parts[] is inner-first; compose outer-first. Ensure exactly one '/'
|
|
* between segments and a leading '/'. */
|
|
char buf[CBM_SZ_256];
|
|
size_t pos = 0;
|
|
for (int i = part_count - 1; i >= 0; i--) {
|
|
const char *seg = parts[i];
|
|
while (*seg == '/') {
|
|
seg++;
|
|
}
|
|
size_t sl = strlen(seg);
|
|
if (sl == 0) {
|
|
continue;
|
|
}
|
|
if (pos + sl + 2 >= sizeof(buf)) {
|
|
return NULL; /* oversized — leave path un-prefixed */
|
|
}
|
|
buf[pos++] = '/';
|
|
memcpy(buf + pos, seg, sl);
|
|
pos += sl;
|
|
while (pos > 1 && buf[pos - 1] == '/') {
|
|
pos--; /* strip trailing slash per segment */
|
|
}
|
|
}
|
|
buf[pos] = '\0';
|
|
return pos ? cbm_arena_strndup(a, buf, pos) : NULL;
|
|
}
|
|
|
|
static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) {
|
|
// Lean 4: skip type-position applies
|
|
if (lang == CBM_LANG_LEAN && strcmp(ts_node_type(node), "apply") == 0) {
|
|
if (lean_is_in_type_position(node)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Helm / Go templates: resolve `include "x"` / `template "x"` to the
|
|
// referenced named template so it links to the define'd Function (#338).
|
|
if (lang == CBM_LANG_GOTEMPLATE) {
|
|
char *g = gotemplate_callee(a, node, source);
|
|
if (g) {
|
|
return g;
|
|
}
|
|
}
|
|
|
|
// Constructor / instantiation nodes (new T(), object_creation, instance_expression):
|
|
// resolve to the constructed type so a CALLS edge links to the class/constructor.
|
|
char *ctor = extract_constructor_callee(a, node, source, ts_node_type(node));
|
|
if (ctor) {
|
|
return ctor;
|
|
}
|
|
|
|
// Ruby: `Widget.new(...)` is a method call on a constant receiver whose
|
|
// method is `new`. The constructor body lives in `initialize`, so a callee
|
|
// of "new" never resolves. Redirect to the receiver type name so the call
|
|
// links to the class/constructor like every other language's `new T()`.
|
|
if (lang == CBM_LANG_RUBY) {
|
|
TSNode m = ts_node_child_by_field_name(node, TS_FIELD("method"));
|
|
TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
|
|
if (!ts_node_is_null(m) && !ts_node_is_null(recv) &&
|
|
strcmp(ts_node_type(recv), "constant") == 0) {
|
|
char *mt = cbm_node_text(a, m, source);
|
|
if (mt && strcmp(mt, "new") == 0) {
|
|
char *rt = cbm_node_text(a, recv, source);
|
|
if (rt && rt[0]) {
|
|
return rt;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* #952: PHP facade route registrations (`Route::get(...)`, a
|
|
* scoped_call_expression) must carry the scope in the callee text — the
|
|
* empty-resolution route fallback keys on the "::get" suffix table, and
|
|
* the bare "get" that generic field resolution would return deliberately
|
|
* never matches (every $obj->get() would become a route). Runs BEFORE
|
|
* field-based resolution, which short-circuits on the `name` field.
|
|
* Gated to the literal `Route` scope AND a route-method match: any other
|
|
* scope (Cache::get) would suffix-match "::get" too and mint junk routes
|
|
* from slash-prefixed keys. Known limitation: aliased facade imports are
|
|
* not recognized. */
|
|
if (lang == CBM_LANG_PHP && strcmp(ts_node_type(node), "scoped_call_expression") == 0) {
|
|
TSNode scope = ts_node_child_by_field_name(node, TS_FIELD("scope"));
|
|
TSNode mname = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
|
if (!ts_node_is_null(scope) && !ts_node_is_null(mname)) {
|
|
char *sc = cbm_node_text(a, scope, source);
|
|
char *mn = cbm_node_text(a, mname, source);
|
|
if (sc && mn && strcmp(sc, "Route") == 0) {
|
|
char *qual = cbm_arena_sprintf(a, "%s::%s", sc, mn);
|
|
if (qual && cbm_service_pattern_route_method(qual) != NULL) {
|
|
return qual;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try common field-based resolution first
|
|
char *name = extract_callee_from_fields(a, node, source);
|
|
if (name) {
|
|
return name;
|
|
}
|
|
|
|
// Language-specific patterns
|
|
name = extract_callee_lang_specific(a, node, source, lang);
|
|
if (name) {
|
|
return name;
|
|
}
|
|
|
|
// Generic fallback: first identifier child
|
|
if (ts_node_child_count(node) > 0) {
|
|
TSNode first = ts_node_child(node, 0);
|
|
if (strcmp(ts_node_type(first), "identifier") == 0) {
|
|
return cbm_node_text(a, first, source);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// Strip quotes and validate a string arg. Returns validated text or NULL.
|
|
static const char *strip_and_validate_string_arg(CBMArena *a, char *text) {
|
|
if (!text || !text[0]) {
|
|
return NULL;
|
|
}
|
|
int len = (int)strlen(text);
|
|
if (len >= CBM_QUOTE_PAIR && (text[0] == '"' || text[0] == '\'')) {
|
|
text = cbm_arena_strndup(a, text + CBM_QUOTE_OFFSET, (size_t)(len - CBM_QUOTE_PAIR));
|
|
len -= CBM_QUOTE_PAIR;
|
|
}
|
|
if (!text || len <= 0 || len >= MAX_STRING_ARG_LEN) {
|
|
return NULL;
|
|
}
|
|
for (int vi = 0; vi < len; vi++) {
|
|
if ((unsigned char)text[vi] < MIN_PRINTABLE && text[vi] != '\t') {
|
|
return NULL;
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
// Return the (dequoted) first string-literal child of a node, or NULL.
|
|
static char *gotemplate_string_child(CBMArena *a, TSNode parent, const char *source) {
|
|
TSNode s = cbm_find_child_by_kind(parent, "interpreted_string_literal");
|
|
if (ts_node_is_null(s)) {
|
|
return NULL;
|
|
}
|
|
char *text = cbm_node_text(a, s, source);
|
|
const char *v = strip_and_validate_string_arg(a, text);
|
|
return (char *)v;
|
|
}
|
|
|
|
// Resolve a Go-template / Helm call to the referenced named template:
|
|
// {{ template "x" . }} -> template_action, name is a string child
|
|
// {{ include "x" . }} -> function_call(include), name is first string arg
|
|
// Returns NULL for any other node so generic resolution names the function.
|
|
static char *gotemplate_callee(CBMArena *a, TSNode node, const char *source) {
|
|
const char *k = ts_node_type(node);
|
|
if (strcmp(k, "template_action") == 0) {
|
|
return gotemplate_string_child(a, node, source);
|
|
}
|
|
if (strcmp(k, "function_call") == 0) {
|
|
TSNode fn = cbm_find_child_by_kind(node, "identifier");
|
|
if (ts_node_is_null(fn)) {
|
|
return NULL;
|
|
}
|
|
char *fname = cbm_node_text(a, fn, source);
|
|
if (!fname || (strcmp(fname, "include") != 0 && strcmp(fname, "template") != 0 &&
|
|
strcmp(fname, "tpl") != 0)) {
|
|
return NULL;
|
|
}
|
|
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
|
|
if (ts_node_is_null(args)) {
|
|
args = cbm_find_child_by_kind(node, "argument_list");
|
|
}
|
|
if (ts_node_is_null(args)) {
|
|
return NULL;
|
|
}
|
|
return gotemplate_string_child(a, args, source);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static const char *extract_nth_string_arg(CBMExtractCtx *ctx, TSNode args, uint32_t n) {
|
|
uint32_t nc = ts_node_named_child_count(args);
|
|
uint32_t found = 0;
|
|
for (uint32_t ai = 0; ai < nc && ai < MAX_POSITIONAL_SCAN + n; ai++) {
|
|
TSNode arg = ts_node_named_child(args, ai);
|
|
const char *ak = ts_node_type(arg);
|
|
if (is_string_like(ak)) {
|
|
if (found == n) {
|
|
char *text = cbm_node_text(ctx->arena, arg, ctx->source);
|
|
return strip_and_validate_string_arg(ctx->arena, text);
|
|
}
|
|
found++;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// --- Unified handler: called once per node by the cursor walk ---
|
|
|
|
// Process a keyword argument (keyword_argument or pair node).
|
|
static void process_keyword_arg(CBMExtractCtx *ctx, TSNode arg_node, CBMCallArg *ca) {
|
|
TSNode key_n = ts_node_child_by_field_name(arg_node, TS_FIELD("name"));
|
|
TSNode val_n = ts_node_child_by_field_name(arg_node, TS_FIELD("value"));
|
|
if (ts_node_is_null(key_n)) {
|
|
key_n = ts_node_child_by_field_name(arg_node, TS_FIELD("key"));
|
|
}
|
|
if (!ts_node_is_null(key_n)) {
|
|
ca->keyword = cbm_node_text(ctx->arena, key_n, ctx->source);
|
|
}
|
|
if (!ts_node_is_null(val_n)) {
|
|
ca->expr = cbm_node_text(ctx->arena, val_n, ctx->source);
|
|
if (strcmp(ts_node_type(val_n), "identifier") == 0 && ca->expr) {
|
|
ca->value = lookup_string_constant(ctx, ca->expr);
|
|
} else if (is_string_like(ts_node_type(val_n)) && ca->expr) {
|
|
ca->value = strip_quotes(ctx->arena, ca->expr);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Extract all arguments from a call expression into call->args[]. */
|
|
static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) {
|
|
uint32_t argc = ts_node_named_child_count(args);
|
|
int positional_idx = 0;
|
|
for (uint32_t ai = 0; ai < argc && call->arg_count < CBM_MAX_CALL_ARGS; ai++) {
|
|
TSNode arg_node = ts_node_named_child(args, ai);
|
|
const char *ak = ts_node_type(arg_node);
|
|
CBMCallArg *ca = &call->args[call->arg_count];
|
|
memset(ca, 0, sizeof(*ca));
|
|
|
|
if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) {
|
|
process_keyword_arg(ctx, arg_node, ca);
|
|
ca->index = positional_idx++;
|
|
call->arg_count++;
|
|
} else if (strcmp(ak, "list_splat") == 0 || strcmp(ak, "dictionary_splat") == 0 ||
|
|
strcmp(ak, "spread_element") == 0) {
|
|
positional_idx++;
|
|
} else {
|
|
ca->expr = cbm_node_text(ctx->arena, arg_node, ctx->source);
|
|
ca->index = positional_idx++;
|
|
if (is_string_like(ak) && ca->expr) {
|
|
ca->value = strip_quotes(ctx->arena, ca->expr);
|
|
} else if (strcmp(ak, "identifier") == 0 && ca->expr) {
|
|
ca->value = lookup_string_constant(ctx, ca->expr);
|
|
}
|
|
call->arg_count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if a keyword name matches URL or topic patterns.
|
|
static bool is_url_or_topic_keyword(const char *key) {
|
|
static const char *url_keywords[] = {"url", "endpoint", "path", "uri",
|
|
"target_url", "base_url", NULL};
|
|
static const char *topic_keywords[] = {"topic", "topic_id", "topic_name",
|
|
"queue", "queue_name", "queue_id",
|
|
"subject", "channel", NULL};
|
|
for (int i = 0; url_keywords[i]; i++) {
|
|
if (strcmp(key, url_keywords[i]) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
for (int i = 0; topic_keywords[i]; i++) {
|
|
if (strcmp(key, topic_keywords[i]) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Check if a struct-field name identifies a queue/topic target. Cloud SDKs pass
|
|
// the destination via a composite-literal input struct rather than a bare string
|
|
// arg (e.g. Go `SendMessageInput{QueueUrl: ...}`, `PublishInput{TopicArn: ...}`).
|
|
// Case-insensitive so QueueUrl/QueueURL/queue_url all match.
|
|
static bool is_queue_topic_field(const char *key) {
|
|
static const char *fields[] = {"QueueUrl", "QueueURL", "TopicArn", "TopicARN", "QueueName",
|
|
"TopicName", "QueueArn", "QueueARN", "Destination", NULL};
|
|
if (!key || !key[0]) {
|
|
return false;
|
|
}
|
|
for (int i = 0; fields[i]; i++) {
|
|
if (strcasecmp(key, fields[i]) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Extract string value from a node (literal or constant reference).
|
|
static const char *extract_string_value(CBMExtractCtx *ctx, TSNode val_node) {
|
|
const char *vk = ts_node_type(val_node);
|
|
if (is_string_like(vk)) {
|
|
char *text = cbm_node_text(ctx->arena, val_node, ctx->source);
|
|
if (text && text[0]) {
|
|
return strip_quotes(ctx->arena, text);
|
|
}
|
|
} else if (strcmp(vk, "identifier") == 0) {
|
|
char *const_name = cbm_node_text(ctx->arena, val_node, ctx->source);
|
|
if (const_name) {
|
|
return lookup_string_constant(ctx, const_name);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Recover a queue/topic identity from a Go composite-literal input struct, e.g.
|
|
// &sqs.SendMessageInput{QueueUrl: queueUrl, MessageBody: body}
|
|
// sns.PublishInput{TopicArn: "arn:aws:sns:..."}
|
|
// The dispatch target is carried by a struct field (QueueUrl/TopicArn/...), not a
|
|
// bare string arg, so the async edge would otherwise degrade to a plain CALLS.
|
|
// Returns the field's value: the string-literal content when present, else the
|
|
// referenced identifier text (which still names the queue/topic for edge formation).
|
|
static const char *extract_composite_queue_field(CBMExtractCtx *ctx, TSNode node) {
|
|
// Unwrap a pointer-of-composite: `&Type{...}` is a unary_expression whose
|
|
// operand is the composite_literal.
|
|
if (strcmp(ts_node_type(node), "unary_expression") == 0) {
|
|
TSNode operand = ts_node_child_by_field_name(node, TS_FIELD("operand"));
|
|
if (ts_node_is_null(operand)) {
|
|
return NULL;
|
|
}
|
|
node = operand;
|
|
}
|
|
if (strcmp(ts_node_type(node), "composite_literal") != 0) {
|
|
return NULL;
|
|
}
|
|
TSNode body = ts_node_child_by_field_name(node, TS_FIELD("body"));
|
|
if (ts_node_is_null(body)) {
|
|
body = cbm_find_child_by_kind(node, "literal_value");
|
|
}
|
|
if (ts_node_is_null(body)) {
|
|
return NULL;
|
|
}
|
|
uint32_t nc = ts_node_named_child_count(body);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode el = ts_node_named_child(body, i);
|
|
if (strcmp(ts_node_type(el), "keyed_element") != 0) {
|
|
continue;
|
|
}
|
|
// keyed_element children: key then value. Each side may be wrapped in a
|
|
// literal_element; unwrap to the underlying identifier/literal.
|
|
uint32_t ec = ts_node_named_child_count(el);
|
|
if (ec < PAIR_LEN) {
|
|
continue;
|
|
}
|
|
TSNode key_n = ts_node_named_child(el, 0);
|
|
TSNode val_n = ts_node_named_child(el, 1);
|
|
if (strcmp(ts_node_type(key_n), "literal_element") == 0 &&
|
|
ts_node_named_child_count(key_n) > 0) {
|
|
key_n = ts_node_named_child(key_n, 0);
|
|
}
|
|
if (strcmp(ts_node_type(val_n), "literal_element") == 0 &&
|
|
ts_node_named_child_count(val_n) > 0) {
|
|
val_n = ts_node_named_child(val_n, 0);
|
|
}
|
|
char *key = cbm_node_text(ctx->arena, key_n, ctx->source);
|
|
if (!is_queue_topic_field(key)) {
|
|
continue;
|
|
}
|
|
const char *resolved = extract_string_value(ctx, val_n);
|
|
if (resolved && resolved[0]) {
|
|
return resolved;
|
|
}
|
|
// Value is a variable/expression (no constant value); use its source text
|
|
// as the queue/topic identity so the async edge still forms.
|
|
char *raw = cbm_node_text(ctx->arena, val_n, ctx->source);
|
|
if (raw && raw[0]) {
|
|
return raw;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Try to extract URL/topic from a keyword_argument or pair node.
|
|
static const char *extract_keyword_url(CBMExtractCtx *ctx, TSNode arg) {
|
|
TSNode key_node = ts_node_child_by_field_name(arg, TS_FIELD("name"));
|
|
TSNode val_node = ts_node_child_by_field_name(arg, TS_FIELD("value"));
|
|
if (ts_node_is_null(key_node)) {
|
|
key_node = ts_node_child_by_field_name(arg, TS_FIELD("key"));
|
|
}
|
|
if (ts_node_is_null(key_node) || ts_node_is_null(val_node)) {
|
|
return NULL;
|
|
}
|
|
char *key = cbm_node_text(ctx->arena, key_node, ctx->source);
|
|
if (!key || !is_url_or_topic_keyword(key)) {
|
|
return NULL;
|
|
}
|
|
return extract_string_value(ctx, val_node);
|
|
}
|
|
|
|
// Try to extract URL/topic from a positional argument (string or constant).
|
|
static const char *extract_positional_url(CBMExtractCtx *ctx, TSNode arg, const char *ak) {
|
|
if (is_string_like(ak)) {
|
|
char *text = cbm_node_text(ctx->arena, arg, ctx->source);
|
|
const char *validated = strip_and_validate_string_arg(ctx->arena, text);
|
|
if (validated) {
|
|
return validated;
|
|
}
|
|
}
|
|
if (strcmp(ak, "identifier") == 0) {
|
|
char *const_name = cbm_node_text(ctx->arena, arg, ctx->source);
|
|
if (const_name) {
|
|
return lookup_string_constant(ctx, const_name);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Extract URL/topic from keyword or positional args.
|
|
static const char *extract_url_or_topic_arg(CBMExtractCtx *ctx, TSNode args) {
|
|
uint32_t nc = ts_node_named_child_count(args);
|
|
for (uint32_t ai = 0; ai < nc; ai++) {
|
|
TSNode arg = ts_node_named_child(args, ai);
|
|
/* PHP and C# wrap each positional argument in an `argument` node;
|
|
* unwrap to the underlying value so the URL string is reachable. */
|
|
if (strcmp(ts_node_type(arg), "argument") == 0 && ts_node_named_child_count(arg) > 0) {
|
|
arg = ts_node_named_child(arg, 0);
|
|
}
|
|
const char *ak = ts_node_type(arg);
|
|
|
|
if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) {
|
|
const char *val = extract_keyword_url(ctx, arg);
|
|
if (val) {
|
|
return val;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* Cloud SDK dispatch via input struct: the queue/topic target is a field
|
|
* of a composite literal (Go `&sqs.SendMessageInput{QueueUrl: ...}`), not
|
|
* a bare string arg. Recover it so the async edge forms. */
|
|
if (strcmp(ak, "composite_literal") == 0 || strcmp(ak, "unary_expression") == 0) {
|
|
const char *val = extract_composite_queue_field(ctx, arg);
|
|
if (val) {
|
|
return val;
|
|
}
|
|
}
|
|
|
|
if (ai < MAX_POSITIONAL_SCAN) {
|
|
const char *val = extract_positional_url(ctx, arg, ak);
|
|
if (val) {
|
|
return val;
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Extract second argument name (handler ref for route registrations).
|
|
/* Normalize a string-form route handler to a resolvable handler name.
|
|
* 'showUsers' → showUsers
|
|
* 'UserController@show' → show (Laravel "Controller@method")
|
|
* The method segment after '@' is the resolvable function/method name. */
|
|
static const char *normalize_string_handler(CBMArena *a, const char *raw) {
|
|
const char *unq = strip_quotes(a, raw);
|
|
if (!unq || !unq[0]) {
|
|
return NULL;
|
|
}
|
|
const char *at = strchr(unq, '@');
|
|
if (at && at[1]) {
|
|
return cbm_arena_strdup(a, at + 1);
|
|
}
|
|
return unq;
|
|
}
|
|
|
|
static const char *extract_handler_arg(CBMExtractCtx *ctx, TSNode args) {
|
|
uint32_t nc = ts_node_named_child_count(args);
|
|
for (uint32_t ai = HANDLER_START_IDX; ai < nc && ai < MAX_HANDLER_SCAN; ai++) {
|
|
TSNode arg2 = ts_node_named_child(args, ai);
|
|
/* PHP wraps each argument in an `argument` node — unwrap to the value. */
|
|
if (strcmp(ts_node_type(arg2), "argument") == 0 && ts_node_named_child_count(arg2) > 0) {
|
|
arg2 = ts_node_named_child(arg2, 0);
|
|
}
|
|
const char *ak2 = ts_node_type(arg2);
|
|
/* `name` = PHP bare identifier handler; string = Laravel string handler
|
|
* ('showUsers' or 'Controller@method'). */
|
|
if (strcmp(ak2, "identifier") == 0 || strcmp(ak2, "member_expression") == 0 ||
|
|
strcmp(ak2, "selector_expression") == 0 || strcmp(ak2, "attribute") == 0 ||
|
|
strcmp(ak2, "field_expression") == 0 || strcmp(ak2, "name") == 0) {
|
|
return cbm_node_text(ctx->arena, arg2, ctx->source);
|
|
}
|
|
if (is_string_like(ak2)) {
|
|
const char *h =
|
|
normalize_string_handler(ctx->arena, cbm_node_text(ctx->arena, arg2, ctx->source));
|
|
if (h && h[0]) {
|
|
return h;
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Extract JSX component refs (uppercase tags) as CALLS edges.
|
|
static void extract_jsx_component_ref(CBMExtractCtx *ctx, TSNode node, const char *kind,
|
|
const char *enclosing_func_qn) {
|
|
if (strcmp(kind, "jsx_self_closing_element") != 0 && strcmp(kind, "jsx_opening_element") != 0) {
|
|
return;
|
|
}
|
|
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
|
if (ts_node_is_null(name_node)) {
|
|
return;
|
|
}
|
|
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
|
|
if (name && name[0] >= 'A' && name[0] <= 'Z') {
|
|
CBMCall call = {0};
|
|
call.callee_name = name;
|
|
call.enclosing_func_qn = enclosing_func_qn;
|
|
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
|
|
}
|
|
}
|
|
|
|
// Kotlin: `a OP b` desugars to an operator-method call `a.<method>(b)`. The
|
|
// generic call walk keys on call_expression nodes and so never sees these
|
|
// precedence-specific binary-expression nodes, leaving the type-aware LSP
|
|
// operator resolution (lsp_kt_operator -> the user `operator fun`) with no call
|
|
// site to attach to. Record a textual call to the operator method's bare name;
|
|
// the operator-token -> method mapping mirrors kotlin_lsp.c's binary handler so
|
|
// the names join. Builtin operands (Int+Int) resolve to a stdlib type with no
|
|
// graph node and drop, exactly as before — only user `operator fun`s gain edges.
|
|
static void extract_kotlin_operator_call(CBMExtractCtx *ctx, TSNode node, const char *kind,
|
|
const char *enclosing_func_qn) {
|
|
if (strcmp(kind, "binary_expression") != 0 && strcmp(kind, "additive_expression") != 0 &&
|
|
strcmp(kind, "multiplicative_expression") != 0 &&
|
|
strcmp(kind, "comparison_expression") != 0 && strcmp(kind, "equality_expression") != 0 &&
|
|
strcmp(kind, "range_expression") != 0) {
|
|
return;
|
|
}
|
|
uint32_t ncc = ts_node_named_child_count(node);
|
|
TSNode lhs = ts_node_child_by_field_name(node, TS_FIELD("left"));
|
|
TSNode rhs = ts_node_child_by_field_name(node, TS_FIELD("right"));
|
|
if (ts_node_is_null(lhs) && ncc >= 1) {
|
|
lhs = ts_node_named_child(node, 0);
|
|
}
|
|
if (ts_node_is_null(rhs) && ncc >= 2) {
|
|
rhs = ts_node_named_child(node, ncc - 1);
|
|
}
|
|
if (ts_node_is_null(lhs) || ts_node_is_null(rhs)) {
|
|
return;
|
|
}
|
|
uint32_t lhs_end = ts_node_end_byte(lhs);
|
|
uint32_t rhs_start = ts_node_start_byte(rhs);
|
|
if (rhs_start <= lhs_end) {
|
|
return;
|
|
}
|
|
const char *between = ctx->source + lhs_end;
|
|
size_t blen = (size_t)(rhs_start - lhs_end);
|
|
const char *op_method = NULL;
|
|
if (cbm_memmem(between, blen, "===", 3) || cbm_memmem(between, blen, "!==", 3)) {
|
|
return; // identity comparison: no operator method
|
|
} else if (cbm_memmem(between, blen, "==", 2) || cbm_memmem(between, blen, "!=", 2)) {
|
|
op_method = "equals";
|
|
} else if (cbm_memmem(between, blen, "..<", 3)) {
|
|
op_method = "rangeUntil";
|
|
} else if (cbm_memmem(between, blen, "..", 2)) {
|
|
op_method = "rangeTo";
|
|
} else if (cbm_memmem(between, blen, "<", 1) || cbm_memmem(between, blen, ">", 1)) {
|
|
op_method = "compareTo"; // covers <, >, <=, >=
|
|
} else if (cbm_memmem(between, blen, "+", 1)) {
|
|
op_method = "plus";
|
|
} else if (cbm_memmem(between, blen, "-", 1)) {
|
|
op_method = "minus";
|
|
} else if (cbm_memmem(between, blen, "*", 1)) {
|
|
op_method = "times";
|
|
} else if (cbm_memmem(between, blen, "/", 1)) {
|
|
op_method = "div";
|
|
} else if (cbm_memmem(between, blen, "%", 1)) {
|
|
op_method = "rem";
|
|
}
|
|
if (!op_method) {
|
|
return;
|
|
}
|
|
CBMCall call = {0};
|
|
call.callee_name = op_method;
|
|
call.enclosing_func_qn = enclosing_func_qn;
|
|
call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET;
|
|
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
|
|
}
|
|
|
|
// Kotlin convention-desugared calls that the call walk never sees as
|
|
// call_expressions: `val (a,b) = e` -> e.component1()/e.component2(); and
|
|
// `for (x in e)` -> e.iterator()/hasNext()/next(). Record textual calls to those
|
|
// operator-convention method names so the LSP's lsp_kt_destructure /
|
|
// lsp_kt_iterator resolutions have a call site to join (names match the LSP's).
|
|
static void kt_push_implicit_call(CBMExtractCtx *ctx, TSNode node, const char *callee,
|
|
const char *enclosing_func_qn) {
|
|
CBMCall call = {0};
|
|
call.callee_name = callee;
|
|
call.enclosing_func_qn = enclosing_func_qn;
|
|
call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET;
|
|
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
|
|
}
|
|
|
|
// C++ overloaded binary operator `a + b`: the operator method (`operator+`) is
|
|
// invoked implicitly, so the call walk never sees a call node. Synthesize a
|
|
// textual call to the bare operator name so the c-LSP's lsp_operator resolution
|
|
// (which keys the same `operator<tok>` member on the lhs type) has a call site to
|
|
// join. The operator token is the first unnamed child, mirroring c_lsp.c's binary
|
|
// handling. Builtin-operand expressions (int + int) synthesize an `operator+`
|
|
// callee too, but no such member exists so the call resolves to nothing and is
|
|
// dropped — no spurious edge.
|
|
static void extract_cpp_operator_call(CBMExtractCtx *ctx, TSNode node, const char *kind,
|
|
const char *enclosing_func_qn) {
|
|
if (strcmp(kind, "binary_expression") != 0) {
|
|
return;
|
|
}
|
|
TSNode lhs = ts_node_child_by_field_name(node, TS_FIELD("left"));
|
|
TSNode rhs = ts_node_child_by_field_name(node, TS_FIELD("right"));
|
|
if (ts_node_is_null(lhs) || ts_node_is_null(rhs)) {
|
|
return;
|
|
}
|
|
for (uint32_t i = 0; i < ts_node_child_count(node); i++) {
|
|
TSNode child = ts_node_child(node, i);
|
|
if (ts_node_is_named(child)) {
|
|
continue;
|
|
}
|
|
char *op = cbm_node_text(ctx->arena, child, ctx->source);
|
|
if (op && op[0]) {
|
|
CBMCall call = {0};
|
|
call.callee_name = cbm_arena_sprintf(ctx->arena, "operator%s", op);
|
|
call.enclosing_func_qn = enclosing_func_qn;
|
|
call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET;
|
|
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// C++ implicit calls that produce no textual call node: the destructor
|
|
// (`delete p`), the copy/move constructor (`T a = b;` copy-init), and the
|
|
// conversion operator (`if (obj)` where obj has `operator bool`). The c-LSP
|
|
// resolves each to the corresponding member but there is no call site to join
|
|
// to (callable=0). Synthesize a textual call sourced to the enclosing function
|
|
// so the lsp_{destructor,copy_constructor,conversion} resolution binds.
|
|
//
|
|
// - destructor: the callee QN embeds the type (`T.~T`), which is not textually
|
|
// available from `delete p`, so it joins via the reason gate — c_lsp stashes
|
|
// the operand text in `reason` and the synthesized callee is that same text.
|
|
// - copy constructor: the callee short-name is the constructed type (`T`),
|
|
// which IS textually present as the declaration's type — join by short-name.
|
|
// - conversion: the callee short-name is the type-independent `operator bool`.
|
|
//
|
|
// Spurious synthesis (a condition/operand that has no such member) resolves to
|
|
// nothing and is dropped, so no extra edge is produced.
|
|
static void extract_cpp_implicit_calls(CBMExtractCtx *ctx, TSNode node, const char *kind,
|
|
const char *enclosing_func_qn) {
|
|
const char *callee = NULL;
|
|
if (strcmp(kind, "delete_expression") == 0) {
|
|
TSNode operand = ts_node_child_by_field_name(node, TS_FIELD("argument"));
|
|
if (ts_node_is_null(operand) && ts_node_named_child_count(node) > 0) {
|
|
operand = ts_node_named_child(node, 0);
|
|
}
|
|
if (!ts_node_is_null(operand)) {
|
|
callee = cbm_node_text(ctx->arena, operand, ctx->source);
|
|
}
|
|
} else if (strcmp(kind, "if_statement") == 0 || strcmp(kind, "while_statement") == 0 ||
|
|
strcmp(kind, "do_statement") == 0) {
|
|
// `if (obj)` invokes obj's `operator bool`. Only a lone-identifier
|
|
// condition triggers it; comparisons/logical exprs evaluate to bool.
|
|
TSNode cond = ts_node_child_by_field_name(node, TS_FIELD("condition"));
|
|
if (!ts_node_is_null(cond)) {
|
|
TSNode inner = cond;
|
|
if (strcmp(ts_node_type(cond), "condition_clause") == 0 &&
|
|
ts_node_named_child_count(cond) == 1) {
|
|
inner = ts_node_named_child(cond, 0);
|
|
}
|
|
if (strcmp(ts_node_type(inner), "identifier") == 0) {
|
|
callee = "operator bool";
|
|
}
|
|
}
|
|
} else if (strcmp(kind, "declaration") == 0) {
|
|
// `T a = b;` — copy-init from an identifier invokes T's copy constructor.
|
|
TSNode type = ts_node_child_by_field_name(node, TS_FIELD("type"));
|
|
TSNode decl = ts_node_child_by_field_name(node, TS_FIELD("declarator"));
|
|
if (!ts_node_is_null(type) && !ts_node_is_null(decl) &&
|
|
strcmp(ts_node_type(decl), "init_declarator") == 0) {
|
|
TSNode value = ts_node_child_by_field_name(decl, TS_FIELD("value"));
|
|
if (!ts_node_is_null(value) && strcmp(ts_node_type(value), "identifier") == 0) {
|
|
char *tn = cbm_node_text(ctx->arena, type, ctx->source);
|
|
if (tn) {
|
|
const char *colon = strrchr(tn, ':');
|
|
callee = colon ? colon + 1 : tn;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (callee && callee[0]) {
|
|
CBMCall call = {0};
|
|
call.callee_name = callee;
|
|
call.enclosing_func_qn = enclosing_func_qn;
|
|
call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET;
|
|
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
|
|
}
|
|
}
|
|
|
|
static void extract_kotlin_desugared_calls(CBMExtractCtx *ctx, TSNode node, const char *kind,
|
|
const char *enclosing_func_qn) {
|
|
if (strcmp(kind, "property_declaration") == 0) {
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode c = ts_node_named_child(node, i);
|
|
if (strcmp(ts_node_type(c), "multi_variable_declaration") != 0) {
|
|
continue;
|
|
}
|
|
// One componentN() call per destructured variable.
|
|
uint32_t vc = ts_node_named_child_count(c);
|
|
uint32_t comp = 0;
|
|
for (uint32_t j = 0; j < vc; j++) {
|
|
TSNode v = ts_node_named_child(c, j);
|
|
if (strcmp(ts_node_type(v), "variable_declaration") != 0) {
|
|
continue;
|
|
}
|
|
comp++;
|
|
kt_push_implicit_call(ctx, node, cbm_arena_sprintf(ctx->arena, "component%u", comp),
|
|
enclosing_func_qn);
|
|
}
|
|
break;
|
|
}
|
|
} else if (strcmp(kind, "for_statement") == 0) {
|
|
kt_push_implicit_call(ctx, node, "iterator", enclosing_func_qn);
|
|
kt_push_implicit_call(ctx, node, "hasNext", enclosing_func_qn);
|
|
kt_push_implicit_call(ctx, node, "next", enclosing_func_qn);
|
|
}
|
|
}
|
|
|
|
// Java method reference `Lhs::name` (e.g. `String::length`, `Foo::new`). The
|
|
// call walk only visits call_expression-like nodes, so a method_reference never
|
|
// becomes a call and the LSP's lsp_method_ref resolution has no call site to
|
|
// attach to. Record a textual call to the referenced method's bare name (the
|
|
// constructor ref `Lhs::new` uses the unnamed `new` token); the LSP join then
|
|
// matches on the bare name. The referenced method IS invoked indirectly, so
|
|
// this is an accurate call edge (mirrors java_lsp.c resolve_method_reference).
|
|
static void extract_java_method_reference(CBMExtractCtx *ctx, TSNode node, const char *kind,
|
|
const char *enclosing_func_qn) {
|
|
if (strcmp(kind, "method_reference") != 0) {
|
|
return;
|
|
}
|
|
uint32_t nc = ts_node_named_child_count(node);
|
|
if (nc < 1) {
|
|
return;
|
|
}
|
|
char *mname = NULL;
|
|
if (nc >= 2) {
|
|
mname = cbm_node_text(ctx->arena, ts_node_named_child(node, nc - 1), ctx->source);
|
|
}
|
|
if (!mname || !mname[0]) {
|
|
mname = "new"; // constructor reference `Lhs::new` — `new` is unnamed
|
|
}
|
|
CBMCall call = {0};
|
|
call.callee_name = mname;
|
|
call.enclosing_func_qn = enclosing_func_qn;
|
|
call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET;
|
|
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
|
|
}
|
|
|
|
void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
|
|
if (!spec->call_node_types || !spec->call_node_types[0]) {
|
|
return;
|
|
}
|
|
|
|
if (cbm_kind_in_set(node, spec->call_node_types)) {
|
|
char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language);
|
|
// Keyword-filter callees, but keep builtins we mint a node for (len, str,
|
|
// ...) so the LSP-resolved builtin call still forms a CALLS edge.
|
|
if (callee && callee[0] &&
|
|
(!cbm_is_keyword(callee, ctx->language) ||
|
|
cbm_is_resolvable_builtin(callee, ctx->language))) {
|
|
CBMCall call = {0};
|
|
call.callee_name = callee;
|
|
call.enclosing_func_qn = state->enclosing_func_qn;
|
|
call.loop_depth = state->loop_depth; // enclosing loop nesting at this call
|
|
call.branch_depth = state->branch_depth; // enclosing branch nesting at this call
|
|
call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET;
|
|
// Perl-only: flag arrow/method calls ($obj->m / Class->m). The
|
|
// generic short-name resolver cannot place a method without a known
|
|
// receiver type, so the call-resolution pass suppresses those edges.
|
|
// Default false for every other language (struct is zero-init).
|
|
if (ctx->language == CBM_LANG_PERL &&
|
|
strcmp(ts_node_type(node), "method_call_expression") == 0) {
|
|
call.is_method = true;
|
|
}
|
|
// TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
|
|
// as the Perl flag above). Flag a member call x.foo() whose receiver
|
|
// is NOT `this`/`super`. When the TS-LSP cannot resolve the receiver
|
|
// type, the call-resolution pass suppresses weak short-name matches
|
|
// for these (so `re.test()` cannot fabricate an edge to a project
|
|
// `test`). this/super receivers stay unflagged — their target is the
|
|
// enclosing class, where a namespace-proximity weak match is usually
|
|
// right. Bare calls (helper()) and new_expression have no member
|
|
// receiver, so they keep is_method=false (struct is zero-init).
|
|
if ((ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TYPESCRIPT ||
|
|
ctx->language == CBM_LANG_TSX) &&
|
|
strcmp(ts_node_type(node), "call_expression") == 0) {
|
|
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
|
|
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "member_expression") == 0) {
|
|
TSNode obj = ts_node_child_by_field_name(fn, TS_FIELD("object"));
|
|
if (!ts_node_is_null(obj)) {
|
|
const char *ok = ts_node_type(obj);
|
|
if (strcmp(ok, "this") != 0 && strcmp(ok, "super") != 0) {
|
|
call.is_method = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
|
|
if (!ts_node_is_null(args)) {
|
|
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
|
|
/* #952: routes registered inside Laravel `prefix()->group()`
|
|
* closures must carry the composed path — the resolve passes
|
|
* only see the flat CBMCall, so the enclosing chain can only
|
|
* be read here where the AST still exists. */
|
|
if (ctx->language == CBM_LANG_PHP && call.first_string_arg &&
|
|
call.first_string_arg[0] == '/' && call.callee_name &&
|
|
cbm_service_pattern_route_method(call.callee_name) != NULL) {
|
|
const char *gp = php_group_prefix_for_call(ctx->arena, node, ctx->source);
|
|
if (gp && gp[0]) {
|
|
const char *rel = call.first_string_arg;
|
|
while (*rel == '/') {
|
|
rel++;
|
|
}
|
|
call.first_string_arg =
|
|
rel[0] ? cbm_arena_sprintf(ctx->arena, "%s/%s", gp, rel)
|
|
: cbm_arena_strndup(ctx->arena, gp, strlen(gp));
|
|
}
|
|
}
|
|
if (call.first_string_arg && call.first_string_arg[0] == '/') {
|
|
call.second_arg_name = extract_handler_arg(ctx, args);
|
|
}
|
|
extract_call_args(ctx, args, &call);
|
|
}
|
|
|
|
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
|
|
|
|
const char **dispatch_suffixes = cbm_string_dispatch_suffixes(ctx->language);
|
|
if (dispatch_suffixes && !ts_node_is_null(args)) {
|
|
const char *cn = call.callee_name;
|
|
size_t len = cn ? strlen(cn) : 0;
|
|
for (const char **nm = dispatch_suffixes; *nm; nm++) {
|
|
size_t nlen = strlen(*nm);
|
|
if (len >= nlen && strcmp(cn + len - nlen, *nm) == 0) {
|
|
const char *cls = extract_nth_string_arg(ctx, args, 0);
|
|
const char *mth = extract_nth_string_arg(ctx, args, 1);
|
|
if (cls && mth) {
|
|
CBMCall xcall = {0};
|
|
xcall.callee_name = cbm_arena_sprintf(ctx->arena, "%s.%s", cls, mth);
|
|
xcall.enclosing_func_qn = call.enclosing_func_qn;
|
|
cbm_calls_push(&ctx->result->calls, ctx->arena, xcall);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ctx->language == CBM_LANG_TSX || ctx->language == CBM_LANG_JAVASCRIPT) {
|
|
extract_jsx_component_ref(ctx, node, ts_node_type(node), state->enclosing_func_qn);
|
|
}
|
|
|
|
if (ctx->language == CBM_LANG_JAVA) {
|
|
extract_java_method_reference(ctx, node, ts_node_type(node), state->enclosing_func_qn);
|
|
}
|
|
|
|
if (ctx->language == CBM_LANG_KOTLIN) {
|
|
extract_kotlin_operator_call(ctx, node, ts_node_type(node), state->enclosing_func_qn);
|
|
extract_kotlin_desugared_calls(ctx, node, ts_node_type(node), state->enclosing_func_qn);
|
|
}
|
|
|
|
if (ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA) {
|
|
extract_cpp_operator_call(ctx, node, ts_node_type(node), state->enclosing_func_qn);
|
|
extract_cpp_implicit_calls(ctx, node, ts_node_type(node), state->enclosing_func_qn);
|
|
}
|
|
}
|