Skip to content

Commit 8a74d81

Browse files
authored
Merge branch 'main' into fix/hook-augment-startup-deadline
2 parents 0657651 + 951e02e commit 8a74d81

4 files changed

Lines changed: 135 additions & 1 deletion

File tree

‎internal/cbm/extract_calls.c‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ static const char *lookup_url_builder(const CBMExtractCtx *ctx, const char *name
6161
static int is_string_like(const char *kind) {
6262
return (strcmp(kind, "string") == 0 || strcmp(kind, "string_literal") == 0 ||
6363
strcmp(kind, "interpreted_string_literal") == 0 ||
64-
strcmp(kind, "raw_string_literal") == 0 || strcmp(kind, "string_content") == 0);
64+
strcmp(kind, "raw_string_literal") == 0 || strcmp(kind, "string_content") == 0 ||
65+
strcmp(kind, "line_string_literal") == 0);
6566
}
6667

6768
/* Strip surrounding quotes from a string, return arena-allocated copy */
@@ -2286,6 +2287,18 @@ static const char *extract_url_or_topic_arg(CBMExtractCtx *ctx, TSNode args) {
22862287
if (strcmp(ts_node_type(arg), "argument") == 0 && ts_node_named_child_count(arg) > 0) {
22872288
arg = ts_node_named_child(arg, 0);
22882289
}
2290+
/* Swift wraps each argument in a value_argument that may lead with its
2291+
* label, so `data(from: url)` would otherwise yield the label `from`
2292+
* rather than the value. Step past a leading value_argument_label. */
2293+
if (strcmp(ts_node_type(arg), "value_argument") == 0 &&
2294+
ts_node_named_child_count(arg) > 0) {
2295+
TSNode val = ts_node_named_child(arg, 0);
2296+
if (strcmp(ts_node_type(val), "value_argument_label") == 0 &&
2297+
ts_node_named_child_count(arg) > 1) {
2298+
val = ts_node_named_child(arg, 1);
2299+
}
2300+
arg = val;
2301+
}
22892302
const char *ak = ts_node_type(arg);
22902303

22912304
if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) {
@@ -3080,6 +3093,19 @@ static TSNode objectscript_call_args(TSNode node) {
30803093
: cbm_find_child_by_kind(macro_function, "method_args");
30813094
}
30823095

3096+
/* Swift models a call as a target expression plus a call_suffix, and its grammar
3097+
* declares no "arguments" field at all, so the generic field lookup finds
3098+
* nothing for every Swift call. Reach the argument list through the suffix
3099+
* instead. A trailing closure has a call_suffix with no value_arguments, which
3100+
* returns a null node and leaves the call without a string argument, as before. */
3101+
static TSNode swift_call_args(TSNode node) {
3102+
TSNode suffix = cbm_find_child_by_kind(node, "call_suffix");
3103+
if (ts_node_is_null(suffix)) {
3104+
return (TSNode){0};
3105+
}
3106+
return cbm_find_child_by_kind(suffix, "value_arguments");
3107+
}
3108+
30833109
static bool node_has_token(TSNode node, const char *token) {
30843110
uint32_t count = ts_node_child_count(node);
30853111
for (uint32_t i = 0; i < count; i++) {
@@ -3676,6 +3702,10 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
36763702
if (ts_node_is_null(args) && is_objectscript_language(ctx->language)) {
36773703
args = objectscript_call_args(node);
36783704
}
3705+
// Swift has no "arguments" field either; its args hang off call_suffix.
3706+
if (ts_node_is_null(args) && ctx->language == CBM_LANG_SWIFT) {
3707+
args = swift_call_args(node);
3708+
}
36793709
if (!ts_node_is_null(args)) {
36803710
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
36813711
/* #952: routes registered inside Laravel `prefix()->group()`

‎src/cli/cli.c‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,14 +1765,25 @@ static bool cbm_json_mcp_command_path_probe_safe(const char *command) {
17651765
#endif
17661766

17671767
#ifdef CBM_CLI_ENABLE_TEST_API
1768+
#ifdef _WIN32
17681769
static CBM_TLS int *g_mcp_command_path_probe_counter = NULL;
1770+
#endif
17691771

17701772
bool cbm_mcp_command_path_probe_safe_for_testing(const char *command, bool windows) {
17711773
return cbm_json_mcp_command_path_probe_safe_for_platform(command, windows);
17721774
}
17731775

17741776
void cbm_set_mcp_command_path_probe_counter_for_testing(int *counter) {
1777+
#ifdef _WIN32
17751778
g_mcp_command_path_probe_counter = counter;
1779+
#else
1780+
/* The command-path classifier (cbm_json_mcp_probe_command_path) is compiled
1781+
* for Windows only, so on POSIX there is no probe to count. The tests still
1782+
* install a counter on every platform and assert it stays at zero; a pointer
1783+
* stored here but never read is what Clang 23 rejects under -Werror
1784+
* (-Wunused-but-set-global). */
1785+
(void)counter;
1786+
#endif
17761787
}
17771788
#endif
17781789

‎tests/test_extraction.c‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4079,6 +4079,41 @@ static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee)
40794079
return NULL;
40804080
}
40814081

4082+
/* #1892: the Swift grammar declares no "arguments" field, so the generic field
4083+
* lookup read nothing and every Swift call lost its arguments. Without the URL
4084+
* the service-pattern table cannot raise an HTTP_CALLS edge or a Route node,
4085+
* even though Alamofire/Moya/URLSession are already listed in it. */
4086+
TEST(swift_call_string_arg_issue1892) {
4087+
CBMFileResult *r =
4088+
extract("func listWidgets() { AF.request(\"https://example.com/api/v1/widgets\") }\n",
4089+
CBM_LANG_SWIFT, "t", "Client.swift");
4090+
ASSERT_NOT_NULL(r);
4091+
ASSERT_FALSE(r->has_error);
4092+
const CBMCall *c = find_call_by_callee(r, "AF.request");
4093+
ASSERT_NOT_NULL(c);
4094+
ASSERT_NOT_NULL(c->first_string_arg);
4095+
ASSERT_STR_EQ(c->first_string_arg, "https://example.com/api/v1/widgets");
4096+
cbm_free_result(r);
4097+
PASS();
4098+
}
4099+
4100+
/* Swift labels its arguments, and each one sits in a value_argument node that
4101+
* leads with the label. Reading the first child alone would return `with`
4102+
* rather than the path. */
4103+
TEST(swift_labeled_call_string_arg_issue1892) {
4104+
CBMFileResult *r =
4105+
extract("func fetch() { URLSession.shared.dataTask(with: \"/api/v1/widgets/1\") }\n",
4106+
CBM_LANG_SWIFT, "t", "Fetch.swift");
4107+
ASSERT_NOT_NULL(r);
4108+
ASSERT_FALSE(r->has_error);
4109+
const CBMCall *c = find_call_by_callee(r, "URLSession.shared.dataTask");
4110+
ASSERT_NOT_NULL(c);
4111+
ASSERT_NOT_NULL(c->first_string_arg);
4112+
ASSERT_STR_EQ(c->first_string_arg, "/api/v1/widgets/1");
4113+
cbm_free_result(r);
4114+
PASS();
4115+
}
4116+
40824117
/* Issue #1009: URL-builder helper pattern — a function returning a URL-shaped
40834118
* literal, consumed as client(buildPath(id)). The builder's URL is recorded in
40844119
* the per-file constant map and resolved at the call site, for both return
@@ -7022,6 +7057,8 @@ SUITE(extraction) {
70227057
RUN_TEST(swift_constructor_call);
70237058
RUN_TEST(swift_chained_call);
70247059
RUN_TEST(swift_force_unwrap_scanner_shift);
7060+
RUN_TEST(swift_call_string_arg_issue1892);
7061+
RUN_TEST(swift_labeled_call_string_arg_issue1892);
70257062
RUN_TEST(objc_interface);
70267063
RUN_TEST(objc_implementation);
70277064
RUN_TEST(dart_top_level_function);

‎tests/test_pipeline.c‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5606,6 +5606,61 @@ TEST(pipeline_native_fetch_classified_as_http_calls) {
56065606
PASS();
56075607
}
56085608

5609+
/* #1892: Swift produced no Route node and no HTTP_CALLS edge, because the
5610+
* Swift grammar has no "arguments" field and the generic lookup therefore read
5611+
* no call arguments at all. Alamofire/URLSession were already in the service
5612+
* pattern table; the URL simply never reached it. This is the Swift twin of
5613+
* the TypeScript fetch case above. */
5614+
TEST(pipeline_swift_http_call_makes_route_issue1892) {
5615+
char tmp[256];
5616+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_swifthttp_XXXXXX");
5617+
if (!cbm_mkdtemp(tmp)) {
5618+
FAIL("tmpdir");
5619+
}
5620+
5621+
/* URLSession, not Alamofire's `AF` shorthand: the service pattern table
5622+
* matches the library name in the callee text, and "AF.request" contains
5623+
* no such name. */
5624+
write_temp_file(tmp, "Sources/Client.swift",
5625+
"import Foundation\n"
5626+
"final class Client {\n"
5627+
" func listWidgets() {\n"
5628+
" URLSession.shared.dataTask(with: \"/api/v1/widgets\")\n"
5629+
" }\n"
5630+
"}\n");
5631+
5632+
char db_path[512];
5633+
snprintf(db_path, sizeof(db_path), "%s/swifthttp.db", tmp);
5634+
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
5635+
ASSERT_NOT_NULL(p);
5636+
ASSERT_EQ(cbm_pipeline_run(p), 0);
5637+
const char *project = cbm_pipeline_project_name(p);
5638+
5639+
cbm_store_t *s = cbm_store_open_path(db_path);
5640+
ASSERT_NOT_NULL(s);
5641+
5642+
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1);
5643+
5644+
/* The edge carries the URL, so pass_route_nodes can mint the Route the
5645+
* cross-repo matcher joins a server route against. */
5646+
cbm_node_t *routes = NULL;
5647+
int route_count = 0;
5648+
cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count);
5649+
int widget_routes = 0;
5650+
for (int i = 0; i < route_count; i++) {
5651+
if (routes[i].qualified_name && strstr(routes[i].qualified_name, "/api/v1/widgets")) {
5652+
widget_routes++;
5653+
}
5654+
}
5655+
cbm_store_free_nodes(routes, route_count);
5656+
ASSERT_GTE(widget_routes, 1);
5657+
5658+
cbm_store_close(s);
5659+
cbm_pipeline_free(p);
5660+
th_rmtree(tmp);
5661+
PASS();
5662+
}
5663+
56095664
/* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's
56105665
* resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls
56115666
* but forces the parallel resolver, since the empty-resolution fallback is a
@@ -13273,6 +13328,7 @@ SUITE(pipeline) {
1327313328
RUN_TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier);
1327413329
RUN_TEST(pipeline_arg_url_rejects_non_http_slash_arguments);
1327513330
RUN_TEST(pipeline_native_fetch_classified_as_http_calls);
13331+
RUN_TEST(pipeline_swift_http_call_makes_route_issue1892);
1327613332
RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls);
1327713333
RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http);
1327813334
/* Git history pass */

0 commit comments

Comments
 (0)