diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 9d32f391a7..4c31c0654b 100644 --- a/internal/cbm/callable_sig.c +++ b/internal/cbm/callable_sig.c @@ -7,6 +7,7 @@ * would misread: such a suffix degrades to its hashed form. */ #include "callable_sig.h" +#include "cbm.h" #include "helpers.h" #include #include @@ -22,20 +23,29 @@ enum { SIG_HASH_HEX = 16, /* FNV-1a 64 rendered as hex */ SIG_OPERATOR_LEN = 8, /* strlen("operator") */ SIG_CONST_LEN = 5, /* strlen("const") */ + SIG_ASYNC_LEN = 5, /* strlen("async") */ + SIG_ARROW_LEN = 2, /* strlen("=>") */ SIG_VOLATILE_LEN = 8, /* strlen("volatile") */ SIG_ARITY_DIGITS = 16, /* "(%d)" scratch */ SIG_CAPPED_TAIL = 1 + SIG_HASH_HEX + 1, /* "#" hex ")" */ + SIG_CHAR_LEN = 1, + SIG_SWIFT_DEFAULT_BITS = 64, }; static const uint64_t SIG_FNV_OFFSET = 0xcbf29ce484222325ULL; /* FNV-1a 64 basis */ static const uint64_t SIG_FNV_PRIME = 0x100000001b3ULL; static const char SIG_OPERATOR_CHARS[] = "+-*/%^&|~!=<>?[]"; +static const char SIG_TYPE_OPEN[] = "([<"; +static const char SIG_TYPE_CLOSE[] = ")]>"; CBMCallableIdentity cbm_callable_identity(CBMLanguage lang) { - /* Every language keeps its historical QN until its enable change lands - * (with the index-format bump that change carries). The planned modes: - * Java/Kotlin/C#/C++/CUDA/Scala TYPED, Swift LABELED_TYPED, ObjC LABELED, - * dynamic tier-2 languages ARITY. */ + /* Each language enables identity with its own index-format bump. */ + if (lang == CBM_LANG_SWIFT) { + return CBM_CALLABLE_ID_LABELED_TYPED; + } + /* Other languages keep their historical QN until their own enable changes + * land with an index-format bump. Planned modes: Java/Kotlin/C#/C++/CUDA/ + * Scala TYPED, ObjC LABELED, dynamic tier-2 languages ARITY. */ (void)lang; return CBM_CALLABLE_ID_NONE; } @@ -57,6 +67,7 @@ typedef struct { size_t open_off; /* offset of the parameter list's "(" */ size_t close_off; /* offset just past its ")" */ bool skip_names; /* nested C parameter lists: leave out parameter names */ + bool expand_swift_closure_aliases; } sig_ctx_t; static bool sig_ident_char(unsigned char ch) { @@ -79,7 +90,9 @@ static void sig_raw(sig_ctx_t *c, const char *s, size_t n) { c->overflow = true; return; } - memcpy(grown, c->buf, c->len); + if (c->len != 0) { + memcpy(grown, c->buf, c->len); + } c->buf = grown; c->cap = cap; } @@ -710,6 +723,131 @@ static void sig_kotlin_params(sig_ctx_t *c, TSNode node) { /* ── Swift ─────────────────────────────────────────────────────── */ +static bool sig_swift_alias_in_scope(const sig_ctx_t *c, TSNode scope, TSNode wanted, + TSNode *target) { + bool found = false; + uint32_t start = ts_node_start_byte(wanted); + uint32_t length = ts_node_end_byte(wanted) - start; + for (uint32_t i = 0; i < ts_node_named_child_count(scope); i++) { + TSNode alias = ts_node_named_child(scope, i); + if (strcmp(ts_node_type(alias), "typealias_declaration") != 0) { + continue; + } + TSNode name = sig_field(alias, "name"); + if (ts_node_is_null(name) || ts_node_end_byte(name) - ts_node_start_byte(name) != length || + memcmp(c->src + ts_node_start_byte(name), c->src + start, length) != 0) { + continue; + } + TSNode function = cbm_find_child_by_kind(alias, "function_type"); + if (ts_node_is_null(function)) { + *target = (TSNode){0}; + return true; + } + *target = function; + found = true; + } + return found; +} + +/* Only direct function aliases in the type/file lexical scope prove a closure. + * Keep their declared spelling in QNs; selectors need only their callable shape. */ +static TSNode sig_swift_selector_type(const sig_ctx_t *c, TSNode parameter, TSNode type) { + if (!c->expand_swift_closure_aliases || strcmp(ts_node_type(type), "user_type") != 0) { + return type; + } + TSNode wanted = {0}; + for (uint32_t i = 0; i < ts_node_named_child_count(type); i++) { + TSNode child = ts_node_named_child(type, i); + if (strcmp(ts_node_type(child), "type_identifier") == 0) { + if (!ts_node_is_null(wanted)) { + return type; /* qualified aliases need their declaring module */ + } + wanted = child; + } + } + if (ts_node_is_null(wanted)) { + return type; + } + for (TSNode parent = ts_node_parent(parameter); !ts_node_is_null(parent); + parent = ts_node_parent(parent)) { + const char *kind = ts_node_type(parent); + TSNode scope = {0}; + if (strcmp(kind, "source_file") == 0) { + scope = parent; + } else if (strcmp(kind, "class_declaration") == 0 || + strcmp(kind, "protocol_declaration") == 0) { + scope = sig_field(parent, "body"); + } + TSNode target = {0}; + if (!ts_node_is_null(scope) && sig_swift_alias_in_scope(c, scope, wanted, &target)) { + return ts_node_is_null(target) ? type : target; + } + } + return type; +} + +static void sig_swift_alias_parameters(sig_ctx_t *c, TSNode function) { + TSNode first = ts_node_child(function, 0); + if (ts_node_is_null(first) || !sig_node_text_is(c, first, "throws")) { + return; + } + /* The Swift grammar can attach the parameter tuple to @Sendable before throws. */ + TSNode modifiers = ts_node_prev_named_sibling(function); + if (ts_node_is_null(modifiers) || strcmp(ts_node_type(modifiers), "type_modifiers") != 0) { + return; + } + TSNode attribute = cbm_find_child_by_kind(modifiers, "attribute"); + if (ts_node_is_null(attribute) || ts_node_child_count(attribute) < 4 || + !sig_node_text_is(c, ts_node_named_child(attribute, 0), "Sendable") || + !sig_node_text_is(c, ts_node_child(attribute, 2), "(")) { + return; + } + for (uint32_t i = 2; i < ts_node_child_count(attribute); i++) { + sig_type_tokens(c, ts_node_child(attribute, i)); + } +} + +static void sig_swift_param_type(sig_ctx_t *c, TSNode p, TSNode internal) { + size_t before = c->len; + uint32_t count = ts_node_child_count(p); + for (uint32_t i = 0; i < count; i++) { + TSNode ch = ts_node_child(p, i); + const char *field = ts_node_field_name_for_child(p, i); + if (field && strcmp(field, "external_name") == 0) { + continue; + } + if (ts_node_eq(ch, internal)) { + continue; + } + /* A default expression selects call arity, not type identity. */ + if ((field && strcmp(field, "default_value") == 0) || sig_node_text_is(c, ch, "=")) { + break; + } + const char *k = ts_node_type(ch); + if (strcmp(k, "parameter_modifiers") == 0) { + uint32_t mc = ts_node_named_child_count(ch); + for (uint32_t j = 0; j < mc; j++) { + TSNode m = ts_node_named_child(ch, j); + uint32_t ms = ts_node_start_byte(m); + if (ts_node_end_byte(m) > ms && c->src[ms] != '@') { + sig_type_tokens(c, m); /* inout; @escaping etc. dropped */ + } + } + } else if (ts_node_is_named(ch)) { + TSNode type = sig_swift_selector_type(c, p, ch); + if (!ts_node_eq(type, ch)) { + sig_swift_alias_parameters(c, type); + } + sig_type_tokens(c, type); + } else if (sig_node_text_is(c, ch, "...")) { + sig_raw_str(c, sig_variadic_marker(c->lang)); + } + } + if (c->len == before) { + sig_raw(c, "?", SIG_CHAR_LEN); + } +} + static void sig_swift_param(sig_ctx_t *c, TSNode p) { TSNode external = sig_field(p, "external_name"); TSNode internal = {0}; @@ -727,35 +865,7 @@ static void sig_swift_param(sig_ctx_t *c, TSNode p) { /* One name is both label and internal name; `_` means unlabeled. */ sig_label_node(c, ts_node_is_null(external) ? internal : external); if (sig_writes_types(c)) { - size_t before = c->len; - for (uint32_t i = 0; i < count; i++) { - TSNode ch = ts_node_child(p, i); - const char *field = ts_node_field_name_for_child(p, i); - if (field && strcmp(field, "external_name") == 0) { - continue; - } - if (ts_node_eq(ch, internal)) { - continue; - } - const char *k = ts_node_type(ch); - if (strcmp(k, "parameter_modifiers") == 0) { - uint32_t mc = ts_node_named_child_count(ch); - for (uint32_t j = 0; j < mc; j++) { - TSNode m = ts_node_named_child(ch, j); - uint32_t ms = ts_node_start_byte(m); - if (ts_node_end_byte(m) > ms && c->src[ms] != '@') { - sig_type_tokens(c, m); /* inout; @escaping etc. dropped */ - } - } - } else if (ts_node_is_named(ch)) { - sig_type_tokens(c, ch); - } else if (sig_node_text_is(c, ch, "...")) { - sig_raw_str(c, sig_variadic_marker(c->lang)); - } - } - if (c->len == before) { - sig_raw(c, "?", 1); - } + sig_swift_param_type(c, p, internal); } sig_entry_close(c); } @@ -770,6 +880,152 @@ static void sig_swift_params(sig_ctx_t *c, TSNode node) { } } +/* Keep generic declarations and requirements in identity, without interpreting + * applicability at call sites. '/' preserves associated-type paths without + * introducing a QN separator; comments and whitespace use the token rules. */ +static TSNode sig_swift_extension_requirements(sig_ctx_t *c, TSNode node) { + for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent); + parent = ts_node_parent(parent)) { + if (strcmp(ts_node_type(parent), "class_declaration") == 0) { + return sig_node_text_is(c, sig_field(parent, "declaration_kind"), "extension") + ? cbm_find_child_by_kind(parent, "type_constraints") + : (TSNode){0}; + } + } + return (TSNode){0}; +} + +static bool sig_swift_generic_root(const sig_ctx_t *c, TSNode params, TSNode requirements, + const char *root, size_t len) { + TSNode lists[] = {params, requirements}; + for (size_t list = 0; list < sizeof(lists) / sizeof(lists[0]); list++) { + if (ts_node_is_null(lists[list])) { + continue; + } + for (uint32_t i = 0; i < ts_node_named_child_count(lists[list]); i++) { + TSNode entry = ts_node_named_child(lists[list], i); + if (ts_node_named_child_count(entry) == 0) { + continue; + } + TSNode name = ts_node_named_child(entry, 0); + if (list != 0) { + TSNode constrained = sig_field(name, "constrained_type"); + if (ts_node_is_null(constrained) || ts_node_named_child_count(constrained) == 0) { + continue; + } + name = ts_node_named_child(constrained, 0); + } + uint32_t start = ts_node_start_byte(name); + if (ts_node_end_byte(name) - start == len && memcmp(c->src + start, root, len) == 0) { + return true; + } + } + } + return false; +} + +static void sig_swift_requirements(sig_ctx_t *c, TSNode requirements, TSNode params) { + size_t start = c->len; + sig_type_tokens(c, requirements); + for (size_t i = start; i < c->len; i++) { + if (c->buf[i] != '.') { + continue; + } + size_t root = i; + while (root > start && (sig_ident_char((unsigned char)c->buf[root - SIG_CHAR_LEN]) || + c->buf[root - SIG_CHAR_LEN] == '/')) { + root--; + } + size_t len = 0; + while (root + len < i && sig_ident_char((unsigned char)c->buf[root + len])) { + len++; + } + if (sig_swift_generic_root(c, params, requirements, c->buf + root, len)) { + c->buf[i] = '/'; + } + } +} + +static void sig_swift_tparams(sig_ctx_t *c, TSNode node) { + TSNode params = cbm_find_child_by_kind(node, "type_parameters"); + TSNode requirements = cbm_find_child_by_kind(node, "type_constraints"); + TSNode extension = sig_swift_extension_requirements(c, node); + if (ts_node_is_null(params) && ts_node_is_null(requirements) && ts_node_is_null(extension)) { + return; + } + size_t start = c->len; + if (!ts_node_is_null(params)) { + sig_type_tokens(c, params); + if (c->len > start && c->buf[c->len - SIG_CHAR_LEN] == '>') { + c->buf[--c->len] = '\0'; + } + } else { + sig_raw_str(c, "<"); + } + if (!ts_node_is_null(requirements)) { + sig_raw_str(c, ";"); + sig_swift_requirements(c, requirements, params); + } + if (!ts_node_is_null(extension)) { + sig_raw_str(c, ";"); + sig_swift_requirements(c, extension, params); + } + sig_raw_str(c, ">"); +} + +static const char *sig_swift_dispatch(sig_ctx_t *c, TSNode node) { + TSNode modifiers = cbm_find_child_by_kind(node, "modifiers"); + uint32_t count = ts_node_is_null(modifiers) ? 0 : ts_node_child_count(modifiers); + for (uint32_t i = 0; i < count; i++) { + TSNode modifier = ts_node_child(modifiers, i); + if (sig_node_text_is(c, modifier, "static")) { + return "static"; + } + if (sig_node_text_is(c, modifier, "class")) { + return "class"; + } + } + return ts_node_is_null(cbm_find_child_by_kind(node, "class")) ? NULL : "class"; +} + +static bool sig_swift_parameter_has_default(TSNode parameter) { + uint32_t children = ts_node_child_count(parameter); + for (uint32_t i = 0; i < children; i++) { + const char *kind = ts_node_type(ts_node_child(parameter, i)); + if (strcmp(kind, "=") == 0 || strstr(kind, "default_value") != NULL) { + return true; + } + } + return false; +} + +uint64_t cbm_swift_default_mask(TSNode node, const char *source, uint8_t *count) { + (void)source; + uint64_t defaults = 0; + unsigned parameters = 0; + int last = -SIG_CHAR_LEN; + uint32_t children = ts_node_child_count(node); + for (uint32_t i = 0; i < children; i++) { + TSNode ch = ts_node_child(node, i); + const char *kind = ts_node_type(ch); + if (strcmp(kind, "parameter") == 0) { + last = (int)parameters++; + if (last < SIG_SWIFT_DEFAULT_BITS && sig_swift_parameter_has_default(ch)) { + defaults |= UINT64_C(1) << last; + } + } else if (last >= 0 && last < SIG_SWIFT_DEFAULT_BITS && strcmp(kind, "=") == 0) { + defaults |= UINT64_C(1) << last; + } else if (last >= 0 && (strcmp(kind, ")") == 0 || strcmp(kind, "function_body") == 0)) { + break; + } + } + if (count) { + /* ponytail: >64 parameters have no bitmask; use a dynamic mask if Swift + * code with that many parameters needs overload call resolution. */ + *count = parameters > SIG_SWIFT_DEFAULT_BITS ? UINT8_MAX : (uint8_t)parameters; + } + return defaults; +} /* ── Scala ─────────────────────────────────────────────────────── */ static void sig_scala_params(sig_ctx_t *c, TSNode node) { @@ -852,10 +1108,12 @@ static void sig_generic_params(sig_ctx_t *c, TSNode node) { /* ── Spelling pass ─────────────────────────────────────────────── */ -/* Qualified type paths keep their last segment; "->" becomes "=>"; any other +/* Qualified type paths keep their last segment. + * "->" becomes "=>"; any other * '.' or "::" (a receiver function type, a leading global scope) is dropped, * so the suffix contract holds whatever the grammar produced. */ -static size_t sig_spell(char *s, size_t n) { +static size_t sig_spell(char *s, size_t n, CBMLanguage lang) { + (void)lang; size_t out = 0; size_t i = 0; while (i < n) { @@ -958,6 +1216,8 @@ static void sig_render(sig_ctx_t *c, TSNode node) { } if (c->lang == CBM_LANG_CSHARP) { sig_cs_tparams(c, node); + } else if (c->lang == CBM_LANG_SWIFT) { + sig_swift_tparams(c, node); } c->open_off = c->len; sig_raw(c, "(", 1); @@ -986,6 +1246,29 @@ static void sig_render(sig_ctx_t *c, TSNode node) { } sig_raw(c, ")", 1); c->close_off = c->len; + if (c->lang == CBM_LANG_SWIFT) { + const char *dispatch = sig_swift_dispatch(c, node); + if (dispatch) { + sig_raw_str(c, dispatch); + } + /* Only direct declaration children: a parameter's async closure type + * is already in params and must not mark the enclosing function. */ + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; i++) { + if (sig_node_text_is(c, ts_node_child(node, i), "async")) { + if (dispatch) { + sig_raw_str(c, " "); + } + sig_raw_str(c, "async"); + break; + } + } + TSNode result = sig_field(node, "return_type"); + if (!ts_node_is_null(result) && sig_writes_types(c)) { + sig_raw_str(c, "=>"); + sig_type_tokens(c, result); + } + } } static bool sig_roundtrips(const char *suffix, size_t n) { @@ -1005,6 +1288,32 @@ static const char *sig_hashed(CBMArena *a, const char *full, size_t n) { return cbm_arena_sprintf(a, "(#%016llx)", (unsigned long long)sig_fnv64(full, n)); } +const char *cbm_swift_selector(CBMArena *a, TSNode node, const char *source) { + if (!a || !source || ts_node_is_null(node)) { + return NULL; + } + sig_ctx_t c = { + .arena = a, + .src = source, + .lang = CBM_LANG_SWIFT, + .mode = CBM_CALLABLE_ID_LABELED_TYPED, + .expand_swift_closure_aliases = true, + }; + sig_raw_str(&c, "("); + sig_swift_params(&c, node); + sig_raw_str(&c, ")"); + TSNode result = sig_field(node, "return_type"); + if (!ts_node_is_null(result)) { + sig_raw_str(&c, "=>"); + sig_type_tokens(&c, result); + } + if (c.overflow) { + return NULL; + } + (void)sig_spell(c.buf, c.len, CBM_LANG_SWIFT); + return c.buf; +} + const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *source, CBMLanguage lang, CBMCallableIdentity mode) { if (!a || !source || ts_node_is_null(func_node) || mode == CBM_CALLABLE_ID_NONE) { @@ -1035,7 +1344,7 @@ const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *sou if (!spelled) { return NULL; } - size_t n = sig_spell(spelled, full); + size_t n = sig_spell(spelled, full, lang); if (n <= CBM_CALLABLE_SIG_MAX) { return sig_roundtrips(spelled, n) ? spelled : sig_hashed(a, spelled, n); } @@ -1046,12 +1355,12 @@ const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *sou int limit = c.too_many ? SIG_MAX_ENTRIES : c.entries; for (int i = 0; i < limit; i++) { char *prefix = cbm_arena_strndup(a, c.buf, c.entry_end[i]); - c.entry_end[i] = prefix ? sig_spell(prefix, c.entry_end[i]) : 0; + c.entry_end[i] = prefix ? sig_spell(prefix, c.entry_end[i], lang) : 0; } char *open_prefix = cbm_arena_strndup(a, c.buf, c.open_off); char *close_prefix = cbm_arena_strndup(a, c.buf, c.close_off); - c.open_off = open_prefix ? sig_spell(open_prefix, c.open_off) : 0; - c.close_off = close_prefix ? sig_spell(close_prefix, c.close_off) : 0; + c.open_off = open_prefix ? sig_spell(open_prefix, c.open_off, lang) : 0; + c.close_off = close_prefix ? sig_spell(close_prefix, c.close_off, lang) : 0; c.buf = spelled; c.len = n; const char *capped = sig_capped(a, &c, spelled, n); @@ -1119,13 +1428,94 @@ size_t cbm_qn_callable_base_len_named(const char *qn, const char *name) { return anchored ? base : len; } +/* Locate only a declaration's return marker, never a nested closure arrow or + * an arrow in generic requirements. Shared by inverse and Swift registry. */ +static bool sig_return_type_valid(const char *type) { + char stack[SIG_DEPTH_LIMIT]; + unsigned depth = 0; + if (!type[0] || strpbrk(type, ".#;{}") || strstr(type, "::") || strstr(type, "->")) { + return false; + } + for (size_t i = 0; type[i]; i++) { + char ch = type[i]; + if (ch == '>' && i > 0 && type[i - SIG_CHAR_LEN] == '=') { + continue; + } + const char *closing = strchr(SIG_TYPE_CLOSE, ch); + if (strchr(SIG_TYPE_OPEN, ch)) { + if (depth == SIG_DEPTH_LIMIT) { + return false; + } + stack[depth++] = ch; + } else if (closing) { + if (!depth) { + return false; + } + depth--; + if (stack[depth] != SIG_TYPE_OPEN[closing - SIG_TYPE_CLOSE]) { + return false; + } + } + } + return depth == 0; +} + +static size_t sig_swift_qualifiers_start(const char *text, size_t end) { + static const char *const qualifiers[] = {"async", "static", "class"}; + for (size_t i = 0; i < sizeof(qualifiers) / sizeof(qualifiers[0]); i++) { + size_t len = strlen(qualifiers[i]); + if (end > len && memcmp(text + end - len, qualifiers[i], len) == 0 && + (text[end - len - 1] == ')' || text[end - len - 1] == ' ')) { + end -= len; + while (end && text[end - 1] == ' ') { + end--; + } + } + } + return end; +} + +size_t cbm_callable_return_offset(const char *suffix) { + size_t len = strlen(suffix); + if (!strstr(suffix, "=>")) { + return len; + } + int parens = 0; + int brackets = 0; + int angles = 0; + for (size_t i = 0; i + SIG_CHAR_LEN < len; i++) { + char ch = suffix[i]; + bool arrow = ch == '=' && suffix[i + SIG_CHAR_LEN] == '>'; + size_t params_end = sig_swift_qualifiers_start(suffix, i); + bool after_params = params_end && suffix[params_end - SIG_CHAR_LEN] == ')'; + if (arrow && after_params && parens == 0 && brackets == 0 && angles == 0) { + return sig_return_type_valid(suffix + i + SIG_ARROW_LEN) ? i : 0; + } + if (ch == '(') { + parens++; + } else if (ch == ')') { + parens--; + } else if (ch == '[') { + brackets++; + } else if (ch == ']') { + brackets--; + } else if (ch == '<' && + (angles > 0 || sig_ident_char((unsigned char)suffix[i + SIG_CHAR_LEN]))) { + angles++; + } else if (ch == '>' && angles > 0 && (i == 0 || suffix[i - SIG_CHAR_LEN] != '=')) { + angles--; + } + } + return len; +} + size_t cbm_qn_callable_base_len(const char *qn) { if (!qn) { return 0; } size_t len = strlen(qn); /* Trailing cvref (C++): const / volatile / & / && after the ')'. */ - size_t t = len; + size_t t = sig_swift_qualifiers_start(qn, cbm_callable_return_offset(qn)); for (;;) { if (t > 0 && qn[t - 1] == '&') { t--; @@ -1171,7 +1561,7 @@ size_t cbm_qn_callable_base_len(const char *qn) { size_t q = p; while (q > 0) { q--; - if (qn[q] == '>') { + if (qn[q] == '>' && (q == 0 || qn[q - SIG_CHAR_LEN] != '=')) { adepth++; } else if (qn[q] == '<') { adepth--; diff --git a/internal/cbm/callable_sig.h b/internal/cbm/callable_sig.h index c7b4d64fc9..da6c485477 100644 --- a/internal/cbm/callable_sig.h +++ b/internal/cbm/callable_sig.h @@ -3,7 +3,11 @@ * * A callable's graph identity is * - * callable_qn := base_qn [] (params) [cvref] + * callable_qn := base_qn [] (params) [cvref/static/class] [async] [=>return_type] + * Swift includes generic declarations and where requirements in + * (associated-type paths use '/' instead of '.') and appends function-level + * async after params and an explicit return type after "=>". Qualified types keep their last + * segment. These distinguish declarations, not call applicability. * * so overloads that share a base QN (Java/C#/C++/Kotlin/Swift/Scala/ObjC) * become distinct nodes instead of merging into one node that collects every @@ -22,8 +26,8 @@ * - it is whitespace-normalized: one space only between two identifier * characters ("unsigned long", "const T&"), none elsewhere; * - type spelling: comments, annotations and attributes are dropped; - * qualified type paths keep their last segment (java.util.List -> List, - * std::vector -> vector); "->" in function types is spelled "=>"; a + * qualified paths keep their last segment (java.util.List + * -> List, std::vector -> vector); "->" in function types is spelled "=>"; a * variadic/rest marker is "[]" for Java (the JVM array it is) and "~" * everywhere else; C++ top-level cv-qualifiers are dropped and array * parameters decay to pointers, as the language itself does; @@ -38,6 +42,7 @@ #include "arena.h" #include "tree_sitter/api.h" #include +#include typedef enum { CBM_CALLABLE_ID_NONE = 0, /* historical base QN, no suffix */ @@ -49,20 +54,26 @@ typedef enum { enum { CBM_CALLABLE_SIG_MAX = 200 }; -/* The identity mode a language uses for callable QNs. Every language is - * CBM_CALLABLE_ID_NONE until its enable change lands (with its index-format - * bump). Kept as a side table, like cbm_string_dispatch_suffixes, rather than - * a CBMLangSpec field: the ~160 positional lang_specs rows would all have to - * spell the new member under -Wmissing-field-initializers. */ +/* The identity mode a language uses for callable QNs. Languages remain + * CBM_CALLABLE_ID_NONE until their enable change lands with an index-format + * bump; Swift is enabled by #2061. Kept as a side table, like + * cbm_string_dispatch_suffixes, rather than a CBMLangSpec field: the ~160 + * positional lang_specs rows would all have to spell the new member under + * -Wmissing-field-initializers. */ CBMCallableIdentity cbm_callable_identity(CBMLanguage lang); +/* Swift parameter defaults are call-resolution metadata, not graph identity. + * Returns a bit per parameter; count > 64 is reported as 255. */ +uint64_t cbm_swift_default_mask(TSNode func_node, const char *source, uint8_t *count); +/* Uncapped parameter and return spelling: matching and chains must not parse a hashed identity. */ +const char *cbm_swift_selector(CBMArena *a, TSNode func_node, const char *source); /* The identity suffix for the callable at `func_node` in the language's own * mode, or NULL when that mode is NONE (or the node carries no parameter * information the mode can use). Arena-owned. */ const char *cbm_callable_sig(CBMArena *a, TSNode func_node, const char *source, CBMLanguage lang); -/* Same, with an explicit mode — the builder the enable changes and the golden - * tables exercise before a language is switched on. */ +/* Same, with an explicit mode — the builder's golden tables exercise modes + * independently of each language's production enablement. */ const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *source, CBMLanguage lang, CBMCallableIdentity mode); @@ -72,6 +83,9 @@ const char *cbm_callable_sig_mode(CBMArena *a, TSNode func_node, const char *sou * name, not a suffix. NULL -> 0. */ size_t cbm_qn_callable_base_len(const char *qn); +/* Top-level declaration return marker; strlen(suffix) if absent, 0 if malformed. */ +size_t cbm_callable_return_offset(const char *suffix); + /* The inverse anchored to the callable's bare `name` (the node's name * column): the suffix is stripped only when the base QN is `name` or ends in * "." / "::" + `name`. Other QN text can end in ')' as well — a Java field diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 88fa02145a..3df903cb4f 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -3331,6 +3331,73 @@ static const char *extract_parse_bytes(CBMArena *a, CBMLanguage language, const return source; } +/* The Swift grammar rejects conditional directives in type bodies. Mask only + * real AST directive tokens, so strings/comments and source coordinates survive. */ +static bool swift_mask_conditional_node(CBMArena *a, TSNode node, const char *source, + int source_len, char **masked) { + const char *kind = ts_node_type(node); + if (strcmp(kind, "#if") == 0 || strcmp(kind, "#elseif") == 0 || strcmp(kind, "#else") == 0 || + strcmp(kind, "#endif") == 0) { + if (!*masked) { + *masked = cbm_arena_strndup(a, source, (size_t)source_len); + if (!*masked) { + return false; + } + } + for (uint32_t i = ts_node_start_byte(node); + i < (uint32_t)source_len && source[i] != '\n' && source[i] != '\r'; i++) { + (*masked)[i] = ' '; + } + } else if (*masked && + (strcmp(kind, "comment") == 0 || strcmp(kind, "multiline_comment") == 0)) { + for (uint32_t i = ts_node_start_byte(node); + i < ts_node_end_byte(node) && i < (uint32_t)source_len; i++) { + (*masked)[i] = source[i]; + } + } + return true; +} + +static const char *swift_mask_conditional_directives(CBMArena *a, TSNode root, const char *source, + int source_len) { + char *masked = NULL; + TSTreeCursor cursor = ts_tree_cursor_new(root); + bool more = true; + while (more) { + TSNode node = ts_tree_cursor_current_node(&cursor); + if (!swift_mask_conditional_node(a, node, source, source_len, &masked)) { + break; + } + if (ts_tree_cursor_goto_first_child(&cursor)) { + continue; + } + while (!ts_tree_cursor_goto_next_sibling(&cursor)) { + if (!ts_tree_cursor_goto_parent(&cursor)) { + more = false; + break; + } + } + } + ts_tree_cursor_delete(&cursor); + return masked ? masked : source; +} + +static TSTree *swift_retry_conditional_parse(CBMArena *a, TSParser *parser, TSTree *tree, + CBMLanguage language, const char *source, + int source_len, TSParseOptions opts) { + if (!tree || language != CBM_LANG_SWIFT || !ts_node_has_error(ts_tree_root_node(tree))) { + return tree; + } + const char *masked = + swift_mask_conditional_directives(a, ts_tree_root_node(tree), source, source_len); + if (masked == source) { + return tree; + } + ts_tree_delete(tree); + ts_parser_reset(parser); + return cbm_parse_source(parser, masked, (uint32_t)source_len, opts); +} + /* Initial block for the per-file traversal scratch arena, chosen by measuring * arena_grow on a 14k-file TypeScript tree: it fires on one file in 12,000 at * both this size and at 1 MB, and on most files at 256 KB, where the two @@ -3634,6 +3701,61 @@ int cbm_test_enclosing_sweep_mismatches(const CBMFileResult *result) { } #endif +/* Extension visibility uses the file's direct imports, including imports + * written after a type declaration. The joined text is shared by its types. */ +static void cbm_swift_type_imports(CBMFileResult *result, CBMLanguage language) { + if (language != CBM_LANG_SWIFT) { + return; + } + size_t length = SKIP_ONE; + for (int i = 0; i < result->imports.count; i++) { + const char *path = result->imports.items[i].module_path; + if (path && path[0]) { + length += strlen(path) + SKIP_ONE; + } + } + char *imports = cbm_arena_alloc(&result->arena, length); + if (!imports) { + return; + } + size_t used = 0; + for (int i = 0; i < result->imports.count; i++) { + const char *path = result->imports.items[i].module_path; + if (!path || !path[0]) { + continue; + } + if (used) { + imports[used++] = '|'; + } + size_t n = strlen(path); + memcpy(imports + used, path, n); + used += n; + } + imports[used] = '\0'; + for (int i = 0; i < result->defs.count; i++) { + CBMDefinition *def = &result->defs.items[i]; + size_t count = 0; + bool is_type = false; + for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) { + is_type = is_type || strcmp(kv[0], "swift_type_path") == 0 || + strcmp(kv[0], "swift_extension") == 0; + count += PAIR_LEN; + } + if (!is_type) { + continue; + } + const char **props = + cbm_arena_alloc(&result->arena, (count + PAIR_LEN + SKIP_ONE) * sizeof(*props)); + if (props) { + memcpy(props, def->extra_props, count * sizeof(*props)); + props[count] = "swift_imports"; + props[count + SKIP_ONE] = imports; + props[count + PAIR_LEN] = NULL; + def->extra_props = props; + } + } +} + static CBMFileResult *extract_file_ex_body(const char *source, int source_len, CBMLanguage language, const char *project, const char *rel_path, int64_t timeout_micros, const char **extra_defines, @@ -3814,6 +3936,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C cbm_sql_values_kept_ranges(source, (uint32_t)source_len, &sql_kept) && ts_parser_set_included_ranges(parser, sql_kept.items, sql_kept.count); TSTree *tree = cbm_parse_source(parser, source, (uint32_t)source_len, opts); + tree = swift_retry_conditional_parse(a, parser, tree, language, source, source_len, opts); if (sql_ranged) { (void)ts_parser_set_included_ranges(parser, NULL, 0); } @@ -3901,6 +4024,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C cbm_extract_imports(&ctx); cbm_extract_python_namespace_facts(&ctx); cbm_extract_unified(&ctx); + cbm_swift_type_imports(result, language); if (cbm_lang_needs_cpp_branch_views(language)) { extract_cpp_branch_views(&ctx, ts_lang, timeout_micros); } diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 5d1abc2157..8dfb4167ed 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -289,6 +289,10 @@ typedef struct { * qualified_name (base QN = the first qn_sig_off bytes); 0 = no suffix. * Always 0 until a language enables its callable_identity mode. */ uint32_t qn_sig_off; + /* Call matching: defaulted parameters, indexed in declaration order. */ + uint64_t param_default_mask; + uint8_t selector_param_count; + const char *call_selector; /* C# only: declared namespace of a TOP-LEVEL type, "" for the global * namespace (proven only by a clean parse), NULL for nested types, an * unknown namespace and every other language. Per type, because the @@ -340,6 +344,9 @@ typedef struct { } CBMCallArg; #define CBM_MAX_CALL_ARGS 8 +/* Swift trailing-closure capture bound (#2061): more than this on one call + * marks the call truncated and the overload matcher fails closed. */ +#define CBM_MAX_TRAILING_CLOSURES 8 /* The qualified name of a C-preprocessor macro (`#define NAME ...` in C, C++, * CUDA, Objective-C, GLSL, ISPC) is `.#macro`. C keeps macros @@ -396,6 +403,20 @@ typedef struct { // (self.compiler.apply_converters()). An object the // class owns, not a parameter: read by the weak-member // guard's unique-name exemption. Default false. + bool has_trailing_closure; + bool args_truncated; + /* Swift multi-trailing-closure capture (#2061). `has_trailing_closure` + * stays the "there is at least one trailing closure" flag manual callers + * and existing tests set; `trailing_closure_count` is the actual number + * captured (0 => fall back to the bool, treated as one unlabelled closure). + * `trailing_closure_labels[i]` is closure i's argument label from the + * grammar's `lambda_literal (simple_identifier ':')*` sequence (NULL when + * unlabelled; entry 0 is always unlabelled). More closures than + * CBM_MAX_TRAILING_CLOSURES sets `trailing_closure_truncated`, and the + * overload matcher then fails closed rather than guessing a partial set. */ + uint8_t trailing_closure_count; + bool trailing_closure_truncated; + const char **trailing_closure_labels; } CBMCall; // What an import statement names, when the syntax says so. Only PHP's @@ -554,8 +575,10 @@ typedef struct { typedef struct { const char *var_name; // variable being assigned - const char *type_name; // class/type name of RHS constructor - const char *enclosing_func_qn; // QN of enclosing function + const char *type_name; // declared type or RHS constructor type + const char *enclosing_func_qn; // enclosing function, class, or module QN + uint32_t value_start_byte; // Swift call initializer span; type_name may be unknown + uint32_t value_end_byte; // exclusive; zero when no initializer call was recorded } CBMTypeAssign; // String reference: URL, config key, or async target found in source. diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 1c5b21678d..b3cd2c44af 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -427,10 +427,42 @@ static char *extract_wolfram_callee(CBMArena *a, TSNode node, const char *source // Language-specific callee extraction for FP and niche languages. // Swift callee extraction from call/constructor expressions. +static TSNode swift_trailing_closure_inner(TSNode node) { + if (ts_node_is_null(node) || strcmp(ts_node_type(node), "call_expression") != 0) { + return (TSNode){0}; + } + TSNode inner = ts_node_named_child(node, 0); + if (ts_node_is_null(inner) || strcmp(ts_node_type(inner), "call_expression") != 0) { + return (TSNode){0}; + } + TSNode suffix = cbm_find_child_by_kind(node, "call_suffix"); + if (ts_node_is_null(suffix) || + !ts_node_is_null(cbm_find_child_by_kind(suffix, "value_arguments")) || + (ts_node_is_null(cbm_find_child_by_kind(suffix, "lambda_literal")) && + ts_node_is_null(cbm_find_child_by_kind(node, "lambda_literal")))) { + return (TSNode){0}; + } + return inner; +} + +static TSNode swift_call_base(TSNode node) { + TSNode inner = swift_trailing_closure_inner(node); + while (!ts_node_is_null(inner)) { + node = inner; + inner = swift_trailing_closure_inner(node); + } + return node; +} + 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; } + TSNode wrapped = swift_trailing_closure_inner(ts_node_parent(node)); + if (!ts_node_is_null(wrapped) && ts_node_eq(wrapped, node)) { + return NULL; + } + node = swift_call_base(node); if (ts_node_named_child_count(node) > 0) { TSNode callee = ts_node_named_child(node, 0); const char *ck = ts_node_type(callee); @@ -2566,6 +2598,7 @@ static void process_keyword_arg(CBMExtractCtx *ctx, TSNode arg_node, CBMCallArg } } +static TSNode swift_argument_value(TSNode arg); static const char *extract_string_value(CBMExtractCtx *ctx, TSNode val_node, bool url_context); /* Whether a call's callee is an HTTP client or a global fetch, which selects @@ -2665,6 +2698,26 @@ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) { CBMCallArg *ca = &call->args[call->arg_count]; memset(ca, 0, sizeof(*ca)); + if (ctx->language == CBM_LANG_SWIFT && strcmp(ak, "value_argument") == 0) { + TSNode first = ts_node_named_child(arg_node, 0); + if (!ts_node_is_null(first) && + strcmp(ts_node_type(first), "value_argument_label") == 0) { + TSNode label = ts_node_named_child(first, 0); + if (ts_node_is_null(label)) { + label = first; + } + ca->keyword = cbm_node_text(ctx->arena, label, ctx->source); + if (ca->keyword) { + size_t n = strlen(ca->keyword); + if (n > 0 && ca->keyword[n - SKIP_ONE] == ':') { + ca->keyword = cbm_arena_strndup(ctx->arena, ca->keyword, n - SKIP_ONE); + } + } + } + arg_node = swift_argument_value(arg_node); + ak = ts_node_type(arg_node); + } + if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) { process_keyword_arg(ctx, arg_node, ca); ca->index = positional_idx++; @@ -3970,6 +4023,7 @@ static TSNode objectscript_call_args(TSNode node) { * instead. A trailing closure has a call_suffix with no value_arguments, which * returns a null node and leaves the call without a string argument, as before. */ static TSNode swift_call_args(TSNode node) { + node = swift_call_base(node); TSNode suffix = cbm_find_child_by_kind(node, "call_suffix"); if (ts_node_is_null(suffix)) { return (TSNode){0}; @@ -3977,6 +4031,79 @@ static TSNode swift_call_args(TSNode node) { return cbm_find_child_by_kind(suffix, "value_arguments"); } +/* Store one bounded trailing-closure label; closure zero is unlabelled. */ +static void swift_store_trailing_label(CBMExtractCtx *ctx, CBMCall *call, uint32_t index, + const char *label, bool has_label) { + if (index == 0) { + call->has_trailing_closure = true; + return; + } + if (index >= CBM_MAX_TRAILING_CLOSURES) { + call->trailing_closure_truncated = true; + return; + } + if (!call->trailing_closure_labels) { + size_t bytes = CBM_MAX_TRAILING_CLOSURES * sizeof(char *); + call->trailing_closure_labels = cbm_arena_calloc(ctx->arena, bytes); + } + if (!call->trailing_closure_labels) { + call->trailing_closure_truncated = true; + return; + } + call->trailing_closure_labels[index] = label; + if (has_label && !label) { + call->trailing_closure_truncated = true; + } +} + +/* Swift records trailing closures on the call_suffix: the hidden + * _fn_call_lambda_arguments rule inlines to + * `lambda_literal (simple_identifier ':')*`, so every closure literal, and the + * label that precedes all but the first, sit flat among the suffix's children + * (after an optional value_arguments). Each lambda_literal is a trailing + * closure; a simple_identifier directly before a closure is that closure's + * label. Closure 0 is always unlabelled. More closures than the bounded + * storage marks the call truncated so the overload matcher fails closed. */ +static void swift_capture_trailing_closures(CBMExtractCtx *ctx, TSNode call_node, TSNode suffix, + CBMCall *call) { + if (ts_node_has_error(call_node)) { + call->trailing_closure_truncated = true; + } + uint32_t children = ts_node_child_count(suffix) + ts_node_child_count(call_node); + uint32_t closures = 0; + const char *pending_label = NULL; + bool has_pending_label = false; + for (uint32_t i = 0; i < children; i++) { + uint32_t suffix_children = ts_node_child_count(suffix); + TSNode child = i < suffix_children ? ts_node_child(suffix, i) + : ts_node_child(call_node, i - suffix_children); + if (i >= suffix_children && ts_node_start_byte(child) < ts_node_end_byte(suffix)) { + continue; + } + const char *kind = ts_node_type(child); + if (ts_node_is_missing(child) || ts_node_has_error(child)) { + call->trailing_closure_truncated = true; + } + if (strcmp(kind, "lambda_literal") == 0) { + swift_store_trailing_label(ctx, call, closures, pending_label, has_pending_label); + closures++; + pending_label = NULL; + has_pending_label = false; + } else if (strcmp(kind, "simple_identifier") == 0) { + if (has_pending_label) { + call->trailing_closure_truncated = true; + } + pending_label = cbm_node_text(ctx->arena, child, ctx->source); + has_pending_label = true; + } + } + if (has_pending_label) { + call->trailing_closure_truncated = true; + } + call->trailing_closure_count = + (uint8_t)(closures > CBM_MAX_TRAILING_CLOSURES ? CBM_MAX_TRAILING_CLOSURES : closures); +} + static bool node_has_token(TSNode node, const char *token) { uint32_t count = ts_node_child_count(node); for (uint32_t i = 0; i < count; i++) { @@ -4389,7 +4516,8 @@ static const char *swift_superclass_init_callee(CBMExtractCtx *ctx, const WalkSt static bool swift_callee_is_initializer(const char *callee) { static const char suffix[] = ".init"; size_t n = callee ? strlen(callee) : 0; - return n > sizeof(suffix) - 1 && strcmp(callee + n - (sizeof(suffix) - 1), suffix) == 0; + return n > sizeof(suffix) - SKIP_ONE && + strcmp(callee + n - (sizeof(suffix) - SKIP_ONE), suffix) == 0; } /* Swift initializer delegation (#2379). `super.init(...)` and `self.init(...)` @@ -4404,10 +4532,9 @@ static bool swift_callee_is_initializer(const char *callee) { * initializer guard in handle_calls keeps a short-name guess from doing so. * * `self.init` delegates to another initializer of the caller's own type. - * Initializer overloads share one node (#2061), so the only edge it could form - * is a self-loop, and that one is false whenever the overload it calls lives - * outside the project (`extension Notification { init(...) { self.init(...) } }` - * calls Foundation's). It forms no CALLS edge. */ + * Keep #2395's no-edge policy until overload-aware delegation is addressed + * separately; the delegated initializer can live outside the project + * (`extension Notification { init(...) { self.init(...) } }` calls Foundation's). */ static const char *resolve_swift_delegating_init(CBMExtractCtx *ctx, const WalkState *state, const char *callee) { if (ctx->language != CBM_LANG_SWIFT || !callee) { @@ -4444,7 +4571,8 @@ static CBMPrimaryCalleeSelection select_primary_callee(CBMExtractCtx *ctx, TSNod return selection; } - selection.expr = language_specific_callee_expr(ctx->language, node); + TSNode callee_node = ctx->language == CBM_LANG_SWIFT ? swift_call_base(node) : node; + selection.expr = language_specific_callee_expr(ctx->language, callee_node); selection.expr = unwrap_await_callee(selection.expr); if (is_dynamic_callee_expr(ctx, selection.expr)) { selection.expr = (TSNode){0}; @@ -4692,6 +4820,23 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML if (ts_node_is_null(args) && ctx->language == CBM_LANG_SWIFT) { args = swift_call_args(node); } + if (ctx->language == CBM_LANG_SWIFT) { + TSNode suffix = cbm_find_child_by_kind(node, "call_suffix"); + if (!ts_node_is_null(suffix)) { + swift_capture_trailing_closures(ctx, node, suffix, &call); + } + uint32_t value_arg_count = 0; + if (!ts_node_is_null(args)) { + uint32_t named_count = ts_node_named_child_count(args); + for (uint32_t ai = 0; ai < named_count; ai++) { + TSNode arg = ts_node_named_child(args, ai); + value_arg_count += strcmp(ts_node_type(arg), "value_argument") == 0; + } + } + if (value_arg_count > CBM_MAX_CALL_ARGS) { + call.args_truncated = true; + } + } if (!ts_node_is_null(args)) { call.first_string_arg = extract_url_or_topic_arg(ctx, args, call.callee_name); /* #1146: a Laravel registration's slashless URI ('users', '') diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 8fbba89614..213b9250fb 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -1,4 +1,5 @@ #include "cbm.h" +#include "callable_sig.h" #include "arena.h" // CBMArena, cbm_arena_alloc/strdup/sprintf #include "doclink.h" // cbm_doclink_note_doc_line #include "helpers.h" @@ -7150,6 +7151,21 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec def.is_test = rust_def_is_test(def.decorators); } + if (ctx->language == CBM_LANG_SWIFT) { + if (!def.qualified_name) { + return; + } + const char *sig = cbm_callable_sig(a, func_node, ctx->source, ctx->language); + const char *qualified = sig ? cbm_arena_sprintf(a, "%s%s", def.qualified_name, sig) : NULL; + if (qualified) { + def.qn_sig_off = (uint32_t)strlen(def.qualified_name); + def.qualified_name = qualified; + } + def.param_default_mask = + cbm_swift_default_mask(func_node, ctx->source, &def.selector_param_count); + def.call_selector = cbm_swift_selector(ctx->arena, func_node, ctx->source); + } + // C++/CUDA: GoogleTest macros are test functions (#1266). if (is_gtest || def.test_role == CBM_TEST_ROLE_CASE) { def.is_test = true; @@ -8800,6 +8816,22 @@ static void emit_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *s if (ctx->language == CBM_LANG_CSHARP && !ctx->enclosing_class_qn) { def.decl_namespace = cs_type_decl_namespace(a, node, ctx->source); } + if (ctx->language == CBM_LANG_SWIFT && ctx->module_qn) { + TSNode kind_node = ts_node_child_by_field_name(node, TS_FIELD("declaration_kind")); + size_t module_len = strlen(ctx->module_qn); + if (strncmp(class_qn, ctx->module_qn, module_len) == 0 && class_qn[module_len] == '.') { + const char **props = cbm_arena_alloc(a, (PAIR_LEN + SKIP_ONE) * sizeof(*props)); + if (props) { + props[0] = + !ts_node_is_null(kind_node) && strcmp(ts_node_type(kind_node), "extension") == 0 + ? "swift_extension" + : "swift_type_path"; + props[1] = class_qn + module_len + 1; + props[2] = NULL; + def.extra_props = props; + } + } + } def.base_classes = extract_base_classes(a, node, ctx->source, ctx->language); // Go identity alias `type A = B` (#2033): keep the aliased type text so the // cross-file registries can set alias_of. A defined type `type A B` is a @@ -9265,6 +9297,20 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node, memset(&def, 0, sizeof(def)); def.name = name; def.qualified_name = method_qn; + if (ctx->language == CBM_LANG_SWIFT) { + if (!method_qn) { + return; + } + const char *sig = cbm_callable_sig(a, child, ctx->source, ctx->language); + const char *qualified = sig ? cbm_arena_sprintf(a, "%s%s", method_qn, sig) : NULL; + if (qualified) { + def.qn_sig_off = (uint32_t)strlen(method_qn); + def.qualified_name = qualified; + } + def.param_default_mask = + cbm_swift_default_mask(child, ctx->source, &def.selector_param_count); + def.call_selector = cbm_swift_selector(ctx->arena, child, ctx->source); + } def.label = "Method"; def.file_path = ctx->rel_path; def.parent_class = class_qn; diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index c19debd806..6b00783cb1 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -2293,6 +2293,18 @@ static void parse_generic_imports(CBMExtractCtx *ctx, const char *node_type) { continue; } + if (ctx->language == CBM_LANG_SWIFT) { + /* Attributes and scoped import kinds precede the actual module path. */ + TSNode identifier = cbm_find_child_by_kind(node, "identifier"); + const char *path = ts_node_is_null(identifier) + ? NULL + : cbm_node_text(ctx->arena, identifier, ctx->source); + if (path && path[0]) { + CBMImport imp = {.local_name = path_last(ctx->arena, path), .module_path = path}; + cbm_imports_push(&ctx->result->imports, ctx->arena, imp); + } + continue; + } if (!try_generic_path_fields(ctx, node)) { generic_import_from_text(ctx, node); } diff --git a/internal/cbm/extract_type_assigns.c b/internal/cbm/extract_type_assigns.c index 7ac30ee1d9..b58dc36c2c 100644 --- a/internal/cbm/extract_type_assigns.c +++ b/internal/cbm/extract_type_assigns.c @@ -102,7 +102,7 @@ static void try_emit_type_assign(CBMExtractCtx *ctx, TSNode var_node, TSNode rhs const char *type_name = extract_constructor_type(ctx->arena, rhs_node, ctx->source, ctx->language); if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; + CBMTypeAssign ta = {0}; ta.var_name = var_name; ta.type_name = type_name; ta.enclosing_func_qn = func_qn; @@ -204,9 +204,90 @@ void cbm_extract_type_assigns(CBMExtractCtx *ctx) { walk_type_assigns(ctx, ctx->root, spec); } +static void swift_parameter_binding(TSNode node, TSNode *name, TSNode *type) { + for (uint32_t i = 0; i < ts_node_child_count(node); i++) { + TSNode child = ts_node_child(node, i); + const char *field = ts_node_field_name_for_child(node, i); + if (!field || strcmp(field, "name") != 0) { + continue; + } + if (strcmp(ts_node_type(child), "simple_identifier") == 0) { + *name = child; + } else { + *type = child; + } + } +} + +static bool swift_initializer_is_call(TSNode value) { + while (!ts_node_is_null(value)) { + const char *kind = ts_node_type(value); + if (strcmp(kind, "call_expression") == 0 || strcmp(kind, "call") == 0) { + return true; + } + if ((strcmp(kind, "try_expression") != 0 && strcmp(kind, "await_expression") != 0) || + ts_node_named_child_count(value) == 0) { + return false; + } + value = ts_node_named_child(value, ts_node_named_child_count(value) - SKIP_ONE); + } + return false; +} + // --- Unified handler --- void handle_type_assigns(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { process_type_assign_node(ctx, node, spec, state->enclosing_func_qn); + if (ctx->language != CBM_LANG_SWIFT) { + return; + } + const char *kind = ts_node_type(node); + bool parameter = strcmp(kind, "parameter") == 0; + if (!parameter && strcmp(kind, "property_declaration") != 0) { + return; + } + TSNode name = {0}; + TSNode type = {0}; + TSNode value = {0}; + if (parameter) { + swift_parameter_binding(node, &name, &type); + } else { + value = ts_node_child_by_field_name(node, TS_FIELD("value")); + TSNode pattern = ts_node_child_by_field_name(node, TS_FIELD("name")); + if (!ts_node_is_null(pattern)) { + name = cbm_find_child_by_kind(pattern, "simple_identifier"); + } + TSNode annotation = cbm_find_child_by_kind(node, "type_annotation"); + if (!ts_node_is_null(annotation)) { + type = ts_node_child_by_field_name(annotation, TS_FIELD("name")); + } + } + const char *type_name = + ts_node_is_null(type) ? NULL : cbm_node_text(ctx->arena, type, ctx->source); + if (!type_name && !parameter) { + if (!ts_node_is_null(value)) { + type_name = extract_constructor_type(ctx->arena, value, ctx->source, ctx->language); + if (strcmp(ts_node_type(value), "array_literal") == 0) { + type_name = "[?]"; + } + } + } + bool initializer_call = swift_initializer_is_call(value); + if (!ts_node_is_null(name) && (type_name || initializer_call)) { + const char *scope = state->enclosing_func_qn; + if (scope == ctx->module_qn && state->enclosing_class_qn) { + scope = state->enclosing_class_qn; + } + CBMTypeAssign assignment = { + .var_name = cbm_node_text(ctx->arena, name, ctx->source), + .type_name = type_name, + .enclosing_func_qn = scope, + }; + if (initializer_call) { + assignment.value_start_byte = ts_node_start_byte(value); + assignment.value_end_byte = ts_node_end_byte(value); + } + cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, assignment); + } } diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index 6c27719d37..37f8cfa959 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -1,6 +1,7 @@ #include "extract_unified.h" #include "arena.h" // cbm_arena_sprintf #include "cbm.h" // CBMExtractCtx +#include "callable_sig.h" #include "helpers.h" #include "extract_url_fold.h" // cbm_url_fold_record_constant (#706, #1147) #include "lang_specs.h" // CBMLangSpec, cbm_lang_spec, CBM_LANG_* @@ -1167,6 +1168,13 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan * an in-body call sources to a QN one or more segments short of the def, and * the edge is dropped at write. */ const char *qn_name = name; + if (ctx->language == CBM_LANG_SWIFT) { + const char *sig = cbm_callable_sig(ctx->arena, node, ctx->source, ctx->language); + const char *qualified = sig ? cbm_arena_sprintf(ctx->arena, "%s%s", name, sig) : NULL; + if (qualified) { + qn_name = qualified; + } + } if (ctx->language == CBM_LANG_NIX) { qn_name = cbm_nix_qn_name(ctx->arena, node, ctx->source, name); if (!qn_name || !qn_name[0]) { diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 150a4e39ab..04f9944130 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -570,9 +570,11 @@ static const char *objc_assign_types[] = {"assignment_expression", NULL}; // protocol_function_declaration: a protocol's method requirements. Needed here as // well as in the name resolvers — extract_class_methods gates on this set, so // without the entry the requirement is walked and then discarded. -static const char *swift_func_types[] = {"function_declaration", "protocol_function_declaration", - "macro_declaration", "init_declaration", - "deinit_declaration", NULL}; +static const char *swift_func_types[] = { + "function_declaration", "protocol_function_declaration", + "macro_declaration", "init_declaration", + "deinit_declaration", NULL, +}; // KNOWN GAP: struct_declaration and enum_declaration are not node types in the // vendored tree-sitter-swift grammar — it models both as class_declaration — so // these two entries never match anything, and a bare Swift `enum` is labeled diff --git a/internal/cbm/result_compact.c b/internal/cbm/result_compact.c index 46bf6eae14..7d79c66e16 100644 --- a/internal/cbm/result_compact.c +++ b/internal/cbm/result_compact.c @@ -276,6 +276,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) { cr_str(c, &d->http_client); cr_str(c, &d->http_base_url); cr_str(c, &d->variants); + cr_str(c, &d->call_selector); cr_list(c, &d->extra_props); cr_str(c, &d->decl_namespace); cr_str(c, &d->variant_key); @@ -303,6 +304,12 @@ static void cr_walk_call(cr_ctx_t *c, CBMCall *call) { call->args = NULL; call->arg_count = 0; } + /* Swift trailing-closure labels are a bounded counted string list. */ + int labels = call->trailing_closure_count; + if (labels > CBM_MAX_TRAILING_CLOSURES) { + labels = CBM_MAX_TRAILING_CLOSURES; + } + cr_counted_list(c, &call->trailing_closure_labels, labels); } /* Facts have counted lists, not sentinel arrays. Bail before touching a failed diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index ee9c6d5f9d..04249bd220 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -11,6 +11,9 @@ * 2. Method suffix → determines HTTP method (get→GET, post→POST) */ #include "service_patterns.h" +#include "callable_sig.h" +#include "foundation/constants.h" +#include "foundation/mem_core.h" #include #include @@ -1147,25 +1150,18 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) { } cbm_svc_kind_t result = CBM_SVC_NONE; - const lib_pattern_t *p; - /* Route registration checked first — prevents gin/echo from matching * as HTTP clients (both have .get/.post suffixes). */ - if ((p = match_qn(resolved_qn, route_reg_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, http_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, async_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, config_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, grpc_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, graphql_libraries))) - result = p->kind; - else if ((p = match_qn(resolved_qn, trpc_libraries))) - result = p->kind; - + const lib_pattern_t *const tables[] = {route_reg_libraries, http_libraries, async_libraries, + config_libraries, grpc_libraries, graphql_libraries, + trpc_libraries}; + for (size_t i = 0; i < sizeof(tables) / sizeof(tables[0]); i++) { + const lib_pattern_t *p = match_qn(resolved_qn, tables[i]); + if (p) { + result = p->kind; + break; + } + } if (_svc_cache) { char *kdup = strdup(resolved_qn); if (kdup) @@ -1174,6 +1170,25 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) { return result; } +cbm_svc_kind_t cbm_service_pattern_match_named(const char *resolved_qn, const char *name) { + if (!resolved_qn || !name) { + return cbm_service_pattern_match(resolved_qn); + } + size_t base_len = cbm_qn_callable_base_len_named(resolved_qn, name); + if (base_len == strlen(resolved_qn)) { + return cbm_service_pattern_match(resolved_qn); + } + char *base = cbm_alloc(CBM_MEM_CLASS_EXTRACT, base_len + SKIP_ONE); + if (!base) { + return CBM_SVC_NONE; + } + memcpy(base, resolved_qn, base_len); + base[base_len] = '\0'; + cbm_svc_kind_t result = cbm_service_pattern_match(base); + cbm_free(CBM_MEM_CLASS_EXTRACT, base); + return result; +} + const char *cbm_service_pattern_http_method(const char *callee_name) { if (!callee_name) { return NULL; diff --git a/internal/cbm/service_patterns.h b/internal/cbm/service_patterns.h index 0a182568e0..d8ca2359f8 100644 --- a/internal/cbm/service_patterns.h +++ b/internal/cbm/service_patterns.h @@ -34,6 +34,7 @@ void cbm_service_patterns_init(void); * Matches on library name substrings in the QN (e.g., "requests" in * "project.venv.requests.api.get"). Import-alias transparent. */ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn); +cbm_svc_kind_t cbm_service_pattern_match_named(const char *resolved_qn, const char *name); /* True for a bare, unqualified call to the native `fetch()` API. Deliberately * NOT part of the substring tables above: those are matched unconditionally diff --git a/src/graph_buffer/graph_buffer.c b/src/graph_buffer/graph_buffer.c index 670717ac18..b496b79e63 100644 --- a/src/graph_buffer/graph_buffer.c +++ b/src/graph_buffer/graph_buffer.c @@ -1234,6 +1234,56 @@ static int gb_qn_claim_rank(const char *label) { return (label && strcmp(label, "Macro") == 0) ? 0 : 1; } +/* A Swift declaration and its same-file extensions share one QN. Keep the + * declaration's metadata even when a later extension wins the source span. */ +static char *gb_swift_type_props(const char *preferred, const char *other) { + if (!preferred || !other || !strstr(preferred, "\"swift_") || !strstr(other, "\"swift_")) { + return NULL; + } + yyjson_doc *first = yyjson_read_opts((char *)preferred, strlen(preferred), + YYJSON_READ_NUMBER_AS_RAW, &gb_yy_alc, NULL); + yyjson_doc *second = + yyjson_read_opts((char *)other, strlen(other), YYJSON_READ_NUMBER_AS_RAW, &gb_yy_alc, NULL); + yyjson_val *left = yyjson_doc_get_root(first); + yyjson_val *right = yyjson_doc_get_root(second); + yyjson_val *declaration = yyjson_is_str(yyjson_obj_get(left, "swift_type_path")) ? left : right; + const char *target = yyjson_get_str(yyjson_obj_get(left, "swift_extension")); + if (!target) { + target = yyjson_get_str(yyjson_obj_get(right, "swift_extension")); + } + const char *path = yyjson_get_str(yyjson_obj_get(declaration, "swift_type_path")); + char *merged = NULL; + if (path && target && strcmp(path, target) == 0) { + yyjson_mut_doc *doc = yyjson_mut_doc_new(&gb_yy_alc); + if (doc) { + yyjson_mut_val *root = yyjson_val_mut_copy(doc, declaration); + if (root) { + yyjson_mut_obj_remove_key(root, "swift_extension"); + if (yyjson_mut_obj_add_strcpy(doc, root, "swift_extension", target)) { + yyjson_mut_doc_set_root(doc, root); + merged = yyjson_mut_write_opts(doc, 0, &gb_yy_alc, NULL, NULL); + } + } + yyjson_mut_doc_free(doc); + } + } + yyjson_doc_free(second); + yyjson_doc_free(first); + return merged; +} + +static const char *gb_swift_merge_node_props(cbm_gbuf_node_t *existing, const char *arrival, + bool arrival_wins) { + char *merged = arrival_wins ? gb_swift_type_props(arrival, existing->properties_json) + : gb_swift_type_props(existing->properties_json, arrival); + if (!merged) { + return arrival; + } + cbm_free(CBM_MEM_CLASS_GBUF_STRING, existing->properties_json); + existing->properties_json = merged; + return merged; +} + int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name, const char *qualified_name, const char *file_path, int start_line, int end_line, const char *properties_json) { @@ -1295,6 +1345,7 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name } gb_def_t arrival = {label, properties_json, file_path, start_line, end_line}; gb_note_collision(gb, existing, &arrival, NULL); + properties_json = gb_swift_merge_node_props(existing, properties_json, c <= 0); if (c > 0) { return existing->id; /* existing entity is the canonical winner */ } @@ -2077,6 +2128,7 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing, gb_def_t src_def = {sn->label, sn->properties_json, sn->file_path, sn->start_line, sn->end_line}; gb_note_collision(dst, existing, &src_def, sn_spans); + const char *properties = gb_swift_merge_node_props(existing, sn->properties_json, sn_wins); if (sn_wins) { /* src's properties; the variant spans both carry are written on * export (cbm_gbuf_settle_variants). */ @@ -2105,8 +2157,7 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing, existing->start_line = sn->start_line; existing->end_line = sn->end_line; if (sn->properties_json) { - cbm_free(CBM_MEM_CLASS_GBUF_STRING, existing->properties_json); - existing->properties_json = heap_strdup(sn->properties_json); + (void)cbm_gbuf_node_set_properties_json(existing, properties); } if (label_changed) { node_ptr_array_t *by_label = get_or_create_node_array( diff --git a/src/pipeline/lsp_surface.c b/src/pipeline/lsp_surface.c index 62c35f3249..d6dc2fe705 100644 --- a/src/pipeline/lsp_surface.c +++ b/src/pipeline/lsp_surface.c @@ -17,9 +17,11 @@ * bytes is the early-cutoff key: a body edit reserializes identically. */ #include "pipeline/lsp_surface.h" +#include "foundation/constants.h" #include "pipeline/pipeline_internal.h" #include +#include #include #include #include @@ -417,6 +419,29 @@ static char *surface_file_to_json(const CBMFileResult *result, CBMLanguage langu } yyjson_mut_obj_add_val(doc, root, "reg", reg); + yyjson_mut_val *swift = NULL; + for (int i = 0; result && i < result->defs.count; i++) { + const CBMDefinition *d = &result->defs.items[i]; + if (!d->qn_sig_off || !d->qualified_name) { + continue; + } + if (!swift) { + swift = yyjson_mut_arr(doc); + } + yyjson_mut_val *o = yyjson_mut_obj(doc); + yyjson_mut_obj_add_str(doc, o, "q", d->qualified_name); + char defaults[CBM_SZ_16 + SKIP_ONE]; + (void)snprintf(defaults, sizeof(defaults), "%016llx", + (unsigned long long)d->param_default_mask); + yyjson_mut_obj_add_strcpy(doc, o, "d", defaults); + yyjson_mut_obj_add_int(doc, o, "p", d->selector_param_count); + yyjson_mut_obj_add_str(doc, o, "s", d->call_selector ? d->call_selector : ""); + yyjson_mut_arr_add_val(swift, o); + } + if (swift) { + yyjson_mut_obj_add_val(doc, root, "callables", swift); + } + /* #1916: an axios instance binding's baseURL is consumed by the files * that import it (their HTTP_CALLS compose base + path), so a changed * base must change the surface or those importers keep stale edges. diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index c60f92555f..4e665c60eb 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -18,6 +18,8 @@ enum { PC_REGEX_GRP = 2, PC_SINGLE_CANDIDATE_COUNT = 1 }; +static const double PC_SWIFT_SINGLE_CONF = 0.90; +static const double PC_SWIFT_AMBIGUOUS_CONF = 0.55; /* Confidence for a service-pattern HTTP/ASYNC edge emitted when registry * resolution is empty (external, unindexed client library) — see #523. */ #define PC_SVC_PATTERN_CONF 0.5 @@ -687,11 +689,32 @@ static int64_t create_svc_route_node(cbm_pipeline_ctx_t *ctx, const char *url, c * by the parallel formatter itself (emit_http_async_edge), because route/service * edge props feed full-only predump passes (create_route_nodes / * create_data_flows) and must not differ between the two passes. */ -/* Append a ,"args":[{"i":0,"e":"","v":""},...] field onto a CALLS +/* Append a ,"args":[{"i":0,"k":"","e":"","v":""},...] field onto a CALLS * edge's JSON props (the props buffer ends in '}'). The sequential pass omitted * this, so data_flow mode had no argument expressions to surface for small * (< 50 file) repos that take the sequential path (#514). Mirrors the parallel * path's append_args_json shape so both pipelines agree. */ +static int calls_format_arg(char *one, size_t cap, const CBMCallArg *arg, const char *prefix) { + char expr[CBM_SZ_256]; + cbm_json_escape(expr, sizeof(expr), arg->expr ? arg->expr : ""); + char keyword[CBM_SZ_256] = ""; + if (arg->keyword) { + char escaped[CBM_SZ_128]; + cbm_json_escape(escaped, sizeof(escaped), arg->keyword); + int length = snprintf(keyword, sizeof(keyword), ",\"k\":\"%s\"", escaped); + if (length <= 0 || (size_t)length >= sizeof(keyword)) { + return 0; + } + } + if (arg->value) { + char value[CBM_SZ_256]; + cbm_json_escape(value, sizeof(value), arg->value); + return snprintf(one, cap, "%s{\"i\":%d%s,\"e\":\"%s\",\"v\":\"%s\"}", prefix, arg->index, + keyword, expr, value); + } + return snprintf(one, cap, "%s{\"i\":%d%s,\"e\":\"%s\"}", prefix, arg->index, keyword, expr); +} + static void calls_append_args(char *props, size_t cap, const CBMCall *call) { if (!call || call->arg_count <= 0) { return; @@ -708,19 +731,8 @@ static void calls_append_args(char *props, size_t cap, const CBMCall *call) { } pos += (size_t)n; for (int i = 0; i < call->arg_count; i++) { - const CBMCallArg *a = &call->args[i]; - char esc_e[CBM_SZ_256]; - cbm_json_escape(esc_e, sizeof(esc_e), a->expr ? a->expr : ""); char one[CBM_SZ_512]; - if (a->value) { - char esc_v[CBM_SZ_256]; - cbm_json_escape(esc_v, sizeof(esc_v), a->value); - n = snprintf(one, sizeof(one), "%s{\"i\":%d,\"e\":\"%s\",\"v\":\"%s\"}", - i > 0 ? "," : "", a->index, esc_e, esc_v); - } else { - n = snprintf(one, sizeof(one), "%s{\"i\":%d,\"e\":\"%s\"}", i > 0 ? "," : "", a->index, - esc_e); - } + n = calls_format_arg(one, sizeof(one), &call->args[i], i > 0 ? "," : ""); /* Add rather than subtract: pos is unsigned, so `cap - pos - PAIR_LEN` * wraps once pos reaches cap - PAIR_LEN and stops bounding the memcpy * below. The closing write at the end of this function already guards @@ -840,7 +852,8 @@ static bool emit_classified_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call, const char **imp_keys, const char **imp_vals, int imp_count, bool suppress_plain_calls, const char *route_mount, CBMLanguage lang) { - cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name); + cbm_svc_kind_t svc = + cbm_service_pattern_match_named(res->qualified_name, target ? target->name : NULL); if (svc == CBM_SVC_ROUTE_REG && route_reg_path(call, lang) != NULL) { return handle_route_registration(ctx, call, source, module_qn, imp_keys, imp_vals, imp_count, route_mount, lang); @@ -1170,15 +1183,52 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM return SKIP_ONE; } - /* #2053: a Rust call the LSP placed on an EXTERNAL symbol (std's - * Path::join, a seeded crate API) is resolved — it just has no graph node. - * The textual registry would bind it to a same-named project method - * instead, so skip it and let the empty-resolution service fallbacks below - * classify the call. MUST match pass_parallel.c. */ + /* Swift label-compatible overload selection (#2061). count > 0: emit an + * edge to every compatible candidate and stop. count < 0: project symbols + * share this name but none matches the call's labels — a bare-name + * registry match would bind a WRONG overload, so resolution is skipped + * and the call falls through as unresolved; the empty-resolution service + * fallbacks below still run, mirroring the Swift block in pass_parallel.c + * (service edges survive overload suppression, per #523/#606/#856). + * count == 0: no overload metadata for this name — resolve normally. */ + int swift_candidates = 0; + if (lang == CBM_LANG_SWIFT) { + const char *candidates[CBM_SZ_256]; + swift_candidates = + cbm_registry_swift_candidates_in_file(ctx->registry, call, result, ctx->gbuf, module_qn, + imp_vals, imp_count, candidates, CBM_SZ_256); + if (swift_candidates > 0) { + int emitted = 0; + for (int i = 0; i < swift_candidates; i++) { + const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, candidates[i]); + if (target && target->id != source_node->id) { + cbm_resolution_t selected = {.qualified_name = candidates[i], + .strategy = "swift_labels", + .confidence = swift_candidates == SKIP_ONE + ? PC_SWIFT_SINGLE_CONF + : PC_SWIFT_AMBIGUOUS_CONF, + .candidate_count = swift_candidates}; + evidence->candidate_qn = + swift_candidates == SKIP_ONE ? target->qualified_name : NULL; + evidence->calls_emitted |= emit_classified_edge( + ctx, call, source_node, target, &selected, module_qn, imp_keys, imp_vals, + imp_count, false, route_mount, lang); + emitted++; + } + } + return emitted > 0 ? SKIP_ONE : 0; + } + } + + /* A Rust call the LSP placed on an EXTERNAL symbol has no graph node. The + * textual registry must not bind it to a same-named project method. */ bool rust_external = lsp && cbm_pipeline_rust_external_target( lang, lsp->strategy, lsp->callee_qn, ctx->project_name); - cbm_resolution_t res = calls_registry_resolution( - ctx, call, module_qn, imp_keys, imp_vals, imp_count, lang, rust_external, php_vendor_call); + cbm_resolution_t res = {0}; + if (swift_candidates >= 0) { + res = calls_registry_resolution(ctx, call, module_qn, imp_keys, imp_vals, imp_count, lang, + rust_external, php_vendor_call); + } /* `p->open(fd)`: the registry does not split a callee on the arrow, and a * member is not a name it could resolve anyway. The object's type decides, * exactly as for the dot call handled further down. MUST match diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 7c7aa438f4..0f937eea56 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -16,6 +16,7 @@ enum { PD_RING = 4, PD_RING_MASK = 3, PD_JSON_MARGIN = 10, PD_ESC_MARGIN = 3, PD /* Fixed bytes around a serialized JSON field: ,"key":"value" / ,"key":[...] * -> comma + 2 key quotes + colon + 2 value quotes (resp. brackets). */ enum { PD_JSON_FIELD_OVERHEAD = 6 }; +enum { PD_SWIFT_PROPS_MARGIN = 80 }; #include "pipeline/pipeline.h" #include #include @@ -308,6 +309,13 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) return; } size_t pos = (size_t)n; + if (def->qn_sig_off && bufsize - pos > PD_SWIFT_PROPS_MARGIN) { + pos += (size_t)snprintf(buf + pos, bufsize - pos, + ",\"param_default_mask\":\"%016llx\",\"selector_param_count\":%u", + (unsigned long long)def->param_default_mask, + (unsigned)def->selector_param_count); + } + append_json_string(buf, bufsize, &pos, "call_selector", def->call_selector); append_json_string(buf, bufsize, &pos, "docstring", def->docstring); /* The buffer is sized for every field below (pd_props_buf), so none can be * squeezed out. */ @@ -389,6 +397,7 @@ static size_t pd_arr_field_len(const char *key, const char **arr) { * allocated. Twin of pass_parallel.c -- keep both in sync. */ static char *pd_props_buf(const CBMDefinition *def, char *stack, size_t *size) { size_t need = PD_PROPS_PREFIX_MAX + pd_str_field_len("docstring", def->docstring); + need += pd_str_field_len("call_selector", def->call_selector); if (def->variants && def->variants[0]) { need += strlen("variants") + strlen(def->variants) + PD_JSON_FIELD_OVERHEAD; } @@ -518,6 +527,11 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label, lang, module_prefix_len); } + if (def->qn_sig_off) { + cbm_registry_set_swift_signature(ctx->registry, def->qualified_name, + def->param_default_mask, def->selector_param_count, + def->call_selector); + } } char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__"); const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn); @@ -1214,6 +1228,9 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t } } + cbm_registry_build_swift_owners(ctx->registry, ctx->gbuf, ctx->repo_path, ctx->excluded_dirs, + ctx->excluded_count); + cbm_log_info("pass.done", "pass", "definitions", "defs", itoa_log(total_defs), "calls", itoa_log(total_calls), "imports", itoa_log(total_imports), "errors", itoa_log(errors)); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index a827d25cb9..4ea9649979 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -21,6 +21,7 @@ enum { /* Fixed bytes around a serialized JSON field: ,"key":"value" / ,"key":[...] * -> comma + 2 key quotes + colon + 2 value quotes (resp. brackets). */ PP_JSON_FIELD_OVERHEAD = 6, + PP_SWIFT_PROPS_MARGIN = 80, PP_ARGS_MARGIN = 20, /* ,"line": -> comma + key (7) + colon + up to 10 digits + NUL. */ PP_LINE_MARGIN = 24, @@ -39,6 +40,8 @@ enum { #define PP_NSEC_PER_SEC 1000000000ULL #define PP_USEC_PER_MS 1000000ULL #define PP_HALF_CONF 0.5 +static const double PP_SWIFT_SINGLE_CONF = 0.90; +static const double PP_SWIFT_AMBIGUOUS_CONF = 0.55; #define PP_FIELD_HINT_CONF 0.85 enum { PP_CSHARP_M_PREFIX_LEN = 2 }; @@ -538,6 +541,13 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) return; } size_t pos = (size_t)n; + if (def->qn_sig_off && bufsize - pos > PP_SWIFT_PROPS_MARGIN) { + pos += (size_t)snprintf(buf + pos, bufsize - pos, + ",\"param_default_mask\":\"%016llx\",\"selector_param_count\":%u", + (unsigned long long)def->param_default_mask, + (unsigned)def->selector_param_count); + } + append_json_string(buf, bufsize, &pos, "call_selector", def->call_selector); append_json_string(buf, bufsize, &pos, "docstring", def->docstring); /* The buffer is sized for every field below (pp_props_buf), so none can be * squeezed out. */ @@ -767,6 +777,7 @@ static size_t pp_arr_field_len(const char *key, const char **arr) { * allocated. Twin of pass_definitions.c -- keep both in sync. */ static char *pp_props_buf(const CBMDefinition *def, char *stack, size_t *size) { size_t need = PP_PROPS_PREFIX_MAX + pp_str_field_len("docstring", def->docstring); + need += pp_str_field_len("call_selector", def->call_selector); if (def->variants && def->variants[0]) { need += strlen("variants") + strlen(def->variants) + PP_JSON_FIELD_OVERHEAD; } @@ -1820,6 +1831,11 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label, lang, module_prefix_len); } + if (def->qn_sig_off) { + cbm_registry_set_swift_signature(ctx->registry, def->qualified_name, + def->param_default_mask, def->selector_param_count, + def->call_selector); + } (*reg_entries)++; } char marked[CBM_SZ_4K]; @@ -2052,6 +2068,9 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap()); cbm_pipeline_set_nsmap(namespace_map); + cbm_registry_build_swift_owners(ctx->registry, ctx->gbuf, ctx->repo_path, ctx->excluded_dirs, + ctx->excluded_count); + cbm_log_info("parallel.registry.done", "entries", itoa_log(reg_entries), "defines", itoa_log(defines_edges), "imports", itoa_log(imports_edges)); return 0; @@ -2849,7 +2868,8 @@ static bool emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const char **imp_keys, const char **imp_vals, int imp_count, bool suppress_plain_calls, const char *route_mount, CBMLanguage lang) { - cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name); + cbm_svc_kind_t svc = + cbm_service_pattern_match_named(res->qualified_name, target ? target->name : NULL); const char *arg = call->first_string_arg; /* Also detect route registration by callee name suffix alone (handles unresolved @@ -3422,8 +3442,16 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB (lsp && !lsp_target && cbm_pipeline_lsp_external_receiver(lsp)); bool rust_external = lsp && cbm_pipeline_rust_external_target( lang, lsp->strategy, lsp->callee_qn, rc->project_name); + /* Typed Swift calls must not scan the global name bucket first. */ + const char *candidates[CBM_SZ_256]; + int count = 0; + if (lang == CBM_LANG_SWIFT && !lsp_target && !call->requires_lsp_resolution) { + count = cbm_registry_swift_candidates_in_file(rc->registry, call, result, rc->main_gbuf, + module_qn, imp_vals, imp_count, + candidates, CBM_SZ_256); + } if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution && - !rust_external && !php_vendor_call) { + !rust_external && !php_vendor_call && count == 0) { /* Python LEGB fallback first (MUST match pass_calls.c). */ res = cbm_registry_resolve_call_scoped(rc->registry, call->callee_name, module_qn, imp_keys, imp_vals, imp_count, lang, @@ -3591,6 +3619,38 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB } } + if (lang == CBM_LANG_SWIFT && !lsp_target) { + if (count > 0) { + for (int i = 0; i < count; i++) { + const cbm_gbuf_node_t *target = + cbm_gbuf_find_by_qn(rc->main_gbuf, candidates[i]); + if (!target || target->id == source_node->id) { + continue; + } + cbm_resolution_t selected = {.qualified_name = candidates[i], + .strategy = "swift_labels", + .confidence = count == SKIP_ONE + ? PP_SWIFT_SINGLE_CONF + : PP_SWIFT_AMBIGUOUS_CONF, + .candidate_count = count}; + site_evidence->candidate_qn = count == SKIP_ONE ? target->qualified_name : NULL; + site_evidence->calls_emitted |= + emit_service_edge(ws->local_edge_buf, source_node, target, call, &selected, + module_qn, rc->registry, rc->main_gbuf, imp_keys, + imp_vals, imp_count, false, route_mount, lang); + ws->calls_resolved++; + } + continue; + } + if (count < 0) { + /* Same-named Swift symbols exist but none is label-compatible: + * clear the bare-name match (it would bind a wrong overload) + * and fall through as unresolved — the empty-resolution + * fallbacks below still run. Mirrors pass_calls.c. */ + res = (cbm_resolution_t){0}; + } + } + /* A call that starts another program (subprocess.run, exec.Command, * posix_spawn): a SPAWNS edge to its Process node instead of a CALLS * edge. `res` holds the LSP answer when there is one, as the diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 7af626b2da..9599fa08fb 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -119,6 +119,7 @@ static void pkg_entries_push(cbm_pkg_entries_t *e, char *pkg_name, char *entry_r } e->items[e->count].pkg_name = pkg_name; e->items[e->count].entry_rel = entry_rel; + e->items[e->count].manifest_rel = NULL; e->count++; } @@ -126,6 +127,7 @@ void cbm_pkg_entries_free(cbm_pkg_entries_t *e) { for (int i = 0; i < e->count; i++) { free(e->items[i].pkg_name); free(e->items[i].entry_rel); + cbm_free(CBM_MEM_CLASS_OTHER, e->items[i].manifest_rel); } free(e->items); e->items = NULL; @@ -872,27 +874,78 @@ static char *swift_extract_after(const char *start, const char *end, const char return swift_quoted_literal(hit + strlen(needle), end); } +static bool swift_normalize_target_path(const char *suffix, char normalized[PKGMAP_PATH_BUF]) { + /* Target paths are directories; normalize dot segments without allowing + * a target to escape the package that declares it. */ + size_t used = 0; + size_t parents[PKGMAP_PATH_BUF / PAIR_LEN]; + size_t depth = 0; + if (suffix[0] == '/' || strpbrk(suffix, "\\:")) { + return false; + } + for (const char *part = suffix; *part;) { + const char *slash = strchr(part, '/'); + size_t len = slash ? (size_t)(slash - part) : strlen(part); + const char *component = part; + part = slash ? slash + SKIP_ONE : part + len; + if (!len || (len == SKIP_ONE && component[0] == '.')) { + continue; + } + if (len == PAIR_LEN && memcmp(component, "..", PAIR_LEN) == 0) { + if (!depth) { + return false; + } + used = parents[--depth]; + continue; + } + if (used + len + PAIR_LEN > PKGMAP_PATH_BUF || + depth == sizeof(parents) / sizeof(parents[0])) { + return false; + } + parents[depth++] = used; + if (used) { + normalized[used++] = '/'; + } + memcpy(normalized + used, component, len); + used += len; + } + normalized[used] = '\0'; + return true; +} + /* Register `target_name` → its resolved source directory: the literal * `path:` argument the target declared, if any, else the conventional * `Sources/` (fixed-convention, same approach parse_cargo_toml * takes for `src/lib`). `literal_path` is borrowed; caller retains * ownership. */ static void swift_register_target(const char *rel_path, const char *target_name, - const char *literal_path, cbm_pkg_entries_t *entries) { + const char *literal_path, const char *source_root, + cbm_pkg_entries_t *entries) { if (!target_name || !target_name[0]) { return; } char suffix_buf[PKGMAP_PATH_BUF]; const char *suffix; - if (literal_path && literal_path[0]) { + if (literal_path) { suffix = literal_path; } else { - snprintf(suffix_buf, sizeof(suffix_buf), "Sources/%s", target_name); + int len = snprintf(suffix_buf, sizeof(suffix_buf), "%s/%s", source_root, target_name); + if (len < 0 || (size_t)len >= sizeof(suffix_buf)) { + return; + } suffix = suffix_buf; } - char *entry = build_entry_path(rel_path, suffix); + char normalized[PKGMAP_PATH_BUF]; + if (!swift_normalize_target_path(suffix, normalized)) { + return; + } + char *entry = normalized[0] ? build_entry_path(rel_path, normalized) : path_dirname(rel_path); if (entry) { + int before = entries->count; pkg_entries_push(entries, strdup(target_name), entry); + if (entries->count > before) { + entries->items[before].manifest_rel = cbm_mem_strdup(CBM_MEM_CLASS_OTHER, rel_path); + } } } @@ -909,14 +962,28 @@ static void swift_register_target(const char *rel_path, const char *target_name, * mint a location that is not just unconfirmed but actively likely wrong. */ static void swift_scan_targets(const char *source, const char *end, const char *rel_path, cbm_pkg_entries_t *entries) { - static const char prefix[] = ".target("; + static const struct { + const char *prefix; + const char *root; + } targets[] = { + {".target(", "Sources"}, {".executableTarget(", "Sources"}, {".testTarget(", "Tests"}, + {".macro(", "Sources"}, {".plugin(", "Plugins"}, + }; const char *cursor = source; while (cursor < end) { - const char *hit = swift_find_code_token(cursor, end, prefix); + const char *hit = NULL; + size_t selected = 0; + for (size_t i = 0; i < sizeof(targets) / sizeof(targets[0]); i++) { + const char *candidate = swift_find_code_token(cursor, end, targets[i].prefix); + if (candidate && (!hit || candidate < hit)) { + hit = candidate; + selected = i; + } + } if (!hit) { return; } - const char *open = hit + strlen(prefix) - SKIP_ONE; + const char *open = hit + strlen(targets[selected].prefix) - SKIP_ONE; const char *close = swift_match_paren(open, end); if (!close) { return; /* unbalanced — nothing further here is trustworthy */ @@ -926,7 +993,8 @@ static void swift_scan_targets(const char *source, const char *end, const char * bool path_present = false; char *literal_path = swift_extract_after(open, close, "path:", &path_present); if (literal_path || !path_present) { - swift_register_target(rel_path, name, literal_path, entries); + swift_register_target(rel_path, name, literal_path, targets[selected].root, + entries); } free(literal_path); free(name); @@ -1024,6 +1092,9 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou for (int w = 0; w < worker_count; w++) { cbm_pkg_entries_t *we = &worker_entries[w]; for (int i = 0; i < we->count; i++) { + if (!we->items[i].pkg_name || !we->items[i].entry_rel) { + continue; /* package-boundary metadata has no import specifier */ + } /* Convert entry_rel to QN: project.dir.parts. A PSR-4 namespace * prefix (key ends in '\\') keeps its repo-relative DIRECTORY * instead: its readers map `Prefix\Sub\Class` onto a file path @@ -1129,6 +1200,26 @@ static bool pkgmap_is_reparse_point(const char *abs_path) { } #endif +static bool pkgmap_wants_manifest(const char *name, bool swift_only) { + return swift_only ? strcmp(name, "Package.swift") == 0 : is_pkgmap_manifest_basename(name); +} + +static bool pkgmap_parse_walk_manifest(const char *name, const char *rel_path, const char *source, + int source_len, cbm_pkg_entries_t *entries, + bool swift_only) { + if (!cbm_pkgmap_try_parse(name, rel_path, source, source_len, entries)) { + return false; + } + if (swift_only) { + int before = entries->count; + pkg_entries_push(entries, NULL, path_dirname(rel_path)); + if (entries->count > before) { + entries->items[before].manifest_rel = cbm_mem_strdup(CBM_MEM_CLASS_OTHER, rel_path); + } + } + return true; +} + /* Recursive filesystem walker that finds and parses package manifest * files independently of the main discovery filter. The main discovery * filter intentionally hides package.json / composer.json etc. from @@ -1144,10 +1235,23 @@ static bool pkgmap_is_reparse_point(const char *abs_path) { * recursion bound — even directory junctions / symlink cycles cannot make * it hang. On Windows we additionally skip reparse points before * descending as a best-effort early-out. */ +static bool pkgmap_dot_entry(const char *name) { + return strcmp(name, ".") == 0 || strcmp(name, "..") == 0; +} + +static bool pkgmap_walk_paths(const char *abs_dir, const char *rel_dir, const char *name, + char abs_path[PKGMAP_PATH_BUF], char rel_path[PKGMAP_PATH_BUF]) { + int abs_len = snprintf(abs_path, PKGMAP_PATH_BUF, "%s/%s", abs_dir, name); + int rel_len = rel_dir && rel_dir[0] + ? snprintf(rel_path, PKGMAP_PATH_BUF, "%s/%s", rel_dir, name) + : snprintf(rel_path, PKGMAP_PATH_BUF, "%s", name); + return abs_len >= 0 && abs_len < PKGMAP_PATH_BUF && rel_len >= 0 && rel_len < PKGMAP_PATH_BUF; +} + static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_entries_t *entries, - int depth, char **excluded_dirs, int excluded_count) { + int depth, char **excluded_dirs, int excluded_count, bool swift_only) { if (depth >= PKGMAP_WALK_MAX_DEPTH) { - cbm_log_info("pkgmap.walk", "depth_cap", rel_dir && rel_dir[0] ? rel_dir : "."); + cbm_log_info("pkgmap.walk", "depth_cap", rel_dir[0] ? rel_dir : "."); return 0; } cbm_dir_t *dir = cbm_opendir(abs_dir); @@ -1158,16 +1262,13 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent cbm_dirent_t *entry; while ((entry = cbm_readdir(dir)) != NULL) { const char *name = entry->name; - if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) { + if (pkgmap_dot_entry(name)) { continue; } char abs_path[PKGMAP_PATH_BUF]; char rel_path[PKGMAP_PATH_BUF]; - snprintf(abs_path, sizeof(abs_path), "%s/%s", abs_dir, name); - if (rel_dir && rel_dir[0]) { - snprintf(rel_path, sizeof(rel_path), "%s/%s", rel_dir, name); - } else { - snprintf(rel_path, sizeof(rel_path), "%s", name); + if (!pkgmap_walk_paths(abs_dir, rel_dir, name, abs_path, rel_path)) { + continue; } struct stat st; if (pkgmap_safe_stat(abs_path, &st) != 0) { @@ -1188,13 +1289,13 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent } #endif parsed += pkgmap_walk_dir(abs_path, rel_path, entries, depth + 1, excluded_dirs, - excluded_count); + excluded_count, swift_only); continue; } if (!S_ISREG(st.st_mode)) { continue; } - if (!is_pkgmap_manifest_basename(name)) { + if (!pkgmap_wants_manifest(name, swift_only)) { continue; } int source_len = 0; @@ -1202,7 +1303,7 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent if (!source) { continue; } - if (cbm_pkgmap_try_parse(name, rel_path, source, source_len, entries)) { + if (pkgmap_parse_walk_manifest(name, rel_path, source, source_len, entries, swift_only)) { parsed++; } cbm_free(CBM_MEM_CLASS_EXTRACT, source); @@ -1227,11 +1328,18 @@ int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries, char if (!repo_path || !entries) { return 0; } - int parsed = pkgmap_walk_dir(repo_path, "", entries, 0, excluded_dirs, excluded_count); + int parsed = pkgmap_walk_dir(repo_path, "", entries, 0, excluded_dirs, excluded_count, false); cbm_log_info("pkgmap.scan_repo", "manifests", pkgmap_itoa(parsed)); return parsed; } +int cbm_pkgmap_scan_swift_targets(const char *repo_path, cbm_pkg_entries_t *entries, + char **excluded_dirs, int excluded_count) { + return repo_path && entries + ? pkgmap_walk_dir(repo_path, "", entries, 0, excluded_dirs, excluded_count, true) + : 0; +} + /* Build pkgmap for sequential path (reads manifest files directly) */ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file_count, const char *project_name) { diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 3f61e122d3..af6a3c9ced 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -2592,7 +2592,7 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c census_len(def->label) + census_len(def->file_path) + census_len(def->parent_class); str_def_sig += census_len(def->signature) + census_len(def->return_type) + - census_len(def->receiver); + census_len(def->receiver) + census_len(def->call_selector); str_def_doc += census_len(def->docstring); str_def_tokens += census_len(def->body_tokens); str_def_profile += census_len(def->structural_profile); diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index 20717275bc..f877cdecc5 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -19,6 +19,7 @@ #include #include +#include "cbm.h" /* CBMCall for overload selection */ #include "discover/discover.h" /* cbm_ignored_file_t (#963) */ #include "foundation/constants.h" /* CBM_SZ_512 */ #include "foundation/hash_table.h" /* CBMHashTable (#1355 pkgmap lookup) */ @@ -286,6 +287,19 @@ void cbm_registry_free(cbm_registry_t *r); void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, const char *label); +/* Swift-only metadata and label-compatible overload selection. The returned + * QNs are borrowed from the registry and all belong to one base callable. + * -1 means this name has Swift definitions but no compatible overload. */ +void cbm_registry_set_swift_signature(cbm_registry_t *r, const char *qualified_name, + uint64_t default_mask, uint8_t param_count, + const char *selector); +int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, + const char *module_qn, const char **import_vals, int import_count, + const char **out, int out_cap); +int cbm_registry_swift_candidates_in_file(const cbm_registry_t *r, const CBMCall *call, + const CBMFileResult *file, const cbm_gbuf_t *graph, + const char *module_qn, const char **import_vals, + int import_count, const char **out, int out_cap); /* As cbm_registry_add, recording `lang`: the DETECTED language of the file * that defines the symbol (CBM_LANG_COUNT = unknown), which the cross-language * veto (cbm_registry_name_guess_vetoed) checks against the caller. Detects no diff --git a/src/pipeline/pipeline_delta.c b/src/pipeline/pipeline_delta.c index ca1dbab469..63c5ef1d19 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -656,12 +656,13 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g * UNIQUE violation the pre-remap patch would have raised. A resolver that * only LOOKS UP still needs its target resident, which is why the list * mirrors the registry's own membership rule instead of guessing. */ - /* The one property a resolver READS off a proxy: an axios instance - * binding's client + baseURL (#1916). A caller re-resolved alone must + /* Preserve the resolver-visible HTTP and Swift signature properties on + * proxies: an axios instance binding's client + baseURL (#1916). + * A caller re-resolved alone must * compose `api.get('/p')` against its unchanged wrapper exactly as a full * build does. Only the two keys, only on the rare Module/Variable rows - * that carry them; every other proxy stays "{}". Proxies are never - * written back (cbm_delta_patch skips id <= max_db_id). + * that carry them; Swift callables retain defaults and parameter counts. + * Proxies are never written back (cbm_delta_patch skips id <= max_db_id). * * The other: a node's parent_class where it changes the name the node * goes by -- a Go method's QN has no receiver, a class-body variable's QN @@ -680,7 +681,21 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g " CASE WHEN label IN ('Module','Variable')" " AND instr(properties, '\"http_client\"') > 0" " THEN json_object('http_client', json_extract(properties, '$.http_client')," - " 'http_base_url', json_extract(properties, '$.http_base_url')) END," + " 'http_base_url', json_extract(properties, '$.http_base_url'))" + " WHEN instr(properties, '\"param_default_mask\"') > 0" + " THEN json_object('param_default_mask', json_extract(properties, " + "'$.param_default_mask')," + " 'selector_param_count', json_extract(properties, '$.selector_param_count')," + " 'call_selector', json_extract(properties, '$.call_selector')," + " 'return_type', json_extract(properties, '$.return_type'))" + " WHEN instr(properties, '\"swift_type_path\"') > 0" + " OR instr(properties, '\"swift_extension\"') > 0" + " THEN json_object('base_classes', json_extract(properties, '$.base_classes')," + " 'swift_type_path', json_extract(properties, '$.swift_type_path')," + " 'swift_extension', json_extract(properties, '$.swift_extension')," + " 'swift_imports', json_extract(properties, '$.swift_imports'))" + " WHEN instr(properties, '\"base_classes\"') > 0" + " THEN json_object('base_classes', json_extract(properties, '$.base_classes')) END," " start_line, end_line," " CASE WHEN label IN ('Method','Variable')" " AND instr(properties, '\"parent_class\"') > 0" diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 4e96e732af..0320b5ed6b 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -371,6 +371,7 @@ static bool semantic_manifest_package_control(const char *name) { strcmp(name, "composer.json") == 0 || strcmp(name, "pubspec.yaml") == 0 || strcmp(name, "pom.xml") == 0 || strcmp(name, "build.gradle") == 0 || strcmp(name, "build.gradle.kts") == 0 || strcmp(name, "mix.exs") == 0 || + strcmp(name, "Package.swift") == 0 || (len >= 8 && strcmp(name + len - 8, ".gemspec") == 0); } @@ -1372,6 +1373,30 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { : 0; cbm_registry_add_lang_in_module(seed->registry, node->name, node->qualified_name, node->label, lang, prefix_len); + if (node->properties_json) { + const char *mask = strstr(node->properties_json, "\"param_default_mask\":\""); + const char *count = strstr(node->properties_json, "\"selector_param_count\":"); + if (mask && count) { + mask += strlen("\"param_default_mask\":\""); + count += strlen("\"selector_param_count\":"); + char *mask_end = NULL; + char *count_end = NULL; + unsigned long long defaults = strtoull(mask, &mask_end, CBM_HEX_BASE); + unsigned long params = strtoul(count, &count_end, CBM_DECIMAL_BASE); + if (mask_end == mask + CBM_SZ_16 && *mask_end == '"' && count_end != count && + params <= UINT8_MAX) { + yyjson_doc *doc = + yyjson_read(node->properties_json, strlen(node->properties_json), 0); + if (doc) { + const char *selector = + yyjson_get_str(yyjson_obj_get(yyjson_doc_get_root(doc), "call_selector")); + cbm_registry_set_swift_signature(seed->registry, node->qualified_name, + (uint64_t)defaults, (uint8_t)params, selector); + yyjson_doc_free(doc); + } + } + } + } } /* Seed the registry from every registry symbol in the graph buffer, with @@ -2903,6 +2928,67 @@ static const char *closure_python_namespace_decline(const char *stored_json, return change > 0 ? "python_namespace_changed" : NULL; } +static bool closure_is_swift_manifest(const char *path) { + const char *slash = strrchr(path, '/'); + return strcmp(slash ? slash + SKIP_ONE : path, "Package.swift") == 0; +} + +static const cbm_file_hash_t *closure_next_swift_manifest(const cbm_file_hash_t *rows, int count, + int *cursor) { + while (*cursor < count) { + const cbm_file_hash_t *row = &rows[(*cursor)++]; + if (closure_is_swift_manifest(row->rel_path)) { + return row; + } + } + return NULL; +} + +static bool closure_manifest_rows_valid(const cbm_file_hash_t *rows, int count) { + if (count < 0 || (count > 0 && !rows)) { + return false; + } + for (int i = 0; i < count; i++) { + if (!rows[i].rel_path || !rows[i].sha256) { + return false; + } + } + return true; +} + +static bool closure_metadata_current(bool metadata_current, const cbm_file_hash_t *stored, + int stored_count, const cbm_file_hash_t *fresh, + int fresh_count) { + if (!metadata_current || !closure_manifest_rows_valid(stored, stored_count) || + !closure_manifest_rows_valid(fresh, fresh_count)) { + return false; + } + CBMHashTable *by_path = cbm_ht_create(CBM_SZ_64); + if (!by_path) { + return false; + } + int new_cursor = 0; + int new_count = 0; + const cbm_file_hash_t *row = closure_next_swift_manifest(fresh, fresh_count, &new_cursor); + while (row) { + cbm_ht_set(by_path, row->rel_path, (void *)row->sha256); + new_count++; + row = closure_next_swift_manifest(fresh, fresh_count, &new_cursor); + } + bool current = cbm_ht_count(by_path) == (uint32_t)new_count; + int old_cursor = 0; + int old_count = 0; + row = closure_next_swift_manifest(stored, stored_count, &old_cursor); + while (current && row) { + const char *sha = cbm_ht_get(by_path, row->rel_path); + current = sha && strcmp(sha, row->sha256) == 0; + old_count++; + row = closure_next_swift_manifest(stored, stored_count, &old_cursor); + } + cbm_ht_free(by_path); + return current && old_count == new_count; +} + /* Decide whether this manifest delta is closure-repairable, and if so build * the plan. Returns 1 with *plan filled, or 0 (decline → FULL) with the * reason logged. Every early exit declines; nothing here mutates state. */ @@ -4083,6 +4169,12 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil * files plus the recorded consumers of any changed SURFACE; every * uncertain case declines inside the planner, making the pre-plan * behaviour (full rebuild) the unconditional fallback. */ + /* Package.swift is also discovered source, but its target paths are + * global resolution inputs, not a file-local declaration surface. + * Compare their SwiftPM subsets before allowing any source-only + * closure repair, independently of database row order. */ + metadata_current = closure_metadata_current(metadata_current, stored, stored_count, + baseline_manifest, baseline_count); if (metadata_current && closure_try_plan(p, store, project, files, file_count, stored, stored_count, baseline_manifest, baseline_count, &closure_plan)) { diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index ca0f0ec358..fe81dcebdf 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -248,6 +248,11 @@ bool cbm_pipeline_frozen_manifests_equal(cbm_pipeline_t *p, const cbm_file_hash_ int left_count, const cbm_file_hash_t *right, int right_count); +/* Freeze Swift declaration/extension ownership after all definitions are registered. */ +void cbm_registry_build_swift_owners(cbm_registry_t *registry, const cbm_gbuf_t *graph, + const char *repo_path, char **excluded_dirs, + int excluded_count); + /* Internal explicit-language resolution; public raw/lineage APIs stay raw. */ cbm_resolution_t cbm_registry_resolve_lang(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, @@ -362,8 +367,9 @@ static inline bool cbm_pipeline_node_is_dir_container(const cbm_gbuf_node_t *nod /* Per-worker manifest collection entry. */ typedef struct { - char *pkg_name; /* heap: "@myorg/pkg", "github.com/foo/bar" */ - char *entry_rel; /* heap: "packages/pkg/src/index" (no extension) */ + char *pkg_name; /* heap: "@myorg/pkg", "github.com/foo/bar" */ + char *entry_rel; /* heap: "packages/pkg/src/index" (no extension) */ + char *manifest_rel; /* heap: owning SwiftPM manifest, NULL for other ecosystems */ } cbm_pkg_entry_t; /* Growable array of package entries (per-worker, no thread contention). */ @@ -935,6 +941,9 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou /* Build pkgmap by reading manifest files from the files array (sequential path). */ int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries, char **excluded_dirs, int excluded_count); +/* Swift target roots plus package boundaries (pkg_name == NULL), using the literal parser. */ +int cbm_pkgmap_scan_swift_targets(const char *repo_path, cbm_pkg_entries_t *entries, + char **excluded_dirs, int excluded_count); CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files, int file_count, const char *project_name, char **excluded_dirs, int excluded_count); diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 997d001949..0a6932ded8 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -29,13 +29,17 @@ enum { REG_MAX_CANDIDATES = 256 }; #include "cbm.h" /* cbm_label_is_relation — the resolve-time relation veto */ #include "foundation/compat.h" /* CBM_TLS */ #include "foundation/hash_table.h" +#include "foundation/arena.h" #include "foundation/dyn_array.h" +#include "foundation/mem_core.h" #include "foundation/platform.h" #include "callable_sig.h" /* cbm_qn_callable_base_len_named: overloads share the name key */ +#include "yyjson/yyjson.h" #include #include #include +#include #include #include @@ -94,6 +98,41 @@ typedef struct { uint8_t *lang; } qn_array_t; +typedef struct { + uint64_t defaults; + uint8_t count; + char selector[]; +} swift_signature_t; +typedef struct swift_owner swift_owner_t; +struct swift_owner { + const char *qn; + const char *path; + const char *module; + const char *module_name; + const char *manifest; + const char *imports; + bool declaration; + swift_owner_t *primary; + swift_owner_t *members; + swift_owner_t *next_member; + swift_owner_t *next_path; + swift_owner_t *next; +}; + +typedef struct { + CBMArena arena; + const cbm_registry_t *registry; + cbm_pkg_entries_t targets; + CBMHashTable *by_source_root; + CBMHashTable *by_manifest_root; + CBMHashTable *by_target_name; + cbm_pkg_entry_t ambiguous_target; + CBMHashTable *by_qn; + CBMHashTable *by_path; + CBMHashTable *by_module_path; + CBMHashTable *return_types; + swift_owner_t *types; +} swift_owners_t; _Static_assert(CBM_LANG_COUNT <= UINT8_MAX, "CBMLanguage must fit the per-entry lang byte"); struct cbm_registry { @@ -107,6 +146,9 @@ struct cbm_registry { /* byName: simpleName → qn_array_t* (heap-owned) */ CBMHashTable *by_name; + CBMHashTable *swift_signatures; /* borrowed exact-map keys; owned values */ + CBMHashTable *swift_overloads; /* base callable QN -> overloads */ + swift_owners_t *swift_owners; }; /* ── Helpers ─────────────────────────────────────────────────────── */ @@ -1465,6 +1507,36 @@ static void free_qn_array(const char *key, void *value, void *ud) { free((void *)key); } +static void free_swift_signature(const char *key, void *value, void *ud) { + (void)key; + (void)ud; + cbm_free(CBM_MEM_CLASS_OTHER, value); +} + +static void free_swift_overloads(const char *key, void *value, void *ud) { + (void)ud; + qn_array_t *overloads = value; + cbm_da_free(overloads); + cbm_free(CBM_MEM_CLASS_OTHER, overloads); + cbm_free(CBM_MEM_CLASS_OTHER, (void *)key); +} + +static void swift_owners_free(swift_owners_t *index) { + if (!index) { + return; + } + cbm_ht_free(index->by_qn); + cbm_ht_free(index->by_path); + cbm_ht_free(index->by_module_path); + cbm_ht_free(index->return_types); + cbm_ht_free(index->by_source_root); + cbm_ht_free(index->by_manifest_root); + cbm_ht_free(index->by_target_name); + cbm_pkg_entries_free(&index->targets); + cbm_arena_destroy(&index->arena); + cbm_free(CBM_MEM_CLASS_OTHER, index); +} + void cbm_registry_free(cbm_registry_t *r) { if (!r) { return; @@ -1472,6 +1544,15 @@ void cbm_registry_free(cbm_registry_t *r) { /* by_name first: its items borrow exact's keys. */ cbm_ht_foreach(r->by_name, free_qn_array, NULL); cbm_ht_free(r->by_name); + if (r->swift_signatures) { + cbm_ht_foreach(r->swift_signatures, free_swift_signature, NULL); + cbm_ht_free(r->swift_signatures); + } + if (r->swift_overloads) { + cbm_ht_foreach(r->swift_overloads, free_swift_overloads, NULL); + cbm_ht_free(r->swift_overloads); + } + swift_owners_free(r->swift_owners); cbm_ht_foreach(r->exact, free_label, NULL); cbm_ht_free(r->exact); for (int i = 0; i < r->label_pool_n; i++) { @@ -1693,6 +1774,69 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name, } } +static void swift_index_overload(cbm_registry_t *r, const char *qn, const char *owned) { + if (!r->swift_overloads) { + r->swift_overloads = cbm_ht_create(CBM_SZ_512); + } + if (!r->swift_overloads) { + return; + } + size_t base_len = cbm_qn_callable_base_len(qn); + char *base = cbm_alloc(CBM_MEM_CLASS_OTHER, base_len + SKIP_ONE); + if (!base) { + return; + } + memcpy(base, qn, base_len); + base[base_len] = '\0'; + qn_array_t *overloads = cbm_ht_get(r->swift_overloads, base); + if (!overloads) { + overloads = cbm_calloc(CBM_MEM_CLASS_OTHER, sizeof(*overloads)); + if (!overloads) { + cbm_free(CBM_MEM_CLASS_OTHER, base); + return; + } + cbm_ht_set(r->swift_overloads, base, overloads); + } else { + cbm_free(CBM_MEM_CLASS_OTHER, base); + } + for (int i = 0; i < overloads->count; i++) { + if (overloads->items[i] == owned) { + return; + } + } + cbm_da_push(overloads, (char *)owned); +} + +void cbm_registry_set_swift_signature(cbm_registry_t *r, const char *qualified_name, + uint64_t default_mask, uint8_t param_count, + const char *selector) { + if (!r || !qualified_name) { + return; + } + const char *owned = cbm_ht_get_key(r->exact, qualified_name); + if (!owned) { + return; + } + if (!r->swift_signatures) { + r->swift_signatures = cbm_ht_create(CBM_SZ_512); + if (!r->swift_signatures) { + return; + } + } + const char *text = + selector ? selector : qualified_name + cbm_qn_callable_base_len(qualified_name); + swift_signature_t *sig = cbm_alloc(CBM_MEM_CLASS_OTHER, sizeof(*sig) + strlen(text) + SKIP_ONE); + if (!sig) { + return; + } + sig->defaults = default_mask; + sig->count = param_count; + memcpy(sig->selector, text, strlen(text) + SKIP_ONE); + cbm_free(CBM_MEM_CLASS_OTHER, cbm_ht_get(r->swift_signatures, owned)); + cbm_ht_set(r->swift_signatures, owned, sig); + swift_index_overload(r, qualified_name, owned); +} + /* A generated member (a Java record's implicit accessor) is a definition the * source never spells: no call names it except through its owner's type. The * exact map holds it for the LSP targets and the import/same-module lookups, @@ -2128,10 +2272,10 @@ static bool receiver_chain_admits(const char *callee_name, const char *candidate * URL carry no underscore and stay guarded. */ int has_underscore = 0; int all_caps = 1; - for (const char *c = dotted; c < last_dot && *c != '.'; c++) { - if (*c == '_') { + for (size_t i = 0; i < w && dotted[i] != '.'; i++) { + if (dotted[i] == '_') { has_underscore = 1; - } else if (*c >= 'a' && *c <= 'z') { + } else if (dotted[i] >= 'a' && dotted[i] <= 'z') { all_caps = 0; break; } @@ -2430,6 +2574,1599 @@ static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const c return res; } +/* Read labels from the uncapped selector. Types may contain commas in + * tuples, generic arguments or function types, so only top-level commas split + * parameters. The type text is used only to recognize a trailing closure. */ +typedef struct { + int parens; + int brackets; + int angles; +} swift_parameter_depth_t; + +static void swift_parameter_depth_step(swift_parameter_depth_t *depth, char ch, char previous) { + switch (ch) { + case '(': + depth->parens++; + break; + case ')': + if (depth->parens > 0) { + depth->parens--; + } + break; + case '[': + depth->brackets++; + break; + case ']': + if (depth->brackets > 0) { + depth->brackets--; + } + break; + case '<': + depth->angles++; + break; + case '>': + if (depth->angles > 0 && previous != '=') { + depth->angles--; + } + break; + default: + break; + } +} + +static bool swift_type_wraps(const char *start, const char *end) { + if (*start != '(') { + return false; + } + swift_parameter_depth_t depth = {0}; + bool comma = false; + for (const char *p = start; p < end; p++) { + swift_parameter_depth_step(&depth, *p, p == start ? '\0' : p[-SKIP_ONE]); + if (*p == ')' && depth.parens == 0) { + return p == end - SKIP_ONE && !comma; + } + if (*p == ',' && depth.parens == SKIP_ONE && depth.brackets == 0 && depth.angles == 0) { + comma = true; + } + } + return false; +} + +static bool swift_type_is_closure(const char *start, const char *end) { + while (start < end) { + if (end[-SKIP_ONE] == '?') { + end--; + continue; + } + if (!swift_type_wraps(start, end)) { + break; + } + start++; + end--; + } + swift_parameter_depth_t depth = {0}; + for (const char *p = start; p + SKIP_ONE < end; p++) { + if (*p == '=' && p[SKIP_ONE] == '>' && depth.parens == 0 && depth.brackets == 0 && + depth.angles == 0) { + return true; + } + swift_parameter_depth_step(&depth, *p, p == start ? '\0' : p[-SKIP_ONE]); + } + return false; +} + +enum { SWIFT_MAX_PARAMETERS = 64, SWIFT_INCOMPATIBLE = -1 }; + +typedef struct { + const char *labels[SWIFT_MAX_PARAMETERS]; + size_t label_lens[SWIFT_MAX_PARAMETERS]; + bool closures[SWIFT_MAX_PARAMETERS]; + bool variadics[SWIFT_MAX_PARAMETERS]; + unsigned count; +} swift_parameters_t; + +/* Exact match for a later trailing closure's label. NULL and empty labels + * never match, so a call with no captured label binds nothing by label. */ +static bool swift_label_matches(const char *label, const char *want, size_t want_len) { + return label && label[0] && strlen(label) == want_len && memcmp(label, want, want_len) == 0; +} + +/* Whether trailing closure used may bind signature parameter i. The first + * unlabelled closure skips a default or variadic parameter only if a required + * parameter precedes the first parameter matching the next trailing label. */ +static bool swift_trailing_binds(const swift_signature_t *meta, const CBMCall *call, + const swift_parameters_t *params, unsigned i, unsigned total, + unsigned used) { + if (used >= total || !params->closures[i]) { + return false; + } + if (used > 0) { + const char *tl = call->trailing_closure_labels ? call->trailing_closure_labels[used] : NULL; + return swift_label_matches(tl, params->labels[i], params->label_lens[i]); + } + if (!params->variadics[i] && (meta->defaults & (UINT64_C(1) << i)) == 0) { + return true; + } + const char *next_label = total > SKIP_ONE && call->trailing_closure_labels + ? call->trailing_closure_labels[SKIP_ONE] + : NULL; + for (unsigned j = i + SKIP_ONE; j < params->count; j++) { + if (swift_label_matches(next_label, params->labels[j], params->label_lens[j])) { + break; + } + if (!params->variadics[j] && (meta->defaults & (UINT64_C(1) << j)) == 0) { + return false; + } + } + return true; +} + +static bool swift_parameter_add(swift_parameters_t *params, const char *entry, const char *end) { + if (params->count >= SWIFT_MAX_PARAMETERS) { + return false; + } + const char *colon = memchr(entry, ':', (size_t)(end - entry)); + if (!colon) { + return false; + } + unsigned i = params->count; + params->labels[i] = entry; + params->label_lens[i] = (size_t)(colon - entry); + params->variadics[i] = end > colon + SKIP_ONE && end[-SKIP_ONE] == '~'; + const char *type_end = params->variadics[i] ? end - SKIP_ONE : end; + params->closures[i] = swift_type_is_closure(colon + SKIP_ONE, type_end); + params->count++; + return true; +} + +/* Declaration requirements distinguish nodes, without proving applicability. */ +static size_t swift_generic_prefix_len(const char *sig, size_t n) { + unsigned depth = 0; + for (size_t i = 0; i < n; i++) { + if (sig[i] == '<') { + depth++; + } else if (sig[i] == '>' && (i == 0 || sig[i - SKIP_ONE] != '=')) { + if (depth == 0) { + return 0; + } + if (--depth == 0) { + return i + SKIP_ONE; + } + } + } + return 0; +} + +/* Split only at top-level commas; preserve the closure/variadic spelling rules. */ +static bool swift_parameters_read(const swift_signature_t *meta, swift_parameters_t *params) { + const char *sig = meta->selector; + size_t n = cbm_callable_return_offset(sig); + const size_t async_len = sizeof("async") - SKIP_ONE; + if (n >= async_len && memcmp(sig + n - async_len, "async", async_len) == 0) { + n -= async_len; + } + /* Declaration generic requirements distinguish nodes, but do not prove + * applicability. Read only params when selecting compatible candidates. */ + if (n && sig[0] == '<') { + size_t i = swift_generic_prefix_len(sig, n); + if (i == 0 || i >= n) { + return false; + } + sig += i; + n -= i; + } + if (n < PAIR_LEN || sig[0] != '(' || sig[n - SKIP_ONE] != ')' || strchr(sig, '#')) { + return false; + } + const char *entry = sig + SKIP_ONE; + const char *end = sig + n - SKIP_ONE; + swift_parameter_depth_t depth = {0}; + for (const char *p = entry; p <= end; p++) { + bool boundary = p == end || (*p == ',' && depth.parens == 0 && depth.brackets == 0 && + depth.angles == 0); + if (boundary) { + if (p > entry && !swift_parameter_add(params, entry, p)) { + return false; + } + entry = p + SKIP_ONE; + continue; + } + char previous = p == entry ? '\0' : p[-SKIP_ONE]; + swift_parameter_depth_step(&depth, *p, previous); + } + return params->count == meta->count; +} + +static bool swift_argument_label_matches(const swift_parameters_t *params, const CBMCall *call, + unsigned i, int arg) { + if (arg >= call->arg_count) { + return false; + } + const char *given = call->args[arg].keyword; + if (!given) { + given = "_"; + } + return strlen(given) == params->label_lens[i] && + memcmp(given, params->labels[i], params->label_lens[i]) == 0; +} + +static int swift_variadic_advance(const CBMCall *call, int arg, bool label_matches) { + if (!label_matches) { + return arg; + } + arg++; + while (arg < call->arg_count && !call->args[arg].keyword) { + arg++; + } + return arg; +} + +static uint16_t swift_trailing_advance(const swift_parameters_t *params, + const swift_signature_t *meta, const CBMCall *call, + unsigned i, unsigned total, uint16_t states) { + uint16_t next = 0; + for (unsigned used = 0; used < total; used++) { + if ((states & (UINT16_C(1) << used)) && + swift_trailing_binds(meta, call, params, i, total, used)) { + next |= (uint16_t)(UINT16_C(1) << (used + SKIP_ONE)); + } + } + return next; +} + +static void swift_parameter_advance(const swift_parameters_t *params, const swift_signature_t *meta, + const CBMCall *call, unsigned i, unsigned total, int arg, + uint16_t states, uint16_t *next) { + uint16_t trailing = + arg == call->arg_count ? swift_trailing_advance(params, meta, call, i, total, states) : 0; + if (params->variadics[i] || (meta->defaults & (UINT64_C(1) << i))) { + /* After explicit arguments, preserve the first eligible trailing + * binding instead of skipping it to a later one. */ + next[arg] |= states & (uint16_t)~(trailing >> SKIP_ONE); + } + if (swift_argument_label_matches(params, call, i, arg)) { + int end = params->variadics[i] ? swift_variadic_advance(call, arg, true) : arg + SKIP_ONE; + next[end] |= states; + if (params->variadics[i] && end == call->arg_count) { + next[end] |= swift_trailing_advance(params, meta, call, i, total, states); + } + } + next[arg] |= trailing; +} + +static bool swift_parameters_match(const swift_parameters_t *params, const swift_signature_t *meta, + const CBMCall *call) { + /* Preserve the legacy bool as one unlabelled closure for manual callers. */ + unsigned trailing_total = call->trailing_closure_count; + if (trailing_total == 0 && call->has_trailing_closure) { + trailing_total = SKIP_ONE; + } + /* Each argument cursor retains the reachable trailing-closure counts. + * A matching label must not greedily consume an earlier default when + * the same label is needed by a later required parameter. */ + uint16_t states[CBM_MAX_CALL_ARGS + SKIP_ONE] = {SKIP_ONE}; + for (unsigned i = 0; i < params->count; i++) { + uint16_t next[CBM_MAX_CALL_ARGS + SKIP_ONE] = {0}; + for (int arg = 0; arg <= call->arg_count; arg++) { + if (!states[arg]) { + continue; + } + swift_parameter_advance(params, meta, call, i, trailing_total, arg, states[arg], next); + } + memcpy(states, next, sizeof(states)); + } + return (states[call->arg_count] & (UINT16_C(1) << trailing_total)) != 0; +} + +static bool swift_signature_matches(const swift_signature_t *meta, const CBMCall *call) { + if (!meta || meta->count == UINT8_MAX || call->arg_count < 0 || + call->arg_count > CBM_MAX_CALL_ARGS || (call->arg_count > 0 && !call->args) || + call->args_truncated || call->trailing_closure_truncated || + call->trailing_closure_count > CBM_MAX_TRAILING_CLOSURES) { + return false; + } + swift_parameters_t params; + params.count = 0; + if (!swift_parameters_read(meta, ¶ms)) { + return false; + } + return swift_parameters_match(¶ms, meta, call); +} + +/* Keep receiver choice ahead of general import/name choice. Without a module, + * this path still uses the deterministic distance tie-break, as before. */ +static const char *swift_choose_receiver(const cbm_registry_t *r, const char **matches, int matched, + const char *name, const CBMCall *call, + const char *module_qn, const char **import_vals, + int import_count, const char **scratch) { + if (!strchr(call->callee_name, '.')) { + return NULL; + } + size_t callee_len = strlen(call->callee_name); + const char **receiver_matches = scratch; + int receiver_count = 0; + for (int i = 0; i < matched; i++) { + size_t base = cbm_qn_callable_base_len_named(matches[i], name); + if (base >= callee_len && + memcmp(matches[i] + base - callee_len, call->callee_name, callee_len) == 0 && + (base == callee_len || matches[i][base - callee_len - SKIP_ONE] == '.')) { + receiver_matches[receiver_count++] = matches[i]; + } + } + if (receiver_count == 0) { + return NULL; + } + const char **reachable = scratch + matched; + int reachable_count = 0; + if (import_vals) { + for (int i = 0; i < receiver_count; i++) { + if (is_import_reachable(receiver_matches[i], import_vals, import_count)) { + reachable[reachable_count++] = receiver_matches[i]; + } + } + } + const char **preferred = reachable_count > 0 ? reachable : receiver_matches; + int preferred_count = reachable_count > 0 ? reachable_count : receiver_count; + return best_by_import_distance(r, preferred, NULL, NULL, preferred_count, module_qn); +} + +static const char *swift_choose_candidate(const cbm_registry_t *r, const char **matches, + int matched, const char *name, const CBMCall *call, + const char *module_qn, const char **import_vals, + int import_count, const char **scratch) { + const char *chosen = swift_choose_receiver(r, matches, matched, name, call, module_qn, + import_vals, import_count, scratch); + if (chosen) { + return chosen; + } + const char **reachable = scratch; + int reachable_count = 0; + if (import_vals) { + for (int i = 0; i < matched; i++) { + if (is_import_reachable(matches[i], import_vals, import_count)) { + reachable[reachable_count++] = matches[i]; + } + } + } + if (reachable_count > 0) { + chosen = module_qn + ? best_by_import_distance(r, reachable, NULL, NULL, reachable_count, module_qn) + : reachable[0]; + } + if (!chosen) { + chosen = module_qn ? best_by_import_distance(r, matches, NULL, NULL, matched, module_qn) + : matches[0]; + } + return chosen; +} + +static int swift_owner_bucket(const cbm_registry_t *r, const CBMCall *call, const char *type_qn, + const char *name, const char **out, int out_cap, int found) { + char base[CBM_SZ_4K]; + int n = snprintf(base, sizeof(base), "%s.%s", type_qn, name); + if (n < 0 || (size_t)n >= sizeof(base)) { + return SWIFT_INCOMPATIBLE; + } + const qn_array_t *bucket = r->swift_overloads ? cbm_ht_get(r->swift_overloads, base) : NULL; + if (!bucket) { + return found; + } + for (int i = 0; i < bucket->count; i++) { + swift_signature_t *meta = cbm_ht_get(r->swift_signatures, bucket->items[i]); + if (meta && swift_signature_matches(meta, call)) { + if (found == out_cap) { + return SWIFT_INCOMPATIBLE; + } + out[found++] = bucket->items[i]; + } + } + return found; +} + +static yyjson_doc *swift_type_properties(const cbm_gbuf_t *graph, const char *qn) { + const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(graph, qn); + if (!node || !node->properties_json) { + return NULL; + } + return yyjson_read(node->properties_json, strlen(node->properties_json), 0); +} + +/* shortcut: files outside literal SwiftPM targets use their source directory, + * add build-system module maps when other project formats are supported. */ +static size_t swift_owner_module_length(const char *file) { + const char *end = strrchr(file, '/'); + for (const char *part = file; *part;) { + const char *slash = strchr(part, '/'); + if (!slash) { + break; + } + size_t n = (size_t)(slash - part); + if (n == strlen("Source") && memcmp(part, "Source", n) == 0) { + end = slash; + break; + } + if ((n == strlen("Sources") && memcmp(part, "Sources", n) == 0) || + (n == strlen("Tests") && memcmp(part, "Tests", n) == 0)) { + const char *target = strchr(slash + SKIP_ONE, '/'); + if (target) { + end = target; + } + break; + } + part = slash + SKIP_ONE; + } + return end ? (size_t)(end - file) : 0; +} + +static const char *swift_owner_module(CBMArena *arena, const char *file) { + return cbm_arena_strndup(arena, file, swift_owner_module_length(file)); +} + +static const char *swift_module_path_key(CBMArena *arena, const char *module, const char *path) { + return cbm_arena_sprintf(arena, "%s\n%s", module, path); +} + +static const cbm_pkg_entry_t *swift_owner_target(const swift_owners_t *index, const char *file) { + char directory[CBM_SZ_4K]; + size_t len = strlen(file); + if (len >= sizeof(directory)) { + return NULL; + } + memcpy(directory, file, len + SKIP_ONE); + for (;;) { + char *slash = strrchr(directory, '/'); + if (slash) { + *slash = '\0'; + } else { + directory[0] = '\0'; + } + const cbm_pkg_entry_t *target = cbm_ht_get(index->by_source_root, directory); + if (target) { + return target; + } + if (cbm_ht_has(index->by_manifest_root, directory)) { + return &index->ambiguous_target; + } + if (!directory[0]) { + return NULL; + } + } +} + +static void swift_owner_assign_module(swift_owners_t *index, swift_owner_t *type, + const char *file) { + const cbm_pkg_entry_t *target = swift_owner_target(index, file); + if (target && target->pkg_name) { + type->module = + cbm_arena_sprintf(&index->arena, "%s\n%s", target->manifest_rel, target->pkg_name); + type->module_name = target->pkg_name; + type->manifest = target->manifest_rel; + } else if (target) { + /* Overlapping target roots need sources/excludes before files can be assigned. */ + type->module = cbm_arena_strdup(&index->arena, file); + type->module_name = ""; + } else { + type->module = swift_owner_module(&index->arena, file); + const char *leaf = type->module ? strrchr(type->module, '/') : NULL; + type->module_name = leaf ? leaf + SKIP_ONE : type->module; + } +} + +static void swift_owner_index_add(swift_owners_t *index, swift_owner_t *type) { + type->next = index->types; + index->types = type; + cbm_ht_set(index->by_qn, type->qn, type); + if (type->declaration) { + type->next_path = cbm_ht_get(index->by_path, type->path); + cbm_ht_set(index->by_path, type->path, type); + const char *key = swift_module_path_key(&index->arena, type->module, type->path); + swift_owner_t *prior = key ? cbm_ht_get(index->by_module_path, key) : NULL; + if (key && (!prior || strcmp(type->qn, prior->qn) < 0)) { + cbm_ht_set(index->by_module_path, key, type); + } + } +} + +static void swift_return_type_collect(const cbm_gbuf_node_t *node, swift_owners_t *index) { + const char *key = cbm_ht_get_key(index->registry->swift_signatures, node->qualified_name); + if (!key || !node->properties_json) { + return; + } + yyjson_doc *doc = yyjson_read(node->properties_json, strlen(node->properties_json), 0); + const char *type = yyjson_get_str(yyjson_obj_get(yyjson_doc_get_root(doc), "return_type")); + const char *owned = type ? cbm_arena_strdup(&index->arena, type) : NULL; + if (owned) { + cbm_ht_set(index->return_types, key, (void *)owned); + } + yyjson_doc_free(doc); +} + +static void swift_owner_collect(const cbm_gbuf_node_t *node, void *userdata) { + swift_owners_t *index = userdata; + swift_return_type_collect(node, index); + if (!node->file_path || !node->properties_json || + (!strstr(node->properties_json, "\"swift_type_path\"") && + !strstr(node->properties_json, "\"swift_extension\""))) { + return; + } + yyjson_doc *doc = yyjson_read(node->properties_json, strlen(node->properties_json), 0); + yyjson_val *props = yyjson_doc_get_root(doc); + const char *declaration = yyjson_get_str(yyjson_obj_get(props, "swift_type_path")); + const char *extension = yyjson_get_str(yyjson_obj_get(props, "swift_extension")); + const char *path = declaration ? declaration : extension; + swift_owner_t *type = path ? cbm_arena_alloc(&index->arena, sizeof(*type)) : NULL; + if (!type) { + yyjson_doc_free(doc); + return; + } + memset(type, 0, sizeof(*type)); + type->qn = cbm_arena_strdup(&index->arena, node->qualified_name); + type->path = cbm_arena_strdup(&index->arena, path); + swift_owner_assign_module(index, type, node->file_path); + const char *imports = yyjson_get_str(yyjson_obj_get(props, "swift_imports")); + type->imports = cbm_arena_strdup(&index->arena, imports ? imports : ""); + type->declaration = declaration != NULL; + if (type->qn && type->path && type->module && type->imports) { + swift_owner_index_add(index, type); + } + yyjson_doc_free(doc); +} + +static bool swift_owner_imports(const swift_owner_t *extension, const char *leaf) { + size_t n = strlen(leaf); + for (const char *p = extension->imports; p && *p;) { + const char *end = strchr(p, '|'); + size_t len = end ? (size_t)(end - p) : strlen(p); + if (n && len >= n && memcmp(p, leaf, n) == 0 && (len == n || p[n] == '.')) { + return true; + } + p = end ? end + SKIP_ONE : NULL; + } + return false; +} + +static swift_owner_t *swift_owner_imported_primary(const swift_owners_t *index, + const swift_owner_t *type) { + swift_owner_t *imported = NULL; + bool imported_local = false; + bool ambiguous = false; + for (swift_owner_t *decl = cbm_ht_get(index->by_path, type->path); decl; + decl = decl->next_path) { + if (swift_owner_imports(type, decl->module_name)) { + bool same_package = + type->manifest && decl->manifest && strcmp(type->manifest, decl->manifest) == 0; + if (imported_local && !same_package) { + continue; + } + if (same_package && !imported_local) { + imported = NULL; + imported_local = true; + ambiguous = false; + } + if (imported && strcmp(imported->module, decl->module) != 0) { + ambiguous = true; + continue; + } + if (!imported || strcmp(decl->qn, imported->qn) < 0) { + imported = decl; + } + } + } + return ambiguous ? NULL : imported; +} + +static swift_owner_t *swift_owner_primary(const swift_owners_t *index, const swift_owner_t *type) { + char key[CBM_SZ_4K]; + int length = snprintf(key, sizeof(key), "%s\n%s", type->module, type->path); + swift_owner_t *local = + length >= 0 && (size_t)length < sizeof(key) ? cbm_ht_get(index->by_module_path, key) : NULL; + return local ? local : swift_owner_imported_primary(index, type); +} + +static void swift_owner_group_members(swift_owners_t *index) { + /* Unindexed external types still share extensions within their own target. */ + for (swift_owner_t *type = index->types; type; type = type->next) { + if (type->primary) { + continue; + } + const char *key = swift_module_path_key(&index->arena, type->module, type->path); + swift_owner_t *prior = key ? cbm_ht_get(index->by_module_path, key) : NULL; + if (key && (!prior || strcmp(type->qn, prior->qn) < 0)) { + cbm_ht_set(index->by_module_path, key, type); + } + } + for (swift_owner_t *type = index->types; type; type = type->next) { + if (!type->primary) { + const char *key = swift_module_path_key(&index->arena, type->module, type->path); + type->primary = key ? cbm_ht_get(index->by_module_path, key) : NULL; + } + if (type->primary) { + type->next_member = type->primary->members; + type->primary->members = type; + } + } +} + +void cbm_registry_build_swift_owners(cbm_registry_t *r, const cbm_gbuf_t *graph, + const char *repo_path, char **excluded_dirs, + int excluded_count) { + if (!r || !graph) { + return; + } + swift_owners_free(r->swift_owners); + r->swift_owners = NULL; + if (!r->swift_signatures) { + return; + } + swift_owners_t *index = cbm_calloc(CBM_MEM_CLASS_OTHER, sizeof(*index)); + if (!index) { + return; + } + cbm_arena_init(&index->arena); + index->registry = r; + cbm_pkg_entries_init(&index->targets); + (void)cbm_pkgmap_scan_swift_targets(repo_path, &index->targets, excluded_dirs, excluded_count); + index->by_source_root = cbm_ht_create(CBM_SZ_512); + index->by_manifest_root = cbm_ht_create(CBM_SZ_512); + index->by_target_name = cbm_ht_create(CBM_SZ_512); + index->by_qn = cbm_ht_create(CBM_SZ_512); + index->by_path = cbm_ht_create(CBM_SZ_512); + index->by_module_path = cbm_ht_create(CBM_SZ_512); + index->return_types = cbm_ht_create(CBM_SZ_512); + if (!index->by_source_root || !index->by_manifest_root || !index->by_target_name || + !index->by_qn || !index->by_path || !index->by_module_path || !index->return_types) { + swift_owners_free(index); + return; + } + for (int i = 0; i < index->targets.count; i++) { + cbm_pkg_entry_t *target = &index->targets.items[i]; + if (target->manifest_rel && !target->pkg_name && target->entry_rel) { + cbm_ht_set(index->by_manifest_root, target->entry_rel, target); + } + if (target->manifest_rel && target->pkg_name && target->entry_rel) { + const char *module = + swift_module_path_key(&index->arena, target->manifest_rel, target->pkg_name); + if (module) { + cbm_ht_set(index->by_target_name, module, target); + } + cbm_pkg_entry_t *prior = cbm_ht_get(index->by_source_root, target->entry_rel); + if (prior && (!prior->pkg_name || strcmp(prior->pkg_name, target->pkg_name) != 0 || + strcmp(prior->manifest_rel, target->manifest_rel) != 0)) { + cbm_ht_set(index->by_source_root, target->entry_rel, &index->ambiguous_target); + } else { + cbm_ht_set(index->by_source_root, target->entry_rel, target); + } + } + } + cbm_gbuf_foreach_node(graph, swift_owner_collect, index); + for (swift_owner_t *type = index->types; type; type = type->next) { + type->primary = swift_owner_primary(index, type); + } + swift_owner_group_members(index); + r->swift_owners = index; +} + +static bool swift_module_visible(const swift_owners_t *index, const cbm_pkg_entry_t *target, + const CBMFileResult *file, const swift_owner_t *owner); + +static const char *swift_caller_path(const CBMFileResult *file, const cbm_gbuf_t *graph, + const char *module_qn) { + const cbm_gbuf_node_t *module = + graph && module_qn ? cbm_gbuf_find_by_qn(graph, module_qn) : NULL; + if (module && module->file_path) { + return module->file_path; + } + return file && file->defs.count ? file->defs.items[0].file_path : NULL; +} + +static bool swift_extension_visible(const swift_owners_t *index, const CBMFileResult *file, + const cbm_gbuf_t *graph, const char *module_qn, + const swift_owner_t *provider) { + if (provider->declaration || !file || + (provider->primary && provider->primary->declaration && + strcmp(provider->module, provider->primary->module) == 0)) { + return true; + } + const char *path = swift_caller_path(file, graph, module_qn); + const cbm_pkg_entry_t *target = path ? swift_owner_target(index, path) : NULL; + if (path && (!target || !target->pkg_name)) { + size_t length = target ? strlen(path) : swift_owner_module_length(path); + if (length == strlen(provider->module) && memcmp(path, provider->module, length) == 0) { + return true; + } + } + return swift_module_visible(index, target, file, provider); +} + +static int swift_owner_candidates(const cbm_registry_t *r, const cbm_gbuf_t *graph, + const CBMFileResult *file, const char *module_qn, + const CBMCall *call, const char *name, const char **out, + int out_cap, const char **owner) { + const swift_owner_t *type = r->swift_owners ? cbm_ht_get(r->swift_owners->by_qn, *owner) : NULL; + if (!type || !type->primary) { + return swift_owner_bucket(r, call, *owner, name, out, out_cap, 0); + } + if (type->primary->declaration) { + *owner = type->primary->qn; + } + int found = 0; + for (const swift_owner_t *member = type->primary->members; member && found >= 0; + member = member->next_member) { + if (swift_extension_visible(r->swift_owners, file, graph, module_qn, member)) { + found = swift_owner_bucket(r, call, member->qn, name, out, out_cap, found); + } + } + return found; +} + +enum { SWIFT_MAX_ANCESTORS = 64 }; + +static bool swift_receiver_type_text(const char *type, char *out, size_t cap); + +static const swift_owner_t *swift_lexical_type(const swift_owners_t *index, + const swift_owner_t *scope, const char *nominal) { + size_t prefix = strlen(scope->path); + while (prefix) { + char key[CBM_SZ_4K]; + int length = snprintf(key, sizeof(key), "%s\n%.*s.%s", scope->module, (int)prefix, + scope->path, nominal); + const swift_owner_t *type = length >= 0 && (size_t)length < sizeof(key) + ? cbm_ht_get(index->by_module_path, key) + : NULL; + if (type) { + return type; + } + while (prefix && scope->path[prefix - SKIP_ONE] != '.') { + prefix--; + } + if (prefix) { + prefix--; + } + } + return NULL; +} + +/* A declared type name belongs to the declaration's file, not the caller's. */ +static const char *swift_type_in_owner(const cbm_registry_t *r, const char *owner, + const char *type) { + char nominal[CBM_SZ_4K]; + if (!swift_receiver_type_text(type, nominal, sizeof(nominal))) { + return NULL; + } + swift_owners_t *index = r->swift_owners; + const swift_owner_t *context = index ? cbm_ht_get(index->by_qn, owner) : NULL; + if (!context) { + return cbm_registry_resolve(r, nominal, owner, NULL, NULL, 0).qualified_name; + } + swift_owner_t reference = *context; + reference.path = nominal; + const swift_owner_t *resolved = swift_lexical_type(index, context, nominal); + if (!resolved && context->primary) { + resolved = swift_lexical_type(index, context->primary, nominal); + } + if (!resolved) { + resolved = swift_owner_primary(index, &reference); + } + if (resolved) { + return resolved->qn; + } + char *dot = strchr(nominal, '.'); + if (!dot) { + return NULL; + } + *dot = '\0'; + bool local = strcmp(nominal, context->module_name) == 0; + if (!local && !swift_owner_imports(context, nominal)) { + return NULL; + } + reference.path = dot + SKIP_ONE; + reference.imports = local ? "" : nominal; + resolved = local ? swift_owner_primary(index, &reference) + : swift_owner_imported_primary(index, &reference); + return resolved ? resolved->qn : NULL; +} + +static bool swift_type_seen(const char **types, int count, const char *qn) { + for (int i = 0; i < count; i++) { + if (strcmp(types[i], qn) == 0) { + return true; + } + } + return false; +} + +static void swift_parent_types(const cbm_registry_t *r, const cbm_gbuf_t *graph, const char *owner, + const char **types, int *count) { + yyjson_doc *doc = swift_type_properties(graph, owner); + if (!doc) { + return; + } + yyjson_val *bases = yyjson_obj_get(yyjson_doc_get_root(doc), "base_classes"); + size_t i; + size_t max; + yyjson_val *base; + yyjson_arr_foreach(bases, i, max, base) { + const char *name = yyjson_get_str(base); + if (*count == SWIFT_MAX_ANCESTORS) { + break; + } + if (!name) { + continue; + } + const char *parent = swift_type_in_owner(r, owner, name); + if (parent && !swift_type_seen(types, *count, parent)) { + types[(*count)++] = parent; + } + } + yyjson_doc_free(doc); +} + +static int swift_typed_candidates(const cbm_registry_t *r, const cbm_gbuf_t *graph, + const CBMFileResult *file, const char *module_qn, + const CBMCall *call, const char *type_qn, const char *name, + const char **out, int out_cap) { + const char *types[SWIFT_MAX_ANCESTORS] = {type_qn}; + int count = SKIP_ONE; + /* shortcut: visit at most 64 hierarchy types, expand for larger valid hierarchies. */ + for (int i = 0; i < count; i++) { + const char *owner = types[i]; + int found = + swift_owner_candidates(r, graph, file, module_qn, call, name, out, out_cap, &owner); + if (found != 0) { + return found; + } + if (!graph || strcmp(name, "init") == 0 || strcmp(name, "deinit") == 0) { + break; + } + swift_parent_types(r, graph, owner, types, &count); + } + return SWIFT_INCOMPATIBLE; +} + +enum { SWIFT_NO_TYPE_RECEIVER = -2 }; + +static const swift_owner_t *swift_target_type(const swift_owners_t *index, + const cbm_pkg_entry_t *target, const char *type) { + char key[CBM_SZ_4K]; + int n = snprintf(key, sizeof(key), "%s\n%s\n%s", target->manifest_rel, target->pkg_name, type); + return n >= 0 && (size_t)n < sizeof(key) ? cbm_ht_get(index->by_module_path, key) : NULL; +} + +static const swift_owner_t *swift_imported_type(const swift_owners_t *index, + const cbm_pkg_entry_t *target, + const CBMFileResult *file, const char *type, + bool *ambiguous) { + const swift_owner_t *selected = NULL; + for (int i = 0; i < file->imports.count; i++) { + const char *name = file->imports.items[i].module_path; + if (!name) { + continue; + } + size_t length = strcspn(name, "."); + char key[CBM_SZ_4K]; + int n = snprintf(key, sizeof(key), "%s\n%.*s", target->manifest_rel, (int)length, name); + const cbm_pkg_entry_t *imported = + n >= 0 && (size_t)n < sizeof(key) ? cbm_ht_get(index->by_target_name, key) : NULL; + if (!imported) { + continue; + } + const char *nominal = strncmp(type, name, length) == 0 && type[length] == '.' + ? type + length + SKIP_ONE + : type; + const swift_owner_t *candidate = swift_target_type(index, imported, nominal); + if (candidate) { + *ambiguous = + *ambiguous || (selected && strcmp(selected->module, candidate->module) != 0); + selected = candidate; + } + } + return selected; +} + +static bool swift_module_visible(const swift_owners_t *index, const cbm_pkg_entry_t *target, + const CBMFileResult *file, const swift_owner_t *owner) { + if (target && target->pkg_name && owner->manifest && + strcmp(owner->manifest, target->manifest_rel) == 0 && + strcmp(owner->module_name, target->pkg_name) == 0) { + return true; + } + bool visible = false; + size_t length = strlen(owner->module_name); + for (int i = 0; i < file->imports.count; i++) { + const char *name = file->imports.items[i].module_path; + visible = visible || (name && length && strncmp(name, owner->module_name, length) == 0 && + (name[length] == '\0' || name[length] == '.')); + } + if (!target || !target->pkg_name) { + return visible; + } + char key[CBM_SZ_4K]; + int n = snprintf(key, sizeof(key), "%s\n%s", target->manifest_rel, owner->module_name); + bool local_module = n >= 0 && (size_t)n < sizeof(key) && cbm_ht_has(index->by_target_name, key); + if (!visible || (local_module && + (!owner->manifest || strcmp(owner->manifest, target->manifest_rel) != 0))) { + return false; + } + return true; +} + +static bool swift_receiver_visible(const swift_owners_t *index, const cbm_pkg_entry_t *target, + const CBMFileResult *file, const swift_owner_t *owner) { + owner = owner->primary && owner->primary->declaration ? owner->primary : owner; + return swift_module_visible(index, target, file, owner); +} + +/* The global import map has one value per name. SwiftPM packages can each + * declare Core, so retain the caller's package before choosing its receiver. */ +static const char *swift_receiver_qn(const cbm_registry_t *r, const CBMFileResult *file, + const cbm_gbuf_t *graph, const char *type, + const char *module_qn, const char **import_vals, + int import_count) { + const swift_owners_t *index = r->swift_owners; + const char *path = swift_caller_path(file, graph, module_qn); + const cbm_pkg_entry_t *target = + index && path ? swift_owner_target(r->swift_owners, path) : NULL; + if (target && target->pkg_name && file) { + const swift_owner_t *selected = swift_target_type(index, target, type); + if (selected) { + return selected->qn; + } + bool ambiguous = false; + selected = swift_imported_type(index, target, file, type, &ambiguous); + if (ambiguous || selected) { + return ambiguous ? NULL : selected->qn; + } + } + cbm_resolution_t fallback = + cbm_registry_resolve(r, type, module_qn, NULL, import_vals, import_count); + const swift_owner_t *owner = + index && fallback.qualified_name ? cbm_ht_get(index->by_qn, fallback.qualified_name) : NULL; + if (owner && target && target->pkg_name && file && + !swift_receiver_visible(index, target, file, owner)) { + return NULL; + } + return fallback.qualified_name; +} + +static int swift_named_candidates(const cbm_registry_t *r, const cbm_gbuf_t *graph, + const CBMFileResult *file, const CBMCall *call, + const char *module_qn, const char **import_vals, int import_count, + const char **out, int out_cap) { + const char *dot = strrchr(call->callee_name, '.'); + if (dot && isupper((unsigned char)call->callee_name[0]) && + (size_t)(dot - call->callee_name) < CBM_SZ_512) { + char type[CBM_SZ_512]; + memcpy(type, call->callee_name, (size_t)(dot - call->callee_name)); + type[dot - call->callee_name] = '\0'; + const char *receiver = + swift_receiver_qn(r, file, graph, type, module_qn, import_vals, import_count); + if (receiver) { + return swift_typed_candidates(r, graph, file, module_qn, call, receiver, + simple_name(call->callee_name), out, out_cap); + } + return SWIFT_INCOMPATIBLE; + } + return SWIFT_NO_TYPE_RECEIVER; +} + +int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, + const char *module_qn, const char **import_vals, int import_count, + const char **out, int out_cap) { + if (!r || !r->swift_signatures || !call || !call->callee_name || !out || out_cap < SKIP_ONE) { + return 0; + } + const char *name = simple_name(call->callee_name); + int typed = swift_named_candidates(r, NULL, NULL, call, module_qn, import_vals, import_count, + out, out_cap); + if (typed != SWIFT_NO_TYPE_RECEIVER) { + return typed; + } + qn_array_t *bucket = cbm_ht_get(r->by_name, name); + if (!bucket) { + return 0; + } + enum { SWIFT_MATCH_BUFFERS = 3 }; + if ((size_t)bucket->count > SIZE_MAX / sizeof(const char *) / SWIFT_MATCH_BUFFERS) { + return SWIFT_INCOMPATIBLE; + } + const char **matches = cbm_alloc( + CBM_MEM_CLASS_OTHER, (size_t)bucket->count * SWIFT_MATCH_BUFFERS * sizeof(*matches)); + if (!matches) { + return SWIFT_INCOMPATIBLE; + } + int matched = 0; + bool has_swift = false; + for (int i = 0; i < bucket->count; i++) { + const char *qn = bucket->items[i]; + swift_signature_t *meta = cbm_ht_get(r->swift_signatures, qn); + has_swift = has_swift || meta != NULL; + if (meta && receiver_chain_admits(call->callee_name, qn) && + swift_signature_matches(meta, call)) { + matches[matched++] = qn; + } + } + if (matched == 0) { + cbm_free(CBM_MEM_CLASS_OTHER, matches); + return has_swift ? SWIFT_INCOMPATIBLE : 0; + } + const char *chosen = swift_choose_candidate(r, matches, matched, name, call, module_qn, + import_vals, import_count, matches + bucket->count); + if (!chosen) { + cbm_free(CBM_MEM_CLASS_OTHER, matches); + return 0; + } + size_t chosen_base = cbm_qn_callable_base_len_named(chosen, name); + int found = 0; + for (int i = 0; i < matched; i++) { + size_t base = cbm_qn_callable_base_len_named(matches[i], name); + if (base == chosen_base && memcmp(matches[i], chosen, base) == 0) { + if (found == out_cap) { + cbm_free(CBM_MEM_CLASS_OTHER, matches); + return SWIFT_INCOMPATIBLE; + } + out[found++] = matches[i]; + } + } + cbm_free(CBM_MEM_CLASS_OTHER, matches); + return found; +} + +enum { SWIFT_SCOPE_MODULE = 1, SWIFT_SCOPE_CLASS = 2, SWIFT_SCOPE_LOCAL = 3 }; +enum { SWIFT_INITIALIZER_BINDINGS = 8 }; + +typedef struct { + const CBMTypeAssign *assignment; + const char *type; + bool ready; +} swift_inferred_binding_t; + +typedef struct { + swift_inferred_binding_t items[SWIFT_INITIALIZER_BINDINGS]; + unsigned count; + const CBMTypeAssign *missing; +} swift_binding_work_t; + +typedef struct { + const cbm_registry_t *registry; + const CBMFileResult *file; + const cbm_gbuf_t *graph; + const char *module_qn; + const char **imports; + int import_count; +} swift_inference_t; + +static const char *swift_initializer_type(const swift_inference_t *context, + swift_binding_work_t *work, + const CBMTypeAssign *assignment); +static int swift_candidates_for_file(const swift_inference_t *context, swift_binding_work_t *work, + const CBMCall *call, const char **out, int out_cap); + +static int swift_binding_rank(const char *binding, const char *scope, const char *class_qn, + const char *module_qn, bool field) { + if (!field && scope && strcmp(binding, scope) == 0) { + return SWIFT_SCOPE_LOCAL; + } + if (class_qn && strcmp(binding, class_qn) == 0) { + return SWIFT_SCOPE_CLASS; + } + if (!field && module_qn && strcmp(binding, module_qn) == 0) { + return SWIFT_SCOPE_MODULE; + } + return 0; +} + +static int swift_assignment_rank(const CBMTypeAssign *assignment, const char *name, + const char *scope, const char *class_qn, const char *module_qn, + bool field) { + if (!assignment->var_name || !assignment->enclosing_func_qn || + strcmp(assignment->var_name, name) != 0) { + return 0; + } + return swift_binding_rank(assignment->enclosing_func_qn, scope, class_qn, module_qn, field); +} + +static int swift_cached_binding(const swift_binding_work_t *work, const CBMTypeAssign *assignment) { + for (unsigned i = 0; i < work->count; i++) { + if (work->items[i].assignment == assignment) { + return (int)i; + } + } + return CBM_NOT_FOUND; +} + +static const char *swift_binding_cached_type(swift_binding_work_t *work, + const CBMTypeAssign *assignment) { + int cached = swift_cached_binding(work, assignment); + if (cached >= 0 && work->items[cached].ready) { + return work->items[cached].type; + } + if (!work->missing) { + work->missing = assignment; + } + return ""; +} + +/* Conflicting block-local bindings stay unknown rather than selecting a + * same-name method on either type. */ +static const char *swift_binding_type(const swift_inference_t *context, swift_binding_work_t *work, + const char *name, const char *scope, const char *class_qn, + const char *module_qn, bool field) { + const CBMFileResult *file = context->file; + int best = 0; + for (int i = 0; i < file->type_assigns.count; i++) { + const CBMTypeAssign *a = &file->type_assigns.items[i]; + int rank = swift_assignment_rank(a, name, scope, class_qn, module_qn, field); + if (rank > best) { + best = rank; + } + } + if (!best) { + return NULL; + } + const char *type = NULL; + for (int i = 0; i < file->type_assigns.count; i++) { + const CBMTypeAssign *a = &file->type_assigns.items[i]; + if (swift_assignment_rank(a, name, scope, class_qn, module_qn, field) != best) { + continue; + } + const char *candidate = a->type_name ? a->type_name : swift_binding_cached_type(work, a); + if (type && strcmp(type, candidate) != 0) { + return ""; + } + type = candidate; + } + return type; +} + +static void swift_enclosing_class(const CBMCall *call, char *class_qn) { + class_qn[0] = '\0'; + if (!call->enclosing_func_qn || strlen(call->enclosing_func_qn) >= CBM_SZ_4K) { + return; + } + size_t n = cbm_qn_callable_base_len(call->enclosing_func_qn); + memcpy(class_qn, call->enclosing_func_qn, n); + class_qn[n] = '\0'; + char *dot = strrchr(class_qn, '.'); + if (dot) { + *dot = '\0'; + } +} + +static const char *swift_file_receiver(const swift_inference_t *context, swift_binding_work_t *work, + const CBMCall *call, const char *module_qn, char *expression, + char *class_qn, char **member) { + swift_enclosing_class(call, class_qn); + char *dot = strchr(expression, '.'); + *dot = '\0'; + *member = dot + SKIP_ONE; + if (strcmp(expression, "self") == 0) { + dot = strchr(*member, '.'); + if (!dot) { + return class_qn; + } + *dot = '\0'; + const char *type = swift_binding_type(context, work, *member, call->enclosing_func_qn, + class_qn, module_qn, true); + *member = dot + SKIP_ONE; + return type; + } + const char *type = swift_binding_type(context, work, expression, call->enclosing_func_qn, + class_qn, module_qn, false); + if (!type && isupper((unsigned char)expression[0])) { + return expression; + } + return type; +} + +/* Dots inside arguments do not separate receiver segments. */ +static char *swift_after_arguments(char *args) { + unsigned depth = SKIP_ONE; + char quote = '\0'; + for (char *p = args + SKIP_ONE; *p; p++) { + if (*p == '\\' && p[SKIP_ONE]) { + p++; + } else if (quote) { + if (*p == quote) { + quote = '\0'; + } + } else if (*p == '"') { + quote = *p; + } else if (*p == '(') { + depth++; + } else if (*p == ')' && --depth == 0) { + p++; + while (isspace((unsigned char)*p)) { + p++; + } + return p; + } + } + return NULL; +} + +static const CBMCall *swift_inner_call(const CBMFileResult *file, const CBMCall *call, + size_t prefix) { + for (int i = 0; i < file->calls.count; i++) { + const CBMCall *candidate = &file->calls.items[i]; + if (candidate->callee_name && strlen(candidate->callee_name) == prefix && + strncmp(candidate->callee_name, call->callee_name, prefix) == 0 && + candidate->site_start_byte >= call->site_start_byte && + candidate->site_end_byte <= call->site_end_byte) { + return candidate; + } + } + return NULL; +} + +static const char *swift_chain_return_spelling(const cbm_registry_t *r, const char *qn) { + const swift_signature_t *sig = cbm_ht_get(r->swift_signatures, qn); + const char *signature = sig ? sig->selector : qn; + size_t offset = cbm_callable_return_offset(signature); + if (!signature[offset]) { + signature = qn; + offset = cbm_callable_return_offset(signature); + } + return offset && signature[offset] ? signature + offset + PAIR_LEN : NULL; +} + +static const char *swift_chain_return_qn(const cbm_registry_t *r, const char *qn, + const char *receiver, const char *member, + const char *returned) { + if (strcmp(returned, "Self") == 0) { + return receiver; + } + char owner[CBM_SZ_4K]; + size_t base = cbm_qn_callable_base_len_named(qn, member); + size_t name_length = strlen(member); + if (base <= name_length || base - name_length - SKIP_ONE >= sizeof(owner)) { + return NULL; + } + size_t length = base - name_length - SKIP_ONE; + memcpy(owner, qn, length); + owner[length] = '\0'; + const char *raw = r->swift_owners ? cbm_ht_get(r->swift_owners->return_types, qn) : NULL; + return swift_type_in_owner(r, owner, raw ? raw : returned); +} + +static const char *swift_function_return_type(const swift_inference_t *context, const char *qn, + const char *returned) { + const cbm_registry_t *r = context->registry; + swift_owners_t *index = r->swift_owners; + const cbm_gbuf_node_t *declaration = cbm_gbuf_find_by_qn(context->graph, qn); + if (!index || !declaration || !declaration->file_path) { + return NULL; + } + const char *raw = cbm_ht_get(index->return_types, qn); + char nominal[CBM_SZ_4K]; + if (!swift_receiver_type_text(raw ? raw : returned, nominal, sizeof(nominal))) { + return NULL; + } + const cbm_pkg_entry_t *target = swift_owner_target(index, declaration->file_path); + const swift_owner_t *type = NULL; + if (target && target->pkg_name) { + type = swift_target_type(index, target, nominal); + } else if (!target) { + char key[CBM_SZ_4K]; + size_t length = swift_owner_module_length(declaration->file_path); + int n = + snprintf(key, sizeof(key), "%.*s\n%s", (int)length, declaration->file_path, nominal); + type = n >= 0 && (size_t)n < sizeof(key) ? cbm_ht_get(index->by_module_path, key) : NULL; + } + if (type) { + return type->qn; + } + const char *caller = swift_caller_path(context->file, context->graph, context->module_qn); + if (caller && strcmp(caller, declaration->file_path) == 0) { + const char *resolved = + swift_receiver_qn(r, context->file, context->graph, nominal, context->module_qn, + context->imports, context->import_count); + const swift_owner_t *owner = resolved ? cbm_ht_get(index->by_qn, resolved) : NULL; + return owner && swift_receiver_visible(index, target, context->file, owner) ? resolved + : NULL; + } + return NULL; /* No declaring-file imports are available for a remote free function. */ +} + +static const char *swift_candidates_return_type(const swift_inference_t *context, int count, + const char **out, const char *receiver, + const char *member) { + const cbm_registry_t *r = context->registry; + const char *type = NULL; + const char *spelling = NULL; + for (int i = 0; i < count; i++) { + const char *returned = swift_chain_return_spelling(r, out[i]); + if (!returned || (spelling && strcmp(spelling, returned) != 0)) { + return NULL; + } + const char *label = cbm_ht_get(r->exact, out[i]); + const char *resolved = label && strcmp(label, "Function") == 0 + ? swift_function_return_type(context, out[i], returned) + : swift_chain_return_qn(r, out[i], receiver, member, returned); + if (!resolved || (type && strcmp(type, resolved) != 0)) { + return NULL; + } + spelling = returned; + type = resolved; + } + return type; +} + +static const char *swift_chain_return(const swift_inference_t *context, const CBMCall *inner, + const char *receiver, const char *member, const char **out, + int out_cap) { + if (!inner) { + return NULL; + } + int count = swift_typed_candidates(context->registry, context->graph, context->file, + context->module_qn, inner, receiver, member, out, out_cap); + return swift_candidates_return_type(context, count, out, receiver, member); +} + +static const CBMCall *swift_initializer_call(const CBMFileResult *file, + const CBMTypeAssign *assignment) { + const CBMCall *selected = NULL; + uint32_t widest = 0; + if (assignment->value_end_byte <= assignment->value_start_byte) { + return NULL; + } + for (int i = 0; i < file->calls.count; i++) { + const CBMCall *call = &file->calls.items[i]; + if (!call->callee_name || call->site_start_byte < assignment->value_start_byte || + call->site_end_byte != assignment->value_end_byte || + call->site_end_byte <= call->site_start_byte) { + continue; + } + uint32_t width = call->site_end_byte - call->site_start_byte; + if (width > widest) { + selected = call; + widest = width; + } else if (width == widest && selected && + (call->site_start_byte != selected->site_start_byte || + strcmp(call->callee_name, selected->callee_name) != 0)) { + return NULL; + } + } + return selected; +} + +static bool swift_initializer_function_visible(const swift_inference_t *context, const char *qn) { + const swift_owners_t *index = context->registry->swift_owners; + const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(context->graph, qn); + if (!index || !node || !node->file_path) { + return false; + } + const cbm_pkg_entry_t *target = swift_owner_target(index, node->file_path); + char module[CBM_SZ_4K]; + swift_owner_t provider = {.module = module}; + if (target && target->pkg_name) { + int n = snprintf(module, sizeof(module), "%s\n%s", target->manifest_rel, target->pkg_name); + if (n < 0 || (size_t)n >= sizeof(module)) { + return false; + } + provider.module_name = target->pkg_name; + provider.manifest = target->manifest_rel; + } else { + size_t length = + target ? strlen(node->file_path) : swift_owner_module_length(node->file_path); + if (length >= sizeof(module)) { + return false; + } + memcpy(module, node->file_path, length); + module[length] = '\0'; + const char *leaf = strrchr(module, '/'); + provider.module_name = leaf ? leaf + SKIP_ONE : module; + } + return swift_extension_visible(index, context->file, context->graph, context->module_qn, + &provider); +} + +static int swift_initializer_member_candidates(const swift_inference_t *context, + swift_binding_work_t *work, const CBMCall *call, + const char **out) { + char expression[CBM_SZ_4K]; + char owner[CBM_SZ_4K]; + if (strlen(call->callee_name) >= sizeof(expression)) { + return SWIFT_INCOMPATIBLE; + } + (void)snprintf(expression, sizeof(expression), "%s", call->callee_name); + char *member = NULL; + const char *type = + swift_file_receiver(context, work, call, context->module_qn, expression, owner, &member); + if (!type || !type[0]) { + return SWIFT_INCOMPATIBLE; /* Unknown roots cannot establish an initializer's type. */ + } + return swift_candidates_for_file(context, work, call, out, REG_MAX_CANDIDATES); +} + +static int swift_initializer_candidates(const swift_inference_t *context, + swift_binding_work_t *work, const CBMTypeAssign *assignment, + const CBMCall *call, const char **out, char *owner) { + owner[0] = '\0'; + if (strchr(call->callee_name, '.')) { + return swift_initializer_member_candidates(context, work, call, out); + } + swift_enclosing_class(call, owner); + const swift_owners_t *index = context->registry->swift_owners; + if (index && cbm_ht_has(index->by_qn, assignment->enclosing_func_qn)) { + (void)snprintf(owner, CBM_SZ_4K, "%s", assignment->enclosing_func_qn); + } + if (index && cbm_ht_has(index->by_qn, owner)) { + int count = swift_typed_candidates(context->registry, context->graph, context->file, + context->module_qn, call, owner, call->callee_name, out, + REG_MAX_CANDIDATES); + if (count > 0) { + return count; + } + } + owner[0] = '\0'; + int count = + cbm_registry_swift_candidates(context->registry, call, context->module_qn, context->imports, + context->import_count, out, REG_MAX_CANDIDATES); + for (int i = 0; i < count; i++) { + const char *label = cbm_ht_get(context->registry->exact, out[i]); + if (!label || strcmp(label, "Function") != 0 || + !swift_initializer_function_visible(context, out[i])) { + return SWIFT_INCOMPATIBLE; + } + } + return count; +} + +static const char *swift_initializer_type(const swift_inference_t *context, + swift_binding_work_t *work, + const CBMTypeAssign *assignment) { + const CBMCall *call = swift_initializer_call(context->file, assignment); + if (!call) { + return NULL; + } + const char *out[REG_MAX_CANDIDATES]; + char owner[CBM_SZ_4K]; + int count = swift_initializer_candidates(context, work, assignment, call, out, owner); + return swift_candidates_return_type(context, count, out, owner[0] ? owner : NULL, + simple_name(call->callee_name)); +} + +/* Evaluate only demanded initializer dependencies. Pending entries identify + * cycles; capacity exhaustion leaves the dependent binding unknown. + * shortcut: at most 8 demanded bindings per call, expand for longer real initializer chains. */ +static void swift_resolve_initializer_bindings(const swift_inference_t *context, + swift_binding_work_t *work, + const CBMTypeAssign *assignment) { + if (work->count >= SWIFT_INITIALIZER_BINDINGS) { + return; + } + unsigned stack[SWIFT_INITIALIZER_BINDINGS]; + unsigned depth = SKIP_ONE; + stack[0] = work->count++; + work->items[stack[0]].assignment = assignment; + while (depth) { + swift_inferred_binding_t *entry = &work->items[stack[depth - SKIP_ONE]]; + work->missing = NULL; + const char *type = swift_initializer_type(context, work, entry->assignment); + if (work->missing) { + int dependency = swift_cached_binding(work, work->missing); + if (dependency < 0 && work->count < SWIFT_INITIALIZER_BINDINGS) { + unsigned next = work->count++; + work->items[next].assignment = work->missing; + stack[depth++] = next; + continue; + } + type = NULL; + } + entry->type = type ? type : ""; + entry->ready = true; + depth--; + } +} + +static bool swift_receiver_type_text(const char *type, char *out, size_t cap) { + size_t n = strcspn(type, " metatype_len && memcmp(type + n - metatype_len, ".Type", metatype_len) == 0) { + n -= metatype_len; + } + if (n == 0 || n >= cap) { + return false; + } + memcpy(out, type, n); + out[n] = '\0'; + return true; +} + +static bool swift_call_uses_named_receiver(const CBMFileResult *file, const CBMCall *call) { + return !file || !strchr(call->callee_name, '.') || + (isupper((unsigned char)call->callee_name[0]) && !strchr(call->callee_name, '(')); +} + +static int swift_candidates_for_file(const swift_inference_t *context, swift_binding_work_t *work, + const CBMCall *call, const char **out, int out_cap) { + const cbm_registry_t *r = context->registry; + const CBMFileResult *file = context->file; + const cbm_gbuf_t *graph = context->graph; + const char *module_qn = context->module_qn; + const char **import_vals = context->imports; + int import_count = context->import_count; + if (!r || !call || !call->callee_name || !out || out_cap < SKIP_ONE) { + return 0; + } + if (swift_call_uses_named_receiver(file, call)) { + int named = swift_named_candidates(r, graph, file, call, module_qn, import_vals, + import_count, out, out_cap); + return named == SWIFT_NO_TYPE_RECEIVER + ? cbm_registry_swift_candidates(r, call, module_qn, import_vals, import_count, + out, out_cap) + : named; + } + char expression[CBM_SZ_4K]; + char class_qn[CBM_SZ_4K] = {0}; + if (strlen(call->callee_name) >= sizeof(expression)) { + return SWIFT_INCOMPATIBLE; + } + (void)snprintf(expression, sizeof(expression), "%s", call->callee_name); + char *member = NULL; + const char *type = + swift_file_receiver(context, work, call, module_qn, expression, class_qn, &member); + if (!type) { + return cbm_registry_swift_candidates(r, call, module_qn, import_vals, import_count, out, + out_cap); + } + bool from_chain = r->swift_owners && cbm_ht_has(r->swift_owners->by_qn, type); + while (type && type[0]) { + char type_text[CBM_SZ_4K]; + if (!from_chain && !swift_receiver_type_text(type, type_text, sizeof(type_text))) { + return SWIFT_INCOMPATIBLE; + } + const char *receiver = from_chain ? cbm_ht_get_key(r->exact, type) + : swift_receiver_qn(r, file, graph, type_text, module_qn, + import_vals, import_count); + if (!receiver) { + return SWIFT_INCOMPATIBLE; /* known non-project receiver */ + } + char *args = strchr(member, '('); + if (!args) { + return swift_typed_candidates(r, graph, file, module_qn, call, receiver, member, out, + out_cap); + } + char *after = swift_after_arguments(args); + if (!after || *after != '.') { + return SWIFT_INCOMPATIBLE; + } + *args = '\0'; + const CBMCall *inner = swift_inner_call(file, call, (size_t)(args - expression)); + type = swift_chain_return(context, inner, receiver, member, out, out_cap); + from_chain = true; + member = after + SKIP_ONE; + } + return SWIFT_INCOMPATIBLE; +} + +int cbm_registry_swift_candidates_in_file(const cbm_registry_t *r, const CBMCall *call, + const CBMFileResult *file, const cbm_gbuf_t *graph, + const char *module_qn, const char **import_vals, + int import_count, const char **out, int out_cap) { + swift_binding_work_t work = {0}; + swift_inference_t context = { + .registry = r, + .file = file, + .graph = graph, + .module_qn = module_qn, + .imports = import_vals, + .import_count = import_count, + }; + for (unsigned attempt = 0; attempt <= SWIFT_INITIALIZER_BINDINGS; attempt++) { + work.missing = NULL; + int count = swift_candidates_for_file(&context, &work, call, out, out_cap); + if (!work.missing) { + return count; + } + if (work.count >= SWIFT_INITIALIZER_BINDINGS) { + return SWIFT_INCOMPATIBLE; + } + swift_resolve_initializer_bindings(&context, &work, work.missing); + } + return SWIFT_INCOMPATIBLE; +} + cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count) { @@ -2815,14 +4552,14 @@ int cbm_registry_find_ending_with(const cbm_registry_t *r, const char *suffix, c /* Build ".suffix" target */ size_t slen = strlen(suffix); - char *target = malloc(slen + REG_SUFFIX_ALLOC); + char *target = cbm_alloc(CBM_MEM_CLASS_OTHER, slen + REG_SUFFIX_ALLOC); target[0] = '.'; memcpy(target + SKIP_ONE, suffix, slen + SKIP_ONE); struct few_ctx ctx = {target, slen + SKIP_ONE, NULL, 0, 0}; cbm_ht_foreach(r->exact, few_scan, &ctx); - free(target); + cbm_free(CBM_MEM_CLASS_OTHER, target); *out = ctx.results; return ctx.count; } diff --git a/src/store/store.h b/src/store/store.h index c3dc5613ce..87237c4ac3 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -34,8 +34,9 @@ typedef struct cbm_store_read_scope cbm_store_read_scope_t; * __image__, registry "unknown" -- no longer * __image__docker.io/[library/]. * 4 #1913: Go method QNs are receiver-qualified (pkg.Recv.method, generic - * receivers unwrapped); free functions stay pkg.func. */ -#define CBM_INDEX_FORMAT_VERSION 4 + * receivers unwrapped); free functions stay pkg.func. + * 5 #2061: Swift declaration identities and uncapped call selectors. */ +#define CBM_INDEX_FORMAT_VERSION 5 #define CBM_STORE_CANCELLED (-3) #define CBM_STORE_SCAN_LIMIT (-4) #define CBM_STORE_CALLBACK_ERR (-5) diff --git a/tests/fixtures/complexity/Swift/Calls.swift b/tests/fixtures/complexity/Swift/Calls.swift new file mode 100644 index 0000000000..c89b33c06f --- /dev/null +++ b/tests/fixtures/complexity/Swift/Calls.swift @@ -0,0 +1,17 @@ +class Box { + init(value: Int) {} + init(name: String) {} + func read(value: Int) -> Int { value } + func read(name: String) -> String { name } +} +func run() { + Box.init(value: 1) + Box.init(name: "x") + let box: Box = Box(value: 1) + box.read(value: 1) + box.read(name: "x") +} +func extensionCaller(box: Box) { + box.extended(value: 1) + _ = Box.init(flag: true) +} diff --git a/tests/fixtures/complexity/Swift/Extensions.swift b/tests/fixtures/complexity/Swift/Extensions.swift new file mode 100644 index 0000000000..63a577942c --- /dev/null +++ b/tests/fixtures/complexity/Swift/Extensions.swift @@ -0,0 +1,4 @@ +extension Box { + convenience init(flag: Bool) { self.init(value: 0) } + func extended(value: Int) {} +} diff --git a/tests/repro/repro_call_argument_matrix_a.c b/tests/repro/repro_call_argument_matrix_a.c index ce0fe18fa2..7188c3c442 100644 --- a/tests/repro/repro_call_argument_matrix_a.c +++ b/tests/repro/repro_call_argument_matrix_a.c @@ -17,6 +17,7 @@ */ #include "test_framework.h" #include "lang_specs.h" +#include "callable_sig.h" #include #include @@ -74,9 +75,9 @@ typedef struct { static int qn_has_terminal_name(const char *qn, const char *name) { if (!qn || !name) return 0; - size_t qn_len = strlen(qn); + size_t qn_len = cbm_qn_callable_base_len(qn); size_t name_len = strlen(name); - if (name_len > qn_len || strcmp(qn + qn_len - name_len, name) != 0) + if (name_len > qn_len || strncmp(qn + qn_len - name_len, name, name_len) != 0) return 0; if (name_len == qn_len) return 1; diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index ad0315032a..9f09e55d0d 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -1,6 +1,5 @@ /* - * test_callable_sig.c — golden tables for the signature-qualified callable - * identity builder and its inverse (#2061, plumbing: no language enabled). + * test_callable_sig.c — golden tables for signature-qualified callable identity (#2061). */ #include "test_framework.h" #include "callable_sig.h" @@ -123,8 +122,9 @@ static const sig_case_t k_cases[] = { "HTTPMethod = .post, headers: [String: String]? = nil, handler: @escaping (Int) -> Void, " "xs: Int..., io: inout Swift.Int, t: (a: Int, String)) -> UploadRequest { }\n}", "function_declaration", 0, - "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(Int)=>Void," - "xs:Int~,io:inout Int,t:(a:Int,String))"}, + "(_:Data,to:URLConvertible,method:HTTPMethod,headers:[String:String]?,handler:(" + "Int)=>Void," + "xs:Int~,io:inout Int,t:(a:Int,String))=>UploadRequest"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class Session {\n func upload(_ data: Data, with request: URLRequest) {}\n}", "function_declaration", 0, "(_:Data,with:URLRequest)"}, @@ -132,11 +132,68 @@ static const sig_case_t k_cases[] = { "init_declaration", 0, "(frame:CGRect)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class C { subscript(index i: Int) -> Int { return 0 } }", "subscript_declaration", 0, - "(index:Int)"}, + "(index:Int)=>Int"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, - "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, "(l:C,r:C)"}, + "class C { static func + (l: C, r: C) -> C { l } }", "function_declaration", 0, + "(l:C,r:C)static=>C"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "struct S { static func f(_ x: Int) {} }", + "function_declaration", 0, "(_:Int)static"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "class S { class func f() -> Int { 1 } }", + "function_declaration", 0, "()class=>Int"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "struct S { static func f() async -> Int { 1 } }", "function_declaration", 0, + "()static async=>Int"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "extension Box where T: P { func f(_ x: Int) {} }", "function_declaration", 0, + "<;where T:P>(_:Int)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "extension Box where T: Q { func f(_ x: U) where U: P {} }", "function_declaration", 0, + "(_:U)"}, {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func extra() {}", "function_declaration", 0, "()"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", "function_declaration", + 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) {}", + "function_declaration", 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) where T: Equatable {}", + "function_declaration", 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) where T.Element == Int {}", + "function_declaration", 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f < T > ( _ x : T ) where T . Element == Int { }", "function_declaration", 0, + "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T) where T.Element == U.Element, U: Module.Q {}", + "function_declaration", 0, "(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: Int) async {}", + "function_declaration", 0, "(_:Int)async"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(body: () async -> Void) {}", + "function_declaration", 0, "(body:()async=>Void)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f(_ x: T) where T == () -> Void {}", + "function_declaration", 0, "Void>(_:T)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func work(_ x: A.Item) {}", + "function_declaration", 0, "(_:Item)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func work(_ x: B.Item) {}", + "function_declaration", 0, "(_:Item)"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func pick() -> Int { 1 }", + "function_declaration", 0, "()=>Int"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func pick() -> String { \"x\" }", + "function_declaration", 0, "()=>String"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: [A.Item], body: (B.Item) -> A.Item) async -> (A.Item, [B.Item]) {}", + "function_declaration", 0, "(_:[Item],body:(Item)=>Item)async=>(Item,[Item])"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f ( _ x : [ A /* comment */ . Item ], body : ( B.Item ) -> A.Item ) async " + "-> ( A.Item, [ B.Item ] ) {}", + "function_declaration", 0, "(_:[Item],body:(Item)=>Item)async=>(Item,[Item])"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, "func f() -> (A.Item) -> B.Item {}", + "function_declaration", 0, "()=>(Item)=>Item"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T, _ y: U) -> Int where T == Box<[Int]>, U == () -> Void {}", + "function_declaration", 0, ",U==()=>Void>(_:T,_:U)=>Int"}, + {CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, + "func f(_ x: T, _ y: U) -> Int where T == Box<() -> Void>, U == () -> Void {}", + "function_declaration", 0, "Void>,U==()=>Void>(_:T,_:U)=>Int"}, /* Scala: every clause flattened, by-name and repeated parameters. */ {CBM_LANG_SCALA, CBM_CALLABLE_ID_TYPED, "class A {\n def work[T <: AnyRef](xs: List[T], n: => Int, rest: String*)(implicit ord: " @@ -220,16 +277,39 @@ TEST(callable_sig_cap_keeps_identity) { char *qn = cbm_arena_sprintf(&a, "p.A.f%s", sa); ASSERT_EQ(cbm_qn_callable_base_len(qn), strlen("p.A.f")); cbm_arena_destroy(&a); + n = (size_t)snprintf(src_a, sizeof(src_a), "func f() -> ("); + for (int i = 0; i < 24; i++) { + n += (size_t)snprintf(src_a + n, sizeof(src_a) - n, "%sA.VeryLongType%d", i ? "," : "", i); + } + memcpy(src_b, src_a, n); + snprintf(src_a + n, sizeof(src_a) - n, ",A.Last) {}"); + snprintf(src_b + n, sizeof(src_b) - n, ",B.Other) {}"); + cbm_arena_init(&a); + sa = + sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_a, "function_declaration", 0); + sa2 = + sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_a, "function_declaration", 0); + sb = + sig_of(&a, CBM_LANG_SWIFT, CBM_CALLABLE_ID_LABELED_TYPED, src_b, "function_declaration", 0); + ASSERT_NOT_NULL(sa); + ASSERT_NOT_NULL(sb); + ASSERT_TRUE(strlen(sa) <= CBM_CALLABLE_SIG_MAX); + ASSERT_STR_EQ(sa, sa2); + ASSERT_STR_NEQ(sa, sb); + qn = cbm_arena_sprintf(&a, "p.A.f%s", sa); + ASSERT_EQ(cbm_qn_callable_base_len(qn), strlen("p.A.f")); + cbm_arena_destroy(&a); PASS(); } -/* PR1 is plumbing: no language mints a suffix yet. */ -TEST(callable_sig_every_language_is_none) { +/* Swift is enabled; the other languages keep their historical QNs. */ +TEST(callable_sig_only_swift_enabled) { CBMArena a; cbm_arena_init(&a); const char *src = "class A { void f(int x) {} }"; for (int lang = 0; lang < CBM_LANG_COUNT; lang++) { - ASSERT_EQ(cbm_callable_identity((CBMLanguage)lang), CBM_CALLABLE_ID_NONE); + ASSERT_EQ(cbm_callable_identity((CBMLanguage)lang), + lang == CBM_LANG_SWIFT ? CBM_CALLABLE_ID_LABELED_TYPED : CBM_CALLABLE_ID_NONE); } TSParser *parser = ts_parser_new(); ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_JAVA)); @@ -244,6 +324,27 @@ TEST(callable_sig_every_language_is_none) { PASS(); } +TEST(callable_sig_swift_default_parameters) { + const char *src = "class C { func f(a: Int, b: String = \"x\", c: () -> Void) {} }"; + TSParser *parser = ts_parser_new(); + ASSERT_NOT_NULL(parser); + ASSERT_TRUE(ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_SWIFT))); + TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)strlen(src)); + ASSERT_NOT_NULL(tree); + TSNode method = {0}; + int left = 0; + ASSERT_TRUE(find_nth(ts_tree_root_node(tree), "function_declaration", &left, &method)); + CBMArena a; + cbm_arena_init(&a); + ASSERT_STR_EQ(cbm_callable_sig(&a, method, src, CBM_LANG_SWIFT), "(a:Int,b:String,c:()=>Void)"); + uint8_t count = 0; + ASSERT_EQ(cbm_swift_default_mask(method, src, &count), UINT64_C(1) << 1); + ASSERT_EQ(count, 3); + cbm_arena_destroy(&a); + ts_tree_delete(tree); + ts_parser_delete(parser); + PASS(); +} /* The inverse is the identity on every historical QN shape. */ TEST(callable_sig_base_len_unsuffixed_is_full) { static const char *const qns[] = { @@ -267,6 +368,9 @@ TEST(callable_sig_base_len_unsuffixed_is_full) { "proj.lib/(group)/page", "proj.C.+", "proj.A.unary_!", + "p.f()=>", + "p.f()=>[Int", + "p.f()=>Int)", }; for (size_t i = 0; i < sizeof(qns) / sizeof(qns[0]); i++) { ASSERT_EQ(cbm_qn_callable_base_len(qns[i]), strlen(qns[i])); @@ -290,6 +394,13 @@ TEST(callable_sig_base_len_suffixed) { {"p.S.f(const vector&,int(*)(int))const&&", "p.S.f"}, {"p.C.Work(ref int)", "p.C.Work"}, {"p.Session.upload(_:Data,to:URL)", "p.Session.upload"}, + {"p.S.pick()=>Int", "p.S.pick"}, + {"p.S.pick()=>String", "p.S.pick"}, + {"p.S.<(_:A,_:A)=>Bool", "p.S.<"}, + {"p.S.>(_:A,_:A)=>Bool", "p.S.>"}, + {"p.S.<(_:T,_:T)=>Bool", "p.S.<"}, + {"p.S.pick()async=>(A/Item)=>B/Item", "p.S.pick"}, + {"p.S.pickVoid>(_:T)=>[A/Item]", "p.S.pick"}, {"p.K.work(this:String,(Int)=>Unit)", "p.K.work"}, {"p.O.initWithFrame(_:style:)", "p.O.initWithFrame"}, {"p.C.+(l:C,r:C)", "p.C.+"}, @@ -340,7 +451,8 @@ TEST(callable_sig_registry_by_name_uses_base_leaf) { SUITE(callable_sig) { RUN_TEST(callable_sig_golden_table); RUN_TEST(callable_sig_cap_keeps_identity); - RUN_TEST(callable_sig_every_language_is_none); + RUN_TEST(callable_sig_only_swift_enabled); + RUN_TEST(callable_sig_swift_default_parameters); RUN_TEST(callable_sig_base_len_unsuffixed_is_full); RUN_TEST(callable_sig_base_len_suffixed); RUN_TEST(callable_sig_leaf_splitters_skip_suffix); diff --git a/tests/test_complexity.c b/tests/test_complexity.c index a67d0d753b..3d396780d6 100644 --- a/tests/test_complexity.c +++ b/tests/test_complexity.c @@ -440,6 +440,8 @@ typedef struct { int edges; int lang_nodes[CX_TEMPLATE_COUNT]; int lang_edges[CX_TEMPLATE_COUNT]; + int swift_extension_calls; + int swift_foreign_extension_calls; CxPassMs passes[CX_MAX_PASSES]; int pass_count; uint64_t perfile_defs; /* Σ defs registered by per-file/overlay registry builds */ @@ -705,6 +707,46 @@ static double cx_now_s(void) { return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; } +/* Positive extension edges keep the scaling guard from passing by dropping calls. */ +static bool cx_swift_extension_population(cbm_store_t *store, const char *project, CxMetrics *out) { + cbm_node_t *callers = NULL; + int count = 0; + if (cbm_store_find_nodes_by_name(store, project, "extensionCaller", &callers, &count) != + CBM_STORE_OK) + return false; + bool ok = true; + for (int i = 0; i < count; i++) { + const cbm_node_t *caller = &callers[i]; + if (!caller->file_path || strncmp(caller->file_path, "fx_Swift/", 9) != 0) + continue; + cbm_edge_t *edges = NULL; + int edge_count = 0; + if (cbm_store_find_edges_by_source_type(store, caller->id, "CALLS", &edges, &edge_count) != + CBM_STORE_OK) { + ok = false; + continue; + } + const char *slash = strrchr(caller->file_path, '/'); + size_t directory = slash ? (size_t)(slash - caller->file_path + 1) : 0; + for (int j = 0; j < edge_count; j++) { + cbm_node_t target = {0}; + if (cbm_store_find_node_by_id(store, edges[j].target_id, &target) != CBM_STORE_OK) { + ok = false; + continue; + } + if (target.file_path && strstr(target.file_path, "/Extensions.swift")) { + out->swift_extension_calls++; + if (strncmp(caller->file_path, target.file_path, directory) != 0) + out->swift_foreign_extension_calls++; + } + cbm_node_free_fields(&target); + } + cbm_store_free_edges(edges, edge_count); + } + cbm_store_free_nodes(callers, count); + return ok; +} + static int cx_run(const char *root, const char *db_path, CxMetrics *out, bool pyext, bool pyrange) { memset(out, 0, sizeof(*out)); @@ -800,7 +842,8 @@ static int cx_run(const char *root, const char *db_path, CxMetrics *out, bool py atomic_load_explicit(&cbm_py_wildcard_invalidations, memory_order_relaxed) - pw0; out->py_wildcard_visits = atomic_load_explicit(&cbm_py_wildcard_invalidation_visits, memory_order_relaxed) - pwv0; - bool extra_ok = (!pyext || cx_pyext_population(s, project, out)) && + bool extra_ok = cx_swift_extension_population(s, project, out) && + (!pyext || cx_pyext_population(s, project, out)) && (!pyrange || cx_pyrange_population(s, project, out)); cbm_store_close(s); return extra_ok ? 0 : -1; @@ -897,6 +940,11 @@ TEST(complexity_replicated_modules_scale_linearly) { ASSERT_TRUE(node_r >= CX_RATIO_LO && node_r <= CX_RATIO_HI); ASSERT_TRUE(edge_r >= CX_RATIO_LO && edge_r <= CX_RATIO_HI); + ASSERT_EQ(a->swift_extension_calls, CX_K_BASE * 2); + ASSERT_EQ(b->swift_extension_calls, CX_K_BASE * 4); + ASSERT_EQ(a->swift_foreign_extension_calls, 0); + ASSERT_EQ(b->swift_foreign_extension_calls, 0); + /* Per-language linearity, from the same run pair (scoped by subtree). If * scoped counting returns 0 for the base run the scope semantics changed — * surface that rather than silently skipping. */ diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 9ac8b1d7a7..75c4e0e24d 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -3823,6 +3823,241 @@ TEST(swift_method_call) { PASS(); } +TEST(swift_type_imports_preserve_markers_and_complete_file_imports) { + CBMFileResult *r = extract("struct Service {}\n" + "extension Service { func run() {} }\n" + "import NIOCore\n" + "@testable import NIOFS\n" + "@_exported import struct Alpha.Service\n", + CBM_LANG_SWIFT, "t", "Types.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_EQ(r->imports.count, 3); + ASSERT_STR_EQ(r->imports.items[1].module_path, "NIOFS"); + ASSERT_STR_EQ(r->imports.items[1].local_name, "NIOFS"); + ASSERT_STR_EQ(r->imports.items[2].module_path, "Alpha.Service"); + int types = 0; + for (int i = 0; i < r->defs.count; i++) { + const CBMDefinition *def = &r->defs.items[i]; + bool marker = false; + const char *imports = NULL; + for (const char **kv = def->extra_props; kv && kv[0] && kv[1]; kv += 2) { + marker |= + strcmp(kv[0], "swift_type_path") == 0 || strcmp(kv[0], "swift_extension") == 0; + if (strcmp(kv[0], "swift_imports") == 0) { + imports = kv[1]; + } + } + if (marker) { + types++; + ASSERT_NOT_NULL(imports); + ASSERT_STR_EQ(imports, "NIOCore|NIOFS|Alpha.Service"); + } else { + ASSERT_NULL(imports); + } + } + ASSERT_EQ(types, 2); + cbm_free_result(r); + PASS(); +} + +TEST(swift_conditional_type_members_keep_owners_and_source_lines) { + const char *source = "open class Session {\n" + "#if canImport(Darwin) /* conditional\n" + " * comment */\n" + "open func request(_ value: Int) { called() }\n" + "#if FEATURE\n" + "open func nested() {}\n" + "#else\n" + "open func alternate() {}\n" + "#endif\n" + "#elseif canImport(FoundationNetworking)\n" + "open func portable() {}\n" + "#else\n" + "open func fallback() {}\n" + "#endif\n" + "open func upload(_ value: Int) {}\n" + "}\n" + "let literal = #\"\"\"\n" + "#if RAW\n" + "func phantom() {}\n" + "#endif\n" + "\"\"\"#\n" + "/* outer\n" + "#if COMMENT\n" + "/* nested */\n" + "func fake() {}\n" + "#endif\n" + "*/\n" + "func outside() {}\n"; + CBMFileResult *r = extract(source, CBM_LANG_SWIFT, "t", "Session.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_FALSE(r->parse_incomplete); + const char *members[] = {"Session.request(_:Int)", "Session.nested()", + "Session.alternate()", "Session.portable()", + "Session.fallback()", "Session.upload(_:Int)"}; + for (size_t i = 0; i < sizeof(members) / sizeof(members[0]); i++) { + const CBMDefinition *def = find_def_in_module(r, members[i]); + ASSERT_NOT_NULL(def); + ASSERT_STR_EQ(def->label, "Method"); + ASSERT_STR_EQ(def->parent_class, "t.Session.Session"); + } + ASSERT_EQ(find_def_in_module(r, "Session.request(_:Int)")->start_line, 4); + ASSERT_EQ(find_def_in_module(r, "Session.upload(_:Int)")->start_line, 15); + const CBMDefinition *outside = find_def_in_module(r, "outside()"); + ASSERT_NOT_NULL(outside); + ASSERT_EQ(outside->start_line, 28); + ASSERT_FALSE(has_def_any(r, "phantom")); + ASSERT_FALSE(has_def_any(r, "fake")); + const CBMCall *call = find_call_by_callee(r, "called"); + ASSERT_NOT_NULL(call); + ASSERT_STR_EQ(call->enclosing_func_qn, "t.Session.Session.request(_:Int)"); + cbm_free_result(r); + PASS(); +} + +TEST(swift_uncapped_selectors_keep_nested_return_types) { + CBMFileResult *r = extract("func generic(_ value: Int) -> Box { fatalError() }\n" + "func callback() -> (Int) -> Result { fatalError() }\n", + CBM_LANG_SWIFT, "t", "Returns.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + const CBMDefinition *generic = find_def_in_module(r, "generic(_:Int)=>Box"); + const CBMDefinition *callback = find_def_in_module(r, "callback()=>(Int)=>Result"); + ASSERT_NOT_NULL(generic); + ASSERT_NOT_NULL(callback); + ASSERT_STR_EQ(generic->call_selector, "(_:Int)=>Box"); + ASSERT_STR_EQ(callback->call_selector, "()=>(Int)=>Result"); + cbm_free_result(r); + PASS(); +} + +TEST(swift_wrapped_trailing_closure_keeps_one_complete_call) { + const char *source = + "func invoke() async throws {\n" + "let request = session.request(.default).responseDecodable(of: Value.self) { value in\n" + " consume(value)\n" + "}\n" + "let storedSession = stored(Session())\n" + "let awaitedSession = try await stored(Session())\n" + "factory()(flag: true)\n" + "}\n"; + CBMFileResult *r = extract(source, CBM_LANG_SWIFT, "t", "Calls.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + int responses = 0; + int factories = 0; + for (int i = 0; i < r->calls.count; i++) { + const CBMCall *call = &r->calls.items[i]; + if (strcmp(call->callee_name, "session.request(.default).responseDecodable") == 0) { + responses++; + ASSERT_EQ(call->arg_count, 1); + ASSERT_STR_EQ(call->args[0].keyword, "of"); + ASSERT_EQ(call->trailing_closure_count, 1); + ASSERT_TRUE(call->has_trailing_closure); + ASSERT_FALSE(call->trailing_closure_truncated); + ASSERT_EQ(source[call->site_end_byte - 1], '}'); + } else if (strcmp(call->callee_name, "factory") == 0) { + factories++; + ASSERT_EQ(call->arg_count, 0); + ASSERT_FALSE(call->has_trailing_closure); + } + } + ASSERT_EQ(responses, 1); + ASSERT_EQ(factories, 1); + ASSERT_NOT_NULL(find_call_by_callee(r, "session.request")); + ASSERT_NOT_NULL(find_call_by_callee(r, "consume")); + const char *names[] = {"storedSession", "awaitedSession"}; + const char *values[] = {"stored(Session())", "try await stored(Session())"}; + for (int compact = 0; compact < 2; compact++) { + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + const CBMTypeAssign *assignment = NULL; + for (int j = 0; j < r->type_assigns.count; j++) { + if (strcmp(r->type_assigns.items[j].var_name, names[i]) == 0) { + assignment = &r->type_assigns.items[j]; + } + } + ASSERT_NOT_NULL(assignment); + ASSERT_NULL(assignment->type_name); + size_t start = (size_t)(strstr(source, values[i]) - source); + ASSERT_EQ(assignment->value_start_byte, start); + ASSERT_EQ(assignment->value_end_byte, start + strlen(values[i])); + } + if (!compact) { + cbm_result_compact(r); + } + } + cbm_free_result(r); + PASS(); +} + +TEST(swift_selector_expands_only_lexical_function_aliases) { + CBMFileResult *r = + extract("typealias Plain = () -> Void\nstruct Payload {}\n" + "class Stream {\n" + "typealias Handler = @Sendable (T) throws -> Void\n" + "func response(on queue: Int = 0, stream: Handler) -> Self { self }\n}\n" + "class Wrong {\ntypealias Handler = [T]\n" + "func response(stream: Handler) {}\n}\n" + "class Top { func use(done: Plain) {} }\n", + CBM_LANG_SWIFT, "t", "Aliases.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + const CBMDefinition *stream = + find_def_in_module(r, "Stream.response(on:Int,stream:Handler)=>Self"); + const CBMDefinition *wrong = find_def_in_module(r, "Wrong.response(stream:Handler)"); + const CBMDefinition *top = find_def_in_module(r, "Top.use(done:Plain)"); + ASSERT_NOT_NULL(stream); + ASSERT_NOT_NULL(wrong); + ASSERT_NOT_NULL(top); + ASSERT_NOT_NULL(strstr(stream->call_selector, "stream:(T)throws=>Void")); + ASSERT_STR_EQ(wrong->call_selector, "(stream:Handler)"); + ASSERT_STR_EQ(top->call_selector, "(done:()=>Void)"); + cbm_free_result(r); + PASS(); +} + +TEST(swift_multiple_trailing_closure_extraction) { + CBMFileResult *r = + extract("func outer() { service.handle { nested() } onError: { recover() } }\n" + "func one() { service.run { nested() } }\n" + "func incomplete() { service.handle { } onError: { recover( } }\n", + CBM_LANG_SWIFT, "t", "Calls.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + CBMCall *multi = NULL; + CBMCall *single = NULL; + CBMCall *incomplete = NULL; + for (int i = 0; i < r->calls.count; i++) { + CBMCall *call = &r->calls.items[i]; + if (call->callee_name && strcmp(call->callee_name, "service.handle") == 0) { + if (call->start_line == 1) { + multi = call; + } else { + incomplete = call; + } + } else if (call->callee_name && strcmp(call->callee_name, "service.run") == 0) { + single = call; + } + } + ASSERT_NOT_NULL(multi); + ASSERT_NOT_NULL(single); + ASSERT_NOT_NULL(incomplete); + ASSERT_EQ(multi->trailing_closure_count, 2); + ASSERT_TRUE(multi->has_trailing_closure); + ASSERT_FALSE(multi->trailing_closure_truncated); + ASSERT_NOT_NULL(multi->trailing_closure_labels); + ASSERT_NULL(multi->trailing_closure_labels[0]); + ASSERT_STR_EQ(multi->trailing_closure_labels[1], "onError"); + ASSERT_EQ(single->trailing_closure_count, 1); + ASSERT_FALSE(single->trailing_closure_truncated); + ASSERT_EQ(incomplete->trailing_closure_count, 2); + ASSERT_TRUE(incomplete->trailing_closure_truncated); + cbm_free_result(r); + PASS(); +} + TEST(swift_constructor_call) { CBMFileResult *r = extract("func create() { let x = MyClass() }\n", CBM_LANG_SWIFT, "t", "create.swift"); @@ -3876,7 +4111,7 @@ TEST(swift_init_struct_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *init_def = find_def_in_module(r, "Box.init"); + const CBMDefinition *init_def = find_def_in_module(r, "Box.init()"); ASSERT_NOT_NULL(init_def); ASSERT_STR_EQ(init_def->label, "Method"); ASSERT_STR_EQ(init_def->name, "init"); @@ -3905,7 +4140,7 @@ TEST(swift_init_class_designated_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *init_def = find_def_in_module(r, "Holder.init"); + const CBMDefinition *init_def = find_def_in_module(r, "Holder.init(seed:Int)"); ASSERT_NOT_NULL(init_def); ASSERT_STR_EQ(init_def->label, "Method"); @@ -3937,12 +4172,11 @@ TEST(swift_init_convenience_delegates_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *init_def = find_def_in_module(r, "Holder.init"); + const CBMDefinition *init_def = find_def_in_module(r, "Holder.init()"); ASSERT_NOT_NULL(init_def); - /* self.init(...) delegates to another overload of Holder.init, which - * shares this node (#2061): it forms no call, because the only edge it - * could produce is a self-loop. Its argument call stays. */ + /* Keep #2395's self.init suppression; its argument call still belongs + * to this initializer. */ ASSERT_FALSE(has_call(r, "self.init")); ASSERT_FALSE(has_call(r, "Holder.init")); @@ -3956,13 +4190,8 @@ TEST(swift_init_convenience_delegates_issue2379) { PASS(); } -/* #2379: two initializers in the same type calling the SAME callee must both - * attribute to the type's init, not to the file's Module scope. Both share - * the QN `Holder.init` -- codebase-memory-mcp already collapses overloaded - * Swift methods onto one node (DeusData/codebase-memory-mcp#2061), and an - * init is no exception; distinguishing overloads by signature is that - * issue's concern, not this one's. */ -TEST(swift_init_overloads_share_one_node_issue2379) { +/* Each initializer keeps its own identity and outgoing calls. */ +TEST(swift_init_overloads_keep_separate_nodes_issue2061) { CBMFileResult *r = extract("func shared() -> Int { 3 }\n" "final class Holder {\n" " let value: Int\n" @@ -3977,20 +4206,29 @@ TEST(swift_init_overloads_share_one_node_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *init_def = find_def_in_module(r, "Holder.init"); - ASSERT_NOT_NULL(init_def); + const CBMDefinition *seeded = find_def_in_module(r, "Holder.init(seed:Int)"); + const CBMDefinition *empty = find_def_in_module(r, "Holder.init()"); + ASSERT_NOT_NULL(seeded); + ASSERT_NOT_NULL(empty); + ASSERT_STR_NEQ(seeded->qualified_name, empty->qualified_name); - int from_init = 0; + int from_seeded = 0; + int from_empty = 0; for (int i = 0; i < r->calls.count; i++) { const CBMCall *c = &r->calls.items[i]; if (!c->callee_name || strcmp(c->callee_name, "shared") != 0 || !c->enclosing_func_qn) { continue; } - ASSERT_STR_EQ(c->enclosing_func_qn, init_def->qualified_name); + if (strcmp(c->enclosing_func_qn, seeded->qualified_name) == 0) { + from_seeded++; + } else { + ASSERT_STR_EQ(c->enclosing_func_qn, empty->qualified_name); + from_empty++; + } ASSERT_STR_NEQ(c->enclosing_func_qn, r->module_qn); - from_init++; } - ASSERT_EQ(from_init, 2); + ASSERT_EQ(from_seeded, 1); + ASSERT_EQ(from_empty, 1); cbm_free_result(r); PASS(); @@ -4009,7 +4247,7 @@ TEST(swift_init_failable_enum_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *init_def = find_def_in_module(r, "Mode.init"); + const CBMDefinition *init_def = find_def_in_module(r, "Mode.init(flag:Bool)"); ASSERT_NOT_NULL(init_def); const CBMCall *call = find_call_by_callee(r, "helper"); @@ -4036,7 +4274,7 @@ TEST(swift_init_extension_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *init_def = find_def_in_module(r, "Box.init"); + const CBMDefinition *init_def = find_def_in_module(r, "Box.init(raw:Int)"); ASSERT_NOT_NULL(init_def); const CBMCall *call = find_call_by_callee(r, "other"); @@ -4064,7 +4302,7 @@ TEST(swift_method_still_attributed_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *read_def = find_def_in_module(r, "Box.read"); + const CBMDefinition *read_def = find_def_in_module(r, "Box.read()=>Int"); ASSERT_NOT_NULL(read_def); const CBMCall *call = find_call_by_callee(r, "other"); @@ -4090,7 +4328,7 @@ TEST(swift_deinit_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *deinit_def = find_def_in_module(r, "Deinitable.deinit"); + const CBMDefinition *deinit_def = find_def_in_module(r, "Deinitable.deinit()"); ASSERT_NOT_NULL(deinit_def); ASSERT_STR_EQ(deinit_def->name, "deinit"); @@ -4119,7 +4357,7 @@ TEST(swift_super_init_names_superclass_issue2379) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - const CBMDefinition *init_def = find_def_in_module(r, "Sub.init"); + const CBMDefinition *init_def = find_def_in_module(r, "Sub.init()"); ASSERT_NOT_NULL(init_def); ASSERT_FALSE(has_call(r, "super.init")); const CBMCall *call = find_call_by_callee(r, "Base.init"); @@ -4148,8 +4386,8 @@ TEST(swift_super_init_without_superclass_no_call_issue2379) { "}\n", CBM_LANG_SWIFT, "t", "Root.swift"); ASSERT_NOT_NULL(r); - ASSERT_NOT_NULL(find_def_in_module(r, "Root.init")); - ASSERT_NOT_NULL(find_def_in_module(r, "Odd.init")); + ASSERT_NOT_NULL(find_def_in_module(r, "Root.init()")); + ASSERT_NOT_NULL(find_def_in_module(r, "Odd.init()")); for (int i = 0; i < r->calls.count; i++) { const char *callee = r->calls.items[i].callee_name; ASSERT(!callee || !strstr(callee, "init")); @@ -10387,22 +10625,23 @@ TEST(extract_cpp_preprocessed_rescue_skips_raw_def_on_same_line) { TEST(extract_cpp_rescue_keeps_expanded_class_when_raw_qn_is_taken) { /* Minimized from holder.h (delta debugging against the rule with and * without this check). */ - CBMFileResult *r = extract( - "class BaseWrapper : public BaseHolder\n" - "{\n" - " class AddressInitHolder\n" - " {\n" - " FORCEINLINE AddressInitHolder(BaseWrapper &holder)\n" - " {\n" - " unknown = static_cast(m_holder.m_value);\n" - " }\n" - "#if defined(FEATURE_COMINTEROP) && !defined(STRIKE)\n" - " FORCEINLINE operator IInspectable **()\n" - "#endif // FEATURE_COMINTEROP\n" - " {\n" - " }\n" - " }\n", - CBM_LANG_CPP, "t", "holder.h"); + CBMFileResult *r = + extract("class BaseWrapper : public BaseHolder\n" + "{\n" + " class AddressInitHolder\n" + " {\n" + " FORCEINLINE AddressInitHolder(BaseWrapper " + "&holder)\n" + " {\n" + " unknown = static_cast(m_holder.m_value);\n" + " }\n" + "#if defined(FEATURE_COMINTEROP) && !defined(STRIKE)\n" + " FORCEINLINE operator IInspectable **()\n" + "#endif // FEATURE_COMINTEROP\n" + " {\n" + " }\n" + " }\n", + CBM_LANG_CPP, "t", "holder.h"); ASSERT_NOT_NULL(r); bool scoped_class = false; for (int i = 0; i < r->defs.count; i++) { @@ -14047,6 +14286,97 @@ TEST(extract_spill_round_trip_keeps_every_field) { PASS(); } +TEST(swift_trailing_closure_boundaries_survive_compact_and_spill) { + const char *src = "func eight() { service.eight { } first: { } second: { } third: { } " + "fourth: { } fifth: { } sixth: { } seventh: { } }\n" + "func nine() { service.nine { } first: { } second: { } third: { } " + "fourth: { } fifth: { } sixth: { } seventh: { } eighth: { } }\n"; + const char *callees[] = {"service.eight", "service.nine"}; + const char *labels[] = {NULL, "first", "second", "third", + "fourth", "fifth", "sixth", "seventh"}; + CBMFileResult *r = extract(src, CBM_LANG_SWIFT, "t", "TrailingBounds.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_NOT_NULL(r->cached_tree); + ASSERT_FALSE(ts_node_has_error(ts_tree_root_node(r->cached_tree))); + + char dir[512]; + snprintf(dir, sizeof(dir), "%s/cbm_spill_XXXXXX", cbm_tmpdir()); + ASSERT_NOT_NULL(cbm_mkdtemp(dir)); + cbm_result_spill_t *sp = cbm_result_spill_open(dir, 1, 1); + ASSERT_NOT_NULL(sp); + CBMFileResult *loaded[2] = {NULL, NULL}; + const CBMCall *loaded_calls[2][2] = {{NULL, NULL}, {NULL, NULL}}; + + /* Check real extraction, compaction, and both independently loaded copies. */ + for (int stage = 0; stage < 4; stage++) { + CBMFileResult *current = r; + if (stage == 1) { + cbm_result_compact(r); + } else if (stage >= 2) { + loaded[stage - 2] = cbm_result_spill_load(sp, 0); + current = loaded[stage - 2]; + } + ASSERT_NOT_NULL(current); + ASSERT_FALSE(current->has_error); + if (stage > 0) { + ASSERT_EQ(current->arena.nblocks, 1); + } + for (int c = 0; c < 2; c++) { + ASSERT_EQ(count_calls_named(current, callees[c]), 1); + const CBMCall *call = find_call_by_callee(current, callees[c]); + ASSERT_NOT_NULL(call); + ASSERT_EQ(call->trailing_closure_count, 8); + ASSERT_TRUE(call->has_trailing_closure); + ASSERT_EQ(call->trailing_closure_truncated, c == 1); + ASSERT_NOT_NULL(call->trailing_closure_labels); + if (stage > 0) { + uintptr_t lo = (uintptr_t)current->arena.blocks[0]; + uintptr_t array = (uintptr_t)call->trailing_closure_labels; + size_t bytes = 8 * sizeof(*call->trailing_closure_labels); + ASSERT(current->arena.used >= bytes); + ASSERT(array >= lo && array - lo <= current->arena.used - bytes); + } + ASSERT_NULL(call->trailing_closure_labels[0]); + for (int i = 1; i < 8; i++) { + ASSERT_NOT_NULL(call->trailing_closure_labels[i]); + size_t label_bytes = strlen(labels[i]) + 1; + if (stage > 0) { + uintptr_t lo = (uintptr_t)current->arena.blocks[0]; + uintptr_t label = (uintptr_t)call->trailing_closure_labels[i]; + ASSERT(label >= lo && label - lo < current->arena.used); + ASSERT(label_bytes <= current->arena.used - (label - lo)); + } + ASSERT_MEM_EQ(call->trailing_closure_labels[i], labels[i], label_bytes); + } + if (stage >= 2) { + loaded_calls[stage - 2][c] = call; + } + } + if (stage == 1) { + ASSERT_TRUE(cbm_result_spill_park(sp, 0, 0, r)); + r = NULL; /* Park owns and frees the compacted result. */ + } + } + + ASSERT(loaded[0] != loaded[1]); + ASSERT(loaded[0]->arena.blocks[0] != loaded[1]->arena.blocks[0]); + for (int c = 0; c < 2; c++) { + ASSERT(loaded_calls[0][c]->trailing_closure_labels != + loaded_calls[1][c]->trailing_closure_labels); + for (int i = 1; i < 8; i++) { + ASSERT(loaded_calls[0][c]->trailing_closure_labels[i] != + loaded_calls[1][c]->trailing_closure_labels[i]); + } + } + + cbm_free_result(loaded[1]); + cbm_free_result(loaded[0]); + cbm_result_spill_close(sp); + cbm_rmdir(dir); + PASS(); +} + /* The low-disk guard, pinned through the free-space seam rather than the host * disk: one byte under the floor refuses (spilling onto a nearly full disk is * a worse failure than the memory pressure it relieves), the floor itself @@ -15486,13 +15816,20 @@ SUITE(extraction) { RUN_TEST(swift_struct); RUN_TEST(swift_simple_call); RUN_TEST(swift_method_call); + RUN_TEST(swift_type_imports_preserve_markers_and_complete_file_imports); + RUN_TEST(swift_conditional_type_members_keep_owners_and_source_lines); + RUN_TEST(swift_uncapped_selectors_keep_nested_return_types); + RUN_TEST(swift_wrapped_trailing_closure_keeps_one_complete_call); + RUN_TEST(swift_selector_expands_only_lexical_function_aliases); + RUN_TEST(swift_multiple_trailing_closure_extraction); + RUN_TEST(swift_trailing_closure_boundaries_survive_compact_and_spill); RUN_TEST(swift_constructor_call); RUN_TEST(swift_chained_call); RUN_TEST(swift_force_unwrap_scanner_shift); RUN_TEST(swift_init_struct_issue2379); RUN_TEST(swift_init_class_designated_issue2379); RUN_TEST(swift_init_convenience_delegates_issue2379); - RUN_TEST(swift_init_overloads_share_one_node_issue2379); + RUN_TEST(swift_init_overloads_keep_separate_nodes_issue2061); RUN_TEST(swift_init_failable_enum_issue2379); RUN_TEST(swift_init_extension_issue2379); RUN_TEST(swift_method_still_attributed_issue2379); diff --git a/tests/test_graph_buffer.c b/tests/test_graph_buffer.c index b573786912..48b861427d 100644 --- a/tests/test_graph_buffer.c +++ b/tests/test_graph_buffer.c @@ -69,6 +69,43 @@ TEST(gbuf_upsert_updates) { PASS(); } +TEST(gbuf_swift_declaration_and_extension_keep_both_markers) { + const char *declaration = "{\"swift_type_path\":\"Service\",\"swift_extension\":null," + "\"base_classes\":[\"Base\"],\"docstring\":\"Nominal type\"}"; + const char *extension = + "{\"swift_extension\":\"Service\",\"swift_type_path\":null,\"base_classes\":[]}"; + for (int merge = 0; merge < 2; merge++) { + for (int reverse = 0; reverse < 2; reverse++) { + for (int extension_first = 0; extension_first < 2; extension_first++) { + cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp"); + cbm_gbuf_t *worker = merge ? cbm_gbuf_new("test", "/tmp") : NULL; + ASSERT_NOT_NULL(gb); + for (int i = 0; i < 2; i++) { + bool is_extension = (i == reverse); + int line = (is_extension == (extension_first != 0)) ? 1 : 10; + cbm_gbuf_t *target = merge && i ? worker : gb; + ASSERT_GT(cbm_gbuf_upsert_node(target, "Class", "Service", "test.Types.Service", + "Types.swift", line, line + 2, + is_extension ? extension : declaration), + 0); + } + if (merge) { + cbm_gbuf_merge(gb, worker); + } + const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(gb, "test.Types.Service"); + ASSERT_NOT_NULL(node); + ASSERT_NOT_NULL(strstr(node->properties_json, "\"swift_type_path\":\"Service\"")); + ASSERT_NOT_NULL(strstr(node->properties_json, "\"swift_extension\":\"Service\"")); + ASSERT_NOT_NULL(strstr(node->properties_json, "\"base_classes\":[\"Base\"]")); + ASSERT_NOT_NULL(strstr(node->properties_json, "\"docstring\":\"Nominal type\"")); + cbm_gbuf_free(worker); + cbm_gbuf_free(gb); + } + } + } + PASS(); +} + TEST(gbuf_find_by_id) { cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp"); int64_t id = cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "foo.go", 1, 5, "{}"); @@ -1443,6 +1480,7 @@ SUITE(graph_buffer) { RUN_TEST(gbuf_free_null); RUN_TEST(gbuf_upsert_node); RUN_TEST(gbuf_upsert_updates); + RUN_TEST(gbuf_swift_declaration_and_extension_keep_both_markers); RUN_TEST(gbuf_find_by_id); RUN_TEST(gbuf_find_by_label); RUN_TEST(gbuf_find_by_name); diff --git a/tests/test_index_format.c b/tests/test_index_format.c index dde173ce5e..a7c2e1a053 100644 --- a/tests/test_index_format.c +++ b/tests/test_index_format.c @@ -244,10 +244,173 @@ TEST(index_format_legacy_index_rebuilds_and_repairs) { PASS(); } -/* ── Test 3: an index from before format 3 drops its invented hosts ─── */ +TEST(index_format_version_one_rebuilds) { + RProj lp; + cbm_store_t *initial = rh_index_files(&lp, k_files, k_nfiles); + ASSERT_NOT_NULL(initial); + cbm_store_close(initial); + + cbm_store_t *writer = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(writer); + ASSERT_EQ(cbm_store_set_format_version(writer, 1), CBM_STORE_OK); + cbm_store_close(writer); + + char *resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(g_log_buf, "format_change_reindex")); + ASSERT_NOT_NULL(strstr(resp, "\"format_migration\":true")); + free(resp); + + cbm_store_t *after_migration = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(after_migration); + int format = -1; + ASSERT_EQ(cbm_store_get_format_version(after_migration, &format), CBM_STORE_OK); + ASSERT_EQ(format, CBM_INDEX_FORMAT_VERSION); + ASSERT_EQ(rh_count_label(after_migration, lp.project, "File"), k_nfiles); + cbm_store_close(after_migration); + + resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + ASSERT_NULL(strstr(g_log_buf, "format_change_reindex")); + ASSERT_NULL(strstr(resp, "format_migration")); + free(resp); + cbm_store_t *settled = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(settled); + ASSERT_EQ(rh_count_label(settled, lp.project, "File"), k_nfiles); + rh_cleanup(&lp, settled); + PASS(); +} + +static int swift_collision_migration_case(int scenario) { + const char *sources[] = {"func pick(_ x: T) { first() }\n" + "func pick(_ x: T) { second() }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { pick(value) }\n" + "func wrong() { pick(key: 1) }\n", + "func pick(_ x: A.Item) { first() }\n" + "func pick(_ x: B.Other) { second() }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { pick(value) }\n" + "func wrong() { pick(key: 1) }\n" + "enum A { struct Item {} }\nenum B { struct Other {} }\n", + "func pick() -> Int { first(); return 1 }\n" + "func pick() -> String { second(); return \"x\" }\n" + "func first() {}\nfunc second() {}\n" + "func caller() { let _: Int = pick() }\n" + "func wrong() { pick(key: 1) }\n"}; + const RFile files[] = {{"Service.swift", sources[scenario]}}; + RProj lp; + cbm_store_t *s = rh_index_files(&lp, files, 1); + ASSERT_NOT_NULL(s); + cbm_store_close(s); + s = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(s); + ASSERT_EQ(cbm_store_delete_nodes_by_label(s, lp.project, "Function"), CBM_STORE_OK); + char legacy_qn[512], wrong_qn[512]; + snprintf(legacy_qn, sizeof(legacy_qn), "%s.Service.%s", lp.project, "pick"); + snprintf(wrong_qn, sizeof(wrong_qn), "%s.Service.wrong()", lp.project); + cbm_node_t legacy = {.project = lp.project, + .label = "Function", + .name = "pick", + .qualified_name = legacy_qn, + .file_path = "Service.swift", + .start_line = 2, + .end_line = 2}; + cbm_node_t wrong = {.project = lp.project, + .label = "Function", + .name = "wrong", + .qualified_name = wrong_qn, + .file_path = "Service.swift", + .start_line = 6, + .end_line = 6}; + int64_t legacy_id = cbm_store_upsert_node(s, &legacy); + int64_t wrong_id = cbm_store_upsert_node(s, &wrong); + ASSERT_GT(legacy_id, 0); + ASSERT_GT(wrong_id, 0); + cbm_edge_t stale = {.project = lp.project, + .source_id = wrong_id, + .target_id = legacy_id, + .type = "CALLS", + .properties_json = "{\"candidates\":1}"}; + ASSERT_GT(cbm_store_insert_edge(s, &stale), 0); + ASSERT_EQ(cbm_store_set_format_version(s, scenario + 2), CBM_STORE_OK); + cbm_store_close(s); + for (int run = 0; run < 2; run++) { + char *resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + if (run == 0) { + ASSERT_NOT_NULL(strstr(g_log_buf, "format_change_reindex")); + ASSERT_NOT_NULL(strstr(resp, "\"format_migration\":true")); + } else { + ASSERT_NULL(strstr(g_log_buf, "format_change_reindex")); + ASSERT_NULL(strstr(resp, "format_migration")); + } + free(resp); + s = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(s); + cbm_node_t old = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, legacy_qn, &old), CBM_STORE_NOT_FOUND); + cbm_node_t *picks = NULL; + int count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, lp.project, "pick", &picks, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 2); + cbm_node_t caller = {0}, bad = {0}; + char qn[512]; + snprintf(qn, sizeof(qn), "%s.Service.caller()", lp.project); + ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, qn, &caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, wrong_qn, &bad), CBM_STORE_OK); + cbm_edge_t *edges = NULL; + int n = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, bad.id, "CALLS", &edges, &n), + CBM_STORE_OK); + ASSERT_EQ(n, 0); + cbm_store_free_edges(edges, n); + ASSERT_NEQ(picks[0].id, picks[1].id); + ASSERT_NEQ(picks[0].start_line, picks[1].start_line); + for (int i = 0; i < count; i++) { + cbm_node_t marker = {0}; + snprintf(qn, sizeof(qn), "%s.Service.%s()", lp.project, + picks[i].start_line == 1 ? "first" : "second"); + ASSERT_EQ(cbm_store_find_node_by_qn(s, lp.project, qn, &marker), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, picks[i].id, "CALLS", &edges, &n), + CBM_STORE_OK); + ASSERT_EQ(n, 1); + ASSERT_EQ(edges[0].target_id, marker.id); + cbm_store_free_edges(edges, n); + cbm_node_free_fields(&marker); + } + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &n), + CBM_STORE_OK); + ASSERT_EQ(n, 2); + for (int i = 0; i < 2; i++) { + ASSERT_TRUE(edges[i].target_id == picks[0].id || edges[i].target_id == picks[1].id); + ASSERT_NOT_NULL(strstr(edges[i].properties_json, "\"candidates\":2")); + ASSERT_EQ(picks[i].start_line, picks[i].end_line); + ASSERT_TRUE(picks[i].start_line == 1 || picks[i].start_line == 2); + } + cbm_store_free_edges(edges, n); + cbm_store_free_nodes(picks, count); + cbm_node_free_fields(&caller); + cbm_node_free_fields(&bad); + int format = -1; + ASSERT_EQ(cbm_store_get_format_version(s, &format), CBM_STORE_OK); + ASSERT_EQ(format, CBM_INDEX_FORMAT_VERSION); + cbm_store_close(s); + } + rh_cleanup(&lp, NULL); + PASS(); +} + +TEST(index_format_swift_collisions_and_stale_calls_rebuild) { + for (int scenario = 0; scenario < 3; scenario++) { + ASSERT_EQ(swift_collision_migration_case(scenario), 0); + } + PASS(); +} -/* Format 2 stored an image without a registry host under an invented - * docker.io identity. Refreshed in place, only touched files would get the +/* Format 2 stored a hostless image under + * an invented docker.io identity. Refreshed in place, only touched files get the * new identity: both forms of one image in a single graph. */ static const RFile k_image_files[] = { {"a/Dockerfile", "FROM nginx:1.27\n"}, @@ -262,7 +425,7 @@ TEST(index_format_v2_images_rebuild_without_invented_host) { ASSERT_NOT_NULL(q0); cbm_store_close(q0); - /* The format-2 shape: the same node, edges kept, under its old identity. */ + /* The old shape: the same node, edges kept, under its old identity. */ cbm_store_t *w = cbm_store_open_path(lp.dbpath); ASSERT_NOT_NULL(w); ASSERT_EQ(sqlite3_exec(cbm_store_get_db(w), @@ -280,7 +443,7 @@ TEST(index_format_v2_images_rebuild_without_invented_host) { ASSERT_NOT_NULL(resp); free(resp); if (!strstr(g_log_buf, "format_change_reindex")) { - FAIL("a format-2 index was not routed through the full-reindex path"); + FAIL("an older index was not routed through the full-reindex path"); } cbm_store_t *r = cbm_store_open_path(lp.dbpath); ASSERT_NOT_NULL(r); @@ -299,5 +462,7 @@ TEST(index_format_v2_images_rebuild_without_invented_host) { SUITE(index_format) { RUN_TEST(index_format_siblings_distinct_and_searchable); RUN_TEST(index_format_legacy_index_rebuilds_and_repairs); + RUN_TEST(index_format_version_one_rebuilds); + RUN_TEST(index_format_swift_collisions_and_stale_calls_rebuild); RUN_TEST(index_format_v2_images_rebuild_without_invented_host); } diff --git a/tests/test_infrascan.c b/tests/test_infrascan.c index faffd78e46..40068852b9 100644 --- a/tests/test_infrascan.c +++ b/tests/test_infrascan.c @@ -183,6 +183,13 @@ TEST(infrascan_service_pattern_match_rejects_ids_inside_words) { {NULL, CBM_SVC_NONE}, }; ASSERT_EQ(svc_case_mismatches(cases), 0); + ASSERT_EQ(cbm_service_pattern_match_named("proj.worker.run(_:Alamofire)", "run"), CBM_SVC_NONE); + ASSERT_EQ(cbm_service_pattern_match_named("proj.Alamofire.request(_:String)", "request"), + CBM_SVC_HTTP); + /* A field whose name contains a call keeps its complete QN. */ + ASSERT_EQ(cbm_service_pattern_match_named("proj.worker.field = make(Alamofire)", + "field = make(Alamofire)"), + cbm_service_pattern_match("proj.worker.field = make(Alamofire)")); PASS(); } @@ -756,10 +763,10 @@ TEST(infrascan_data_flows_reach_every_infra_handler) { PASS(); } -/* R1 + R2 (user decision, 2026-10-18): a caller in a test file pairs only with handlers in that same - * file, and a handler in a test file pairs only with callers in that same file. A test's request to - * the application, a production caller matched to a test's mock server, and the private mock servers - * of two tests pairing across files are not data flows of the application. */ +/* R1 + R2 (user decision, 2026-10-18): a caller in a test file pairs only with handlers in that + * same file, and a handler in a test file pairs only with callers in that same file. A test's + * request to the application, a production caller matched to a test's mock server, and the private + * mock servers of two tests pairing across files are not data flows of the application. */ static int64_t r12_fn(cbm_gbuf_t *gb, const char *name, const char *file) { char qn[96]; snprintf(qn, sizeof(qn), "test.%s", name); @@ -785,7 +792,8 @@ TEST(infrascan_data_flows_test_callers_pair_only_within_their_file) { int64_t route = r12_route(gb, "/api/orders"); int64_t handler = r12_fn(gb, "list_orders", "app/views.py"); cbm_gbuf_insert_edge(gb, handler, route, "HANDLES", "{}"); - /* Test files by each language's own rule: a tests/ directory, a Java *Test.java, a TS .spec.ts. */ + /* Test files by each language's own rule: a tests/ directory, a Java *Test.java, a TS .spec.ts. + */ int64_t py_test = r12_fn(gb, "test_list_orders", "tests/test_orders.py"); int64_t java_test = r12_fn(gb, "listsOrders", "app/OrderServiceTest.java"); int64_t ts_spec = r12_fn(gb, "fetchesOrders", "web/orders.spec.ts"); diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 11be4581a2..38379cf989 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -3274,6 +3274,75 @@ static yyjson_val *trace_grouped_row_named(yyjson_val *leg, const char *name, return NULL; } +TEST(tool_swift_overload_presentation_issue2061) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + cbm_store_t *store = cbm_mcp_server_store(srv); + const char *project = "swift-overloads"; + cbm_mcp_server_set_project(srv, project); + ASSERT_EQ(cbm_store_upsert_project(store, project, "/tmp/swift-overloads"), CBM_STORE_OK); + const char *names[] = {"work", "work", "target", "onlyCallsOverloadB"}; + const char *qns[] = { + "swift-overloads.Service.work(flag:Bool)", "swift-overloads.Service.work(name:String)", + "swift-overloads.Sink.target()", "swift-overloads.Caller.onlyCallsOverloadB()"}; + int64_t ids[4]; + for (int i = 0; i < 4; i++) { + cbm_node_t node = {.project = project, + .label = "Method", + .name = names[i], + .qualified_name = qns[i], + .file_path = "Sources/Service.swift", + .start_line = i == 0 ? 5 : 10, + .end_line = i == 0 ? 7 : 12}; + ids[i] = cbm_store_upsert_node(store, &node); + ASSERT_GT(ids[i], 0); + } + cbm_edge_t flag = { + .project = project, .source_id = ids[0], .target_id = ids[2], .type = "CALLS"}; + cbm_edge_t name = { + .project = project, .source_id = ids[3], .target_id = ids[1], .type = "CALLS"}; + ASSERT_GT(cbm_store_insert_edge(store, &flag), 0); + ASSERT_GT(cbm_store_insert_edge(store, &name), 0); + char *response = + cbm_mcp_handle_tool(srv, "search_graph", + "{\"project\":\"swift-overloads\",\"name_pattern\":\"^work$\"," + "\"format\":\"json\"}"); + char *text = extract_text_content(response); + ASSERT_NOT_NULL(text); + yyjson_doc *doc = yyjson_read(text, strlen(text), 0); + ASSERT_NOT_NULL(doc); + yyjson_val *root = yyjson_doc_get_root(doc); + ASSERT_EQ(yyjson_get_int(yyjson_obj_get(root, "total")), 2); + const char *prefix = NULL; + yyjson_val *row = trace_grouped_row_named(root, "work(flag:Bool)", &prefix); + ASSERT_NOT_NULL(row); + ASSERT_STR_EQ(prefix, "swift-overloads.Service"); + ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(row, 2)), "5-7"); + row = trace_grouped_row_named(root, "work(name:String)", NULL); + ASSERT_NOT_NULL(row); + ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(row, 2)), "10-12"); + yyjson_doc_free(doc); + free(text); + free(response); + response = cbm_mcp_handle_tool(srv, "trace_path", + "{\"project\":\"swift-overloads\",\"function_name\":\"target\"," + "\"direction\":\"inbound\",\"depth\":3,\"format\":\"json\"}"); + text = extract_text_content(response); + ASSERT_NOT_NULL(text); + doc = yyjson_read(text, strlen(text), 0); + ASSERT_NOT_NULL(doc); + root = yyjson_doc_get_root(doc); + ASSERT_EQ(yyjson_get_int(yyjson_obj_get(root, "callers_total")), 1); + row = trace_grouped_row_named(yyjson_obj_get(root, "callers"), "work(flag:Bool)", NULL); + ASSERT_NOT_NULL(row); + ASSERT_EQ(yyjson_get_int(yyjson_arr_get(row, 1)), 1); + yyjson_doc_free(doc); + free(text); + free(response); + cbm_mcp_server_free(srv); + PASS(); +} + /* callers_total/callees_total must count what the caller can enumerate: with * include_tests=false (default) test-file rows are hidden from the table, so * the totals must apply the same filter — a raw visited_count overstated the @@ -28015,6 +28084,7 @@ SUITE(mcp) { RUN_TEST(tool_search_graph_empty_hint_describes_combined_filters_issue1366); RUN_TEST(tool_search_graph_semantic_only_skips_structural_results_issue1295); RUN_TEST(tool_search_graph_grouped_dotless_qn_round_trips); + RUN_TEST(tool_swift_overload_presentation_issue2061); RUN_TEST(tool_trace_totals_respect_test_filter); RUN_TEST(tool_trace_totals_respect_test_filter_tests_root_subtree_issue1294); RUN_TEST(tool_get_architecture_cycles_detects_scc); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 8e4686b0e2..5b8ee2e9ef 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -5210,10 +5210,11 @@ TEST(incremental_release_values_edit_rebuilds) { write_temp_file(tmp, "kz/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"); write_temp_file(tmp, "kz/charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"); - write_temp_file(tmp, "kz/charts/app/templates/deploy.yaml", - "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n" - " template:\n spec:\n containers:\n - name: app\n" - " image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"); + write_temp_file( + tmp, "kz/charts/app/templates/deploy.yaml", + "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n" + " template:\n spec:\n containers:\n - name: app\n" + " image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/relvalues.db", tmp); static const char v2[] = "SELECT COUNT(*) FROM edges WHERE type='DEPLOYS' AND " @@ -5462,7 +5463,8 @@ TEST(helm_render_long_key_mark_whole) { * assign is UNKNOWN after it: `$tag = .Values.image.tagNew` under a * .Capabilities test leaves no image naming the old tag. */ TEST(helm_unknown_branch_spoils_what_it_assigns) { - static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\", tagNew: \"2.0\"}\n"; + static const char vals[] = + "image: {repository: ghcr.io/acme/app, tag: \"1.0\", tagNew: \"2.0\"}\n"; static const char tpl[] = "{{- $tag := .Values.image.tag -}}\n" "{{- if semverCompare \">=1.29-0\" .Capabilities.KubeVersion.Version }}" @@ -5587,9 +5589,10 @@ TEST(helm_skipped_set_of_a_call_result) { * through it (`set $img ...` of `index $d "image"`) reaches .Values. */ TEST(helm_own_map_given_a_shared_one) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"; - static const char tpl[] = "{{- $d := dict -}}{{- $_ := set $d \"image\" .Values.image -}}\n" - "{{- $img := index $d \"image\" -}}{{- $_ := set $img \"tag\" \"2.0\" -}}\n" - "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; + static const char tpl[] = + "{{- $d := dict -}}{{- $_ := set $d \"image\" .Values.image -}}\n" + "{{- $img := index $d \"image\" -}}{{- $_ := set $img \"tag\" \"2.0\" -}}\n" + "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); @@ -5599,10 +5602,10 @@ TEST(helm_own_map_given_a_shared_one) { /* A set whose target is an UNKNOWN call result (index by .Release.Name) may * change any map of the values: every image after it is UNKNOWN. */ TEST(helm_set_of_an_unknown_call_result) { - static const char vals[] = - "apps: {web: {repository: ghcr.io/acme/web, tag: \"1.0\"}}\n"; - static const char tpl[] = "{{- $_ := set (index .Values.apps .Release.Name) \"tag\" \"2.0\" }}\n" - "image: \"{{ .Values.apps.web.repository }}:{{ .Values.apps.web.tag }}\"\n"; + static const char vals[] = "apps: {web: {repository: ghcr.io/acme/web, tag: \"1.0\"}}\n"; + static const char tpl[] = + "{{- $_ := set (index .Values.apps .Release.Name) \"tag\" \"2.0\" }}\n" + "image: \"{{ .Values.apps.web.repository }}:{{ .Values.apps.web.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); @@ -5640,9 +5643,10 @@ TEST(helm_skipped_set_after_redefinition) { "{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}" "{{- $cfg = .Values.image }}{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; - static const char kept[] = "{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}" - "{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n" - "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; + static const char kept[] = + "{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}" + "{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n" + "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, redefined, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); @@ -5674,7 +5678,8 @@ TEST(helm_many_changed_maps_keep_reads_right) { * what reads it. */ TEST(helm_define_memo_under_mutual_recursion) { static const char defs_src[] = - "{{- define \"A\" -}}{{- include \"B\" . -}}{{- $_ := set . \"tag\" \"2.0\" -}}{{- end -}}\n" + "{{- define \"A\" -}}{{- include \"B\" . -}}{{- $_ := set . \"tag\" \"2.0\" -}}{{- end " + "-}}\n" "{{- define \"B\" -}}{{- if false }}{{ include \"A\" . }}{{ end -}}{{- end -}}\n"; cbm_helm_defines_t *d = cbm_helm_defines_new(); ASSERT_NOT_NULL(d); @@ -5765,8 +5770,8 @@ TEST(helm_render_keeps_only_its_results) { size_t before = cbm_arena_total(&a); bool ok = cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, NULL, &probe, 1); size_t kept = cbm_arena_total(&a) - before; - bool image = ok && probe.n == 1 && probe.image[0] && - strcmp(probe.image[0], "ghcr.io/acme/app:1") == 0; + bool image = + ok && probe.n == 1 && probe.image[0] && strcmp(probe.image[0], "ghcr.io/acme/app:1") == 0; cbm_arena_destroy(&a); if (doc) { yyjson_mut_doc_free(doc); @@ -5814,10 +5819,9 @@ TEST(helm_render_budget_logs_its_reason) { * ($img) was taken of before: every image after it is UNKNOWN. */ TEST(helm_merge_into_values_root) { static const char vals[] = "image: {repository: ghcr.io/acme/app}\n"; - static const char tpl[] = - "{{- $img := .Values.image }}\n" - "{{- $_ := merge .Values (dict \"image\" (dict \"tag\" \"2.0\")) }}\n" - "image: \"{{ $img.repository }}:{{ $img.tag | default \"1.0\" }}\"\n"; + static const char tpl[] = "{{- $img := .Values.image }}\n" + "{{- $_ := merge .Values (dict \"image\" (dict \"tag\" \"2.0\")) }}\n" + "image: \"{{ $img.repository }}:{{ $img.tag | default \"1.0\" }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); @@ -5828,8 +5832,7 @@ TEST(helm_merge_into_values_root) { * the outer $vals (.Values.image), a nested range's own $vals aside -- * leaves every image after it UNKNOWN. */ TEST(helm_skipped_set_beside_a_nested_scope) { - static const char vals[] = - "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\njobs: [a]\n"; + static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\njobs: [a]\n"; static const char tpl[] = "{{- $vals := .Values.image }}\n" "{{- if .Capabilities.APIVersions.Has \"batch/v1\" }}\n" "{{- range .Values.jobs }}{{- $vals := dict }}{{- end }}\n" @@ -6015,10 +6018,9 @@ static const char HELM_IMAGE_VALUES[] = * the map above it (.Values.cfg) reaches .Values.image there: the image after * it is UNKNOWN. */ TEST(helm_deep_merge_over_a_change_below) { - static const char tpl[] = - "{{- $_ := set .Values.cfg.a \"d\" .Values.image }}\n" - "{{- $_ := mergeOverwrite .Values.cfg (dict \"a\" (dict \"d\" (dict \"tag\" \"2.0\"))) }}\n" - HELM_IMAGE_LINE; + static const char tpl[] = "{{- $_ := set .Values.cfg.a \"d\" .Values.image }}\n" + "{{- $_ := mergeOverwrite .Values.cfg (dict \"a\" (dict \"d\" (dict " + "\"tag\" \"2.0\"))) }}\n" HELM_IMAGE_LINE; char *got = helm_capture(HELM_IMAGE_VALUES, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); @@ -6053,9 +6055,9 @@ TEST(helm_and_or_with_an_unknown_operand) { * conditions after it -- a set there, or an assignment ($tag = ...), may run: * the image after it is UNKNOWN. */ TEST(helm_later_condition_of_a_skipped_if) { - static const char set[] = - "{{- if regexMatch \"^v\" .Values.image.tag }}{{- else if set .Values.image \"tag\" \"2.0\" }}" - "{{- end }}\n" HELM_IMAGE_LINE; + static const char set[] = "{{- if regexMatch \"^v\" .Values.image.tag }}{{- else if set " + ".Values.image \"tag\" \"2.0\" }}" + "{{- end }}\n" HELM_IMAGE_LINE; char *got = helm_capture(HELM_IMAGE_VALUES, set, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); @@ -6089,9 +6091,8 @@ TEST(helm_parse_error_spoils) { char *got = helm_capture(HELM_IMAGE_VALUES, bad, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); - static const char defs_src[] = - "{{- define \"bad\" -}}{{- $x := }}{{- end -}}\n" - "{{- define \"good\" -}}ok{{- end -}}\n"; + static const char defs_src[] = "{{- define \"bad\" -}}{{- $x := }}{{- end -}}\n" + "{{- define \"good\" -}}ok{{- end -}}\n"; cbm_helm_defines_t *d = cbm_helm_defines_new(); ASSERT_NOT_NULL(d); cbm_helm_defines_add(d, "charts/app/templates/_bad.tpl", defs_src, strlen(defs_src)); @@ -6099,8 +6100,8 @@ TEST(helm_parse_error_spoils) { cbm_helm_defines_add(d, "charts/app/templates/bad.yaml", bad_file, strlen(bad_file)); static const char good[] = "x: {{ include \"good\" . }}\n" HELM_IMAGE_LINE; static const char via_define[] = "x: {{ include \"bad\" . }}\n" HELM_IMAGE_LINE; - static const char via_file[] = - "x: {{ include (print $.Template.BasePath \"/bad.yaml\") . | sha256sum }}\n" HELM_IMAGE_LINE; + static const char via_file[] = "x: {{ include (print $.Template.BasePath \"/bad.yaml\") . | " + "sha256sum }}\n" HELM_IMAGE_LINE; char *ok = helm_capture_chart(HELM_IMAGE_VALUES, good, d, &HELM_APP, 2); char *by_define = helm_capture_chart(HELM_IMAGE_VALUES, via_define, d, &HELM_APP, 2); char *by_file = helm_capture_chart(HELM_IMAGE_VALUES, via_file, d, &HELM_APP, 2); @@ -6122,8 +6123,9 @@ TEST(helm_captures_after_a_huge_capture) { size_t cap = (size_t)ITEMS * 8 + CBM_SZ_256; char *vals = malloc(cap); ASSERT_NOT_NULL(vals); - size_t o = (size_t)snprintf(vals, cap, "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n" - "big: ["); + size_t o = (size_t)snprintf(vals, cap, + "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n" + "big: ["); for (int i = 0; i < ITEMS; i++) { o += (size_t)snprintf(vals + o, cap - o, "%s{x: a}", i ? "," : ""); } @@ -16310,6 +16312,1526 @@ TEST(pipeline_swift_http_call_makes_route_issue1892) { PASS(); } +static int pipeline_has_calls_edge(cbm_store_t *s, int64_t source_id, int64_t target_id) { + cbm_edge_t *edges = NULL; + int edge_count = 0; + if (cbm_store_find_edges_by_source_type(s, source_id, "CALLS", &edges, &edge_count) != + CBM_STORE_OK) { + return -1; + } + + int found = 0; + for (int i = 0; i < edge_count; i++) { + if (edges[i].target_id == target_id) { + found = 1; + break; + } + } + cbm_store_free_edges(edges, edge_count); + return found; +} + +/* #2061: distinct signature QNs keep each overload's CALLS edges separate. */ +TEST(pipeline_swift_overloads_keep_argument_labels_issue2061) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftoverload_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + + write_temp_file(tmp, "Sources/Sink.swift", + "class Sink {\n" + " func target() {}\n" + "}\n"); + write_temp_file( + tmp, "Sources/Service.swift", + "class Service {\n" + " let sink = Sink()\n" + "\n" + " // Overload A: DOES call target()\n" + " func work(flag: Bool) {\n" + " self.sink.target()\n" + " }\n" + "\n" + " // Overload B: does NOT call target()\n" + " func work(name: String) {\n" + " print(name)\n" + " }\n" + " func upload(_ data: Data, to url: URL) { sink.target() }\n" + " func upload(_ fileURL: URL, to url: URL) { print(fileURL) }\n" + " func record(path: String, session: URLSession) {}\n" + " func withDefault(message: String, flag: Bool = true) { sink.target() }\n" + " func withCallback(completion: () -> Void) { sink.target() }\n" + " func many(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, h: Int) {}\n" + " func handle(completion: () -> Void) { sink.target() }\n" + " func handle(completion: () -> Void, onError: () -> Void) { sink.target() }\n" + "}\n"); + write_temp_file( + tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + "\n" + " // Calls ONLY overload B, which never reaches target()\n" + " func onlyCallsOverloadB() {\n" + " self.service.work(name: \"x\")\n" + " }\n" + " func callAmbiguousUpload() {\n" + " self.service.upload(Data(), to: URL(string: \"/x\")!)\n" + " }\n" + " func callWithDefault() { self.service.withDefault(message: \"x\") }\n" + " func callWithClosure() { self.service.withCallback { } }\n" + " func callRecord() { self.service.record(path: \"/audit\", session: URLSession.shared) " + "}\n" + " func callManyWithComment() {\n" + " self.service.many(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 // trailing " + "comment\n" + " )\n" + " }\n" + " func callHandle() { self.service.handle { } onError: { } }\n" + " func callHandleWrongLabel() { self.service.handle { } bogus: { } }\n" + " func callHandleIncomplete() { self.service.handle { } onError: { recover( } }\n" + "}\n"); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swiftoverload.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + cbm_node_t *works = NULL; + int work_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "work", &works, &work_count), CBM_STORE_OK); + ASSERT_EQ(work_count, 2); + cbm_store_free_nodes(works, work_count); + cbm_node_t *uploads = NULL; + int upload_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "upload", &uploads, &upload_count), + CBM_STORE_OK); + ASSERT_EQ(upload_count, 2); + cbm_store_free_nodes(uploads, upload_count); + + char flag_qn[512]; + char name_qn[512]; + char target_qn[512]; + char caller_qn[512]; + snprintf(flag_qn, sizeof(flag_qn), "%s.Sources.Service.Service.work(flag:Bool)", project); + snprintf(name_qn, sizeof(name_qn), "%s.Sources.Service.Service.work(name:String)", project); + snprintf(target_qn, sizeof(target_qn), "%s.Sources.Sink.Sink.target()", project); + snprintf(caller_qn, sizeof(caller_qn), "%s.Sources.Caller.Caller.onlyCallsOverloadB()", + project); + + cbm_node_t flag = {0}; + cbm_node_t name = {0}; + cbm_node_t target = {0}; + cbm_node_t caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, flag_qn, &flag), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, name_qn, &name), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, target_qn, &target), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, caller_qn, &caller), CBM_STORE_OK); + ASSERT_EQ(flag.start_line, 5); + ASSERT_EQ(flag.end_line, 7); + ASSERT_EQ(name.start_line, 10); + ASSERT_EQ(name.end_line, 12); + + ASSERT_EQ(pipeline_has_calls_edge(s, flag.id, target.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, name.id, target.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, name.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, flag.id), 0); + + char data_qn[512], url_qn[512], upload_caller_qn[512]; + char default_qn[512], default_caller_qn[512], closure_qn[512], closure_caller_qn[512]; + snprintf(data_qn, sizeof(data_qn), "%s.Sources.Service.Service.upload(_:Data,to:URL)", project); + snprintf(url_qn, sizeof(url_qn), "%s.Sources.Service.Service.upload(_:URL,to:URL)", project); + snprintf(upload_caller_qn, sizeof(upload_caller_qn), + "%s.Sources.Caller.Caller.callAmbiguousUpload()", project); + snprintf(default_qn, sizeof(default_qn), + "%s.Sources.Service.Service.withDefault(message:String,flag:Bool)", project); + snprintf(default_caller_qn, sizeof(default_caller_qn), + "%s.Sources.Caller.Caller.callWithDefault()", project); + snprintf(closure_qn, sizeof(closure_qn), + "%s.Sources.Service.Service.withCallback(completion:()=>Void)", project); + snprintf(closure_caller_qn, sizeof(closure_caller_qn), + "%s.Sources.Caller.Caller.callWithClosure()", project); + char record_qn[512], record_caller_qn[512], many_qn[512], many_caller_qn[512]; + snprintf(record_qn, sizeof(record_qn), + "%s.Sources.Service.Service.record(path:String,session:URLSession)", project); + snprintf(record_caller_qn, sizeof(record_caller_qn), "%s.Sources.Caller.Caller.callRecord()", + project); + snprintf(many_qn, sizeof(many_qn), + "%s.Sources.Service.Service.many(a:Int,b:Int,c:Int,d:Int,e:Int,f:Int,g:Int,h:Int)", + project); + snprintf(many_caller_qn, sizeof(many_caller_qn), + "%s.Sources.Caller.Caller.callManyWithComment()", project); + cbm_node_t record = {0}, record_caller = {0}, many = {0}, many_caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, record_qn, &record), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, record_caller_qn, &record_caller), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, many_qn, &many), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, many_caller_qn, &many_caller), CBM_STORE_OK); + cbm_node_t data = {0}, url = {0}, upload_caller = {0}; + cbm_node_t with_default = {0}, default_caller = {0}, with_closure = {0}, closure_caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, data_qn, &data), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, url_qn, &url), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, upload_caller_qn, &upload_caller), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, default_qn, &with_default), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, default_caller_qn, &default_caller), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, closure_qn, &with_closure), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, closure_caller_qn, &closure_caller), + CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(s, upload_caller.id, data.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, upload_caller.id, url.id), 1); + cbm_edge_t *upload_edges = NULL; + int upload_edges_count = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, upload_caller.id, "CALLS", &upload_edges, + &upload_edges_count), + CBM_STORE_OK); + int ambiguous_edges = 0; + for (int i = 0; i < upload_edges_count; i++) { + if ((upload_edges[i].target_id == data.id || upload_edges[i].target_id == url.id) && + upload_edges[i].properties_json && + strstr(upload_edges[i].properties_json, "\"candidates\":2")) { + ambiguous_edges++; + } + } + ASSERT_EQ(ambiguous_edges, 2); + cbm_store_free_edges(upload_edges, upload_edges_count); + ASSERT_EQ(pipeline_has_calls_edge(s, default_caller.id, with_default.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, closure_caller.id, with_closure.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, record_caller.id, record.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, many_caller.id, many.id), 1); + ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 0); + + /* #2061: two trailing closures select only the overload that can take + * them; a second closure whose label matches no parameter binds neither. */ + char handle_one_qn[512], handle_two_qn[512], handle_caller_qn[512], handle_wrong_qn[512], + handle_incomplete_qn[512]; + snprintf(handle_one_qn, sizeof(handle_one_qn), + "%s.Sources.Service.Service.handle(completion:()=>Void)", project); + snprintf(handle_two_qn, sizeof(handle_two_qn), + "%s.Sources.Service.Service.handle(completion:()=>Void,onError:()=>Void)", project); + snprintf(handle_caller_qn, sizeof(handle_caller_qn), "%s.Sources.Caller.Caller.callHandle()", + project); + snprintf(handle_wrong_qn, sizeof(handle_wrong_qn), + "%s.Sources.Caller.Caller.callHandleWrongLabel()", project); + snprintf(handle_incomplete_qn, sizeof(handle_incomplete_qn), + "%s.Sources.Caller.Caller.callHandleIncomplete()", project); + cbm_node_t handle_one = {0}, handle_two = {0}, handle_caller = {0}; + cbm_node_t handle_wrong_caller = {0}, handle_incomplete_caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_one_qn, &handle_one), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_two_qn, &handle_two), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_caller_qn, &handle_caller), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_wrong_qn, &handle_wrong_caller), + CBM_STORE_OK); + ASSERT_EQ( + cbm_store_find_node_by_qn(s, project, handle_incomplete_qn, &handle_incomplete_caller), + CBM_STORE_OK); + /* `handle { } onError: { }`: only the two-closure overload can take both. */ + ASSERT_EQ(pipeline_has_calls_edge(s, handle_caller.id, handle_two.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_caller.id, handle_one.id), 0); + /* A second closure whose label matches no parameter binds neither. */ + ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong_caller.id, handle_two.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong_caller.id, handle_one.id), 0); + /* An incomplete second closure marks candidate input unknown; resolver must not emit a + * bare-name edge. */ + ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete_caller.id, handle_two.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete_caller.id, handle_one.id), 0); + cbm_node_free_fields(&handle_incomplete_caller); + cbm_node_free_fields(&handle_wrong_caller); + cbm_node_free_fields(&handle_one); + cbm_node_free_fields(&handle_two); + cbm_node_free_fields(&handle_caller); + cbm_node_free_fields(&record); + cbm_node_free_fields(&record_caller); + cbm_node_free_fields(&many); + cbm_node_free_fields(&many_caller); + cbm_node_free_fields(&data); + cbm_node_free_fields(&url); + cbm_node_free_fields(&upload_caller); + cbm_node_free_fields(&with_default); + cbm_node_free_fields(&default_caller); + cbm_node_free_fields(&with_closure); + cbm_node_free_fields(&closure_caller); + + cbm_node_free_fields(&flag); + cbm_node_free_fields(&name); + cbm_node_free_fields(&target); + cbm_node_free_fields(&caller); + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +/* Distinct declarations retain their own source ranges and outgoing edges; + * labels/arity alone cannot choose generic constraints or sync vs async. */ +static int swift_declaration_identity_case(const char **suffixes, int total, + const char *service_source, const char *caller_source) { + for (int mode = 0; mode < 2; mode++) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_generic_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", service_source); + write_temp_file(tmp, "Sources/Caller.swift", caller_source); + for (int i = 0; mode && i < 50; i++) { + char path[64], source[80]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(source, sizeof(source), "func filler%d() {}\n", i); + write_temp_file(tmp, path, source); + } + const char *previous = getenv("CBM_WORKERS"); + char *saved = previous ? strdup(previous) : NULL; + cbm_setenv("CBM_WORKERS", mode ? "4" : "1", 1); + char db[512], project[512]; + snprintf(db, sizeof(db), "%s/generic.db", tmp); + for (int phase = 0; phase < 2; phase++) { + if (phase) { + char path[512]; + snprintf(path, sizeof(path), "%s/Sources/Caller.swift", tmp); + char changed[1024]; + snprintf(changed, sizeof(changed), "%s// changed\n", caller_source); + write_temp_file(tmp, "Sources/Caller.swift", changed); + ASSERT_EQ(pipeline_test_set_mtime(path, 2000000000, 0), 0); + } + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + if (phase) { + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), + CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + } + snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); + cbm_store_t *s = cbm_store_open_path(db); + ASSERT_NOT_NULL(s); + cbm_node_t *overloads = NULL; + int count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "pick", &overloads, &count), + CBM_STORE_OK); + ASSERT_EQ(count, total); + cbm_store_free_nodes(overloads, count); + char qn[512]; + snprintf(qn, sizeof(qn), "%s.Sources.Caller.Caller.invoke()", project); + cbm_node_t caller = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &caller), CBM_STORE_OK); + for (int i = 0; i < total; i++) { + cbm_node_t target = {0}, marker = {0}; + snprintf(qn, sizeof(qn), "%s.Sources.Service.Service.%s", project, suffixes[i]); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &target), CBM_STORE_OK); + ASSERT_STR_EQ(target.name, "pick"); + ASSERT_EQ(target.start_line, i + 2); + ASSERT_EQ(target.end_line, i + 2); + snprintf(qn, sizeof(qn), "%s.Sources.Service.mark%d()", project, i); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &marker), CBM_STORE_OK); + cbm_edge_t *edges = NULL; + int n = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, target.id, "CALLS", &edges, &n), + CBM_STORE_OK); + ASSERT_EQ(n, 1); + ASSERT_EQ(edges[0].target_id, marker.id); + cbm_store_free_edges(edges, n); + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &n), + CBM_STORE_OK); + ASSERT_EQ(n, total); + int compatible = 0; + for (int e = 0; e < n; e++) { + if (edges[e].target_id == target.id) { + char candidates[64]; + snprintf(candidates, sizeof(candidates), "\"candidates\":%d", total); + ASSERT_NOT_NULL(strstr(edges[e].properties_json, candidates)); + compatible++; + } + } + ASSERT_EQ(compatible, 1); + cbm_store_free_edges(edges, n); + cbm_node_free_fields(&target); + cbm_node_free_fields(&marker); + } + cbm_node_free_fields(&caller); + cbm_store_close(s); + cbm_pipeline_free(p); + } + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + free(saved); + th_rmtree(tmp); + } + PASS(); +} + +TEST(pipeline_swift_generic_async_identity_issue2061) { + const char *suffixes[] = {"pick(_:T)", + "pick(_:T)", + "pick(_:T)", + "pick(_:T)", + "pick(_:Int)", + "pick(_:Int)async"}; + return swift_declaration_identity_case(suffixes, 6, + "class Service {\n" + "func pick(_ x: T) { mark0() }\n" + "func pick(_ x: T) { mark1() }\n" + "func pick(_ x: T) where T: Hashable { mark2() }\n" + "func pick(_ x: T) where T: Comparable { mark3() }\n" + "func pick(_ x: Int) { mark4() }\n" + "func pick(_ x: Int) async { mark5() }\n}\n" + "func mark0() {}\nfunc mark1() {}\nfunc mark2() {}\n" + "func mark3() {}\nfunc mark4() {}\nfunc mark5() {}\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(value) }\n}\n"); +} + +TEST(pipeline_swift_static_instance_identity_issue2061) { + const char *suffixes[] = {"pick(_:Int)static", "pick(_:Int)"}; + return swift_declaration_identity_case( + suffixes, 2, + "class Service {\nstatic func pick(_ x: Int) { mark0() }\n" + "func pick(_ x: Int) { mark1() }\n}\n" + "func mark0() {}\nfunc mark1() {}\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(1) }\n}\n"); +} + +TEST(pipeline_swift_extension_constraints_identity_issue2061) { + const char *suffixes[] = {"pick<;where T:P>(_:Int)", "pick<;where T:Q>(_:Int)"}; + return swift_declaration_identity_case( + suffixes, 2, + "class Service {}\n" + "extension Service where T: P { func pick(_ x: Int) { mark0() } }\n" + "extension Service where T: Q { func pick(_ x: Int) { mark1() } }\n" + "func mark0() {}\nfunc mark1() {}\nprotocol P {}\nprotocol Q {}\nstruct OnlyP: P {}\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { self.service.pick(1) }\n}\n"); +} + +TEST(pipeline_swift_cross_file_extension_issue2061) { + for (int mode = 0; mode < 2; mode++) { + char tmp[256] = "/tmp/cbm_swift_extension_XXXXXX"; + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Service.swift", + "struct Service { func run(value: String) {} }\n" + "struct Other { func run(value: Int) {} }\n"); + write_temp_file(tmp, "ZExtensions.swift", + "extension Service {\n" + " init(seed: Int) {}\n" + " func run(value: Int) {}\n" + "}\n"); + const char *caller = "func invoke(service: Service) {\n" + " service.run(value: 1)\n" + " _ = Service.init(seed: 1)\n" + "}\n"; + write_temp_file(tmp, "Caller.swift", caller); + for (int i = 0; mode && i < 50; i++) { + char path[64]; + snprintf(path, sizeof(path), "Filler%d.swift", i); + write_temp_file(tmp, path, "// filler\n"); + } + XlangEnvSave saved = xlang_env_select(mode != 0); + char db[512], path[512]; + snprintf(db, sizeof(db), "%s/graph.db", tmp); + snprintf(path, sizeof(path), "%s/Caller.swift", tmp); + for (int phase = 0; phase < 2; phase++) { + if (phase) { + char changed[256]; + snprintf(changed, sizeof(changed), "%s// changed\n", caller); + write_temp_file(tmp, "Caller.swift", changed); + ASSERT_EQ(pipeline_test_set_mtime(path, 2000000000, 0), 0); + } + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + if (phase) { + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), + CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + } + cbm_store_t *s = cbm_store_open_path(db); + ASSERT_NOT_NULL(s); + const char *project = cbm_pipeline_project_name(p); + const char *suffixes[] = { + "Caller.invoke(service:Service)", "ZExtensions.Service.run(value:Int)", + "ZExtensions.Service.init(seed:Int)", "Service.Other.run(value:Int)", + "Service.Service.run(value:String)"}; + cbm_node_t nodes[5] = {0}; + for (size_t i = 0; i < sizeof(nodes) / sizeof(nodes[0]); i++) { + char qn[512]; + snprintf(qn, sizeof(qn), "%s.%s", project, suffixes[i]); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &nodes[i]), CBM_STORE_OK); + } + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[0].id, nodes[1].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[0].id, nodes[2].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[0].id, nodes[3].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[0].id, nodes[4].id), 1); + for (size_t i = 0; i < sizeof(nodes) / sizeof(nodes[0]); i++) { + cbm_node_free_fields(&nodes[i]); + } + cbm_store_close(s); + cbm_pipeline_free(p); + } + xlang_env_restore(&saved); + th_rmtree(tmp); + } + PASS(); +} + +TEST(pipeline_swift_same_named_extensions_stay_in_module_issue2061) { + for (int mode = 0; mode < 2; mode++) { + char tmp[256] = "/tmp/cbm_swift_extension_scope_XXXXXX"; + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + const char *targets[] = {"Alpha", "Beta"}; + const char *caller = "func invoke(service: Service) {\n" + " service.run(value: 1)\n" + " _ = Service.init(seed: 1)\n" + "}\n"; + char file[128]; + for (int target = 0; target < 2; target++) { + snprintf(file, sizeof(file), "Sources/%s/Service.swift", targets[target]); + write_temp_file(tmp, file, "struct Service {}\n"); + snprintf(file, sizeof(file), "Sources/%s/Extensions.swift", targets[target]); + write_temp_file(tmp, file, + "extension Service {\n" + " init(seed: Int) {}\n" + " func run(value: Int) {}\n" + "}\n"); + snprintf(file, sizeof(file), "Sources/%s/Caller.swift", targets[target]); + write_temp_file(tmp, file, caller); + snprintf(file, sizeof(file), "Sources/%s/AStringExtension.swift", targets[target]); + write_temp_file(tmp, file, + "protocol StringBehavior {}\n" + "extension StringBehavior { func inherited() {} }\n" + "extension String { func local() {} }\n"); + snprintf(file, sizeof(file), "Sources/%s/StringCaller.swift", targets[target]); + write_temp_file(tmp, file, + "extension String: StringBehavior {\n" + " func invokeStringExtension() { self.local(); self.inherited() }\n" + "}\n"); + } + write_temp_file(tmp, "Sources/Unrelated/Types.swift", "struct String {}\n"); + write_temp_file(tmp, "Package.swift", + "let package = Package(name: \"Fixture\", targets: [\n" + " .target(name: \"Alpha\"), .target(name: \"Beta\"),\n" + " .target(name: \"AlphaExtras\", dependencies: [\"Alpha\"]),\n" + " .target(name: \"Unrelated\"),\n" + " .target(name: \"LegacyProduct\", path: \"Source\"),\n" + " .testTarget(name: \"LegacyTests\", path: \"Tests\")])\n"); + write_temp_file(tmp, "Source/Core/Legacy.swift", "struct Legacy {}\n"); + write_temp_file(tmp, "Tests/Legacy.swift", + "import LegacyProduct\nextension Legacy { func legacy() {} }\n"); + write_temp_file(tmp, "Tests/LegacyCaller.swift", + "import LegacyProduct\n" + "func legacyCaller(value: Legacy) { value.legacy() }\n"); + write_temp_file(tmp, "Sources/AlphaExtras/Extensions.swift", + "@testable import Alpha\n" + "extension Service { func extra() {} }\n"); + write_temp_file(tmp, "Sources/AlphaExtras/Caller.swift", + "@testable import Alpha\n" + "func invokeExtra(service: Service) { service.extra() }\n"); + for (int i = 0; mode && i < 50; i++) { + snprintf(file, sizeof(file), "Sources/Alpha/Filler%d.swift", i); + write_temp_file(tmp, file, "// filler\n"); + } + XlangEnvSave saved = xlang_env_select(mode != 0); + char db[512], changed_path[512]; + snprintf(db, sizeof(db), "%s/graph.db", tmp); + snprintf(changed_path, sizeof(changed_path), "%s/Sources/Alpha/Caller.swift", tmp); + for (int phase = 0; phase < 2; phase++) { + if (phase) { + char changed[256]; + snprintf(changed, sizeof(changed), "%s// changed\n", caller); + write_temp_file(tmp, "Sources/Alpha/Caller.swift", changed); + ASSERT_EQ(pipeline_test_set_mtime(changed_path, 2000000000, 0), 0); + } + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + if (phase) { + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), + CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + } + cbm_store_t *s = cbm_store_open_path(db); + ASSERT_NOT_NULL(s); + const char *project = cbm_pipeline_project_name(p); + const char *suffixes[] = {"Sources.Alpha.Caller.invoke(service:Service)", + "Sources.Beta.Caller.invoke(service:Service)", + "Sources.Alpha.Extensions.Service.run(value:Int)", + "Sources.Beta.Extensions.Service.run(value:Int)", + "Sources.Alpha.Extensions.Service.init(seed:Int)", + "Sources.Beta.Extensions.Service.init(seed:Int)", + "Sources.AlphaExtras.Caller.invokeExtra(service:Service)", + "Sources.AlphaExtras.Extensions.Service.extra()", + "Sources.Alpha.AStringExtension.String.local()", + "Sources.Alpha.StringCaller.String.invokeStringExtension()", + "Sources.Beta.AStringExtension.String.local()", + "Sources.Beta.StringCaller.String.invokeStringExtension()", + "Tests.Legacy.Legacy.legacy()", + "Tests.LegacyCaller.legacyCaller(value:Legacy)", + "Sources.Alpha.AStringExtension.StringBehavior.inherited()", + "Sources.Beta.AStringExtension.StringBehavior.inherited()"}; + cbm_node_t nodes[16] = {0}; + for (size_t i = 0; i < sizeof(nodes) / sizeof(nodes[0]); i++) { + char qn[768]; + snprintf(qn, sizeof(qn), "%s.%s", project, suffixes[i]); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &nodes[i]), CBM_STORE_OK); + } + for (int target = 0; target < 2; target++) { + for (int owner = 0; owner < 2; owner++) { + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[target].id, nodes[2 + owner].id), + target == owner); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[target].id, nodes[4 + owner].id), + target == owner); + } + } + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[6].id, nodes[7].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[13].id, nodes[12].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[9].id, nodes[14].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[9].id, nodes[15].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[11].id, nodes[15].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[11].id, nodes[14].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[9].id, nodes[8].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[9].id, nodes[10].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[11].id, nodes[10].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[11].id, nodes[8].id), 0); + for (size_t i = 0; i < sizeof(nodes) / sizeof(nodes[0]); i++) { + cbm_node_free_fields(&nodes[i]); + } + cbm_store_close(s); + cbm_pipeline_free(p); + } + xlang_env_restore(&saved); + th_rmtree(tmp); + } + PASS(); +} + +TEST(pipeline_swift_custom_target_paths_and_manifest_edit_issue2061) { + for (int mode = 0; mode < 2; mode++) { + char tmp[256] = "/tmp/cbm_swift_custom_targets_XXXXXX"; + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + const char *roots[] = {"LibA", "LibB", "Nested/LibA"}; + char file[128]; + for (int i = 0; i < 3; i++) { + snprintf(file, sizeof(file), "%s/Core/Types.swift", roots[i]); + write_temp_file(tmp, file, "struct Service {}\n"); + snprintf(file, sizeof(file), "%s/Extra/Extensions.swift", roots[i]); + write_temp_file(tmp, file, "extension Service { func run() {} }\n"); + snprintf(file, sizeof(file), "%s/Core/Caller.swift", roots[i]); + write_temp_file(tmp, file, + "func invoke(service: Service) { service.run(); service.inspect() }\n"); + } + const char *extensions = "@testable import Alpha\n" + "extension Service { func inspect() {} }\n"; + write_temp_file(tmp, "Checks/Extensions.swift", extensions); + write_temp_file(tmp, "Nested/Checks/Extensions.swift", extensions); + const char *imported_caller = + "import Alpha\n" + "func importedCaller(service: Service) { service.run(); service.inspect() }\n"; + write_temp_file(tmp, "Checks/Caller.swift", imported_caller); + write_temp_file(tmp, "Nested/Checks/Caller.swift", imported_caller); + write_temp_file(tmp, "LibA/Core/Only.swift", "struct OnlyAlpha { func only() {} }\n"); + write_temp_file(tmp, "Checks/UnknownCaller.swift", + "import Beta\nfunc missing(value: OnlyAlpha) { value.only() }\n"); + write_temp_file(tmp, "Common/Base.swift", + "public class SharedBase { public func own() {} }\n"); + write_temp_file(tmp, "Common/Extra.swift", + "extension SharedBase { public func ownExtension() {} }\n"); + const char *base_roots[] = {"LibA", "LibB"}; + for (int i = 0; i < 2; i++) { + char source[512]; + snprintf(file, sizeof(file), "%s/Extra/SharedExtensions.swift", base_roots[i]); + snprintf(source, sizeof(source), + "import Base\nextension SharedBase { public func %s() {} }\n", + i ? "betaOnly" : "alphaOnly"); + write_temp_file(tmp, file, source); + snprintf(file, sizeof(file), "%s/Core/LocalChild.swift", base_roots[i]); + write_temp_file(tmp, file, "import Base\nclass LocalChild: SharedBase {}\n"); + snprintf(file, sizeof(file), "%s/Core/SharedCaller.swift", base_roots[i]); + write_temp_file( + tmp, file, + "func sharedCaller(value: LocalChild) {\n" + " value.own(); value.ownExtension(); value.alphaOnly(); value.betaOnly()\n}\n"); + } + write_temp_file( + tmp, "Both/Caller.swift", + "import Alpha\nimport Beta\nimport Base\n" + "func bothCaller(value: SharedBase) {\n" + " value.own(); value.ownExtension(); value.alphaOnly(); value.betaOnly()\n}\n"); + const char *factory = "import Beta\n" + "public class Result { public func finish() {} }\n" + "public class Base { public func baseWork() {} }\n" + "public class Factory {\n" + "public func make() -> Result { Result() }\n" + "public func qualified() -> Beta.Result { fatalError() }\n}\n" + "public class Outer {\n" + "public class Result { public func finish() {} }\n" + "public func make() -> Result { Result() }\n}\n"; + write_temp_file(tmp, "LibA/Core/Factory.swift", factory); + write_temp_file(tmp, "LibB/Core/Factory.swift", factory); + const char *factory_caller = + "import Alpha\n" + "class Result { func finish() {} }\n" + "func callFactory(_ factory: Factory) { factory.make().finish() }\n" + "func qualifiedFactory(_ factory: Factory) { factory.qualified().finish() }\n" + "func lexicalFactory(_ factory: Outer) { factory.make().finish() }\n"; + write_temp_file(tmp, "Checks/FactoryCaller.swift", factory_caller); + write_temp_file(tmp, "Checks/BaseCaller.swift", + "import Beta\nclass Child: Base {}\n" + "func inheritedCall(_ child: Child) { child.baseWork() }\n"); + write_temp_file(tmp, "Nested/Package.swift", + "let package = Package(name: \"Nested\", targets: [\n" + " .target(name: \"Alpha\", path: \"LibA\"),\n" + " .testTarget(name: \"Checks\", path: \"Checks\")])\n"); + for (int i = 0; mode && i < 50; i++) { + snprintf(file, sizeof(file), "LibA/Core/Filler%d.swift", i); + write_temp_file(tmp, file, "// filler\n"); + } + XlangEnvSave saved = xlang_env_select(mode != 0); + char db[512], manifest[512]; + snprintf(db, sizeof(db), "%s/graph.db", tmp); + snprintf(manifest, sizeof(manifest), "%s/Package.swift", tmp); + for (int phase = 0; phase < 4; phase++) { + bool swapped = phase >= 2; + if (phase == 0 || phase == 2) { + char source[512]; + snprintf(source, sizeof(source), + "let package = Package(name: \"UnrelatedProductName\", targets: [\n" + " .target(name: \"Alpha\", path: \"%s\"),\n" + " .target(name: \"Beta\", path: \"%s\"),\n" + " .target(name: \"Base\", path: \"Common\"),\n" + " .testTarget(name: \"Both\", path: \"Both\"),\n" + " .testTarget(name: \"Checks\", path: \"Checks\")])\n", + swapped ? "LibB" : "LibA", swapped ? "LibA" : "LibB"); + write_temp_file(tmp, "Package.swift", source); + if (phase) { + ASSERT_EQ(pipeline_test_set_mtime(manifest, 2000000000, 0), 0); + } + } else if (phase == 1) { + char changed[1024], caller_path[512]; + snprintf(changed, sizeof(changed), "%s// changed\n", factory_caller); + write_temp_file(tmp, "Checks/FactoryCaller.swift", changed); + snprintf(caller_path, sizeof(caller_path), "%s/Checks/FactoryCaller.swift", tmp); + ASSERT_EQ(pipeline_test_set_mtime(caller_path, 2000000000, 0), 0); + } else { + snprintf(db, sizeof(db), "%s/fresh.db", tmp); + } + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + if (phase == 1) { + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), + CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + } + cbm_store_t *s = cbm_store_open_path(db); + ASSERT_NOT_NULL(s); + const char *suffixes[] = {"LibA.Core.Caller.invoke(service:Service)", + "LibB.Core.Caller.invoke(service:Service)", + "Nested.LibA.Core.Caller.invoke(service:Service)", + "LibA.Extra.Extensions.Service.run()", + "LibB.Extra.Extensions.Service.run()", + "Nested.LibA.Extra.Extensions.Service.run()", + "Checks.Extensions.Service.inspect()", + "Nested.Checks.Extensions.Service.inspect()", + "Checks.Caller.importedCaller(service:Service)", + "Nested.Checks.Caller.importedCaller(service:Service)", + "Checks.UnknownCaller.missing(value:OnlyAlpha)", + "LibA.Core.Only.OnlyAlpha.only()", + "Checks.FactoryCaller.callFactory(_:Factory)", + "Checks.FactoryCaller.qualifiedFactory(_:Factory)", + "LibA.Core.Factory.Result.finish()", + "LibB.Core.Factory.Result.finish()", + "Checks.FactoryCaller.Result.finish()", + "Checks.BaseCaller.inheritedCall(_:Child)", + "LibA.Core.Factory.Base.baseWork()", + "LibB.Core.Factory.Base.baseWork()", + "Checks.FactoryCaller.lexicalFactory(_:Outer)", + "LibA.Core.Factory.Outer.Result.finish()", + "LibB.Core.Factory.Outer.Result.finish()", + "LibA.Core.SharedCaller.sharedCaller(value:LocalChild)", + "LibB.Core.SharedCaller.sharedCaller(value:LocalChild)", + "Both.Caller.bothCaller(value:SharedBase)", + "Common.Base.SharedBase.own()", + "LibA.Extra.SharedExtensions.SharedBase.alphaOnly()", + "LibB.Extra.SharedExtensions.SharedBase.betaOnly()", + "Common.Extra.SharedBase.ownExtension()"}; + cbm_node_t nodes[30] = {0}; + const char *project = cbm_pipeline_project_name(p); + for (size_t i = 0; i < sizeof(nodes) / sizeof(nodes[0]); i++) { + char qn[768]; + snprintf(qn, sizeof(qn), "%s.%s", project, suffixes[i]); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, qn, &nodes[i]), CBM_STORE_OK); + } + for (int caller = 0; caller < 3; caller++) { + for (int owner = 0; owner < 3; owner++) { + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[caller].id, nodes[3 + owner].id), + caller == owner); + } + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[caller].id, nodes[6].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[caller].id, nodes[7].id), 0); + } + for (int owner = 0; owner < 3; owner++) { + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[8].id, nodes[3 + owner].id), + owner == (swapped ? 1 : 0)); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[9].id, nodes[3 + owner].id), owner == 2); + } + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[8].id, nodes[6].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[8].id, nodes[7].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[9].id, nodes[6].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[9].id, nodes[7].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[10].id, nodes[11].id), swapped); + for (int owner = 0; owner < 2; owner++) { + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[12].id, nodes[14 + owner].id), + owner == (swapped ? 1 : 0)); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[13].id, nodes[14 + owner].id), + owner == (swapped ? 0 : 1)); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[17].id, nodes[18 + owner].id), + owner == (swapped ? 0 : 1)); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[20].id, nodes[21 + owner].id), + owner == (swapped ? 1 : 0)); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[20].id, nodes[14 + owner].id), 0); + } + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[12].id, nodes[16].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[13].id, nodes[16].id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[20].id, nodes[16].id), 0); + for (int caller = 0; caller < 3; caller++) { + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[23 + caller].id, nodes[26].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[23 + caller].id, nodes[29].id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[23 + caller].id, nodes[27].id), + caller != 1); + ASSERT_EQ(pipeline_has_calls_edge(s, nodes[23 + caller].id, nodes[28].id), + caller != 0); + } + for (size_t i = 0; i < sizeof(nodes) / sizeof(nodes[0]); i++) { + cbm_node_free_fields(&nodes[i]); + } + cbm_store_close(s); + cbm_pipeline_free(p); + } + xlang_env_restore(&saved); + th_rmtree(tmp); + } + PASS(); +} + +TEST(pipeline_swift_capped_selector_survives_incremental_issue2061) { + char type[2049], service[4608], caller[2304]; + memset(type, 'T', sizeof(type) - 1); + type[sizeof(type) - 1] = '\0'; + snprintf(service, sizeof(service), + "struct %s {}\nclass Service {\nfunc pick(value: %s) { marker() }\n}\n" + "func marker() {}\n", + type, type); + snprintf(caller, sizeof(caller), + "class Caller {\nlet service = Service()\n" + "func invokeLong(_ value: %s) { service.pick(value: value) }\n}\n", + type); + for (int mode = 0; mode < 2; mode++) { + char tmp[256] = "/tmp/cbm_swift_capped_XXXXXX"; + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", service); + write_temp_file(tmp, "Sources/Caller.swift", caller); + for (int i = 0; mode && i < 50; i++) { + char path[64], filler[64]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(filler, sizeof(filler), "func filler%d() {}\n", i); + write_temp_file(tmp, path, filler); + } + XlangEnvSave saved = xlang_env_select(mode != 0); + char db[512], path[512]; + snprintf(db, sizeof(db), "%s/capped.db", tmp); + snprintf(path, sizeof(path), "%s/Sources/Caller.swift", tmp); + for (int phase = 0; phase < 2; phase++) { + if (phase) { + char changed[2400]; + snprintf(changed, sizeof(changed), "%s// changed\n", caller); + write_temp_file(tmp, "Sources/Caller.swift", changed); + ASSERT_EQ(pipeline_test_set_mtime(path, 2000000000, 0), 0); + } + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + if (phase) { + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), + CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + } + cbm_store_t *s = cbm_store_open_path(db); + ASSERT_NOT_NULL(s); + const char *project = cbm_pipeline_project_name(p); + cbm_node_t *picks = NULL, *calls = NULL; + int pick_count = 0, call_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "pick", &picks, &pick_count), + CBM_STORE_OK); + ASSERT_EQ(pick_count, 1); + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "invokeLong", &calls, &call_count), + CBM_STORE_OK); + ASSERT_EQ(call_count, 1); + ASSERT_NOT_NULL(strchr(picks[0].qualified_name, '#')); + ASSERT_NOT_NULL(strstr(picks[0].properties_json, "\"call_selector\":\"(value:")); + ASSERT_NOT_NULL(strstr(picks[0].properties_json, type)); + ASSERT_EQ(pipeline_has_calls_edge(s, calls[0].id, picks[0].id), 1); + cbm_store_free_nodes(picks, pick_count); + cbm_store_free_nodes(calls, call_count); + cbm_store_close(s); + cbm_pipeline_free(p); + } + xlang_env_restore(&saved); + th_rmtree(tmp); + } + PASS(); +} + +TEST(pipeline_swift_qualified_and_return_identity_issue2061) { + char tmp[256] = "/tmp/cbm_swift_qualified_XXXXXX"; + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Types.swift", + "enum A { struct Item {} }\n" + "enum B { struct Item {} }\nclass Service {\n" + "func pick(_ value: A.Item) { first() }\n" + "func pick(_ value: B.Item) { second() }\n}\n" + "func first() {}\nfunc second() {}\n"); + char db[512]; + snprintf(db, sizeof(db), "%s/graph.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + cbm_store_t *store = cbm_store_open_path(db); + ASSERT_NOT_NULL(store); + cbm_node_t *nodes = NULL; + int count = 0; + ASSERT_EQ( + cbm_store_find_nodes_by_name(store, cbm_pipeline_project_name(p), "pick", &nodes, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 1); + ASSERT_NOT_NULL(strstr(nodes[0].qualified_name, ".Service.pick(_:Item)")); + cbm_edge_t *edges = NULL; + int edges_count = 0; + ASSERT_EQ( + cbm_store_find_edges_by_source_type(store, nodes[0].id, "CALLS", &edges, &edges_count), + CBM_STORE_OK); + ASSERT_EQ(edges_count, 2); + cbm_store_free_edges(edges, edges_count); + cbm_store_free_nodes(nodes, count); + cbm_store_close(store); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +TEST(pipeline_swift_receiver_and_chained_closures_issue2061) { + const char *types = + "class Parent { func inherited() {} }\n" + "class Session: Parent {\n" + "func request(_ value: Int) -> Request { Request() }\n" + "func streamRequest(_ value: Int) -> Stream { Stream() }\n}\n" + "class Request { func responseDecodable(of type: Any.Type, completion: () -> Void) {} }\n" + "class Stream { func responseStream(completion: () -> Void) {} }\n" + "class Unrelated { func request(_ value: Int) {}\nfunc sorted() {}\n" + "func inherited() {} }\n"; + const char *source = + "class Caller { let session: Session = Session()\n" + "func typed(_ value: Session) { value.request(1) }\n" + "func inherited(_ value: Session) { value.inherited() }\n" + "func external(_ output: [String]) { output.sorted() }\n" + "func literal() { let output = [\"x\"]; output.sorted() }\n" + "func shadowed() { let session = Unrelated(); session.request(1) }\n" + "func chained() { session.request(1).responseDecodable(of: String.self) { } }\n" + "func streamed() { session.streamRequest(1).responseStream { } }\n}\n"; + const struct { + const char *caller; + const char *target; + int expected; + } pairs[] = { + {"Caller.typed(_:Session)", "Session.request(_:Int)=>Request", 1}, + {"Caller.typed(_:Session)", "Unrelated.request(_:Int)", 0}, + {"Caller.inherited(_:Session)", "Parent.inherited()", 1}, + {"Caller.inherited(_:Session)", "Unrelated.inherited()", 0}, + {"Caller.external(_:[String])", "Unrelated.sorted()", 0}, + {"Caller.literal()", "Unrelated.sorted()", 0}, + {"Caller.shadowed()", "Unrelated.request(_:Int)", 1}, + {"Caller.shadowed()", "Session.request(_:Int)=>Request", 0}, + {"Caller.chained()", "Request.responseDecodable(of:Type,completion:()=>Void)", 1}, + {"Caller.streamed()", "Stream.responseStream(completion:()=>Void)", 1}, + }; + for (int parallel = 0; parallel < 2; parallel++) { + char tmp[256] = "/tmp/cbm_swift_receiver_XXXXXX"; + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Types.swift", types); + write_temp_file(tmp, "Calls.swift", source); + for (int i = 0; parallel && i < 55; i++) { + char name[64], body[64]; + snprintf(name, sizeof(name), "Pad%d.swift", i); + snprintf(body, sizeof(body), "func pad%d() {}\n", i); + write_temp_file(tmp, name, body); + } + XlangEnvSave saved = xlang_env_select(parallel != 0); + char db[512]; + snprintf(db, sizeof(db), "%s/graph.db", tmp); + for (int phase = 0; phase < 2; phase++) { + if (phase) { + char changed[2048], path[512]; + snprintf(changed, sizeof(changed), "%s// changed\n", source); + write_temp_file(tmp, "Calls.swift", changed); + snprintf(path, sizeof(path), "%s/Calls.swift", tmp); + ASSERT_EQ(pipeline_test_set_mtime(path, 2000000000, 0), 0); + } + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + if (phase) { + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), + CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + } + cbm_store_t *store = cbm_store_open_path(db); + ASSERT_NOT_NULL(store); + for (unsigned i = 0; i < sizeof(pairs) / sizeof(pairs[0]); i++) { + char qn[1024]; + cbm_node_t caller = {0}, target = {0}; + snprintf(qn, sizeof(qn), "%s.Calls.%s", cbm_pipeline_project_name(p), + pairs[i].caller); + ASSERT_EQ( + cbm_store_find_node_by_qn(store, cbm_pipeline_project_name(p), qn, &caller), + CBM_STORE_OK); + snprintf(qn, sizeof(qn), "%s.Types.%s", cbm_pipeline_project_name(p), + pairs[i].target); + ASSERT_EQ( + cbm_store_find_node_by_qn(store, cbm_pipeline_project_name(p), qn, &target), + CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(store, caller.id, target.id), pairs[i].expected); + cbm_node_free_fields(&caller); + cbm_node_free_fields(&target); + } + cbm_store_close(store); + cbm_pipeline_free(p); + } + xlang_env_restore(&saved); + th_rmtree(tmp); + } + PASS(); +} + +TEST(pipeline_swift_capped_chain_returns_and_metatype_receivers) { + char long_type[129]; + memset(long_type, 'T', sizeof(long_type) - 1); + long_type[sizeof(long_type) - 1] = '\0'; + char types[4096]; + int length = snprintf( + types, sizeof(types), + "struct %s {}\n" + "open class Session {\n#if DEBUG\n" + "open func request(_ value: Int, first: %s? = nil, second: %s? = nil) -> Request { " + "Request() }\n" + "#else\n" + "open func request(_ value: String, first: %s? = nil, second: %s? = nil) -> Request { " + "Request() }\n" + "#endif\n" + "func streamRequest(_ value: Int, first: %s? = nil, second: %s? = nil) -> Type { " + "fatalError() }\n" + "func streamRequest(_ value: String, first: %s? = nil, second: %s? = nil) -> Type { " + "fatalError() }\n" + "func diverge(_ value: Int, first: %s? = nil, second: %s? = nil) -> Request { Request() }\n" + "func diverge(_ value: String, first: %s? = nil, second: %s? = nil) -> OtherRequest { " + "OtherRequest() }\n" + "static func run(flag: Bool) {}\nopen func upload(_ value: Int) {}\n}\n" + "class ParentRequest {\nfunc validate() -> Self { self }\n" + "func selfOrOther(_ value: Int) -> Self { self }\n" + "func selfOrOther(_ value: String) -> OtherRequest { OtherRequest() }\n}\n" + "class Request: ParentRequest {\n" + "func responseDecodable(of type: Any.Type, completion: () -> Void) {}\n" + "func responseDecodable(wrong type: Any.Type, completion: () -> Void) {}\n}\n" + "class OtherRequest { func responseDecodable(of type: Any.Type, completion: () -> Void) {} " + "}\n" + "class Type {\n" + "typealias Handler = @Sendable (Success) throws -> Void\n" + "func responseStream(stream: Handler) {}\n" + "func moveReaderIndex(forwardBy amount: Int) {}\n}\n" + "class Factory { func make() -> Session { Session() } }\n" + "class Unrelated { static func run(flag: Bool) {}\nfunc upload(_ value: Int) {} }\n", + long_type, long_type, long_type, long_type, long_type, long_type, long_type, long_type, + long_type, long_type, long_type, long_type, long_type); + ASSERT_GT(length, 0); + ASSERT_LT(length, (int)sizeof(types)); + const char *calls = + "func same(_ session: Session) { session.request(.default)\n" + " .validate().responseDecodable(of: String.self) {} }\n" + "func streamed(_ session: Session) { session.streamRequest(.bytes(1)).responseStream {} }\n" + "func divergent(_ session: Session) { session.diverge(.default).responseDecodable(of: " + "String.self) {} }\n" + "func wrongTail(_ session: Session) { session.request(.default).responseDecodable(unknown: " + "String.self) {} }\n" + "func selfDivergent(_ session: Session) { session.request(.default).selfOrOther(.default)" + ".responseDecodable(of: String.self) {} }\n" + "func metatype(_ type: Session.Type) { type.run(flag: true) }\n" + "func external(_ type: URLSession.Type) { type.run(flag: true) }\n" + "func upload(_ session: Session) { session.upload(1) }\n" + "func makeFactory() -> Factory { Factory() }\n" + "class LocalBase { func stored(_ session: Session) -> Session { session } }\n" + "class LocalSuite: LocalBase {\n" + "func wrapped() { let session = stored(Session()); session.upload(1) }\n}\n" + "class CrossSuite: BaseTestCase {\n" + "func wrapped() { let session = stored(Session()); session.request(.default)" + ".validate().responseDecodable(of: String.self) {} }\n" + "func storedStream() { let stream = storedStream(Type()); stream.responseStream {} }\n" + "func awaited() async throws { var chunk = try await self.readFromFile(); " + "chunk.moveReaderIndex(forwardBy: 1) }\n" + "func mixedFactory() { let session = mixed(.default); session.upload(1) }\n}\n" + "func nestedFactory() { let factory = makeFactory(); let session = factory.make(); " + "session.upload(1) }\n" + "func unknownFactory() { let session = missing(Session()); session.upload(1) }\n" + "func unknownRoot() { let session = unknown().make(); session.upload(1) }\n" + "func cyclicFactory() { let first = second.make(); let second = first.make(); " + "first.upload(1) }\n"; + const struct { + const char *caller; + const char *target; + int expected; + } pairs[] = { + {"same(_:Session)", "Request.responseDecodable(of:Type,completion:()=>Void)", 1}, + {"same(_:Session)", "Request.responseDecodable(wrong:Type,completion:()=>Void)", 0}, + {"same(_:Session)", "ParentRequest.validate()=>Self", 1}, + {"streamed(_:Session)", "Type.responseStream(stream:Handler)", 1}, + {"LocalSuite.wrapped()", "Session.upload(_:Int)", 1}, + {"LocalSuite.wrapped()", "Unrelated.upload(_:Int)", 0}, + {"CrossSuite.wrapped()", "ParentRequest.validate()=>Self", 1}, + {"CrossSuite.wrapped()", "Request.responseDecodable(of:Type,completion:()=>Void)", 1}, + {"CrossSuite.wrapped()", "OtherRequest.responseDecodable(of:Type,completion:()=>Void)", 0}, + {"CrossSuite.storedStream()", "Type.responseStream(stream:Handler)", 1}, + {"CrossSuite.awaited()async", "Type.moveReaderIndex(forwardBy:Int)", 1}, + {"CrossSuite.mixedFactory()", "Session.upload(_:Int)", 0}, + {"CrossSuite.mixedFactory()", "Unrelated.upload(_:Int)", 0}, + {"nestedFactory()", "Session.upload(_:Int)", 1}, + {"nestedFactory()", "Unrelated.upload(_:Int)", 0}, + {"unknownFactory()", "Session.upload(_:Int)", 0}, + {"unknownFactory()", "Unrelated.upload(_:Int)", 0}, + {"unknownRoot()", "Session.upload(_:Int)", 0}, + {"unknownRoot()", "Unrelated.upload(_:Int)", 0}, + {"cyclicFactory()", "Session.upload(_:Int)", 0}, + {"cyclicFactory()", "Unrelated.upload(_:Int)", 0}, + {"divergent(_:Session)", "Request.responseDecodable(of:Type,completion:()=>Void)", 0}, + {"divergent(_:Session)", "OtherRequest.responseDecodable(of:Type,completion:()=>Void)", 0}, + {"wrongTail(_:Session)", "Request.responseDecodable(of:Type,completion:()=>Void)", 0}, + {"wrongTail(_:Session)", "Request.responseDecodable(wrong:Type,completion:()=>Void)", 0}, + {"selfDivergent(_:Session)", "Request.responseDecodable(of:Type,completion:()=>Void)", 0}, + {"selfDivergent(_:Session)", "OtherRequest.responseDecodable(of:Type,completion:()=>Void)", + 0}, + {"metatype(_:Type)", "Session.run(flag:Bool)static", 1}, + {"metatype(_:Type)", "Unrelated.run(flag:Bool)static", 0}, + {"external(_:Type)", "Session.run(flag:Bool)static", 0}, + {"external(_:Type)", "Unrelated.run(flag:Bool)static", 0}, + {"upload(_:Session)", "Session.upload(_:Int)", 1}, + {"upload(_:Session)", "Unrelated.upload(_:Int)", 0}, + }; + for (int parallel = 0; parallel < 2; parallel++) { + char tmp[256] = "/tmp/cbm_swift_chain_XXXXXX"; + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Types.swift", types); + write_temp_file(tmp, "Calls.swift", calls); + write_temp_file(tmp, "BaseTestCase.swift", + "class BaseTestCase {\n" + "func stored(_ session: Session) -> Session { session }\n" + "func storedStream(_ stream: Type) -> Type { stream }\n" + "func readFromFile() async throws -> Type { fatalError() }\n" + "func mixed(_ value: Int) -> Session { Session() }\n" + "func mixed(_ value: String) -> Unrelated { Unrelated() }\n}\n" + "class Foreign { func missing(_ value: Session) -> Session { value } }\n"); + for (int i = 0; parallel && i < 55; i++) { + char path[64]; + snprintf(path, sizeof(path), "Pad%d.swift", i); + write_temp_file(tmp, path, "// filler\n"); + } + XlangEnvSave saved = xlang_env_select(parallel != 0); + char db[512], path[512]; + snprintf(db, sizeof(db), "%s/graph.db", tmp); + snprintf(path, sizeof(path), "%s/Calls.swift", tmp); + for (int phase = 0; phase < 2; phase++) { + if (phase) { + char changed[8192]; + int changed_length = snprintf(changed, sizeof(changed), "%s// changed\n", calls); + ASSERT_GT(changed_length, 0); + ASSERT_LT(changed_length, (int)sizeof(changed)); + write_temp_file(tmp, "Calls.swift", changed); + ASSERT_EQ(pipeline_test_set_mtime(path, 2000000000, 0), 0); + } + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + if (phase) { + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), + CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + } + cbm_store_t *store = cbm_store_open_path(db); + ASSERT_NOT_NULL(store); + const char *project = cbm_pipeline_project_name(p); + cbm_node_t *requests = NULL; + int count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(store, project, "request", &requests, &count), + CBM_STORE_OK); + ASSERT_EQ(count, 2); + for (int i = 0; i < count; i++) { + ASSERT_NOT_NULL(strchr(requests[i].qualified_name, '#')); + ASSERT_NOT_NULL(strstr(requests[i].properties_json, "=>Request")); + } + cbm_store_free_nodes(requests, count); + for (size_t i = 0; i < sizeof(pairs) / sizeof(pairs[0]); i++) { + char qn[1024]; + cbm_node_t caller = {0}, target = {0}; + snprintf(qn, sizeof(qn), "%s.Calls.%s", project, pairs[i].caller); + ASSERT_EQ(cbm_store_find_node_by_qn(store, project, qn, &caller), CBM_STORE_OK); + snprintf(qn, sizeof(qn), "%s.Types.%s", project, pairs[i].target); + ASSERT_EQ(cbm_store_find_node_by_qn(store, project, qn, &target), CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(store, caller.id, target.id), pairs[i].expected); + if (pairs[i].expected && + (strcmp(pairs[i].target, "Session.run(flag:Bool)static") == 0 || + strcmp(pairs[i].target, "Type.moveReaderIndex(forwardBy:Int)") == 0)) { + cbm_edge_t *edges = NULL; + int edge_count = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(store, caller.id, "CALLS", &edges, + &edge_count), + CBM_STORE_OK); + const char *keyword = strcmp(pairs[i].caller, "metatype(_:Type)") == 0 + ? "\"k\":\"flag\"" + : "\"k\":\"forwardBy\""; + int labelled = 0; + for (int edge = 0; edge < edge_count; edge++) { + if (edges[edge].target_id == target.id && edges[edge].properties_json && + strstr(edges[edge].properties_json, keyword)) { + labelled++; + } + } + ASSERT_EQ(labelled, 1); + cbm_store_free_edges(edges, edge_count); + } + cbm_node_free_fields(&caller); + cbm_node_free_fields(&target); + } + cbm_store_close(store); + cbm_pipeline_free(p); + } + xlang_env_restore(&saved); + th_rmtree(tmp); + } + PASS(); +} + +TEST(pipeline_swift_return_identity_issue2061) { + const char *returns[] = {"pick()=>Int", "pick()=>String"}; + return swift_declaration_identity_case( + returns, 2, + "class Service {\nfunc pick() -> Int { mark0(); return 1 }\n" + "func pick() -> String { mark1(); return \"x\" }\n}\n" + "func mark0() {}\nfunc mark1() {}\n", + "class Caller {\nlet service = Service()\n" + "func invoke() { let _: Int = self.service.pick() }\n}\n"); +} + +/* The parallel call pass must retain both type-only overload candidates. */ +TEST(pipeline_swift_overloads_parallel_candidates_issue2061) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftoverload_par_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " func upload(_ data: Data, to url: URL) {}\n" + " func upload(_ file: URL, to url: URL) {}\n" + " func record(path: String, session: URLSession) {}\n" + " func handle(completion: () -> Void) {}\n" + " func handle(completion: () -> Void, onError: () -> Void) {}\n" + "}\n"); + write_temp_file( + tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + " func invoke() {\n" + " self.service.upload(Data(), to: URL(string: \"/x\")!)\n" + " self.service.record(path: \"/audit\", session: URLSession.shared)\n" + " }\n" + " func callHandle() { self.service.handle { } onError: { } }\n" + " func callHandleWrongLabel() { self.service.handle { } bogus: { } }\n" + " func callHandleIncomplete() { self.service.handle { } onError: { recover( } }\n" + "}\n"); + for (int i = 0; i < 50; i++) { + char path[64], source[80]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(source, sizeof(source), "func filler%d() {}\n", i); + write_temp_file(tmp, path, source); + } + + char *previous_workers = getenv("CBM_WORKERS"); + char *saved_workers = previous_workers ? strdup(previous_workers) : NULL; + cbm_setenv("CBM_WORKERS", "4", 1); + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swiftoverload.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + int run_result = p ? cbm_pipeline_run(p) : -1; + if (saved_workers) { + cbm_setenv("CBM_WORKERS", saved_workers, 1); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + free(saved_workers); + ASSERT_NOT_NULL(p); + ASSERT_EQ(run_result, 0); + const char *project = cbm_pipeline_project_name(p); + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + char caller_qn[512], data_qn[512], url_qn[512]; + snprintf(caller_qn, sizeof(caller_qn), "%s.Sources.Caller.Caller.invoke()", project); + snprintf(data_qn, sizeof(data_qn), "%s.Sources.Service.Service.upload(_:Data,to:URL)", project); + snprintf(url_qn, sizeof(url_qn), "%s.Sources.Service.Service.upload(_:URL,to:URL)", project); + char record_qn[512]; + snprintf(record_qn, sizeof(record_qn), + "%s.Sources.Service.Service.record(path:String,session:URLSession)", project); + cbm_node_t caller = {0}, data = {0}, url = {0}, record = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, caller_qn, &caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, data_qn, &data), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, url_qn, &url), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, record_qn, &record), CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, data.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, url.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, caller.id, record.id), 1); + ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 0); + cbm_edge_t *edges = NULL; + int edge_count = 0; + ASSERT_EQ(cbm_store_find_edges_by_source_type(s, caller.id, "CALLS", &edges, &edge_count), + CBM_STORE_OK); + int ambiguous = 0; + for (int i = 0; i < edge_count; i++) { + if ((edges[i].target_id == data.id || edges[i].target_id == url.id) && + edges[i].properties_json && strstr(edges[i].properties_json, "\"candidates\":2")) { + ambiguous++; + } + } + ASSERT_EQ(ambiguous, 2); + cbm_store_free_edges(edges, edge_count); + + /* #2061: the parallel resolver must reach the same trailing-closure + * verdicts as the serial one. */ + char handle_one_qn[512], handle_two_qn[512], handle_qn[512], handle_wrong_qn[512], + handle_incomplete_qn[512]; + snprintf(handle_one_qn, sizeof(handle_one_qn), + "%s.Sources.Service.Service.handle(completion:()=>Void)", project); + snprintf(handle_two_qn, sizeof(handle_two_qn), + "%s.Sources.Service.Service.handle(completion:()=>Void,onError:()=>Void)", project); + snprintf(handle_qn, sizeof(handle_qn), "%s.Sources.Caller.Caller.callHandle()", project); + snprintf(handle_wrong_qn, sizeof(handle_wrong_qn), + "%s.Sources.Caller.Caller.callHandleWrongLabel()", project); + snprintf(handle_incomplete_qn, sizeof(handle_incomplete_qn), + "%s.Sources.Caller.Caller.callHandleIncomplete()", project); + cbm_node_t handle_one = {0}, handle_two = {0}, handle = {0}; + cbm_node_t handle_wrong = {0}, handle_incomplete = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_one_qn, &handle_one), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_two_qn, &handle_two), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_qn, &handle), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_wrong_qn, &handle_wrong), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, handle_incomplete_qn, &handle_incomplete), + CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(s, handle.id, handle_two.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(s, handle.id, handle_one.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong.id, handle_two.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_wrong.id, handle_one.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete.id, handle_two.id), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, handle_incomplete.id, handle_one.id), 0); + cbm_node_free_fields(&handle_incomplete); + cbm_node_free_fields(&handle_wrong); + cbm_node_free_fields(&handle_one); + cbm_node_free_fields(&handle_two); + cbm_node_free_fields(&handle); + cbm_node_free_fields(&caller); + cbm_node_free_fields(&data); + cbm_node_free_fields(&url); + cbm_node_free_fields(&record); + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +TEST(pipeline_swift_default_before_required_candidates_issue2061) { + for (int mode = 0; mode < 2; mode++) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_default_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " func choose(x first: Int = 0, x second: Int) {}\n" + " func choose(x first: String = \"\", x second: String) {}\n" + " func consume(_ first: Int = 0, _ second: Int) {}\n" + "}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + " func labeled() { self.service.choose(x: 1) }\n" + " func unlabeled() { self.service.consume(1) }\n" + " func wrong() { self.service.choose(y: 1) }\n" + "}\n"); + for (int i = 0; mode && i < 50; i++) { + char path[64], source[80]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(source, sizeof(source), "func filler%d() {}\n", i); + write_temp_file(tmp, path, source); + } + const char *previous = getenv("CBM_WORKERS"); + char *saved = previous ? strdup(previous) : NULL; + cbm_setenv("CBM_WORKERS", mode ? "4" : "1", 1); + char db[512]; + snprintf(db, sizeof(db), "%s/defaults.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + int result = p ? cbm_pipeline_run(p) : -1; + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + free(saved); + ASSERT_NOT_NULL(p); + ASSERT_EQ(result, 0); + const char *project = cbm_pipeline_project_name(p); + cbm_store_t *store = cbm_store_open_path(db); + ASSERT_NOT_NULL(store); + const char *targets[] = {"choose(x:Int,x:Int)", "choose(x:String,x:String)", + "consume(_:Int,_:Int)"}; + for (int i = 0; i < 3; i++) { + char caller_qn[512], target_qn[512], wrong_qn[512]; + snprintf(caller_qn, sizeof(caller_qn), "%s.Sources.Caller.Caller.%s()", project, + i < 2 ? "labeled" : "unlabeled"); + snprintf(target_qn, sizeof(target_qn), "%s.Sources.Service.Service.%s", project, + targets[i]); + snprintf(wrong_qn, sizeof(wrong_qn), "%s.Sources.Caller.Caller.wrong()", project); + cbm_node_t caller = {0}, target = {0}, wrong = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(store, project, caller_qn, &caller), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(store, project, target_qn, &target), CBM_STORE_OK); + ASSERT_EQ(cbm_store_find_node_by_qn(store, project, wrong_qn, &wrong), CBM_STORE_OK); + ASSERT_EQ(pipeline_has_calls_edge(store, caller.id, target.id), 1); + ASSERT_EQ(pipeline_has_calls_edge(store, wrong.id, target.id), 0); + cbm_edge_t *edges = NULL; + int count = 0, compatible = 0; + ASSERT_EQ( + cbm_store_find_edges_by_source_type(store, caller.id, "CALLS", &edges, &count), + CBM_STORE_OK); + for (int e = 0; e < count; e++) { + if (edges[e].target_id == target.id && edges[e].properties_json && + strstr(edges[e].properties_json, + i < 2 ? "\"candidates\":2" : "\"candidates\":1")) { + compatible++; + } + } + ASSERT_EQ(compatible, 1); + cbm_store_free_edges(edges, count); + cbm_node_free_fields(&caller); + cbm_node_free_fields(&target); + cbm_node_free_fields(&wrong); + } + cbm_store_close(store); + cbm_pipeline_free(p); + th_rmtree(tmp); + } + PASS(); +} + +/* #2061: a Swift call matching NONE of a same-named project symbol's + * overloads must not bind to it (a bare-name match would bind a wrong + * overload), while the empty-resolution service fallbacks still apply — + * the contract both resolver paths share (Swift blocks in pass_calls.c + * and pass_parallel.c). Local `fetch(id:)` cannot take a one-argument + * unlabeled URL call, so the call stays unresolved and classifies as the + * global API (#856). */ +static int swift_incompatible_overload_fixture(bool parallel) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftincompat_par_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + + write_temp_file(tmp, "Sources/Api.swift", + "func fetch(id: Int) -> Int {\n" + " return id\n" + "}\n" + "func get(id: Int) {}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " func run() -> Int {\n" + " return fetch(\"https://api.example.com/data\")\n" + " }\n" + " func wrongLabel() { get(key: 1) }\n" + "}\n"); + for (int i = 0; parallel && i < 52; i++) { + char path[64], source[80]; + snprintf(path, sizeof(path), "Sources/Filler%d.swift", i); + snprintf(source, sizeof(source), "func filler%d() {}\n", i); + write_temp_file(tmp, path, source); + } + + char *previous_workers = getenv("CBM_WORKERS"); + char *saved_workers = previous_workers ? strdup(previous_workers) : NULL; + cbm_setenv("CBM_WORKERS", parallel ? "4" : "1", 1); + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swiftincompat.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + int run_result = p ? cbm_pipeline_run(p) : -1; + if (saved_workers) { + cbm_setenv("CBM_WORKERS", saved_workers, 1); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + free(saved_workers); + ASSERT_NOT_NULL(p); + ASSERT_EQ(run_result, 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + cbm_node_t *fetches = NULL; + int fetch_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "fetch", &fetches, &fetch_count), + CBM_STORE_OK); + ASSERT_EQ(fetch_count, 1); + cbm_node_t *runs = NULL; + int run_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "run", &runs, &run_count), CBM_STORE_OK); + ASSERT_EQ(run_count, 1); + char wrong_qn[512]; + snprintf(wrong_qn, sizeof(wrong_qn), "%s.Sources.Caller.Caller.wrongLabel()", project); + cbm_node_t wrong = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(s, project, wrong_qn, &wrong), CBM_STORE_OK); + cbm_node_t *gets = NULL; + int get_count = 0; + ASSERT_EQ(cbm_store_find_nodes_by_name(s, project, "get", &gets, &get_count), CBM_STORE_OK); + ASSERT_EQ(get_count, 1); + ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 0); + /* Fault injection: the same checker must detect an incompatible edge, + * which would make the preceding negative assertion fail in either path. */ + cbm_edge_t injected = { + .project = project, .source_id = wrong.id, .target_id = gets[0].id, .type = "CALLS"}; + ASSERT_GT(cbm_store_insert_edge(s, &injected), 0); + ASSERT_EQ(pipeline_has_calls_edge(s, wrong.id, gets[0].id), 1); + cbm_store_free_nodes(gets, get_count); + cbm_node_free_fields(&wrong); + ASSERT_EQ(pipeline_has_calls_edge(s, runs[0].id, fetches[0].id), 0); + cbm_store_free_nodes(fetches, fetch_count); + cbm_store_free_nodes(runs, run_count); + + ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +TEST(pipeline_swift_incompatible_overload_unresolved_issue2061) { + return swift_incompatible_overload_fixture(false); +} + +/* >=50 files exercise the separate parallel resolver with identical assertions. */ +TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061) { + return swift_incompatible_overload_fixture(true); +} + /* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's * resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls * but forces the parallel resolver, since the empty-resolution fallback is a @@ -23020,18 +24542,22 @@ static const char *const SWIFT_INIT_INITS_SRC = "func helper() -> Int { 1 }\n" " init(raw: Int) {\n" " value = other()\n" " }\n" + " init(nibName: String?, bundle: Bundle?) {\n" + " value = 0\n" + " }\n" "}\n"; static const char *const SWIFT_INIT_BASE_SRC = "open class Base {\n" - " public init() {}\n" + " public init(name: String) {}\n" + " public init(seed: Int) {}\n" "}\n"; static const char *const SWIFT_INIT_SUB_SRC = "import Kit\n" "import UIKit\n" "\n" "class Sub: Base {\n" - " override init() {\n" - " super.init()\n" + " override init(name: String) {\n" + " super.init(name: name)\n" " }\n" "}\n" "class Screen: UIViewController {\n" @@ -23061,6 +24587,7 @@ typedef struct { int run_rc; int calls; /* CALLS edges seen; -1 if the store was not read */ int init_usages; /* USAGE edges into a node named init/deinit */ + bool compatible_init_present; char edges[SWIFT_INIT_EDGE_CAP][SWIFT_INIT_EDGE_LEN]; /* "src -> tgt", sorted */ } SwiftInitObservation; @@ -23075,6 +24602,13 @@ static const char *swift_init_rel_qn(const char *qn, const char *project) { } static void swift_init_observe(cbm_store_t *s, const char *project, SwiftInitObservation *o) { + char compatible_qn[512]; + snprintf(compatible_qn, sizeof(compatible_qn), + "%s.Sources.Kit.Inits.Box.init(nibName:String?,bundle:Bundle?)", project); + cbm_node_t compatible = {0}; + o->compatible_init_present = + cbm_store_find_node_by_qn(s, project, compatible_qn, &compatible) == CBM_STORE_OK; + cbm_node_free_fields(&compatible); cbm_edge_t *edges = NULL; int count = 0; o->calls = 0; @@ -23176,17 +24710,18 @@ static void swift_init_case(bool parallel, int pad, SwiftInitObservation *o) { * convenience `self.init` is not a self-loop. */ static int swift_init_assert_exact(const SwiftInitObservation *o) { static const char *const expected[] = { - "Sources.App.Sub.Sub.init -> Sources.Kit.Base.Base.init", - "Sources.App.Sub.explicit -> Sources.Kit.Inits.Box.init", - "Sources.Kit.Inits.Box.init -> Sources.Kit.Inits.helper", - "Sources.Kit.Inits.Box.init -> Sources.Kit.Inits.other", - "Sources.Kit.Inits.Box.read -> Sources.Kit.Inits.other", - "Sources.Kit.Inits.Holder.init -> Sources.Kit.Inits.helper", - "Sources.Kit.Inits.Holder.init -> Sources.Kit.Inits.other", - "Sources.Kit.Inits.Mode.init -> Sources.Kit.Inits.helper", + "Sources.App.Sub.Sub.init(name:String) -> Sources.Kit.Base.Base.init(name:String)", + "Sources.App.Sub.explicit()=>Box -> Sources.Kit.Inits.Box.init(raw:Int)", + "Sources.Kit.Inits.Box.init() -> Sources.Kit.Inits.helper()=>Int", + "Sources.Kit.Inits.Box.init(raw:Int) -> Sources.Kit.Inits.other()=>Int", + "Sources.Kit.Inits.Box.read()=>Int -> Sources.Kit.Inits.other()=>Int", + "Sources.Kit.Inits.Holder.init() -> Sources.Kit.Inits.other()=>Int", + "Sources.Kit.Inits.Holder.init(seed:Int) -> Sources.Kit.Inits.helper()=>Int", + "Sources.Kit.Inits.Mode.init(flag:Bool) -> Sources.Kit.Inits.helper()=>Int", }; enum { EXPECTED_CALLS = sizeof(expected) / sizeof(expected[0]) }; ASSERT_EQ(o->run_rc, 0); + ASSERT_TRUE(o->compatible_init_present); ASSERT_EQ(o->init_usages, 0); ASSERT_EQ(o->calls, (int)EXPECTED_CALLS); for (int i = 0; i < (int)EXPECTED_CALLS; i++) { @@ -26282,6 +27817,33 @@ static const char *pkg_entries_entry_for(const cbm_pkg_entries_t *e, const char * plus fail-closed ambiguous-name cases. See pipeline_swift_cross_package_import * above for the full end-to-end proof. */ +TEST(pkgmap_swift_target_kinds_and_normalized_paths) { + const char *source = "let package = Package(name: \"IgnoredProduct\", targets: [\n" + " .target(name: \"Core\", path: \"./Old/../Library.v2\"),\n" + " .target(name: \"Root\", path: \".\"),\n" + " .testTarget(name: \"CoreTests\"),\n" + " .executableTarget(name: \"Tool\"),\n" + " .macro(name: \"Macros\"), .plugin(name: \"Plugin\"),\n" + " .target(name: \"Escapes\", path: \"../Other\"),\n" + " .testTarget(name: \"Computed\", path: targetPath)])\n"; + cbm_pkg_entries_t entries; + cbm_pkg_entries_init(&entries); + ASSERT_TRUE(cbm_pkgmap_try_parse("Package.swift", "Nested/Package.swift", source, + (int)strlen(source), &entries)); + ASSERT_EQ(entries.count, 6); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Core"), "Nested/Library.v2"); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Root"), "Nested"); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "CoreTests"), "Nested/Tests/CoreTests"); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Tool"), "Nested/Sources/Tool"); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Macros"), "Nested/Sources/Macros"); + ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Plugin"), "Nested/Plugins/Plugin"); + for (int i = 0; i < entries.count; i++) { + ASSERT_STR_EQ(entries.items[i].manifest_rel, "Nested/Package.swift"); + } + cbm_pkg_entries_free(&entries); + PASS(); +} + TEST(pkgmap_swift_targets_registers_module) { static const char src[] = "// swift-tools-version:5.9\n" "import PackageDescription\n" @@ -26543,6 +28105,11 @@ TEST(pkgmap_scan_repo_honors_discovery_exclusions) { "{\"name\":\"@org/app\",\"main\":\"index.js\"}\n"); write_temp_file(tmpdir, "vendor_big/lib/package.json", "{\"name\":\"@org/vendored\",\"main\":\"index.js\"}\n"); + write_temp_file(tmpdir, "packages/app/Package.swift", + "let package = Package(name: \"App\", targets: [.target(name: \"App\")])\n"); + write_temp_file(tmpdir, "vendor_big/lib/Package.swift", + "let package = Package(name: \"Vendored\", targets: [" + ".target(name: \"Vendored\")])\n"); /* Control: NULL exclusion list — the walk reaches and parses BOTH * manifests (proves the excluded one is reachable + parseable). */ @@ -26552,6 +28119,11 @@ TEST(pkgmap_scan_repo_honors_discovery_exclusions) { ASSERT_TRUE(pkg_entries_has_name(&control, "@org/app")); ASSERT_TRUE(pkg_entries_has_name(&control, "@org/vendored")); cbm_pkg_entries_free(&control); + cbm_pkg_entries_init(&control); + cbm_pkgmap_scan_swift_targets(tmpdir, &control, NULL, 0); + ASSERT_TRUE(pkg_entries_has_name(&control, "App")); + ASSERT_TRUE(pkg_entries_has_name(&control, "Vendored")); + cbm_pkg_entries_free(&control); /* With vendor_big excluded (as discovery reports for a gitignored * subtree): the walk must not descend into it. */ @@ -26562,6 +28134,15 @@ TEST(pkgmap_scan_repo_honors_discovery_exclusions) { ASSERT_TRUE(pkg_entries_has_name(&entries, "@org/app")); ASSERT_FALSE(pkg_entries_has_name(&entries, "@org/vendored")); cbm_pkg_entries_free(&entries); + cbm_pkg_entries_init(&entries); + cbm_pkgmap_scan_swift_targets(tmpdir, &entries, excluded, 1); + ASSERT_TRUE(pkg_entries_has_name(&entries, "App")); + ASSERT_FALSE(pkg_entries_has_name(&entries, "Vendored")); + for (int i = 0; i < entries.count; i++) { + ASSERT_NOT_NULL(entries.items[i].manifest_rel); + ASSERT_FALSE(strncmp(entries.items[i].manifest_rel, "vendor_big/", 11) == 0); + } + cbm_pkg_entries_free(&entries); th_rmtree(tmpdir); PASS(); @@ -31223,6 +32804,124 @@ TEST(pipeline_objectscript_export_range_join_keeps_one_trailing_marker) { } #endif +static bool swift_default_restore_graph_is_valid(cbm_store_t *store, const char *project, + bool has_default) { + char service_qn[512], good_qn[512], bad_qn[512]; + snprintf(service_qn, sizeof(service_qn), "%s.Sources.Service.Service.ping(a:Int,c:Int)", + project); + snprintf(good_qn, sizeof(good_qn), "%s.Sources.Caller.Caller.good()", project); + snprintf(bad_qn, sizeof(bad_qn), "%s.Sources.Caller.Caller.bad()", project); + cbm_node_t service = {0}, good = {0}, bad = {0}; + bool valid = cbm_store_find_node_by_qn(store, project, service_qn, &service) == CBM_STORE_OK && + cbm_store_find_node_by_qn(store, project, good_qn, &good) == CBM_STORE_OK && + cbm_store_find_node_by_qn(store, project, bad_qn, &bad) == CBM_STORE_OK; + if (valid) { + valid = service.name && strcmp(service.name, "ping") == 0 && service.properties_json && + strstr(service.properties_json, + has_default ? "\"param_default_mask\":\"0000000000000002\"" + : "\"param_default_mask\":\"0000000000000000\"") && + strstr(service.properties_json, "\"selector_param_count\":2") && + pipeline_has_calls_edge(store, good.id, service.id) == has_default && + pipeline_has_calls_edge(store, bad.id, service.id) == 0; + } + cbm_node_free_fields(&service); + cbm_node_free_fields(&good); + cbm_node_free_fields(&bad); + return valid; +} + +TEST(pipeline_swift_incremental_restores_default_signature_issue2061) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_restore_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " func ping(a: Int, c: Int = 0) {}\n" + " func other(a: Int, b: Int = 0) {}\n" + "}\n"); + write_temp_file(tmp, "Sources/Caller.swift", + "class Caller {\n" + " let service = Service()\n" + " func good() { self.service.ping(a: 1) }\n" + " func bad() { self.service.ping(x: 1) }\n" + "}\n"); + char db_path[512], closure_db_path[512], caller_path[512], project[512], service_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swift-restore.db", tmp); + snprintf(closure_db_path, sizeof(closure_db_path), "%s/swift-closure.db", tmp); + snprintf(caller_path, sizeof(caller_path), "%s/Sources/Caller.swift", tmp); + snprintf(service_path, sizeof(service_path), "%s/Sources/Service.swift", tmp); + + cbm_pipeline_t *first = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(first); + ASSERT_EQ(cbm_pipeline_run(first), 0); + const char *project_name = cbm_pipeline_project_name(first); + ASSERT_NOT_NULL(project_name); + snprintf(project, sizeof(project), "%s", project_name); + cbm_store_t *store = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(store); + ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, true)); + cbm_store_close(store); + cbm_pipeline_free(first); + + FILE *caller = fopen(caller_path, "ab"); + ASSERT_NOT_NULL(caller); + ASSERT_TRUE(fputs("// incremental marker\n", caller) >= 0); + ASSERT_EQ(fclose(caller), 0); + ASSERT_EQ(pipeline_test_set_mtime(caller_path, 2000000000, 0), 0); + cbm_pipeline_incremental_test_reset_faults(); + cbm_pipeline_incremental_test_force_legacy_partial_once(); + cbm_pipeline_t *legacy = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(legacy); + ASSERT_EQ(cbm_pipeline_run(legacy), 0); + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); + store = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(store); + ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, true)); + cbm_store_close(store); + cbm_pipeline_free(legacy); + cbm_pipeline_incremental_test_reset_faults(); + /* The test-only legacy partial route predates persisted LSP surfaces. + * Seed a separate full generation for closure-repair assertions. */ + cbm_pipeline_t *closure_seed = cbm_pipeline_new(tmp, closure_db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(closure_seed); + ASSERT_EQ(cbm_pipeline_run(closure_seed), 0); + cbm_pipeline_free(closure_seed); + + caller = fopen(caller_path, "ab"); + ASSERT_NOT_NULL(caller); + ASSERT_TRUE(fputs("// closure marker\n", caller) >= 0); + ASSERT_EQ(fclose(caller), 0); + ASSERT_EQ(pipeline_test_set_mtime(caller_path, 2000000001, 0), 0); + cbm_pipeline_t *closure = cbm_pipeline_new(tmp, closure_db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(closure); + ASSERT_EQ(cbm_pipeline_run(closure), 0); + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + store = cbm_store_open_path(closure_db_path); + ASSERT_NOT_NULL(store); + ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, true)); + cbm_store_close(store); + cbm_pipeline_free(closure); + + write_temp_file(tmp, "Sources/Service.swift", + "class Service {\n" + " func ping(a: Int, c: Int) {}\n" + " func other(a: Int, b: Int = 0) {}\n" + "}\n"); + ASSERT_EQ(pipeline_test_set_mtime(service_path, 2000000002, 0), 0); + cbm_pipeline_t *defaults_removed = cbm_pipeline_new(tmp, closure_db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(defaults_removed); + ASSERT_EQ(cbm_pipeline_run(defaults_removed), 0); + ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + store = cbm_store_open_path(closure_db_path); + ASSERT_NOT_NULL(store); + ASSERT_TRUE(swift_default_restore_graph_is_valid(store, project, false)); + cbm_store_close(store); + cbm_pipeline_free(defaults_removed); + cbm_pipeline_incremental_test_reset_faults(); + th_rmtree(tmp); + PASS(); +} + /* #1153: a C++ method called on the object a static factory returns * (`SecdManager::getInstance()->m()`, `Registry::instance().m()`, * `auto w = Widget::create(); w->m()`) got no CALLS edge when the class lives @@ -31932,6 +33631,23 @@ SUITE(pipeline) { RUN_TEST(pipeline_native_fetch_template_base_url_makes_route); RUN_TEST(pipeline_swift_nested_url_makes_route_issue1892); RUN_TEST(pipeline_swift_http_call_makes_route_issue1892); + RUN_TEST(pipeline_swift_overloads_keep_argument_labels_issue2061); + RUN_TEST(pipeline_swift_overloads_parallel_candidates_issue2061); + RUN_TEST(pipeline_swift_generic_async_identity_issue2061); + RUN_TEST(pipeline_swift_static_instance_identity_issue2061); + RUN_TEST(pipeline_swift_extension_constraints_identity_issue2061); + RUN_TEST(pipeline_swift_capped_selector_survives_incremental_issue2061); + RUN_TEST(pipeline_swift_cross_file_extension_issue2061); + RUN_TEST(pipeline_swift_same_named_extensions_stay_in_module_issue2061); + RUN_TEST(pipeline_swift_custom_target_paths_and_manifest_edit_issue2061); + RUN_TEST(pipeline_swift_qualified_and_return_identity_issue2061); + RUN_TEST(pipeline_swift_return_identity_issue2061); + RUN_TEST(pipeline_swift_receiver_and_chained_closures_issue2061); + RUN_TEST(pipeline_swift_capped_chain_returns_and_metatype_receivers); + RUN_TEST(pipeline_swift_default_before_required_candidates_issue2061); + RUN_TEST(pipeline_swift_incompatible_overload_unresolved_issue2061); + RUN_TEST(pipeline_swift_incompatible_overload_parallel_fallback_issue2061); + RUN_TEST(pipeline_swift_incremental_restores_default_signature_issue2061); RUN_TEST(pipeline_route_prefix_bridge_is_order_free_and_root_bridges_nothing); RUN_TEST(pipeline_go_servemux_method_patterns_issue2537); RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls); @@ -32095,6 +33811,7 @@ SUITE(pipeline) { RUN_TEST(k8s_extract_manifest_multidoc); /* SwiftPM Package.swift manifest resolution (issue #551 item 1) */ RUN_TEST(pkgmap_swift_targets_registers_module); + RUN_TEST(pkgmap_swift_target_kinds_and_normalized_paths); RUN_TEST(pkgmap_swift_products_do_not_register_alias); RUN_TEST(pkgmap_swift_target_name_immediately_before_close_paren); RUN_TEST(pkgmap_swift_target_honors_literal_path); diff --git a/tests/test_registry.c b/tests/test_registry.c index b711e32edc..f92b25bf79 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -5,6 +5,7 @@ * resolution strategies, and qualified name computation. */ #include "test_framework.h" +#include "test_helpers.h" #include "pipeline/pipeline.h" #include "pipeline/pipeline_internal.h" /* cbm_python_import_binding_contradicts (#2127) */ @@ -966,11 +967,11 @@ TEST(registry_tie_break_is_independent_of_registration_order) { ASSERT_STR_EQ(lb.qualified_name, "proj.include.linux.scatterlist.sg_next"); /* Proximity outranks the tie order: the sibling wins over a shallower stranger. */ - cbm_registry_t *near = cbm_registry_new(); - cbm_registry_add(near, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function"); - cbm_registry_add(near, "vnic_rq_free", "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free", + cbm_registry_t *proximity = cbm_registry_new(); + cbm_registry_add(proximity, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function"); + cbm_registry_add(proximity, "vnic_rq_free", "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free", "Function"); - cbm_resolution_t nr = cbm_registry_resolve(near, "vnic_rq_free", + cbm_resolution_t nr = cbm_registry_resolve(proximity, "vnic_rq_free", "proj.drivers.scsi.fnic.fnic_res", NULL, NULL, 0); ASSERT_STR_EQ(nr.qualified_name, "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free"); @@ -978,7 +979,7 @@ TEST(registry_tie_break_is_independent_of_registration_order) { cbm_registry_free(backward); cbm_registry_free(lex_a); cbm_registry_free(lex_b); - cbm_registry_free(near); + cbm_registry_free(proximity); PASS(); } @@ -2059,10 +2060,10 @@ TEST(external_import_shadow_declared_package_kept) { /* The walk is by dotted segment, not by substring: a tree that declares * "org.example.tests" must not claim "org.example.testsuite.helper". */ - CBMImport near[] = { + CBMImport adjacent_imports[] = { {.local_name = "near", .module_path = "org.example.testsuite.helper.near"}, }; - CBMImportArray narr = {.items = near, .count = 1, .cap = 1}; + CBMImportArray narr = {.items = adjacent_imports, .count = 1, .cap = 1}; CBMHashTable *prefix_only = cbm_ht_create(8); ASSERT_NOT_NULL(prefix_only); cbm_ht_set(prefix_only, "org.example.tests", (void *)"qn"); @@ -2177,6 +2178,464 @@ TEST(resolve_import_map_alias_with_suffix_hits_method) { PASS(); } +TEST(swift_overload_labels_defaults_and_trailing_closure) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + cbm_registry_add(r, "Service", "proj.Service", "Class"); + const char *qns[] = { + "proj.Service.work(flag:Bool)", + "proj.Service.work(name:String)", + "proj.Service.work(name:Int)", + "proj.Other.work(name:String)", + "proj.Service.send(value:Int,completion:()=>Void)", + "proj.Service.configure(a:Int,b:Int,c:Int)", + "proj.Service.consume(values:[()=>Void])", + "proj.Service.consume(completion:()=>Void)", + "proj.Service.accept(value:Pair<()=>Void,Int>)", + "proj.Service.perform(completion:(()=>Void)?)", + "proj.Service.perform(completion:((Int)=>Void))", + "proj.Service.perform(completion:(()=>Void,Int))", + "proj.Service.perform(completion:(()=>Result)?)", + "proj.Service.collect(_:Int~)", + "proj.Service.named(name:Int~)", + "proj.Service.run(completion:()=>Void,flag:Bool)", + "proj.Service.batch(completions:(()=>Void)~)", + "proj.Service.sheet(onDismiss:(()=>Void)?,content:()=>Void)", + "proj.Service.combine(callbacks:(()=>Void)~,completion:()=>Void)", + "proj.Service.repeated(x:Int,x:Int)", + "proj.Service.repeated(x:String,x:String)", + "proj.Service.unlabeled(_:Int,_:Int)", + }; + const char *names[] = {"work", "work", "work", "work", "send", "configure", + "consume", "consume", "accept", "perform", "perform", "perform", + "perform", "collect", "named", "run", "batch", "sheet", + "combine", "repeated", "repeated", "unlabeled"}; + const uint8_t counts[] = {1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 2}; + for (int i = 0; i < 22; i++) { + cbm_registry_add(r, names[i], qns[i], "Method"); + uint64_t defaults = (i == 5 || i == 15) ? UINT64_C(1) << 1 + : i == 17 || i >= 19 ? UINT64_C(1) + : 0; + cbm_registry_set_swift_signature(r, qns[i], defaults, counts[i], NULL); + } + + const char *out[8] = {0}; + const char *generic_qns[] = {"proj.Service.constrainedVoid>(_:T)", + "proj.Service.constrained(_:T)async"}; + for (int i = 0; i < 2; i++) { + cbm_registry_add(r, "constrained", generic_qns[i], "Method"); + cbm_registry_set_swift_signature(r, generic_qns[i], 0, 1, NULL); + } + CBMCallArg generic_arg = {0}; + CBMCall generic_call = { + .callee_name = "Service.constrained", .args = &generic_arg, .arg_count = 1}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 2); + const char *nested = "proj.Service.nested,U==()=>Void>(_:T)=>Int"; + cbm_registry_add(r, "nested", nested, "Method"); + cbm_registry_set_swift_signature(r, nested, 0, 1, NULL); + generic_call.callee_name = "Service.nested"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], nested); + const char *returns[] = {"proj.Service.pick()=>Int", + "proj.Service.pick()async=>(A/Item)=>B/Item"}; + for (int i = 0; i < 2; i++) { + cbm_registry_add(r, "pick", returns[i], "Method"); + cbm_registry_set_swift_signature(r, returns[i], 0, 0, NULL); + } + CBMCall returned = {.callee_name = "Service.pick"}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &returned, "proj.Caller", NULL, 0, out, 8), 2); + const char *malformed[] = {"proj.Service.bad()=>", "proj.Service.bad()=>[Int", + "proj.Service.bad()=>Int)"}; + for (int i = 0; i < 3; i++) { + cbm_registry_add(r, "bad", malformed[i], "Method"); + cbm_registry_set_swift_signature(r, malformed[i], 0, 0, NULL); + } + returned.callee_name = "Service.bad"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &returned, "proj.Caller", NULL, 0, out, 8), -1); + const char *operators[] = {"<", ">"}; + for (int i = 0; i < 2; i++) { + char qn[128]; + snprintf(qn, sizeof(qn), "proj.Service.%s(_:A,_:A)=>Bool", operators[i]); + cbm_registry_add(r, operators[i], qn, "Method"); + cbm_registry_set_swift_signature(r, qn, 0, 2, NULL); + CBMCallArg operands[2] = {{0}}; + CBMCall operation = {.callee_name = operators[i], .args = operands, .arg_count = 2}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &operation, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qn); + } + CBMCallArg args[3] = {{.keyword = "name"}}; + CBMCall call = {.callee_name = "Service.work", .args = args, .arg_count = 1}; + int n = cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8); + ASSERT_EQ(n, 2); /* type is unknown: keep both compatible overloads */ + ASSERT_TRUE((strcmp(out[0], qns[1]) == 0 && strcmp(out[1], qns[2]) == 0) || + (strcmp(out[0], qns[2]) == 0 && strcmp(out[1], qns[1]) == 0)); + + args[0].keyword = "flag"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[0]); + + args[1].keyword = "unexpected"; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.arg_count = 0; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.arg_count = 1; + call.has_trailing_closure = true; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.has_trailing_closure = false; + + args[1].keyword = NULL; + args[0].keyword = "missing"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + + args[0].keyword = "a"; + args[1].keyword = "c"; + call.callee_name = "configure"; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[5]); + + /* The first default must remain skippable even when its label matches + * the argument needed by the following required parameter. */ + call.callee_name = "Service.repeated"; + call.arg_count = 1; + args[0].keyword = "x"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 2); + call.arg_count = 2; + args[1].keyword = "x"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 2); + call.arg_count = 0; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.arg_count = 1; + args[0].keyword = "wrong"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + call.callee_name = "Service.unlabeled"; + args[0].keyword = NULL; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[21]); + + args[0].keyword = "value"; + call.callee_name = "send"; + call.arg_count = 1; + call.has_trailing_closure = true; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[4]); + call.callee_name = "Service.consume"; + call.arg_count = 0; + call.has_trailing_closure = true; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[7]); + call.callee_name = "Service.accept"; + call.arg_count = 1; + call.has_trailing_closure = false; + args[0].keyword = "value"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[8]); + call.callee_name = "Service.perform"; + call.arg_count = 0; + call.has_trailing_closure = true; + n = cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8); + ASSERT_EQ(n, 3); /* wrappers transparent; tuple must not match */ + bool has_optional = false, has_grouped = false, has_generic_result = false; + for (int i = 0; i < n; i++) { + has_optional |= strcmp(out[i], qns[9]) == 0; + has_grouped |= strcmp(out[i], qns[10]) == 0; + has_generic_result |= strcmp(out[i], qns[12]) == 0; + } + ASSERT_TRUE(has_optional && has_grouped && has_generic_result); + + call.callee_name = "run"; + call.arg_count = 0; + call.has_trailing_closure = true; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[15]); /* trailing closure before defaulted parameter */ + call.callee_name = "batch"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[16]); /* variadic closure accepts trailing closure */ + call.arg_count = 1; + args[0].keyword = "completions"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[16]); /* explicit and trailing variadic closures */ + call.arg_count = 0; + + call.callee_name = "sheet"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[17]); /* skip default closure for required trailing closure */ + call.callee_name = "combine"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + ASSERT_STR_EQ(out[0], qns[18]); /* skip variadic closures for required trailing closure */ + call.has_trailing_closure = false; + args[1].keyword = NULL; + call.callee_name = "collect"; + call.arg_count = 0; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + args[0].keyword = NULL; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + call.arg_count = 3; + args[2].keyword = NULL; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + args[0].keyword = "name"; + call.callee_name = "named"; + call.arg_count = 1; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + args[1].keyword = NULL; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), 1); + args[0].keyword = "other"; + call.arg_count = 1; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + args[0].keyword = "name"; + args[1].keyword = "name"; + call.arg_count = 2; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 8), -1); + cbm_registry_free(r); + PASS(); +} + +TEST(swift_manifest_root_targets_respect_nested_package_boundaries) { + char *root = th_mktempdir("cbm_swift_root_targets"); + ASSERT_NOT_NULL(root); + ASSERT_EQ(th_write_file(TH_PATH(root, "Package.swift"), + "let package = Package(name: \"Product\", targets: [" + ".target(name: \"Core\", path: \".\")])\n"), + 0); + ASSERT_EQ(th_write_file(TH_PATH(root, "Nested/Package.swift"), + "let package = Package(name: \"Nested\", targets: [" + ".target(name: \"Core\", path: \".\")])\n"), + 0); + ASSERT_EQ(th_write_file(TH_PATH(root, "Unknown/Package.swift"), + "let package = Package(name: \"Unknown\", targets: [" + ".target(name: \"Computed\", path: computed)])\n"), + 0); + cbm_registry_t *r = cbm_registry_new(); + cbm_gbuf_t *graph = cbm_gbuf_new("proj", root); + ASSERT_NOT_NULL(r); + ASSERT_NOT_NULL(graph); + const char *qns[] = {"proj.Root.Type.Service", "proj.Root.Ext.Service", + "proj.Nested.Type.Service", "proj.Nested.Ext.Service", + "proj.Unknown.Ext.Service"}; + const char *files[] = {"Core/Types.swift", "Extra/Extensions.swift", "Nested/Core/Types.swift", + "Nested/Extra/Extensions.swift", "Unknown/Extra/Extensions.swift"}; + const char *methods[] = {"proj.Root.Ext.Service.run()", "proj.Nested.Ext.Service.run()", + "proj.Unknown.Ext.Service.run()"}; + for (int i = 0; i < 5; i++) { + cbm_registry_add(r, "Service", qns[i], "Class"); + ASSERT_GT(cbm_gbuf_upsert_node(graph, "Class", "Service", qns[i], files[i], 1, 3, + i == 0 || i == 2 ? "{\"swift_type_path\":\"Service\"}" + : "{\"swift_extension\":\"Service\"}"), + 0); + } + for (int i = 0; i < 3; i++) { + cbm_registry_add(r, "run", methods[i], "Method"); + cbm_registry_set_swift_signature(r, methods[i], 0, 0, "()"); + } + cbm_registry_build_swift_owners(r, graph, root, NULL, 0); + CBMCall call = {.callee_name = "Service.run"}; + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates_in_file(r, &call, NULL, graph, "proj.Root.Type", NULL, + 0, out, 4), + 1); + ASSERT_STR_EQ(out[0], methods[0]); + ASSERT_EQ(cbm_registry_swift_candidates_in_file(r, &call, NULL, graph, "proj.Nested.Type", NULL, + 0, out, 4), + 1); + ASSERT_STR_EQ(out[0], methods[1]); + ASSERT_EQ(th_write_file(TH_PATH(root, "Package.swift"), + "let package = Package(name: \"Product\", targets: [" + ".target(name: \"Core\", path: \".\")," + ".target(name: \"Other\", path: \".\")])\n"), + 0); + cbm_registry_build_swift_owners(r, graph, root, NULL, 0); + ASSERT_EQ(cbm_registry_swift_candidates_in_file(r, &call, NULL, graph, "proj.Root.Type", NULL, + 0, out, 4), + -1); + cbm_registry_free(r); + cbm_gbuf_free(graph); + ASSERT_EQ(th_rmtree(root), 0); + PASS(); +} + +TEST(swift_receiver_resolution_is_independent_of_registration_order) { + const char *alpha = "proj.alpha.Service.work(flag:Bool)"; + const char *beta = "proj.beta.Service.work(flag:Bool)"; + CBMCallArg arg = {.keyword = "flag"}; + CBMCall call = {.callee_name = "Service.work", .args = &arg, .arg_count = 1}; + for (int reverse = 0; reverse < 2; reverse++) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + cbm_registry_add(r, "Service", "proj.alpha.Service", "Class"); + cbm_registry_add(r, "Service", "proj.beta.Service", "Class"); + const char *first = reverse ? beta : alpha; + const char *second = reverse ? alpha : beta; + cbm_registry_add(r, "work", first, "Method"); + cbm_registry_add(r, "work", second, "Method"); + cbm_registry_set_swift_signature(r, first, 0, 1, NULL); + cbm_registry_set_swift_signature(r, second, 0, 1, NULL); + + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.alpha.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], alpha); + cbm_registry_free(r); + } + PASS(); +} + +/* Two trailing closures bind positionally: the first (unlabelled) to the + * first free closure parameter, later ones by their exact label (#2061). + * `handle { } onError: { }` can only be the two-closure overload; a wrong + * second label matches neither overload. */ +TEST(swift_multiple_trailing_closures_match_labels) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + cbm_registry_add(r, "Service", "proj.Service", "Class"); + const char *one_qn = "proj.Service.handle(completion:()=>Void)"; + const char *two_qn = "proj.Service.handle(completion:()=>Void,onError:()=>Void)"; + cbm_registry_add(r, "handle", one_qn, "Method"); + cbm_registry_add(r, "handle", two_qn, "Method"); + cbm_registry_set_swift_signature(r, one_qn, 0, 1, NULL); + cbm_registry_set_swift_signature(r, two_qn, 0, 2, NULL); + + const char *good[2] = {NULL, "onError"}; + CBMCall call = {.callee_name = "Service.handle", + .has_trailing_closure = true, + .trailing_closure_count = 2, + .trailing_closure_labels = good}; + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], two_qn); + + /* A second closure whose label matches no parameter matches nothing. */ + const char *bad[2] = {NULL, "unknown"}; + call.trailing_closure_labels = bad; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + + /* Two closures cannot fit the one-closure overload even with no labels. */ + const char *anon[2] = {NULL, NULL}; + call.trailing_closure_labels = anon; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + + call.trailing_closure_count = 1; + call.trailing_closure_truncated = true; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + call.trailing_closure_count = 2; + call.trailing_closure_truncated = false; + + /* A later label satisfies its required closure, so the first unlabelled + * closure can skip an earlier defaulted closure and bind the required one. */ + const char *delayed_qn = + "proj.Service.delayed(onDismiss:(()=>Void)?,content:()=>Void,onError:(()=>Void)?)"; + cbm_registry_add(r, "delayed", delayed_qn, "Method"); + cbm_registry_set_swift_signature(r, delayed_qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 2), 3, + NULL); + call.callee_name = "Service.delayed"; + call.trailing_closure_labels = good; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], delayed_qn); + + cbm_registry_free(r); + PASS(); +} + +/* Repeated external labels must stop the default-skip lookahead at the + * next trailing label, even if a still later closure has the same label. */ +TEST(swift_repeated_labels_skip_default_before_next_trailing_label) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + cbm_registry_add(r, "Service", "proj.Service", "Class"); + const char *qn = "proj.Service.repeated(a:()=>Void,b:()=>Void,a:()=>Void,b:()=>Void)"; + cbm_registry_add(r, "repeated", qn, "Method"); + cbm_registry_set_swift_signature(r, qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 3), 4, NULL); + const char *labels[3] = {NULL, "a", "b"}; + CBMCall call = {.callee_name = "Service.repeated", + .has_trailing_closure = true, + .trailing_closure_count = 3, + .trailing_closure_labels = labels}; + const char *out[4] = {0}; + /* Parameter 1 is required before the next label at parameter 2, so + * parameter 0 uses its default and the closures bind parameters 1-3. */ + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], qn); + cbm_registry_free(r); + PASS(); +} + +TEST(swift_repeated_labels_do_not_skip_default_past_next_trailing_label) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + cbm_registry_add(r, "Service", "proj.Service", "Class"); + const char *qn = "proj.Service.repeated(a:()=>Void,b:()=>Void,a:()=>Void,b:()=>Void)"; + cbm_registry_add(r, "repeated", qn, "Method"); + cbm_registry_set_swift_signature(r, qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 1), 4, NULL); + const char *labels[2] = {NULL, "b"}; + CBMCall call = {.callee_name = "Service.repeated", + .has_trailing_closure = true, + .trailing_closure_count = 2, + .trailing_closure_labels = labels}; + const char *out[4] = {0}; + /* The next label matches parameter 1 immediately. The first closure + * binds parameter 0, leaving required parameters 2 and 3 unsupplied. */ + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + cbm_registry_free(r); + PASS(); +} + +/* A variadic closure parameter already skipped by the parenthesized + * flag argument cannot consume an earlier trailing closure. */ +TEST(swift_variadic_trailing_closures_do_not_precede_parenthesized_argument) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + cbm_registry_add(r, "Service", "proj.Service", "Class"); + const char *qn = "proj.Service.f(callbacks:(()=>Void)~,flag:Bool,done:()=>Void)"; + cbm_registry_add(r, "f", qn, "Method"); + cbm_registry_set_swift_signature(r, qn, UINT64_C(1) << 1, 3, NULL); + CBMCallArg arg = {.keyword = "flag"}; + const char *labels[2] = {NULL, "done"}; + CBMCall call = {.callee_name = "Service.f", + .args = &arg, + .arg_count = 1, + .has_trailing_closure = true, + .trailing_closure_count = 2, + .trailing_closure_labels = labels}; + const char *out[4] = {0}; + /* f(flag: true) { } done: { } cannot fill callbacks after flag was + * supplied. Only the done parameter remains for two trailing closures. */ + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + cbm_registry_free(r); + PASS(); +} + +TEST(swift_overloaded_bucket_fails_closed) { + cbm_registry_t *r = cbm_registry_new(); + ASSERT_NOT_NULL(r); + cbm_registry_add(r, "Service", "proj.Service", "Class"); + const char *first_qn = "proj.Service.work(flag:Bool)"; + cbm_registry_add(r, "work", first_qn, "Method"); + cbm_registry_set_swift_signature(r, first_qn, 0, 1, NULL); + const char *other = "proj.Other.work(flag:Bool)"; + cbm_registry_add(r, "work", other, "Method"); + cbm_registry_set_swift_signature(r, other, 0, 1, NULL); + char qns[256][64]; + for (int i = 1; i < 256; i++) { + snprintf(qns[i], sizeof(qns[i]), "proj.Other%d.work(flag:Bool)", i); + cbm_registry_add(r, "work", qns[i], "Method"); + cbm_registry_set_swift_signature(r, qns[i], 0, 1, NULL); + } + CBMCall call = {.callee_name = "Service.work"}; + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), -1); + CBMCallArg arg = {.keyword = "flag"}; + call.args = &arg; + call.arg_count = 1; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], first_qn); + call.callee_name = "work"; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Service", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], first_qn); + cbm_registry_free(r); + PASS(); +} + /* Python package scope is an explicit resolver input, not a property inferred * from a QN shared by other languages. Keep these fixtures separate from the * raw public-API tests above. */ @@ -2552,7 +3011,7 @@ TEST(registry_resolve_lexical_walks_enclosing_defs_issue1548) { * index: discovery order; incremental: node-id order), and the lexical walk of * a caller in layout.py never enters the __init__ function. */ TEST(registry_python_local_bound_at_own_file_issue1548) { - const size_t init_prefix = strlen("proj.pkg"); /* pkg/__init__.py */ + const size_t init_prefix = strlen("proj.pkg"); /* pkg/__init__.py */ const size_t mod_prefix = strlen("proj.pkg.layout"); /* pkg/layout.py */ for (int order = 0; order < 2; order++) { cbm_registry_t *r = cbm_registry_new(); @@ -2593,8 +3052,8 @@ TEST(registry_python_local_bound_at_own_file_issue1548) { /* The prefix every def of a file carries, without allocating; the same as * cbm_pipeline_fqn_compute(project, path, name) minus ".name". */ TEST(fqn_symbol_prefix_len_matches_fqn_compute) { - static const char *const paths[] = {"pkg/layout.py", "pkg/__init__.py", "a.py", - "deep/x/y/mod.py", "web/index.py", "noext"}; + static const char *const paths[] = {"pkg/layout.py", "pkg/__init__.py", "a.py", + "deep/x/y/mod.py", "web/index.py", "noext"}; for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { char *qn = cbm_pipeline_fqn_compute("proj", paths[i], "sym"); ASSERT_NOT_NULL(qn); @@ -2651,6 +3110,14 @@ SUITE(registry) { RUN_TEST(resolve_import_map_bare_alias); RUN_TEST(resolve_import_map_aliased_from_import); RUN_TEST(resolve_import_map_alias_with_suffix_hits_method); + RUN_TEST(swift_receiver_resolution_is_independent_of_registration_order); + RUN_TEST(swift_manifest_root_targets_respect_nested_package_boundaries); + RUN_TEST(swift_overload_labels_defaults_and_trailing_closure); + RUN_TEST(swift_multiple_trailing_closures_match_labels); + RUN_TEST(swift_repeated_labels_skip_default_before_next_trailing_label); + RUN_TEST(swift_repeated_labels_do_not_skip_default_past_next_trailing_label); + RUN_TEST(swift_variadic_trailing_closures_do_not_precede_parenthesized_argument); + RUN_TEST(swift_overloaded_bucket_fails_closed); RUN_TEST(resolve_unique_name); RUN_TEST(resolve_unresolved); RUN_TEST(resolve_many_nodes);