diff --git a/internal/cbm/callable_sig.c b/internal/cbm/callable_sig.c index 9d32f391a7..f63965c119 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; } @@ -79,7 +89,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; } @@ -737,6 +749,10 @@ static void sig_swift_param(sig_ctx_t *c, TSNode p) { 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); @@ -770,6 +786,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 +1014,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 +1122,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 +1152,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 +1194,22 @@ 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}; + sig_raw_str(&c, "("); + sig_swift_params(&c, node); + sig_raw_str(&c, ")"); + 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 +1240,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 +1251,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 +1324,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 +1457,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..9b6dd3a30c 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 spelling: matching 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.h b/internal/cbm/cbm.h index ade41fea57..eea1b92563 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 (NULL for nested types, * the global namespace and every other language). Per type, because the * file-level namespace_name records only a file's first namespace. */ @@ -325,6 +329,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 @@ -381,6 +388,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 @@ -539,8 +560,8 @@ 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 } 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 4eb02659c4..1832f5d515 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -2486,6 +2486,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 @@ -2585,6 +2586,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++; @@ -3897,6 +3918,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++) { @@ -4278,6 +4372,70 @@ static TSNode language_specific_callee_leaf(CBMLanguage language, TSNode expr, return terminal_callee_leaf(ctx, expr); } +/* `Base.init` for a `super.init(...)` call: Base is the first entry of the + * enclosing class's inheritance clause, where Swift requires the superclass to + * stand. The class node is carried by the walk, so no ancestor climb. NULL (no + * call) outside a class, without a clause, or when the entry does not read as + * a plain type name -- a lower-case or attributed root would pass the + * registry's receiver-chain check and bind by short name, which is the false + * edge this exists to prevent. */ +static const char *swift_superclass_init_callee(CBMExtractCtx *ctx, const WalkState *state) { + TSNode cls = state ? state->enclosing_class_node : (TSNode){0}; + if (ts_node_is_null(cls) || strcmp(ts_node_type(cls), "class_declaration") != 0) { + return NULL; + } + TSNode spec = cbm_find_child_by_kind(cls, "inheritance_specifier"); + char *base = ts_node_is_null(spec) ? NULL : cbm_node_text(ctx->arena, spec, ctx->source); + if (!base || base[0] < 'A' || base[0] > 'Z') { + return NULL; + } + size_t len = strcspn(base, "<"); /* Base names Base */ + for (size_t i = 0; i < len; i++) { + if (!isalnum((unsigned char)base[i]) && base[i] != '_' && base[i] != '.') { + return NULL; + } + } + base[len] = '\0'; + return cbm_arena_sprintf(ctx->arena, "%s.init", base); +} + +/* True for a Swift initializer callee `T.init` (T non-empty). */ +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) - SKIP_ONE && + strcmp(callee + n - (sizeof(suffix) - SKIP_ONE), suffix) == 0; +} + +/* Swift initializer delegation (#2379). `super.init(...)` and `self.init(...)` + * name no type, and a lower-case receiver passes the registry's receiver-chain + * check, so a short-name match binds them to whichever project `init` is + * nearest: a nested type's, a sibling type's, or the caller's own. On + * Alamofire all 8 such edges were false. + * + * `super.init` is rewritten to name the superclass (`Base.init`), so the + * qualified strategies place it on the project's Base.init. When Base is not + * in the project (UIViewController, NSObject) nothing places it, and the + * 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. + * 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) { + return callee; + } + if (strcmp(callee, "self.init") == 0) { + return NULL; + } + if (strcmp(callee, "super.init") == 0) { + return swift_superclass_init_callee(ctx, state); + } + return callee; +} + // Produce the textual callee and the AST occurrence from one operation. The // generic fallback retains the former field/terminal behavior, while grammars // whose extractor uses a sibling, a special field, or a synthetic literal are @@ -4288,6 +4446,7 @@ static CBMPrimaryCalleeSelection select_primary_callee(CBMExtractCtx *ctx, TSNod CBMPrimaryCalleeSelection selection = {0}; selection.name = extract_callee_name(ctx->arena, node, ctx->source, ctx->language); selection.name = resolve_objectscript_callee(ctx, node, state, selection.name); + selection.name = resolve_swift_delegating_init(ctx, state, selection.name); if (!selection.name || !selection.name[0]) { return selection; } @@ -4511,6 +4670,16 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML call.is_method = true; } } + // Swift initializer guard (#2379; same direction). Flag `T.init(...)`, + // as written or as a rewritten super.init. Nearly every type has an + // `init`, so a short-name match onto one is a guess: with a project + // module imported, `UIViewController.init` bound an unrelated + // project initializer. The registry's qualified / same-module / + // import strategies still place T.init when T is a project type. + // No other Swift call carries the flag. + if (ctx->language == CBM_LANG_SWIFT && swift_callee_is_initializer(call.callee_name)) { + call.is_method = true; + } TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); /* tree-sitter-elixir attaches NO field name to a call's arguments @@ -4537,6 +4706,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 a2c3676771..625b44b014 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" @@ -799,6 +800,15 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) { return cbm_find_child_by_kind(node, "query_name"); } + /* Swift init/deinit (#2379) have no name node at all. The declaration + * itself stands in for it, and cbm_func_name_node_text maps that + * node kind to "init" / "deinit". Gated on Swift: Slang's grammar + * also has an init_declaration node. */ + if (lang == CBM_LANG_SWIFT && + (strcmp(kind, "init_declaration") == 0 || strcmp(kind, "deinit_declaration") == 0)) { + return node; + } + TSNode name = func_name_node(node); if (lang == CBM_LANG_R && strcmp(kind, "function_definition") == 0) { @@ -7122,6 +7132,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; @@ -8699,6 +8724,12 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) { return cbm_find_child_by_kind(child, "simple_identifier"); } + // Swift init/deinit: named in one place, cbm_resolve_func_name (#2379). + if (lang == CBM_LANG_SWIFT && + (strcmp(ck, "init_declaration") == 0 || strcmp(ck, "deinit_declaration") == 0)) { + return cbm_resolve_func_name(child, lang); + } + // Squirrel: function_declaration's name is a plain `identifier` child. if (lang == CBM_LANG_SQUIRREL && strcmp(ck, "function_declaration") == 0) { return cbm_find_child_by_kind(child, "identifier"); @@ -8744,6 +8775,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_type_assigns.c b/internal/cbm/extract_type_assigns.c index 7ac30ee1d9..502afaed5d 100644 --- a/internal/cbm/extract_type_assigns.c +++ b/internal/cbm/extract_type_assigns.c @@ -204,9 +204,65 @@ 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; + } + } +} + // --- 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}; + if (parameter) { + swift_parameter_binding(node, &name, &type); + } else { + 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) { + TSNode value = ts_node_child_by_field_name(node, TS_FIELD("value")); + 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 = "[?]"; + } + } + } + if (!ts_node_is_null(name) && type_name) { + const char *scope = state->enclosing_func_qn; + if (scope == ctx->module_qn && state->enclosing_class_qn) { + scope = state->enclosing_class_qn; + } + CBMTypeAssign assignment = {cbm_node_text(ctx->arena, name, ctx->source), type_name, scope}; + 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 fbb7e12c3b..a5ef8eb5b7 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_* @@ -324,6 +325,7 @@ static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const cha * CBMWalkScope comment for the quadratic recompute this replaces. */ f->prev_enclosing_func_qn = state->enclosing_func_qn; f->prev_enclosing_class_qn = state->enclosing_class_qn; + f->prev_enclosing_class_node = state->enclosing_class_node; f->prev_invocation_kind = state->invocation_kind; f->prev_callee_expr = state->callee_expr; f->prev_callee_leaf = state->callee_leaf; @@ -337,6 +339,8 @@ static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const cha case SCOPE_CLASS: case SCOPE_NAMESPACE: state->enclosing_class_qn = qn; + /* push_lexical_scope records the node; a node-less push has none. */ + state->enclosing_class_node = (TSNode){0}; break; case SCOPE_CALL: /* The invocation triple is applied by push_call_scope once the caller @@ -364,6 +368,9 @@ static bool push_lexical_scope(WalkState *state, uint8_t walk_kind, uint32_t dep state->lexical_binding_tracking_failed = true; return false; } + if (walk_kind == SCOPE_CLASS || walk_kind == SCOPE_NAMESPACE) { + state->enclosing_class_node = node; + } state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id = add_lexical_scope(state, node, lexical_kind); return true; @@ -442,6 +449,7 @@ static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) { const CBMWalkScope *f = &state->scopes[--state->scope_top]; state->enclosing_func_qn = f->prev_enclosing_func_qn; state->enclosing_class_qn = f->prev_enclosing_class_qn; + state->enclosing_class_node = f->prev_enclosing_class_node; state->invocation_kind = f->prev_invocation_kind; state->callee_expr = f->prev_callee_expr; state->callee_leaf = f->prev_callee_leaf; @@ -1138,6 +1146,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]) { @@ -2671,6 +2686,7 @@ void cbm_extract_unified(CBMExtractCtx *ctx) { * recompute_state call): module scope, nothing else active. */ state.enclosing_func_qn = ctx->module_qn; state.enclosing_class_qn = NULL; + state.enclosing_class_node = (TSNode){0}; state.invocation_kind = CBM_INVOCATION_NONE; state.callee_expr = (TSNode){0}; state.callee_leaf = (TSNode){0}; diff --git a/internal/cbm/extract_unified.h b/internal/cbm/extract_unified.h index f80237229d..8e40d6a2cf 100644 --- a/internal/cbm/extract_unified.h +++ b/internal/cbm/extract_unified.h @@ -78,6 +78,7 @@ typedef struct { * suite-budget kills on every non-M4 venue). */ const char *prev_enclosing_func_qn; const char *prev_enclosing_class_qn; + TSNode prev_enclosing_class_node; CBMInvocationKind prev_invocation_kind; TSNode prev_callee_expr; TSNode prev_callee_leaf; @@ -133,6 +134,7 @@ typedef struct { typedef struct { const char *enclosing_func_qn; // current function QN (module_qn at top level) const char *enclosing_class_qn; // current class QN (NULL outside class) + TSNode enclosing_class_node; // that class scope's node (null when not known) const TSTreeCursor *current_cursor; // unified walk cursor at the current node TSTreeCursor *occurrence_cursor; // reusable parent-preserving classifier cursor CBMInvocationKind invocation_kind; // exact active invocation/reference role diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index 9e310bc6c9..871b49dc98 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -301,6 +301,16 @@ bool cbm_is_keyword(const char *name, CBMLanguage lang) { case CBM_LANG_PUPPET: keywords = puppet_keywords; break; + case CBM_LANG_SWIFT: + /* `init` and `deinit` are Swift keywords. A reference to one + * (`.init(...)`, `Foo.init`) names an initializer of a type the bare + * name does not carry, so resolving it by name can only guess among + * every type's initializer (#2379). Otherwise the generic set. */ + if (strcmp(name, "init") == 0 || strcmp(name, "deinit") == 0) { + return true; + } + keywords = generic_keywords; + break; default: keywords = generic_keywords; break; @@ -1187,6 +1197,16 @@ bool cbm_is_c_preprocessor_lang(CBMLanguage lang) { // const", and a member lookup for "operator bool" (the implicit call in // `if (obj)`) misses. char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source, CBMLanguage lang) { + if (lang == CBM_LANG_SWIFT) { + const char *kind = ts_node_type(name_node); + if (strcmp(kind, "init_declaration") == 0) { + return cbm_arena_strdup(a, "init"); + } + if (strcmp(kind, "deinit_declaration") == 0) { + return cbm_arena_strdup(a, "deinit"); + } + } + char *text = cbm_node_text(a, name_node, source); /* A C keyword is never a function name: the definition is an error-recovery * artifact. No name means no def and no call scope (calls inside fall back diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 11e8a6beca..a5b8f14dae 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -570,8 +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", 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 76606ac510..13f7335775 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); @@ -302,6 +303,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 7d428b8e31..93a42f3b56 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 @@ -1128,25 +1131,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) @@ -1155,6 +1151,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 f06c85571a..e41c6836dc 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/scripts/memory-core-baseline.txt b/scripts/memory-core-baseline.txt index d3c3ef8bd2..9233aa14ee 100644 --- a/scripts/memory-core-baseline.txt +++ b/scripts/memory-core-baseline.txt @@ -77,7 +77,7 @@ src/pipeline/path_alias.c 40 src/pipeline/pipeline.c 132 src/pipeline/pipeline_delta.c 19 src/pipeline/pipeline_incremental.c 140 -src/pipeline/registry.c 17 +src/pipeline/registry.c 15 src/pipeline/worker_pool.c 2 src/store/store.c 354 src/ui/config.c 12 diff --git a/src/pipeline/lsp_surface.c b/src/pipeline/lsp_surface.c index 308fcf9590..cfca686536 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 @@ -414,6 +416,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 61a72f1415..28b8ece3cd 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -12,6 +12,8 @@ #include "foundation/constants.h" enum { PC_RING = 4, PC_RING_MASK = 3, PC_SIG_SCAN = 15, PC_REGEX_GRP = 2 }; +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 @@ -814,7 +816,8 @@ static void 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 && call->first_string_arg && call->first_string_arg[0] == '/') { handle_route_registration(ctx, call, source, module_qn, imp_keys, imp_vals, imp_count, route_mount, lang); @@ -980,15 +983,46 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM } } - /* #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}; + 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 = {0}; - if (!rust_external && !php_vendor_call) { + if (swift_candidates >= 0 && !rust_external && !php_vendor_call) { /* Python LEGB fallback first (MUST match pass_parallel.c). */ res = cbm_registry_resolve_call_scoped(ctx->registry, call->callee_name, module_qn, imp_keys, @@ -1133,7 +1167,11 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM * to a docs bundle (2026-09-16 probe: 4,207 junk * edges on JetBrains/Exposed). */ lang == CBM_LANG_HTML || lang == CBM_LANG_VUE || - lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO; + lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO || + /* Swift flags only initializer calls `T.init(...)` + * (#2379, extract_calls.c); every other Swift call + * keeps is_method=false and is unaffected. */ + lang == CBM_LANG_SWIFT; /* Bare-call local-binding suppression. A member call has a receiver the * guard above can reason about; a bare `run()` has none, so that guard * cannot see this class at all. Python-only today because the extraction diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 549b20a3a4..284fd0fcdc 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; } @@ -513,6 +522,11 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) { 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); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 988da30600..2b33abc049 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; } @@ -1815,6 +1826,11 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d if (cbm_label_is_registry_symbol(def->label)) { 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]; @@ -2794,7 +2810,8 @@ static void 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 @@ -3317,8 +3334,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, @@ -3405,7 +3430,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB lang == CBM_LANG_ARKTS || /* embedded-script hosts — see pass_calls.c */ lang == CBM_LANG_HTML || lang == CBM_LANG_VUE || - lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO; + lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO || + /* Swift initializer calls only — see pass_calls.c */ + lang == CBM_LANG_SWIFT; /* Bare-call local-binding suppression — see the note in pass_calls.c. * This gate MUST stay identical to the one there. */ bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON; @@ -3483,6 +3510,36 @@ 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}; + 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/pipeline.c b/src/pipeline/pipeline.c index 698d9dc7a6..2728354471 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -2134,7 +2134,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 306ff62266..87e7a5d54b 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) */ @@ -258,6 +259,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 895cded935..76b8d949ae 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -623,12 +623,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 @@ -642,7 +643,14 @@ 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'))" + " 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 bc2eb71ddb..edc8bfe727 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1365,6 +1365,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 diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 3dd11d5da0..a67dee54b2 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -30,12 +30,15 @@ enum { REG_MAX_CANDIDATES = 256 }; #include "foundation/compat.h" /* CBM_TLS */ #include "foundation/hash_table.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 +97,11 @@ typedef struct { uint8_t *lang; } qn_array_t; +typedef struct { + uint64_t defaults; + uint8_t count; + char selector[]; +} swift_signature_t; _Static_assert(CBM_LANG_COUNT <= UINT8_MAX, "CBMLanguage must fit the per-entry lang byte"); struct cbm_registry { @@ -107,6 +115,8 @@ 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 */ }; /* ── Helpers ─────────────────────────────────────────────────────── */ @@ -1390,6 +1400,20 @@ 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); +} + void cbm_registry_free(cbm_registry_t *r) { if (!r) { return; @@ -1397,6 +1421,14 @@ 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); + } cbm_ht_foreach(r->exact, free_label, NULL); cbm_ht_free(r->exact); for (int i = 0; i < r->label_pool_n; i++) { @@ -1618,6 +1650,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); +} + /* ── Lookup ──────────────────────────────────────────────────────── */ bool cbm_registry_exists(const cbm_registry_t *r, const char *qn) { @@ -2027,10 +2122,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; } @@ -2329,6 +2424,728 @@ 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_candidates(const cbm_registry_t *r, const CBMCall *call, const char *type_qn, + const char *name, const char **out, int out_cap) { + 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 0; + } + int found = 0; + 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; +} + +enum { SWIFT_MAX_ANCESTORS = 64 }; + +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) { + const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(graph, owner); + if (!node || !node->properties_json) { + return; + } + yyjson_doc *doc = yyjson_read(node->properties_json, strlen(node->properties_json), 0); + 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; + } + cbm_resolution_t parent = cbm_registry_resolve(r, name, owner, NULL, NULL, 0); + if (parent.qualified_name && !swift_type_seen(types, *count, parent.qualified_name)) { + types[(*count)++] = parent.qualified_name; + } + } + yyjson_doc_free(doc); +} + +static int swift_typed_candidates(const cbm_registry_t *r, const cbm_gbuf_t *graph, + 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++) { + int found = swift_owner_candidates(r, call, types[i], name, out, out_cap); + if (found != 0) { + return found; + } + if (!graph || strcmp(name, "init") == 0 || strcmp(name, "deinit") == 0) { + break; + } + swift_parent_types(r, graph, types[i], types, &count); + } + return SWIFT_INCOMPATIBLE; +} + +enum { SWIFT_NO_TYPE_RECEIVER = -2 }; + +static int swift_named_candidates(const cbm_registry_t *r, const cbm_gbuf_t *graph, + 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'; + cbm_resolution_t receiver = + cbm_registry_resolve(r, type, module_qn, NULL, import_vals, import_count); + if (receiver.qualified_name) { + return swift_typed_candidates(r, graph, call, receiver.qualified_name, + 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, 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 }; + +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; +} + +/* Conflicting block-local bindings stay unknown rather than selecting a + * same-name method on either type. */ +static const char *swift_binding_type(const CBMFileResult *file, const char *name, + const char *scope, const char *class_qn, + const char *module_qn, bool field) { + const char *type = NULL; + int best = 0; + for (int i = 0; i < file->type_assigns.count; i++) { + const CBMTypeAssign *a = &file->type_assigns.items[i]; + if (!a->var_name || !a->enclosing_func_qn || strcmp(a->var_name, name) != 0) { + continue; + } + int rank = swift_binding_rank(a->enclosing_func_qn, scope, class_qn, module_qn, field); + if (rank > best) { + best = rank; + type = a->type_name; + } else if (rank && rank == best && type && a->type_name && + strcmp(type, a->type_name) != 0) { + type = ""; + } + } + return type; +} + +static const char *swift_file_receiver(const CBMFileResult *file, const CBMCall *call, + const char *module_qn, char *expression, char *class_qn, + char **member) { + if (call->enclosing_func_qn && strlen(call->enclosing_func_qn) < CBM_SZ_4K) { + 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'; + } + } + 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(file, *member, call->enclosing_func_qn, class_qn, module_qn, true); + *member = dot + SKIP_ONE; + return type; + } + const char *type = + swift_binding_type(file, 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) { + return p + SKIP_ONE; + } + } + 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(const cbm_registry_t *r, const cbm_gbuf_t *graph, + const CBMCall *inner, const char *receiver, + const char *member, const char **out, int out_cap) { + if (!inner) { + return NULL; + } + int count = swift_typed_candidates(r, graph, inner, receiver, member, out, out_cap); + const char *type = NULL; + for (int i = 0; i < count; i++) { + size_t offset = cbm_callable_return_offset(out[i]); + const char *returned = out[i][offset] ? out[i] + offset + PAIR_LEN : NULL; + if (!returned || (type && strcmp(type, returned) != 0)) { + return NULL; + } + type = returned; + } + return type; +} + +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) { + if (!r || !call || !call->callee_name || !out || out_cap < SKIP_ONE) { + return 0; + } + if (!file || !strchr(call->callee_name, '.') || + (isupper((unsigned char)call->callee_name[0]) && !strchr(call->callee_name, '('))) { + int named = swift_named_candidates(r, graph, 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(file, 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); + } + while (type && type[0]) { + char type_text[CBM_SZ_4K]; + size_t n = strcspn(type, "= sizeof(type_text)) { + return SWIFT_INCOMPATIBLE; + } + memcpy(type_text, type, n); + type_text[n] = '\0'; + cbm_resolution_t receiver = + cbm_registry_resolve(r, type_text, module_qn, NULL, import_vals, import_count); + if (!receiver.qualified_name) { + return SWIFT_INCOMPATIBLE; /* known non-project receiver */ + } + char *args = strchr(member, '('); + if (!args) { + return swift_typed_candidates(r, graph, call, receiver.qualified_name, 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(r, graph, inner, receiver.qualified_name, member, out, out_cap); + member = after + SKIP_ONE; + } + 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) { @@ -2714,14 +3531,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 0f291daf9c..54fc301fff 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..abd4f06e98 --- /dev/null +++ b/tests/fixtures/complexity/Swift/Calls.swift @@ -0,0 +1,13 @@ +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") +} 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_extraction.c b/tests/test_extraction.c index aadde696d6..9727c2c260 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -79,6 +79,28 @@ static int has_def_qn(CBMFileResult *r, const char *qn) { return 0; } +/* Find a definition by its qualified name relative to the file's module QN + * (e.g. "Box.init" resolves to ".Box.init"). */ +static const CBMDefinition *find_def_in_module(CBMFileResult *r, const char *rel_qn) { + char qn[512]; + snprintf(qn, sizeof(qn), "%s.%s", r->module_qn, rel_qn); + for (int i = 0; i < r->defs.count; i++) { + if (r->defs.items[i].qualified_name && strcmp(r->defs.items[i].qualified_name, qn) == 0) + return &r->defs.items[i]; + } + return NULL; +} + +/* Find an in-body call by its raw callee text; returns the call or NULL. */ +static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) { + for (int i = 0; i < r->calls.count; i++) { + if (r->calls.items[i].callee_name && strcmp(r->calls.items[i].callee_name, callee) == 0) { + return &r->calls.items[i]; + } + } + return NULL; +} + static int count_defs_with_label(CBMFileResult *r, const char *label) { int count = 0; for (int i = 0; i < r->defs.count; i++) { @@ -2568,6 +2590,46 @@ TEST(swift_method_call) { 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"); @@ -2606,6 +2668,366 @@ TEST(swift_force_unwrap_scanner_shift) { PASS(); } +/* #2379: a struct's plain `init` is a callable, not a name-less hole -- a call + * inside it must source from the initializer, not fall back to the file's + * Module scope. */ +TEST(swift_init_struct_issue2379) { + CBMFileResult *r = extract("func helper() -> Int { 1 }\n" + "struct Box {\n" + " let value: Int\n" + " init() {\n" + " value = helper()\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Box.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + 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"); + + const CBMCall *call = find_call_by_callee(r, "helper"); + ASSERT_NOT_NULL(call); + ASSERT_NOT_NULL(call->enclosing_func_qn); + ASSERT_STR_EQ(call->enclosing_func_qn, init_def->qualified_name); + ASSERT_STR_NEQ(call->enclosing_func_qn, r->module_qn); + + cbm_free_result(r); + PASS(); +} + +/* #2379: a class's designated init takes parameters -- coverage that a + * parameterized init is extracted the same way as a bare one. */ +TEST(swift_init_class_designated_issue2379) { + CBMFileResult *r = extract("func helper() -> Int { 1 }\n" + "final class Holder {\n" + " let value: Int\n" + " init(seed: Int) {\n" + " value = helper() + seed\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Holder.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + 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"); + + const CBMCall *call = find_call_by_callee(r, "helper"); + ASSERT_NOT_NULL(call); + ASSERT_NOT_NULL(call->enclosing_func_qn); + ASSERT_STR_EQ(call->enclosing_func_qn, init_def->qualified_name); + ASSERT_STR_NEQ(call->enclosing_func_qn, r->module_qn); + + cbm_free_result(r); + PASS(); +} + +/* #2379: a convenience init delegates via `self.init(...)`. Its call to + * `other()` (an argument of the delegating call) must source from the + * initializer, not from the file. */ +TEST(swift_init_convenience_delegates_issue2379) { + CBMFileResult *r = extract("func other() -> Int { 2 }\n" + "final class Holder {\n" + " let value: Int\n" + " init(seed: Int) {\n" + " value = seed\n" + " }\n" + " convenience init() {\n" + " self.init(seed: other())\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Holder.swift"); + 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); + + /* 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")); + + const CBMCall *call = find_call_by_callee(r, "other"); + ASSERT_NOT_NULL(call); + ASSERT_NOT_NULL(call->enclosing_func_qn); + ASSERT_STR_EQ(call->enclosing_func_qn, init_def->qualified_name); + ASSERT_STR_NEQ(call->enclosing_func_qn, r->module_qn); + + cbm_free_result(r); + PASS(); +} + +/* 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" + " init(seed: Int) {\n" + " value = shared()\n" + " }\n" + " convenience init() {\n" + " self.init(seed: shared())\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Holder.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + 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_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; + } + 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); + } + ASSERT_EQ(from_seeded, 1); + ASSERT_EQ(from_empty, 1); + + cbm_free_result(r); + PASS(); +} + +/* #2379: a failable `init?` in an enum is still a callable initializer. */ +TEST(swift_init_failable_enum_issue2379) { + CBMFileResult *r = extract("func helper() -> Int { 1 }\n" + "enum Mode {\n" + " case on\n" + " init?(flag: Bool) {\n" + " if helper() > 0 { self = .on } else { return nil }\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Mode.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + 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"); + ASSERT_NOT_NULL(call); + ASSERT_NOT_NULL(call->enclosing_func_qn); + ASSERT_STR_EQ(call->enclosing_func_qn, init_def->qualified_name); + ASSERT_STR_NEQ(call->enclosing_func_qn, r->module_qn); + + cbm_free_result(r); + PASS(); +} + +/* #2379: `extension X { }` is modeled by the vendored grammar as a + * class_declaration named X, so an init declared in an extension goes through + * the same class-method path as one declared in the primary type. */ +TEST(swift_init_extension_issue2379) { + CBMFileResult *r = extract("func other() -> Int { 2 }\n" + "extension Box {\n" + " init(raw: Int) {\n" + " value = other()\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Box+Raw.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + 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"); + ASSERT_NOT_NULL(call); + ASSERT_NOT_NULL(call->enclosing_func_qn); + ASSERT_STR_EQ(call->enclosing_func_qn, init_def->qualified_name); + ASSERT_STR_NEQ(call->enclosing_func_qn, r->module_qn); + + cbm_free_result(r); + PASS(); +} + +/* #2379: regression guard -- an ordinary method in the same type keeps its own + * attribution unchanged (this already worked pre-fix; the initializer fix + * must not disturb it). */ +TEST(swift_method_still_attributed_issue2379) { + CBMFileResult *r = extract("func other() -> Int { 2 }\n" + "struct Box {\n" + " init() {}\n" + " func read() -> Int {\n" + " other()\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Box.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + 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"); + ASSERT_NOT_NULL(call); + ASSERT_NOT_NULL(call->enclosing_func_qn); + ASSERT_STR_EQ(call->enclosing_func_qn, read_def->qualified_name); + + cbm_free_result(r); + PASS(); +} + +/* #2379: a `deinit` has the same gap as `init` -- no source-level name, so a + * call inside its body attributed to the file's Module scope instead of the + * type. */ +TEST(swift_deinit_issue2379) { + CBMFileResult *r = extract("func helper() -> Int { 1 }\n" + "final class Deinitable {\n" + " deinit {\n" + " _ = helper()\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Deinitable.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + const CBMDefinition *deinit_def = find_def_in_module(r, "Deinitable.deinit()"); + ASSERT_NOT_NULL(deinit_def); + ASSERT_STR_EQ(deinit_def->name, "deinit"); + + const CBMCall *call = find_call_by_callee(r, "helper"); + ASSERT_NOT_NULL(call); + ASSERT_NOT_NULL(call->enclosing_func_qn); + ASSERT_STR_EQ(call->enclosing_func_qn, deinit_def->qualified_name); + ASSERT_STR_NEQ(call->enclosing_func_qn, r->module_qn); + + cbm_free_result(r); + PASS(); +} + +/* #2379: `super.init(...)` names no type. Left as written, the registry binds it + * by short name to whichever project `init` is nearest (on Alamofire: a nested + * `Options.init`, a sibling decoder's). It is emitted as the superclass's + * initializer instead -- the first inheritance entry, generic arguments + * dropped -- and stays attributed to the calling initializer. */ +TEST(swift_super_init_names_superclass_issue2379) { + CBMFileResult *r = extract("final class Sub: Base, Sendable {\n" + " override init() {\n" + " super.init()\n" + " }\n" + "}\n", + CBM_LANG_SWIFT, "t", "Sub.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + 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"); + ASSERT_NOT_NULL(call); + ASSERT_NOT_NULL(call->enclosing_func_qn); + ASSERT_STR_EQ(call->enclosing_func_qn, init_def->qualified_name); + + cbm_free_result(r); + PASS(); +} + +/* #2379: without a superclass to name -- no inheritance clause, or a first entry + * that does not read as a type -- `super.init` forms no call at all. A + * lower-case root would pass the registry's receiver-chain check and bind by + * short name again. */ +TEST(swift_super_init_without_superclass_no_call_issue2379) { + CBMFileResult *r = extract("class Root {\n" + " init() {\n" + " super.init()\n" + " }\n" + "}\n" + "class Odd: base {\n" + " init() {\n" + " super.init()\n" + " }\n" + "}\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()")); + for (int i = 0; i < r->calls.count; i++) { + const char *callee = r->calls.items[i].callee_name; + ASSERT(!callee || !strstr(callee, "init")); + } + + cbm_free_result(r); + PASS(); +} + +/* #2379: an initializer call `T.init(...)` is flagged so the resolvers' + * weak-member guard keeps a short-name guess from placing it (nearly every + * type has an `init`); every other Swift call stays unflagged and resolves + * exactly as before. */ +TEST(swift_initializer_calls_flagged_issue2379) { + CBMFileResult *r = extract("func run(obj: Worker) {\n" + " _ = UIAlertAction.init(title: nil)\n" + " helper()\n" + " obj.start()\n" + "}\n", + CBM_LANG_SWIFT, "t", "Run.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + const CBMCall *init_call = find_call_by_callee(r, "UIAlertAction.init"); + const CBMCall *bare = find_call_by_callee(r, "helper"); + const CBMCall *member = find_call_by_callee(r, "obj.start"); + ASSERT_NOT_NULL(init_call); + ASSERT_NOT_NULL(bare); + ASSERT_NOT_NULL(member); + ASSERT_TRUE(init_call->is_method); + ASSERT_FALSE(bare->is_method); + ASSERT_FALSE(member->is_method); + + cbm_free_result(r); + PASS(); +} + +/* #2379: `init` is a Swift keyword. A reference to one (`.init(...)`, + * `Box.init`) names the initializer of a type the bare name does not carry, + * so it is not recorded as a usage -- by name it could only be guessed among + * every type's initializer (on Alamofire, 26 references landed on one nested + * `Output.init`). The type in `Box.init` is still a usage. */ +TEST(swift_init_reference_is_not_a_usage_issue2379) { + CBMFileResult *r = extract("struct Box {\n" + " init(raw: Int) {}\n" + "}\n" + "func make() -> Box {\n" + " .init(raw: 1)\n" + "}\n" + "func maker() -> (Int) -> Box {\n" + " Box.init\n" + "}\n", + CBM_LANG_SWIFT, "t", "Make.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + int init_usages = 0; + int box_usages = 0; + for (int i = 0; i < r->usages.count; i++) { + const char *ref = r->usages.items[i].ref_name; + init_usages += ref && strcmp(ref, "init") == 0; + box_usages += ref && strcmp(ref, "Box") == 0; + } + ASSERT_EQ(init_usages, 0); + ASSERT_GT(box_usages, 0); + + cbm_free_result(r); + PASS(); +} + /* --- Objective-C --- */ TEST(objc_interface) { CBMFileResult *r = @@ -5843,16 +6265,6 @@ TEST(extract_ts_decorators_survive_interleaved_comment) { PASS(); } -/* Find an in-body call by its raw callee text; returns the call or NULL. */ -static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) { - for (int i = 0; i < r->calls.count; i++) { - if (r->calls.items[i].callee_name && strcmp(r->calls.items[i].callee_name, callee) == 0) { - return &r->calls.items[i]; - } - } - return NULL; -} - /* #1892: the Swift grammar declares no "arguments" field, so the generic field * lookup read nothing and every Swift call lost its arguments. Without the URL * the service-pattern table cannot raise an HTTP_CALLS edge or a Route node, @@ -8346,22 +8758,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++) { @@ -11963,6 +12376,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 @@ -13342,9 +13846,23 @@ SUITE(extraction) { RUN_TEST(swift_struct); RUN_TEST(swift_simple_call); RUN_TEST(swift_method_call); + 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_keep_separate_nodes_issue2061); + RUN_TEST(swift_init_failable_enum_issue2379); + RUN_TEST(swift_init_extension_issue2379); + RUN_TEST(swift_method_still_attributed_issue2379); + RUN_TEST(swift_deinit_issue2379); + RUN_TEST(swift_super_init_names_superclass_issue2379); + RUN_TEST(swift_super_init_without_superclass_no_call_issue2379); + RUN_TEST(swift_initializer_calls_flagged_issue2379); + RUN_TEST(swift_init_reference_is_not_a_usage_issue2379); RUN_TEST(swift_call_string_arg_issue1892); RUN_TEST(swift_nested_url_constructor_issue1892); RUN_TEST(swift_nested_url_no_bang_issue1892); 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 3b31f62e1f..2d819b73df 100644 --- a/tests/test_infrascan.c +++ b/tests/test_infrascan.c @@ -176,6 +176,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(); } @@ -737,10 +744,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); @@ -766,7 +773,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 377c8c609d..c858bfda1a 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -3217,6 +3217,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 @@ -25193,6 +25262,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 6a526bd0a1..f980e05c80 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -1693,7 +1693,8 @@ TEST(pipeline_incremental_drops_terraform_reads_of_a_moved_module) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } - write_temp_file(g_tmpdir, "infra/main.tf", "module \"net\" {\n source = \"./modules/net\"\n}\n"); + write_temp_file(g_tmpdir, "infra/main.tf", + "module \"net\" {\n source = \"./modules/net\"\n}\n"); write_temp_file(g_tmpdir, "infra/outputs.tf", "output \"net_vpc\" {\n value = module.net.vpc_id\n}\n"); write_temp_file(g_tmpdir, "infra/modules/net/main.tf", @@ -3850,8 +3851,9 @@ TEST(incremental_shell_deploy_edit_keeps_its_edges) { } write_temp_file(tmp, "manifests/app.yaml", "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app\n"); - write_temp_file(tmp, "hack/deploy.sh", - "#!/bin/sh\ncd \"$(dirname \"$0\")/..\"\nkubectl apply -f manifests/app.yaml\n"); + write_temp_file( + tmp, "hack/deploy.sh", + "#!/bin/sh\ncd \"$(dirname \"$0\")/..\"\nkubectl apply -f manifests/app.yaml\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/shinc.db", tmp); static const char deploys[] = "SELECT COUNT(*) FROM edges WHERE type='DEPLOYS'"; @@ -3905,8 +3907,7 @@ static int incremental_spawner_delete(bool legacy, cbm_incremental_route_t *rout snprintf(path, sizeof(path), "%s/hack/a.sh", tmp); (void)remove(path); if (!legacy) { - *after_script = - k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, processes) : -1; + *after_script = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, processes) : -1; *route = cbm_pipeline_incremental_test_last_route(); } snprintf(path, sizeof(path), "%s/app.py", tmp); @@ -4152,7 +4153,8 @@ static char *incremental_env_edit(bool legacy, bool dockerfile, cbm_incremental_ return NULL; } write_temp_file(tmp, "Dockerfile", - dockerfile ? "FROM alpine:3.20\nENV OLD_D=1\n" : "FROM alpine:3.20\nENV KEEP_D=1\n"); + dockerfile ? "FROM alpine:3.20\nENV OLD_D=1\n" + : "FROM alpine:3.20\nENV KEEP_D=1\n"); write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: acme/web\n environment:\n OLD_C: x\n"); write_temp_file(tmp, "compose.other.yaml", @@ -4364,10 +4366,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 " @@ -4616,7 +4619,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 }}" @@ -4741,9 +4745,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); @@ -4753,10 +4758,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); @@ -4794,9 +4799,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); @@ -4828,7 +4834,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); @@ -4919,8 +4926,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); @@ -4968,10 +4975,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); @@ -4982,8 +4988,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" @@ -5169,10 +5174,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); @@ -5207,9 +5211,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); @@ -5243,9 +5247,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)); @@ -5253,8 +5256,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); @@ -5276,8 +5279,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 ? "," : ""); } @@ -5375,8 +5379,9 @@ static char *incremental_docker_build_keep(bool legacy, cbm_incremental_route_t return NULL; } write_temp_file(tmp, "svc/Dockerfile", "FROM golang:1.22\n"); - write_temp_file(tmp, "build.sh", - "#!/bin/sh\ncd \"$(dirname \"$0\")\"\ndocker build -t ghcr.io/acme/svc:1 svc\n"); + write_temp_file( + tmp, "build.sh", + "#!/bin/sh\ncd \"$(dirname \"$0\")\"\ndocker build -t ghcr.io/acme/svc:1 svc\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/dockerbuild.db", tmp); static const char builds[] = @@ -5427,10 +5432,12 @@ TEST(incremental_second_build_declarer_keeps_the_edge) { snprintf(tmp, sizeof(tmp), "/tmp/cbm_twodecl_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "svc/Dockerfile", "FROM golang:1.22\n"); - write_temp_file(tmp, "a.sh", "#!/bin/sh\ncd \"$(dirname \"$0\")\"\n" - "docker build -t ghcr.io/acme/svc:1 svc\n"); - write_temp_file(tmp, "b.sh", "#!/bin/sh\ncd \"$(dirname \"$0\")\"\n" - "docker build -t ghcr.io/acme/svc:2 svc\n"); + write_temp_file(tmp, "a.sh", + "#!/bin/sh\ncd \"$(dirname \"$0\")\"\n" + "docker build -t ghcr.io/acme/svc:1 svc\n"); + write_temp_file(tmp, "b.sh", + "#!/bin/sh\ncd \"$(dirname \"$0\")\"\n" + "docker build -t ghcr.io/acme/svc:2 svc\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/twodecl.db", tmp); static const char builds[] = @@ -6000,10 +6007,12 @@ static void stem_repo(const char *tmp, int fillers) { write_temp_file(tmp, "rs/foo.rs", "pub mod bar;\npub fn top() {}\n"); write_temp_file(tmp, "rs/foo/bar.rs", "pub fn inner() {}\n"); write_temp_file(tmp, "c/foo.h", "int foo_add(int a, int b);\n"); - write_temp_file(tmp, "c/foo.c", "#include \"foo.h\"\nint foo_add(int a, int b) { return a + b; }\n"); + write_temp_file(tmp, "c/foo.c", + "#include \"foo.h\"\nint foo_add(int a, int b) { return a + b; }\n"); write_temp_file(tmp, "c/foo/x.c", "int foo_x(void) { return 1; }\n"); write_temp_file(tmp, "ts/foo.test.ts", "export function stemProbeA(): number { return 1; }\n"); - write_temp_file(tmp, "ts/foo/test/a.ts", "export function stemProbeB(): number { return 2; }\n"); + write_temp_file(tmp, "ts/foo/test/a.ts", + "export function stemProbeB(): number { return 2; }\n"); write_temp_file(tmp, "go.mod", "module example.com/stem\n\ngo 1.21\n"); write_temp_file(tmp, "goapi/api.sh", "#!/bin/sh\nrun_api() {\n echo api\n}\n"); write_temp_file(tmp, "goapi/api/server.go", "package api\n\nfunc Serve() {}\n"); @@ -6056,7 +6065,8 @@ TEST(pipeline_stem_folder_collision_keeps_both_nodes) { "C stem pair Module"}, {"SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.c.foo/'", "c/foo/ Folder marked"}, - {"SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.ts.foo.test' AND " + {"SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.ts.foo.test' " + "AND " "file_path='ts/foo.test.ts'", "dotted foo.test.ts Module"}, {"SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.ts.foo.test/'", @@ -6235,8 +6245,9 @@ TEST(incremental_env_join_repair_keeps_unchanged_pairs) { " value: prod\n"); write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: acme/web:1\n build: ./web\n"); - write_temp_file(tmp, "web/Dockerfile", - "FROM node:20\nCOPY src ./src\nCOPY lib ./lib\nCMD [\"node\", \"src/server.js\"]\n"); + write_temp_file( + tmp, "web/Dockerfile", + "FROM node:20\nCOPY src ./src\nCOPY lib ./lib\nCMD [\"node\", \"src/server.js\"]\n"); write_temp_file(tmp, "web/src/server.js", "const port = process.env.PORT;\nconst mode = process.env.MODE;\n"); write_temp_file(tmp, "web/lib/conf.js", "module.exports = process.env.MODE;\n"); @@ -6246,9 +6257,9 @@ TEST(incremental_env_join_repair_keeps_unchanged_pairs) { snprintf(db, sizeof(db), "%s/envjoin.db", tmp); static const char joins[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id " "WHERE e.type='CONFIGURES' AND s.label='EnvVar'"; -#define ENVJOIN_PAIR(key, file) \ - "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " \ - "t.id=e.target_id WHERE e.type='CONFIGURES' AND s.label='EnvVar' AND s.qualified_name LIKE " \ +#define ENVJOIN_PAIR(key, file) \ + "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " \ + "t.id=e.target_id WHERE e.type='CONFIGURES' AND s.label='EnvVar' AND s.qualified_name LIKE " \ "'%#env/web/" key "' AND t.file_path='" file "'" static const char mode_server[] = ENVJOIN_PAIR("MODE", "web/src/server.js"); static const char mode_conf[] = ENVJOIN_PAIR("MODE", "web/lib/conf.js"); @@ -11194,9 +11205,9 @@ TEST(pipeline_c_call_reaches_header_function_beside_field) { /* Both namesakes must exist for the probe to mean anything. */ ASSERT_GTE(fixture_node_count(s, project, "include/linux/mm.h", "put_page", "Function"), 1); - ASSERT_GTE(fixture_node_count(s, project, "arch/arm64/include/asm/kvm_pgtable.h", "put_page", - "Field"), - 1); + ASSERT_GTE( + fixture_node_count(s, project, "arch/arm64/include/asm/kvm_pgtable.h", "put_page", "Field"), + 1); ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Function"), 1); ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Function"), 1); /* A bare C call never names the struct member. */ @@ -11230,9 +11241,9 @@ TEST(pipeline_c_call_reaches_header_function_beside_field_parallel) { ASSERT_NOT_NULL(s); ASSERT_GTE(fixture_node_count(s, project, "include/linux/mm.h", "put_page", "Function"), 1); - ASSERT_GTE(fixture_node_count(s, project, "arch/arm64/include/asm/kvm_pgtable.h", "put_page", - "Field"), - 1); + ASSERT_GTE( + fixture_node_count(s, project, "arch/arm64/include/asm/kvm_pgtable.h", "put_page", "Field"), + 1); ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Function"), 1); ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Function"), 1); ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Field"), 0); @@ -13945,6 +13956,917 @@ 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_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_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 @@ -15901,8 +16823,8 @@ static int python_init_scope_pipeline_case(bool parallel, const char *failure_st : body); } if (written && cross_walk_stage) { - written = python_module_write_fixture_file(root, "memo_def.py", - "class S:\n def m(self):\n return 1\n"); + written = python_module_write_fixture_file( + root, "memo_def.py", "class S:\n def m(self):\n return 1\n"); } if (written && !failure_stage) { written = python_module_write_fixture_file(root, "plain.py", body) && @@ -15967,8 +16889,10 @@ static int python_init_scope_pipeline_case(bool parallel, const char *failure_st correct = project && cbm_store_find_nodes_by_label(store, project, "File", &files, &file_count) == CBM_STORE_OK && - file_count == - (cross_walk_stage ? 2 : failure_stage ? 1 : 5) + (parallel ? 52 : 0); + file_count == (cross_walk_stage ? 2 + : failure_stage ? 1 + : 5) + + (parallel ? 52 : 0); cbm_store_free_nodes(files, file_count); if (project && !failure_stage) { const char *modules[] = {"plain", "pkg"}; @@ -17982,14 +18906,13 @@ static int pyn_assert_shadowing(cbm_store_t *s, const char *project) { snprintf(local_def, sizeof(local_def), "%s.uses_local.helper", files[f]); snprintf(module_caller, sizeof(module_caller), "%s.uses_module", files[f]); snprintf(module_def, sizeof(module_def), "%s.helper", files[f]); - ASSERT_TRUE( - pyn_edge_with(s, project, local_caller, local_def, "CALLS", local_strategy[f])); + ASSERT_TRUE(pyn_edge_with(s, project, local_caller, local_def, "CALLS", local_strategy[f])); ASSERT_FALSE(pyn_edge(s, project, local_caller, module_def, "CALLS")); ASSERT_TRUE(pyn_edge(s, project, module_caller, module_def, "CALLS")); ASSERT_FALSE(pyn_edge(s, project, module_caller, local_def, "CALLS")); } - ASSERT_TRUE(pyn_edge_with(s, project, "sa.uses_decorated", "sa.uses_decorated.helper", - "CALLS", "\"strategy\":\"lexical_scope\"")); + ASSERT_TRUE(pyn_edge_with(s, project, "sa.uses_decorated", "sa.uses_decorated.helper", "CALLS", + "\"strategy\":\"lexical_scope\"")); ASSERT_FALSE(pyn_edge(s, project, "sa.uses_decorated", "sa.helper", "CALLS")); return 0; } @@ -18302,9 +19225,8 @@ static int pyn_run_init_collision(int fillers) { const char *project = cbm_pipeline_project_name(p); imports[run] = pyn_edge(s, project, "tests.test_app.py.__file__", "src.rowpkg.layout.Layout", "IMPORTS"); - calls[run] = - pyn_edge(s, project, "tests.test_app.test_build", "src.rowpkg.layout.Layout", - "CALLS"); + calls[run] = pyn_edge(s, project, "tests.test_app.test_build", + "src.rowpkg.layout.Layout", "CALLS"); method[run] = pyn_edge(s, project, "tests.test_app.test_build", "src.rowpkg.layout.Layout.create", "CALLS"); cbm_store_close(s); @@ -19655,6 +20577,266 @@ TEST(pipeline_swift_cross_package_import) { PASS(); } +/* #2379 at graph level, as a SwiftPM package. Sources/Kit/Inits.swift is the + * issue's reproduction. Sources/App/Sub.swift imports the Kit target -- which + * sends name resolution down the import-aware path -- and calls initializers + * the way real code does: `super.init` to a project superclass in another + * target, `super.init` to a superclass outside the project, an explicit + * `T.init(...)` on a project type and on a type outside the project, `self.init` + * from an extension of a type outside the project, and an implicit `.init(...)`. + * Each of those can attract a false CALLS edge now that initializers are nodes. */ +static const char *const SWIFT_INIT_PACKAGE_SRC = + "let package = Package(\n" + " name: \"Kit\",\n" + " products: [.library(name: \"Kit\", targets: [\"Kit\"])],\n" + " targets: [\n" + " .target(name: \"Kit\", dependencies: []),\n" + " .target(name: \"App\", dependencies: [\"Kit\"])\n" + " ]\n" + ")\n"; + +static const char *const SWIFT_INIT_INITS_SRC = "func helper() -> Int { 1 }\n" + "func other() -> Int { 2 }\n" + "\n" + "struct Box {\n" + " let value: Int\n" + " init() {\n" + " value = helper()\n" + " }\n" + " func read() -> Int {\n" + " other()\n" + " }\n" + "}\n" + "\n" + "final class Holder {\n" + " let value: Int\n" + " init(seed: Int) {\n" + " value = helper() + seed\n" + " }\n" + " convenience init() {\n" + " self.init(seed: other())\n" + " }\n" + "}\n" + "\n" + "enum Mode {\n" + " case on\n" + " init?(flag: Bool) {\n" + " if helper() > 0 { self = .on } else { " + "return nil }\n" + " }\n" + "}\n" + "\n" + "extension Box {\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(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(name: String) {\n" + " super.init(name: name)\n" + " }\n" + "}\n" + "class Screen: UIViewController {\n" + " init() {\n" + " super.init(nibName: nil, bundle: nil)\n" + " }\n" + " func alert() -> UIAlertAction {\n" + " UIAlertAction.init(title: nil, style: " + ".default)\n" + " }\n" + "}\n" + "extension Notification {\n" + " init(tag: String) {\n" + " self.init(name: Notification.Name(tag))\n" + " }\n" + "}\n" + "func make() -> Box {\n" + " .init(raw: 1)\n" + "}\n" + "func explicit() -> Box {\n" + " Box.init(raw: 2)\n" + "}\n"; + +enum { SWIFT_INIT_EDGE_CAP = 32, SWIFT_INIT_EDGE_LEN = 192 }; + +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; + +static int swift_init_edge_cmp(const void *a, const void *b) { + return strcmp((const char *)a, (const char *)b); +} + +/* Qualified name relative to the project ("Inits.Box.init"). */ +static const char *swift_init_rel_qn(const char *qn, const char *project) { + size_t plen = strlen(project); + return strncmp(qn, project, plen) == 0 && qn[plen] == '.' ? qn + plen + 1 : qn; +} + +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; + if (cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &count) == CBM_STORE_OK) { + for (int i = 0; i < count; i++) { + cbm_node_t src = {0}; + cbm_node_t tgt = {0}; + if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && + cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK && + o->calls < SWIFT_INIT_EDGE_CAP) { + snprintf(o->edges[o->calls], SWIFT_INIT_EDGE_LEN, "%s -> %s", + swift_init_rel_qn(src.qualified_name, project), + swift_init_rel_qn(tgt.qualified_name, project)); + } + o->calls++; + cbm_node_free_fields(&src); + cbm_node_free_fields(&tgt); + } + if (edges) { + cbm_store_free_edges(edges, count); + } + } + int kept = o->calls < SWIFT_INIT_EDGE_CAP ? o->calls : SWIFT_INIT_EDGE_CAP; + qsort(o->edges, (size_t)kept, sizeof(o->edges[0]), swift_init_edge_cmp); + + edges = NULL; + count = 0; + o->init_usages = 0; + if (cbm_store_find_edges_by_type(s, project, "USAGE", &edges, &count) == CBM_STORE_OK) { + for (int i = 0; i < count; i++) { + cbm_node_t tgt = {0}; + if (cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK && + tgt.name && (strcmp(tgt.name, "init") == 0 || strcmp(tgt.name, "deinit") == 0)) { + o->init_usages++; + } + cbm_node_free_fields(&tgt); + } + if (edges) { + cbm_store_free_edges(edges, count); + } + } +} + +/* `pad` filler files push the run past MIN_FILES_FOR_PARALLEL so that, with + * `parallel`, it goes through pass_parallel.c rather than pass_calls.c. */ +static void swift_init_case(bool parallel, int pad, SwiftInitObservation *o) { + memset(o, 0, sizeof(*o)); + o->run_rc = -1; + o->calls = -1; + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_init_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + return; + } + char path[512]; + bool written = true; + const char *names[] = {"Package.swift", "Sources/Kit/Inits.swift", "Sources/Kit/Base.swift", + "Sources/App/Sub.swift"}; + const char *sources[] = {SWIFT_INIT_PACKAGE_SRC, SWIFT_INIT_INITS_SRC, SWIFT_INIT_BASE_SRC, + SWIFT_INIT_SUB_SRC}; + for (int i = 0; i < 4 && written; i++) { + snprintf(path, sizeof(path), "%s/%s", tmp, names[i]); + written = th_write_file(path, sources[i]) == 0; + } + for (int i = 0; i < pad && written; i++) { + char source[96]; + snprintf(path, sizeof(path), "%s/Pad%02d.swift", tmp, i); + snprintf(source, sizeof(source), "func pad%02d() -> Int { %d }\n", i, i); + written = th_write_file(path, source) == 0; + } + if (written) { + XlangEnvSave save = xlang_env_select(parallel); + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/swift_init.db", tmp); + cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + if (pipeline) { + o->run_rc = cbm_pipeline_run(pipeline); + const char *project = cbm_pipeline_project_name(pipeline); + cbm_store_t *store = cbm_store_open_path(db_path); + if (store && project) { + swift_init_observe(store, project, o); + } + if (store) { + cbm_store_close(store); + } + cbm_pipeline_free(pipeline); + } + xlang_env_restore(&save); + } + th_rmtree(tmp); +} + +/* The exact CALLS set. Every initializer's own calls come from that + * initializer (the repro's file-level collapse is gone); Sub.init reaches + * Base.init in the other target and `Box.init(raw:)` reaches Box.init. + * UIViewController's and UIAlertAction's initializers, Notification's own and + * the implicit `.init(...)` are outside the project and form no edge -- in + * particular none to a project `init` that merely shares the name. Holder's + * 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(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++) { + ASSERT_STR_EQ(o->edges[i], expected[i]); + } + return 0; +} + +TEST(pipeline_swift_init_exact_calls_sequential_issue2379) { + SwiftInitObservation o; + swift_init_case(false, 0, &o); + if (swift_init_assert_exact(&o) != 0) { + return 1; + } + PASS(); +} + +/* Same input through pass_parallel.c: 55 fillers exceed MIN_FILES_FOR_PARALLEL. */ +TEST(pipeline_swift_init_exact_calls_parallel_issue2379) { + SwiftInitObservation o; + swift_init_case(true, 55, &o); + if (swift_init_assert_exact(&o) != 0) { + return 1; + } + PASS(); +} + TEST(pipeline_python_cross_module_call) { /* Port of TestPythonCrossModuleCallViaImport */ const char *files[] = {"utils.py", "main.py"}; @@ -27598,6 +28780,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 @@ -28247,6 +29547,19 @@ SUITE(pipeline) { RUN_TEST(pipeline_native_fetch_classified_as_http_calls); 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_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_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_native_fetch_parallel_classified_as_http_calls); RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http); @@ -28320,6 +29633,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_imports_multi_symbol_edges); RUN_TEST(pipeline_go_cross_package_call); RUN_TEST(pipeline_swift_cross_package_import); + RUN_TEST(pipeline_swift_init_exact_calls_sequential_issue2379); + RUN_TEST(pipeline_swift_init_exact_calls_parallel_issue2379); RUN_TEST(pipeline_python_cross_module_call); RUN_TEST(pipeline_cross_language_same_name_does_not_share_calls_issue725); RUN_TEST(pipeline_go_type_classification); diff --git a/tests/test_registry.c b/tests/test_registry.c index 08b87783d4..46c0e0a725 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -2053,6 +2053,401 @@ 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_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. */ @@ -2428,7 +2823,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(); @@ -2469,8 +2864,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); @@ -2527,6 +2922,13 @@ 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_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);