|
5 | 5 | #include "extract_unified.h" |
6 | 6 | #include "foundation/constants.h" |
7 | 7 | #include "extract_node_stack.h" |
8 | | -#include "tree_sitter/api.h" // TSNode, ts_node_* |
9 | | -#include <stdint.h> // uint32_t |
| 8 | +#include "service_patterns.h" // cbm_service_pattern_route_method (#952) |
| 9 | +#include "tree_sitter/api.h" // TSNode, ts_node_* |
| 10 | +#include <stdint.h> // uint32_t |
10 | 11 | #include <stdio.h> |
11 | 12 | #include <stdlib.h> |
12 | 13 | #include <string.h> |
@@ -463,7 +464,8 @@ static char *extract_scripting_callee(CBMArena *a, TSNode node, const char *sour |
463 | 464 | if (ts_node_is_null(func_node)) { |
464 | 465 | func_node = ts_node_child_by_field_name(node, TS_FIELD("name")); |
465 | 466 | } |
466 | | - return ts_node_is_null(func_node) ? NULL : cbm_node_text(a, func_node, source); |
| 467 | + char *pn = ts_node_is_null(func_node) ? NULL : cbm_node_text(a, func_node, source); |
| 468 | + return pn; |
467 | 469 | } |
468 | 470 | if (lang == CBM_LANG_KOTLIN && ts_node_child_count(node) > 0) { |
469 | 471 | return cbm_node_text(a, ts_node_child(node, 0), source); |
@@ -1165,6 +1167,126 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char * |
1165 | 1167 | } |
1166 | 1168 |
|
1167 | 1169 | // Extract callee name from a call node |
| 1170 | +/* #952: compose Laravel group prefixes into a route path. Walks UP from a |
| 1171 | + * route-registration call: every enclosing anonymous function that is the |
| 1172 | + * argument of a `->group(...)` member call contributes the `prefix('...')` |
| 1173 | + * (or `Route::prefix('...')`) found on that group call's receiver chain. |
| 1174 | + * Chain methods that don't shape the path (middleware, name, as, domain) |
| 1175 | + * are skipped. Outer groups accumulate before inner ones; nested groups |
| 1176 | + * compose left-to-right. Returns the composed path (arena) or NULL when no |
| 1177 | + * enclosing group carries a prefix. */ |
| 1178 | +enum { PHP_GROUP_WALK_MAX = 64, PHP_PREFIX_PARTS_MAX = 8 }; |
| 1179 | + |
| 1180 | +static const char *php_chain_prefix_arg(CBMArena *a, TSNode call_node, const char *source) { |
| 1181 | + /* call_node is a member_call_expression / scoped_call_expression whose |
| 1182 | + * name is `prefix`; return its first string argument (unquoted). */ |
| 1183 | + TSNode args = ts_node_child_by_field_name(call_node, TS_FIELD("arguments")); |
| 1184 | + if (ts_node_is_null(args)) { |
| 1185 | + return NULL; |
| 1186 | + } |
| 1187 | + uint32_t nc = ts_node_named_child_count(args); |
| 1188 | + for (uint32_t i = 0; i < nc; i++) { |
| 1189 | + TSNode arg = ts_node_named_child(args, i); |
| 1190 | + TSNode inner = arg; |
| 1191 | + if (strcmp(ts_node_type(arg), "argument") == 0 && ts_node_named_child_count(arg) > 0) { |
| 1192 | + inner = ts_node_named_child(arg, 0); |
| 1193 | + } |
| 1194 | + if (strcmp(ts_node_type(inner), "string") == 0 || |
| 1195 | + strcmp(ts_node_type(inner), "encapsed_string") == 0) { |
| 1196 | + char *txt = cbm_node_text(a, inner, source); |
| 1197 | + if (txt && txt[0]) { |
| 1198 | + size_t len = strlen(txt); |
| 1199 | + if (len >= 2 && (txt[0] == '\'' || txt[0] == '"')) { |
| 1200 | + txt[len - 1] = '\0'; |
| 1201 | + txt++; |
| 1202 | + } |
| 1203 | + return txt[0] ? txt : NULL; |
| 1204 | + } |
| 1205 | + } |
| 1206 | + } |
| 1207 | + return NULL; |
| 1208 | +} |
| 1209 | + |
| 1210 | +static const char *php_group_prefix_for_call(CBMArena *a, TSNode node, const char *source) { |
| 1211 | + const char *parts[PHP_PREFIX_PARTS_MAX]; |
| 1212 | + int part_count = 0; |
| 1213 | + TSNode cur = ts_node_parent(node); |
| 1214 | + for (int depth = 0; depth < PHP_GROUP_WALK_MAX && !ts_node_is_null(cur); depth++) { |
| 1215 | + if (strcmp(ts_node_type(cur), "anonymous_function") == 0 || |
| 1216 | + strcmp(ts_node_type(cur), "arrow_function") == 0) { |
| 1217 | + /* Is this closure the argument of a ->group(...) call? Walk to |
| 1218 | + * the enclosing call and check its method name. */ |
| 1219 | + TSNode p = ts_node_parent(cur); |
| 1220 | + while (!ts_node_is_null(p) && strcmp(ts_node_type(p), "member_call_expression") != 0 && |
| 1221 | + strcmp(ts_node_type(p), "scoped_call_expression") != 0) { |
| 1222 | + if (strcmp(ts_node_type(p), "statement") == 0 || |
| 1223 | + strcmp(ts_node_type(p), "expression_statement") == 0) { |
| 1224 | + break; /* left the argument position */ |
| 1225 | + } |
| 1226 | + p = ts_node_parent(p); |
| 1227 | + } |
| 1228 | + if (!ts_node_is_null(p) && (strcmp(ts_node_type(p), "member_call_expression") == 0 || |
| 1229 | + strcmp(ts_node_type(p), "scoped_call_expression") == 0)) { |
| 1230 | + TSNode gname = ts_node_child_by_field_name(p, TS_FIELD("name")); |
| 1231 | + char *gtxt = ts_node_is_null(gname) ? NULL : cbm_node_text(a, gname, source); |
| 1232 | + if (gtxt && strcmp(gtxt, "group") == 0) { |
| 1233 | + /* Scan the receiver chain for prefix('...'). */ |
| 1234 | + TSNode recv = ts_node_child_by_field_name(p, TS_FIELD("object")); |
| 1235 | + for (int hops = 0; hops < PHP_GROUP_WALK_MAX && !ts_node_is_null(recv); |
| 1236 | + hops++) { |
| 1237 | + const char *rk = ts_node_type(recv); |
| 1238 | + if (strcmp(rk, "member_call_expression") == 0 || |
| 1239 | + strcmp(rk, "scoped_call_expression") == 0) { |
| 1240 | + TSNode rname = ts_node_child_by_field_name(recv, TS_FIELD("name")); |
| 1241 | + char *rtxt = |
| 1242 | + ts_node_is_null(rname) ? NULL : cbm_node_text(a, rname, source); |
| 1243 | + if (rtxt && strcmp(rtxt, "prefix") == 0) { |
| 1244 | + const char *pf = php_chain_prefix_arg(a, recv, source); |
| 1245 | + if (pf && part_count < PHP_PREFIX_PARTS_MAX) { |
| 1246 | + parts[part_count++] = pf; /* inner-first */ |
| 1247 | + } |
| 1248 | + break; |
| 1249 | + } |
| 1250 | + recv = ts_node_child_by_field_name(recv, TS_FIELD("object")); |
| 1251 | + } else { |
| 1252 | + break; |
| 1253 | + } |
| 1254 | + } |
| 1255 | + } |
| 1256 | + } |
| 1257 | + } |
| 1258 | + cur = ts_node_parent(cur); |
| 1259 | + } |
| 1260 | + if (part_count == 0) { |
| 1261 | + return NULL; |
| 1262 | + } |
| 1263 | + /* parts[] is inner-first; compose outer-first. Ensure exactly one '/' |
| 1264 | + * between segments and a leading '/'. */ |
| 1265 | + char buf[CBM_SZ_256]; |
| 1266 | + size_t pos = 0; |
| 1267 | + for (int i = part_count - 1; i >= 0; i--) { |
| 1268 | + const char *seg = parts[i]; |
| 1269 | + while (*seg == '/') { |
| 1270 | + seg++; |
| 1271 | + } |
| 1272 | + size_t sl = strlen(seg); |
| 1273 | + if (sl == 0) { |
| 1274 | + continue; |
| 1275 | + } |
| 1276 | + if (pos + sl + 2 >= sizeof(buf)) { |
| 1277 | + return NULL; /* oversized — leave path un-prefixed */ |
| 1278 | + } |
| 1279 | + buf[pos++] = '/'; |
| 1280 | + memcpy(buf + pos, seg, sl); |
| 1281 | + pos += sl; |
| 1282 | + while (pos > 1 && buf[pos - 1] == '/') { |
| 1283 | + pos--; /* strip trailing slash per segment */ |
| 1284 | + } |
| 1285 | + } |
| 1286 | + buf[pos] = '\0'; |
| 1287 | + return pos ? cbm_arena_strndup(a, buf, pos) : NULL; |
| 1288 | +} |
| 1289 | + |
1168 | 1290 | static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) { |
1169 | 1291 | // Lean 4: skip type-position applies |
1170 | 1292 | if (lang == CBM_LANG_LEAN && strcmp(ts_node_type(node), "apply") == 0) { |
@@ -1208,6 +1330,31 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C |
1208 | 1330 | } |
1209 | 1331 | } |
1210 | 1332 |
|
| 1333 | + /* #952: PHP facade route registrations (`Route::get(...)`, a |
| 1334 | + * scoped_call_expression) must carry the scope in the callee text — the |
| 1335 | + * empty-resolution route fallback keys on the "::get" suffix table, and |
| 1336 | + * the bare "get" that generic field resolution would return deliberately |
| 1337 | + * never matches (every $obj->get() would become a route). Runs BEFORE |
| 1338 | + * field-based resolution, which short-circuits on the `name` field. |
| 1339 | + * Gated to the literal `Route` scope AND a route-method match: any other |
| 1340 | + * scope (Cache::get) would suffix-match "::get" too and mint junk routes |
| 1341 | + * from slash-prefixed keys. Known limitation: aliased facade imports are |
| 1342 | + * not recognized. */ |
| 1343 | + if (lang == CBM_LANG_PHP && strcmp(ts_node_type(node), "scoped_call_expression") == 0) { |
| 1344 | + TSNode scope = ts_node_child_by_field_name(node, TS_FIELD("scope")); |
| 1345 | + TSNode mname = ts_node_child_by_field_name(node, TS_FIELD("name")); |
| 1346 | + if (!ts_node_is_null(scope) && !ts_node_is_null(mname)) { |
| 1347 | + char *sc = cbm_node_text(a, scope, source); |
| 1348 | + char *mn = cbm_node_text(a, mname, source); |
| 1349 | + if (sc && mn && strcmp(sc, "Route") == 0) { |
| 1350 | + char *qual = cbm_arena_sprintf(a, "%s::%s", sc, mn); |
| 1351 | + if (qual && cbm_service_pattern_route_method(qual) != NULL) { |
| 1352 | + return qual; |
| 1353 | + } |
| 1354 | + } |
| 1355 | + } |
| 1356 | + } |
| 1357 | + |
1211 | 1358 | // Try common field-based resolution first |
1212 | 1359 | char *name = extract_callee_from_fields(a, node, source); |
1213 | 1360 | if (name) { |
@@ -1919,6 +2066,24 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk |
1919 | 2066 | TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); |
1920 | 2067 | if (!ts_node_is_null(args)) { |
1921 | 2068 | call.first_string_arg = extract_url_or_topic_arg(ctx, args); |
| 2069 | + /* #952: routes registered inside Laravel `prefix()->group()` |
| 2070 | + * closures must carry the composed path — the resolve passes |
| 2071 | + * only see the flat CBMCall, so the enclosing chain can only |
| 2072 | + * be read here where the AST still exists. */ |
| 2073 | + if (ctx->language == CBM_LANG_PHP && call.first_string_arg && |
| 2074 | + call.first_string_arg[0] == '/' && call.callee_name && |
| 2075 | + cbm_service_pattern_route_method(call.callee_name) != NULL) { |
| 2076 | + const char *gp = php_group_prefix_for_call(ctx->arena, node, ctx->source); |
| 2077 | + if (gp && gp[0]) { |
| 2078 | + const char *rel = call.first_string_arg; |
| 2079 | + while (*rel == '/') { |
| 2080 | + rel++; |
| 2081 | + } |
| 2082 | + call.first_string_arg = |
| 2083 | + rel[0] ? cbm_arena_sprintf(ctx->arena, "%s/%s", gp, rel) |
| 2084 | + : cbm_arena_strndup(ctx->arena, gp, strlen(gp)); |
| 2085 | + } |
| 2086 | + } |
1922 | 2087 | if (call.first_string_arg && call.first_string_arg[0] == '/') { |
1923 | 2088 | call.second_arg_name = extract_handler_arg(ctx, args); |
1924 | 2089 | } |
|
0 commit comments