Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ static const char *lookup_url_builder(const CBMExtractCtx *ctx, const char *name
static int is_string_like(const char *kind) {
return (strcmp(kind, "string") == 0 || strcmp(kind, "string_literal") == 0 ||
strcmp(kind, "interpreted_string_literal") == 0 ||
strcmp(kind, "raw_string_literal") == 0 || strcmp(kind, "string_content") == 0);
strcmp(kind, "raw_string_literal") == 0 || strcmp(kind, "string_content") == 0 ||
strcmp(kind, "line_string_literal") == 0);
}

/* Strip surrounding quotes from a string, return arena-allocated copy */
Expand Down Expand Up @@ -2286,6 +2287,18 @@ static const char *extract_url_or_topic_arg(CBMExtractCtx *ctx, TSNode args) {
if (strcmp(ts_node_type(arg), "argument") == 0 && ts_node_named_child_count(arg) > 0) {
arg = ts_node_named_child(arg, 0);
}
/* Swift wraps each argument in a value_argument that may lead with its
* label, so `data(from: url)` would otherwise yield the label `from`
* rather than the value. Step past a leading value_argument_label. */
if (strcmp(ts_node_type(arg), "value_argument") == 0 &&
ts_node_named_child_count(arg) > 0) {
TSNode val = ts_node_named_child(arg, 0);
if (strcmp(ts_node_type(val), "value_argument_label") == 0 &&
ts_node_named_child_count(arg) > 1) {
val = ts_node_named_child(arg, 1);
}
arg = val;
}
const char *ak = ts_node_type(arg);

if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) {
Expand Down Expand Up @@ -3045,6 +3058,19 @@ static TSNode objectscript_call_args(TSNode node) {
: cbm_find_child_by_kind(macro_function, "method_args");
}

/* Swift models a call as a target expression plus a call_suffix, and its grammar
* declares no "arguments" field at all, so the generic field lookup finds
* nothing for every Swift call. Reach the argument list through the suffix
* instead. A trailing closure has a call_suffix with no value_arguments, which
* returns a null node and leaves the call without a string argument, as before. */
static TSNode swift_call_args(TSNode node) {
TSNode suffix = cbm_find_child_by_kind(node, "call_suffix");
if (ts_node_is_null(suffix)) {
return (TSNode){0};
}
return cbm_find_child_by_kind(suffix, "value_arguments");
}

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++) {
Expand Down Expand Up @@ -3634,6 +3660,10 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
if (ts_node_is_null(args) && is_objectscript_language(ctx->language)) {
args = objectscript_call_args(node);
}
// Swift has no "arguments" field either; its args hang off call_suffix.
if (ts_node_is_null(args) && ctx->language == CBM_LANG_SWIFT) {
args = swift_call_args(node);
}
if (!ts_node_is_null(args)) {
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
/* #952: routes registered inside Laravel `prefix()->group()`
Expand Down
37 changes: 37 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -4027,6 +4027,41 @@ static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee)
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,
* even though Alamofire/Moya/URLSession are already listed in it. */
TEST(swift_call_string_arg_issue1892) {
CBMFileResult *r =
extract("func listWidgets() { AF.request(\"https://example.com/api/v1/widgets\") }\n",
CBM_LANG_SWIFT, "t", "Client.swift");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMCall *c = find_call_by_callee(r, "AF.request");
ASSERT_NOT_NULL(c);
ASSERT_NOT_NULL(c->first_string_arg);
ASSERT_STR_EQ(c->first_string_arg, "https://example.com/api/v1/widgets");
cbm_free_result(r);
PASS();
}

/* Swift labels its arguments, and each one sits in a value_argument node that
* leads with the label. Reading the first child alone would return `with`
* rather than the path. */
TEST(swift_labeled_call_string_arg_issue1892) {
CBMFileResult *r =
extract("func fetch() { URLSession.shared.dataTask(with: \"/api/v1/widgets/1\") }\n",
CBM_LANG_SWIFT, "t", "Fetch.swift");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMCall *c = find_call_by_callee(r, "URLSession.shared.dataTask");
ASSERT_NOT_NULL(c);
ASSERT_NOT_NULL(c->first_string_arg);
ASSERT_STR_EQ(c->first_string_arg, "/api/v1/widgets/1");
cbm_free_result(r);
PASS();
}

/* Issue #1009: URL-builder helper pattern — a function returning a URL-shaped
* literal, consumed as client(buildPath(id)). The builder's URL is recorded in
* the per-file constant map and resolved at the call site, for both return
Expand Down Expand Up @@ -6782,6 +6817,8 @@ SUITE(extraction) {
RUN_TEST(swift_constructor_call);
RUN_TEST(swift_chained_call);
RUN_TEST(swift_force_unwrap_scanner_shift);
RUN_TEST(swift_call_string_arg_issue1892);
RUN_TEST(swift_labeled_call_string_arg_issue1892);
RUN_TEST(objc_interface);
RUN_TEST(objc_implementation);
RUN_TEST(dart_top_level_function);
Expand Down
56 changes: 56 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -5480,6 +5480,61 @@ TEST(pipeline_native_fetch_classified_as_http_calls) {
PASS();
}

/* #1892: Swift produced no Route node and no HTTP_CALLS edge, because the
* Swift grammar has no "arguments" field and the generic lookup therefore read
* no call arguments at all. Alamofire/URLSession were already in the service
* pattern table; the URL simply never reached it. This is the Swift twin of
* the TypeScript fetch case above. */
TEST(pipeline_swift_http_call_makes_route_issue1892) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_swifthttp_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}

/* URLSession, not Alamofire's `AF` shorthand: the service pattern table
* matches the library name in the callee text, and "AF.request" contains
* no such name. */
write_temp_file(tmp, "Sources/Client.swift",
"import Foundation\n"
"final class Client {\n"
" func listWidgets() {\n"
" URLSession.shared.dataTask(with: \"/api/v1/widgets\")\n"
" }\n"
"}\n");

char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/swifthttp.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);

ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1);

/* The edge carries the URL, so pass_route_nodes can mint the Route the
* cross-repo matcher joins a server route against. */
cbm_node_t *routes = NULL;
int route_count = 0;
cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count);
int widget_routes = 0;
for (int i = 0; i < route_count; i++) {
if (routes[i].qualified_name && strstr(routes[i].qualified_name, "/api/v1/widgets")) {
widget_routes++;
}
}
cbm_store_free_nodes(routes, route_count);
ASSERT_GTE(widget_routes, 1);

cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}

/* 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
Expand Down Expand Up @@ -13145,6 +13200,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier);
RUN_TEST(pipeline_arg_url_rejects_non_http_slash_arguments);
RUN_TEST(pipeline_native_fetch_classified_as_http_calls);
RUN_TEST(pipeline_swift_http_call_makes_route_issue1892);
RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls);
RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http);
/* Git history pass */
Expand Down
Loading