Skip to content

Commit 909051c

Browse files
authored
Merge pull request DeusData#1010 from CharlesQueiroz/fix/url-builder-routes
fix: resolve URL-builder helper calls to Route/HTTP_CALLS
2 parents 92a27de + 606052a commit 909051c

4 files changed

Lines changed: 437 additions & 2 deletions

File tree

‎internal/cbm/cbm.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ typedef struct {
551551
typedef struct {
552552
const char *names[CBM_MAX_STRING_CONSTANTS];
553553
const char *values[CBM_MAX_STRING_CONSTANTS];
554+
bool is_url_builder[CBM_MAX_STRING_CONSTANTS];
554555
int count;
555556
} CBMStringConstantMap;
556557

‎internal/cbm/extract_calls.c‎

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,29 @@ enum { MIN_PRINTABLE = 0x20 };
3030
/* Handler arg scan start index (skip first positional). */
3131
enum { HANDLER_START_IDX = 1 };
3232

33-
/* Look up a module-level string constant by name. */
33+
/* Look up a module-level string constant by name. URL-builder entries share the
34+
* map but are not constants: a bare `thingPath` reference is the function, not
35+
* the URL it would build (issue #1009). */
3436
static const char *lookup_string_constant(const CBMExtractCtx *ctx, const char *name) {
3537
if (!name || !name[0]) {
3638
return NULL;
3739
}
3840
const CBMStringConstantMap *map = &ctx->string_constants;
3941
for (int i = 0; i < map->count; i++) {
40-
if (strcmp(map->names[i], name) == 0) {
42+
if (!map->is_url_builder[i] && strcmp(map->names[i], name) == 0) {
43+
return map->values[i];
44+
}
45+
}
46+
return NULL;
47+
}
48+
49+
static const char *lookup_url_builder(const CBMExtractCtx *ctx, const char *name) {
50+
if (!name || !name[0]) {
51+
return NULL;
52+
}
53+
const CBMStringConstantMap *map = &ctx->string_constants;
54+
for (int i = 0; i < map->count; i++) {
55+
if (map->is_url_builder[i] && strcmp(map->names[i], name) == 0) {
4156
return map->values[i];
4257
}
4358
}
@@ -1855,8 +1870,22 @@ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) {
18551870
ca->index = positional_idx++;
18561871
if (is_string_like(ak) && ca->expr) {
18571872
ca->value = strip_quotes(ctx->arena, ca->expr);
1873+
} else if (strcmp(ak, "template_string") == 0) {
1874+
/* Flattened {} form so downstream url-arg detection joins the
1875+
* canonical server route shape (issue #1006/#1009). */
1876+
ca->value = cbm_template_string_text(ctx->arena, arg_node, ctx->source);
18581877
} else if (strcmp(ak, "identifier") == 0 && ca->expr) {
18591878
ca->value = lookup_string_constant(ctx, ca->expr);
1879+
} else if (strcmp(ak, "call_expression") == 0) {
1880+
/* URL-builder helper call (issue #1009): resolve
1881+
* client(buildPath(id)) through the per-file builder map. */
1882+
TSNode fn = ts_node_child_by_field_name(arg_node, TS_FIELD("function"));
1883+
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "identifier") == 0) {
1884+
char *fname = cbm_node_text(ctx->arena, fn, ctx->source);
1885+
if (fname) {
1886+
ca->value = lookup_url_builder(ctx, fname);
1887+
}
1888+
}
18601889
}
18611890
call->arg_count++;
18621891
}
@@ -2085,6 +2114,19 @@ static const char *extract_url_or_topic_arg(CBMExtractCtx *ctx, TSNode args) {
20852114
}
20862115
}
20872116

2117+
/* URL-builder helper call (issue #1009): `client(buildPath(id))` — the
2118+
* builder's returned URL was recorded in the per-file constant map. */
2119+
if (strcmp(ak, "call_expression") == 0) {
2120+
TSNode fn = ts_node_child_by_field_name(arg, TS_FIELD("function"));
2121+
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "identifier") == 0) {
2122+
char *fname = cbm_node_text(ctx->arena, fn, ctx->source);
2123+
const char *val = fname ? lookup_url_builder(ctx, fname) : NULL;
2124+
if (val) {
2125+
return val;
2126+
}
2127+
}
2128+
}
2129+
20882130
if (ai < MAX_POSITIONAL_SCAN) {
20892131
const char *val = extract_positional_url(ctx, arg, ak);
20902132
if (val) {

‎internal/cbm/extract_unified.c‎

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,227 @@ static void handle_string_refs(CBMExtractCtx *ctx, TSNode node, const WalkState
10991099
cbm_stringref_push(&ctx->result->string_refs, ctx->arena, ref);
11001100
}
11011101

1102+
// --- URL-builder helpers (issue #1009) ---
1103+
1104+
/* Map-aware template flatten for builder bodies: a ${...} substitution that is
1105+
* a bare identifier or a call to an already-recorded name (const or an earlier
1106+
* builder in the same file) inlines that value; anything else becomes "{}".
1107+
* The query string is not part of a route's identity, so the result is
1108+
* truncated at the first '?'. Handles the composed-builder shape
1109+
* `return \`${basePath(id)}?${params}\``. */
1110+
static const char *builder_template_text(CBMExtractCtx *ctx, TSNode node) {
1111+
enum { BLD_BUF = 512 };
1112+
char buf[BLD_BUF];
1113+
size_t pos = 0;
1114+
uint32_t nc = ts_node_named_child_count(node);
1115+
for (uint32_t i = 0; i < nc; i++) {
1116+
TSNode c = ts_node_named_child(node, i);
1117+
const char *k = ts_node_type(c);
1118+
const char *piece = NULL;
1119+
if (strcmp(k, "string_fragment") == 0) {
1120+
piece = cbm_node_text(ctx->arena, c, ctx->source);
1121+
} else if (strcmp(k, "template_substitution") == 0) {
1122+
piece = "{}";
1123+
if (ts_node_named_child_count(c) > 0) {
1124+
TSNode expr = ts_node_named_child(c, 0);
1125+
/* `${basePath(id)}` inlines a builder, `${BASE}` a const. A bare
1126+
* name never resolves to a builder: that reads the function. */
1127+
bool want_builder = strcmp(ts_node_type(expr), "call_expression") == 0;
1128+
TSNode name_node =
1129+
want_builder ? ts_node_child_by_field_name(expr, TS_FIELD("function")) : expr;
1130+
if (!ts_node_is_null(name_node) &&
1131+
strcmp(ts_node_type(name_node), "identifier") == 0) {
1132+
char *nm = cbm_node_text(ctx->arena, name_node, ctx->source);
1133+
if (nm) {
1134+
const CBMStringConstantMap *map = &ctx->string_constants;
1135+
for (int mi = 0; mi < map->count; mi++) {
1136+
if (map->values[mi] && map->is_url_builder[mi] == want_builder &&
1137+
strcmp(map->names[mi], nm) == 0) {
1138+
piece = map->values[mi];
1139+
break;
1140+
}
1141+
}
1142+
}
1143+
}
1144+
}
1145+
} else {
1146+
continue;
1147+
}
1148+
if (!piece) {
1149+
continue;
1150+
}
1151+
size_t pl = strlen(piece);
1152+
if (pos + pl >= BLD_BUF) {
1153+
return NULL;
1154+
}
1155+
memcpy(buf + pos, piece, pl);
1156+
pos += pl;
1157+
}
1158+
/* Route identity excludes the query string. */
1159+
for (size_t qi = 0; qi < pos; qi++) {
1160+
if (buf[qi] == '?') {
1161+
pos = qi;
1162+
break;
1163+
}
1164+
}
1165+
if (pos == 0) {
1166+
return NULL;
1167+
}
1168+
return cbm_arena_strndup(ctx->arena, buf, pos);
1169+
}
1170+
1171+
/* Flatten a string-ish node (plain string or template literal) to text. */
1172+
static const char *url_builder_literal_text(CBMExtractCtx *ctx, TSNode value_node) {
1173+
const char *kind = ts_node_type(value_node);
1174+
if (strcmp(kind, "template_string") == 0) {
1175+
return builder_template_text(ctx, value_node);
1176+
}
1177+
if (!is_string_node(kind)) {
1178+
return NULL;
1179+
}
1180+
char *text = cbm_node_text(ctx->arena, value_node, ctx->source);
1181+
if (!text || !text[0]) {
1182+
return NULL;
1183+
}
1184+
int len = (int)strlen(text);
1185+
if (len >= CBM_QUOTE_PAIR && (text[0] == '"' || text[0] == '\'')) {
1186+
text = cbm_arena_strndup(ctx->arena, text + SKIP_ONE, (size_t)(len - PAIR_LEN));
1187+
}
1188+
return text;
1189+
}
1190+
1191+
/* A route-shaped URL literal: an absolute path that classifies as a URL. */
1192+
static const char *builder_route_url(CBMExtractCtx *ctx, TSNode value_node) {
1193+
const char *url = url_builder_literal_text(ctx, value_node);
1194+
if (!url || url[0] != '/' || cbm_classify_string(url, (int)strlen(url)) != CBM_STRREF_URL) {
1195+
return NULL;
1196+
}
1197+
return url;
1198+
}
1199+
1200+
static void record_url_builder(CBMExtractCtx *ctx, const char *name, const char *url) {
1201+
if (!name || !name[0] || !url) {
1202+
return;
1203+
}
1204+
CBMStringConstantMap *map = &ctx->string_constants;
1205+
for (int i = 0; i < map->count; i++) {
1206+
if (strcmp(map->names[i], name) == 0) {
1207+
if (map->is_url_builder[i] && map->values[i] && strcmp(map->values[i], url) != 0) {
1208+
map->values[i] = NULL; /* ambiguous builder */
1209+
}
1210+
return;
1211+
}
1212+
}
1213+
if (map->count < CBM_MAX_STRING_CONSTANTS) {
1214+
map->names[map->count] = (char *)name;
1215+
map->values[map->count] = (char *)url;
1216+
map->is_url_builder[map->count] = true;
1217+
map->count++;
1218+
}
1219+
}
1220+
1221+
/* Every `return` this function owns yields a route-shaped URL literal. A body
1222+
* that also returns a computed path (`return computePath(kind)`) would attribute
1223+
* its one literal to call sites that never produce it, so a mixed builder is
1224+
* declined rather than guessed. Returns inside a nested function belong to that
1225+
* function, not to this one. */
1226+
static bool builder_returns_only_urls(CBMExtractCtx *ctx, TSNode func) {
1227+
TSNode body = ts_node_child_by_field_name(func, TS_FIELD("body"));
1228+
if (ts_node_is_null(body)) {
1229+
return false;
1230+
}
1231+
bool only_urls = true;
1232+
TSTreeCursor cursor = ts_tree_cursor_new(body);
1233+
for (;;) {
1234+
TSNode node = ts_tree_cursor_current_node(&cursor);
1235+
if (strcmp(ts_node_type(node), "return_statement") == 0 &&
1236+
ts_node_eq(cbm_find_enclosing_func(node, ctx->language), func)) {
1237+
if (ts_node_named_child_count(node) == 0 ||
1238+
!builder_route_url(ctx, ts_node_named_child(node, 0))) {
1239+
only_urls = false;
1240+
break;
1241+
}
1242+
}
1243+
if (ts_tree_cursor_goto_first_child(&cursor)) {
1244+
continue;
1245+
}
1246+
if (ts_tree_cursor_goto_next_sibling(&cursor)) {
1247+
continue;
1248+
}
1249+
bool found = false;
1250+
while (ts_tree_cursor_goto_parent(&cursor)) {
1251+
if (ts_tree_cursor_goto_next_sibling(&cursor)) {
1252+
found = true;
1253+
break;
1254+
}
1255+
}
1256+
if (!found) {
1257+
break;
1258+
}
1259+
}
1260+
ts_tree_cursor_delete(&cursor);
1261+
return only_urls;
1262+
}
1263+
1264+
/* URL-builder helper pattern (issue #1009): a small function whose return value
1265+
* is a URL-shaped literal, consumed as `client(buildPath(id))`. The literal
1266+
* never appears as a call argument, so first_string_arg resolution cannot see
1267+
* it; record `functionName -> url` in the per-file constant map and let the
1268+
* call-site call_expression branch resolve it. Covers `return`-statement bodies
1269+
* and arrow-function expression bodies.
1270+
*
1271+
* JS/TS only. The predicate accepts any absolute pathname, and `return` plus a
1272+
* string literal is also the shape of every C or Go helper handing back a
1273+
* filesystem path (`/etc/...`, `/proc/self/...`); recording those would mint a
1274+
* Route node and an HTTP_CALLS edge in a language that speaks no HTTP. */
1275+
static void handle_url_builders(CBMExtractCtx *ctx, TSNode node, const WalkState *state) {
1276+
if (ctx->language != CBM_LANG_JAVASCRIPT && ctx->language != CBM_LANG_TYPESCRIPT &&
1277+
ctx->language != CBM_LANG_TSX) {
1278+
return;
1279+
}
1280+
const char *kind = ts_node_type(node);
1281+
1282+
if (strcmp(kind, "arrow_function") == 0) {
1283+
TSNode body = ts_node_child_by_field_name(node, TS_FIELD("body"));
1284+
if (ts_node_is_null(body) || strcmp(ts_node_type(body), "statement_block") == 0) {
1285+
return;
1286+
}
1287+
const char *url = builder_route_url(ctx, body);
1288+
if (!url) {
1289+
return;
1290+
}
1291+
TSNode parent = ts_node_parent(node);
1292+
if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declarator") == 0) {
1293+
TSNode name_node = ts_node_child_by_field_name(parent, TS_FIELD("name"));
1294+
if (!ts_node_is_null(name_node)) {
1295+
record_url_builder(ctx, cbm_node_text(ctx->arena, name_node, ctx->source), url);
1296+
}
1297+
}
1298+
return;
1299+
}
1300+
1301+
if (strcmp(kind, "return_statement") != 0) {
1302+
return;
1303+
}
1304+
if (!state->enclosing_func_qn || state->enclosing_func_qn == ctx->module_qn) {
1305+
return;
1306+
}
1307+
if (ts_node_named_child_count(node) == 0) {
1308+
return;
1309+
}
1310+
const char *url = builder_route_url(ctx, ts_node_named_child(node, 0));
1311+
if (!url) {
1312+
return;
1313+
}
1314+
TSNode func = cbm_find_enclosing_func(node, ctx->language);
1315+
if (ts_node_is_null(func) || !builder_returns_only_urls(ctx, func)) {
1316+
return;
1317+
}
1318+
const char *name = strrchr(state->enclosing_func_qn, '.');
1319+
name = name ? name + 1 : state->enclosing_func_qn;
1320+
record_url_builder(ctx, name, url);
1321+
}
1322+
11021323
// --- YAML nested field extraction (D2) ---
11031324

11041325
/* Recursively walk YAML block_mapping_pair nodes, building dotted key paths.
@@ -2160,6 +2381,7 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
21602381

21612382
handle_string_constants(ctx, node, &state);
21622383
handle_objectscript_type_map(ctx, node, &state);
2384+
handle_url_builders(ctx, node, &state);
21632385
CBMInvocationDescriptor invocation = handle_calls(ctx, node, spec, &state);
21642386
handle_usages(ctx, node, spec, &state);
21652387
handle_throws(ctx, node, spec, &state);

0 commit comments

Comments
 (0)