mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(pipeline): infer HTTP method for Angular and verb-suffix client calls (#2768)
Signed-off-by: Divakant Pandey <divakant01@gmail.com>
This commit is contained in:
1 parent
9f0fdcb400
commit
c548d8e46a
7 files changed
+332
-10
No files matched your search
@@ -46,6 +46,13 @@ static const lib_pattern_t http_libraries[] = {
|
||||
* case-sensitive, so without this entry `Axios.get(url)` fell through to
|
||||
* the `.get` route-suffix fallback and became a route registration. */
|
||||
{"Axios", CBM_SVC_HTTP, NULL},
|
||||
{"@angular/common/http", CBM_SVC_HTTP, NULL},
|
||||
{"angular.common.http", CBM_SVC_HTTP, NULL},
|
||||
{"HttpClient", CBM_SVC_HTTP, NULL},
|
||||
{"httpClient", CBM_SVC_HTTP, NULL},
|
||||
{"this.http", CBM_SVC_HTTP, NULL},
|
||||
{"this.httpClient", CBM_SVC_HTTP, NULL},
|
||||
{"$http", CBM_SVC_HTTP, NULL},
|
||||
{"superagent", CBM_SVC_HTTP, NULL},
|
||||
{"needle", CBM_SVC_HTTP, NULL},
|
||||
{"node-fetch", CBM_SVC_HTTP, NULL},
|
||||
|
||||
@@ -939,15 +939,20 @@ static int calls_emit_unresolved_service(cbm_pipeline_ctx_t *ctx, const CBMCall
|
||||
const char **imp_keys, const char **imp_vals,
|
||||
int imp_count, const char *route_mount, CBMLanguage lang,
|
||||
CBMCallEvidence *evidence) {
|
||||
if (calls_emit_route_call(ctx, call, source_node, module_qn, imp_keys, imp_vals, imp_count,
|
||||
route_mount, lang, evidence)) {
|
||||
return SKIP_ONE;
|
||||
}
|
||||
cbm_svc_kind_t svc = cbm_service_pattern_match(call->callee_name);
|
||||
if (svc == CBM_SVC_NONE && cbm_service_pattern_is_global_fetch(call->callee_name)) {
|
||||
svc = CBM_SVC_HTTP;
|
||||
}
|
||||
return calls_emit_service_call(ctx, call, source_node, svc, NULL, evidence);
|
||||
if (svc == CBM_SVC_HTTP || svc == CBM_SVC_ASYNC) {
|
||||
if (calls_emit_service_call(ctx, call, source_node, svc, NULL, evidence)) {
|
||||
return SKIP_ONE;
|
||||
}
|
||||
}
|
||||
if (calls_emit_route_call(ctx, call, source_node, module_qn, imp_keys, imp_vals, imp_count,
|
||||
route_mount, lang, evidence)) {
|
||||
return SKIP_ONE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Apply the field-call policy after a registry target has been found. */
|
||||
|
||||
@@ -720,6 +720,16 @@ static cr_match_result_t match_http_routes(cbm_store_t *src_store, const char *s
|
||||
char method[CBM_SZ_32] = {0};
|
||||
json_str_prop(props, "url_path", url_path, sizeof(url_path));
|
||||
json_str_prop(props, "method", method, sizeof(method));
|
||||
if (!method[0]) {
|
||||
char callee[CBM_SZ_256] = {0};
|
||||
json_str_prop(props, "callee", callee, sizeof(callee));
|
||||
if (callee[0]) {
|
||||
const char *m = cbm_service_pattern_http_method(callee);
|
||||
if (m) {
|
||||
snprintf(method, sizeof(method), "%s", m);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!url_path[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2591,19 +2591,34 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
|
||||
if (!cbm_service_pattern_is_http_route_literal(norm, call->callee_name)) {
|
||||
continue;
|
||||
}
|
||||
const char *method = cbm_service_pattern_http_method(call->callee_name);
|
||||
const char *prefix = method ? method : "ANY";
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
char cpath[CBM_SZ_256];
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s",
|
||||
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", prefix,
|
||||
cbm_route_canon_path(norm, cpath, sizeof(cpath)));
|
||||
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0,
|
||||
"{\"source\":\"arg_url\"}");
|
||||
char route_props[CBM_SZ_256];
|
||||
if (method) {
|
||||
snprintf(route_props, sizeof(route_props), "{\"source\":\"arg_url\",\"method\":\"%s\"}",
|
||||
method);
|
||||
} else {
|
||||
snprintf(route_props, sizeof(route_props), "{\"source\":\"arg_url\"}");
|
||||
}
|
||||
int64_t route_id =
|
||||
cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0, route_props);
|
||||
char esc_c[CBM_SZ_256];
|
||||
char esc_n[CBM_SZ_256];
|
||||
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
|
||||
cbm_json_escape(esc_n, sizeof(esc_n), norm);
|
||||
char eprops[CBM_SZ_512];
|
||||
snprintf(eprops, sizeof(eprops),
|
||||
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\"}", esc_c, esc_n);
|
||||
if (method) {
|
||||
snprintf(eprops, sizeof(eprops),
|
||||
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\",\"method\":\"%s\"}",
|
||||
esc_c, esc_n, method);
|
||||
} else {
|
||||
snprintf(eprops, sizeof(eprops),
|
||||
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\"}", esc_c, esc_n);
|
||||
}
|
||||
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "HTTP_CALLS", eprops);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -113,6 +113,9 @@ static void route_edge_visitor(const cbm_gbuf_edge_t *edge, void *userdata) {
|
||||
json_extract(edge->properties_json, "method", method_buf, sizeof(method_buf));
|
||||
const char *broker =
|
||||
json_extract(edge->properties_json, "broker", broker_buf, sizeof(broker_buf));
|
||||
if (!method && strcmp(edge->type, "HTTP_CALLS") == 0 && callee) {
|
||||
method = cbm_service_pattern_http_method(callee);
|
||||
}
|
||||
|
||||
/* Build Route QN */
|
||||
char route_qn[CBM_ROUTE_QN_SIZE];
|
||||
|
||||
@@ -944,10 +944,197 @@ TEST(cross_repo_caller_without_local_handler_keeps_cross_http_issue1459) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
static bool cross_repo_seed_server_route(const cross_repo_fixture_t *fixture,
|
||||
const char *target_project, const char *method,
|
||||
const char *route_path, const char *handler_name) {
|
||||
char target_path[512];
|
||||
if (!cross_repo_project_path(fixture, target_project, target_path, sizeof(target_path))) {
|
||||
return false;
|
||||
}
|
||||
cbm_store_t *target = cbm_store_open_path(target_path);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
bool ok = cbm_store_upsert_project(target, target_project, fixture->cache) == CBM_STORE_OK;
|
||||
char target_route_qn[256];
|
||||
char handler_qn[256];
|
||||
char route_name[128];
|
||||
snprintf(target_route_qn, sizeof(target_route_qn), "__route__%s__%s", method, route_path);
|
||||
snprintf(handler_qn, sizeof(handler_qn), "%s.%s", target_project, handler_name);
|
||||
snprintf(route_name, sizeof(route_name), "%s %s", method, route_path);
|
||||
|
||||
cbm_node_t target_route = {.project = target_project,
|
||||
.label = "Route",
|
||||
.name = route_name,
|
||||
.qualified_name = target_route_qn,
|
||||
.file_path = "server.java"};
|
||||
cbm_node_t handler = {.project = target_project,
|
||||
.label = "Function",
|
||||
.name = handler_name,
|
||||
.qualified_name = handler_qn,
|
||||
.file_path = "server.java"};
|
||||
int64_t target_route_id = ok ? cbm_store_upsert_node(target, &target_route) : 0;
|
||||
int64_t handler_id = ok ? cbm_store_upsert_node(target, &handler) : 0;
|
||||
cbm_edge_t handles = {.project = target_project,
|
||||
.source_id = handler_id,
|
||||
.target_id = target_route_id,
|
||||
.type = "HANDLES"};
|
||||
ok = ok && target_route_id > 0 && handler_id > 0 && cbm_store_insert_edge(target, &handles) > 0;
|
||||
cbm_store_close(target);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool cross_repo_seed_client_call(const cross_repo_fixture_t *fixture,
|
||||
const char *source_project, const char *caller_name,
|
||||
const char *callee_name, const char *url_path,
|
||||
const char *method) {
|
||||
char source_path[512];
|
||||
if (!cross_repo_project_path(fixture, source_project, source_path, sizeof(source_path))) {
|
||||
return false;
|
||||
}
|
||||
cbm_store_t *source = cbm_store_open_path(source_path);
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
bool ok = cbm_store_upsert_project(source, source_project, fixture->cache) == CBM_STORE_OK;
|
||||
char caller_qn[256];
|
||||
char local_route_qn[256];
|
||||
char edge_props[512];
|
||||
snprintf(caller_qn, sizeof(caller_qn), "%s.%s", source_project, caller_name);
|
||||
snprintf(local_route_qn, sizeof(local_route_qn), "__route__%s__%s", method ? method : "ANY",
|
||||
url_path);
|
||||
if (method) {
|
||||
snprintf(edge_props, sizeof(edge_props),
|
||||
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"method\":\"%s\"}", callee_name,
|
||||
url_path, method);
|
||||
} else {
|
||||
snprintf(edge_props, sizeof(edge_props),
|
||||
"{\"callee\":\"%s\",\"url_path\":\"%s\"}", callee_name, url_path);
|
||||
}
|
||||
cbm_node_t caller = {.project = source_project,
|
||||
.label = "Function",
|
||||
.name = caller_name,
|
||||
.qualified_name = caller_qn,
|
||||
.file_path = "item.service.ts"};
|
||||
cbm_node_t local_route = {.project = source_project,
|
||||
.label = "Route",
|
||||
.name = url_path,
|
||||
.qualified_name = local_route_qn,
|
||||
.file_path = "item.service.ts"};
|
||||
int64_t caller_id = ok ? cbm_store_upsert_node(source, &caller) : 0;
|
||||
int64_t local_route_id = ok ? cbm_store_upsert_node(source, &local_route) : 0;
|
||||
cbm_edge_t http_call = {.project = source_project,
|
||||
.source_id = caller_id,
|
||||
.target_id = local_route_id,
|
||||
.type = "HTTP_CALLS",
|
||||
.properties_json = edge_props};
|
||||
ok = ok && caller_id > 0 && local_route_id > 0 && cbm_store_insert_edge(source, &http_call) > 0;
|
||||
cbm_store_close(source);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int cross_repo_count_edges_like(const cross_repo_fixture_t *fixture, const char *project,
|
||||
const char *edge_type, const char *pattern) {
|
||||
char path[512];
|
||||
if (!cross_repo_project_path(fixture, project, path, sizeof(path))) {
|
||||
return -1;
|
||||
}
|
||||
cbm_store_t *store = cbm_store_open_path_query(path);
|
||||
if (!store) {
|
||||
return -1;
|
||||
}
|
||||
sqlite3_stmt *s = NULL;
|
||||
int count = -1;
|
||||
if (sqlite3_prepare_v2(
|
||||
cbm_store_get_db(store),
|
||||
"SELECT COUNT(*) FROM edges WHERE project = ?1 AND type = ?2 AND properties LIKE ?3",
|
||||
-1, &s, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(s, 1, project, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(s, 2, edge_type, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(s, 3, pattern, -1, SQLITE_STATIC);
|
||||
if (sqlite3_step(s) == SQLITE_ROW) {
|
||||
count = sqlite3_column_int(s, 0);
|
||||
}
|
||||
sqlite3_finalize(s);
|
||||
}
|
||||
cbm_store_close(store);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* #2768: Angular HttpClient calls must match cross-repo routes with exact HTTP verbs.
|
||||
* When client makes GET requests to /items/list, /items/42, and /items/job/10,
|
||||
* and the server defines overlapping routes:
|
||||
* DELETE /items/{id} (seeded first in DB)
|
||||
* GET /items/{id} (seeded second in DB)
|
||||
* POST /items/{parentId}/{id} (seeded first in DB)
|
||||
* GET /items/job/{jobId} (seeded second in DB)
|
||||
* the fuzzy route matcher must enforce method compatibility, linking the GET calls
|
||||
* to their respective GET endpoints and never to DELETE or POST endpoints. */
|
||||
TEST(cross_repo_angular_http_client_exact_verb_matching_issue2768) {
|
||||
cross_repo_fixture_t fixture;
|
||||
bool setup = cross_repo_fixture_begin(&fixture) &&
|
||||
/* Target server with overlapping endpoints */
|
||||
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/list",
|
||||
"handle_list_items") &&
|
||||
cross_repo_seed_server_route(&fixture, "api-2768", "DELETE", "/items/{}",
|
||||
"handle_delete_item") &&
|
||||
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/{}",
|
||||
"handle_get_item") &&
|
||||
cross_repo_seed_server_route(&fixture, "api-2768", "POST", "/items/{}/{}",
|
||||
"handle_post_nested_item") &&
|
||||
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/job/{}",
|
||||
"handle_get_job") &&
|
||||
/* Angular client making GET requests */
|
||||
cross_repo_seed_client_call(&fixture, "angular-2768", "getItems",
|
||||
"this.http.get", "/items/list", "GET") &&
|
||||
cross_repo_seed_client_call(&fixture, "angular-2768", "getItem",
|
||||
"this.http.get", "/items/42", "GET") &&
|
||||
cross_repo_seed_client_call(&fixture, "angular-2768", "getJob",
|
||||
"this.http.get", "/items/job/10", "GET");
|
||||
if (!setup) {
|
||||
cross_repo_fixture_end(&fixture);
|
||||
FAIL("failed to seed issue 2768 fixture");
|
||||
}
|
||||
|
||||
const char *target = "api-2768";
|
||||
cbm_cross_repo_result_t fwd = cbm_cross_repo_match("angular-2768", &target, 1);
|
||||
ASSERT_FALSE(fwd.failed);
|
||||
ASSERT_EQ(fwd.http_edges, 3);
|
||||
|
||||
/* Assert that client CROSS_HTTP_CALLS edges are linked to GET handlers, not DELETE or POST */
|
||||
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
|
||||
"%handle_delete_item%"),
|
||||
0);
|
||||
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
|
||||
"%handle_post_nested_item%"),
|
||||
0);
|
||||
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
|
||||
"%handle_list_items%"),
|
||||
1);
|
||||
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
|
||||
"%handle_get_item%"),
|
||||
1);
|
||||
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
|
||||
"%handle_get_job%"),
|
||||
1);
|
||||
|
||||
/* Reverse pass */
|
||||
const char *consumer = "angular-2768";
|
||||
cbm_cross_repo_result_t rev = cbm_cross_repo_match("api-2768", &consumer, 1);
|
||||
ASSERT_FALSE(rev.failed);
|
||||
ASSERT_EQ(rev.http_edges, 3);
|
||||
|
||||
cross_repo_fixture_end(&fixture);
|
||||
cbm_cross_repo_result_free(&fwd);
|
||||
cbm_cross_repo_result_free(&rev);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(cross_repo) {
|
||||
RUN_TEST(cross_repo_caller_local_handler_blocks_cross_http_issue1459);
|
||||
RUN_TEST(cross_repo_methodless_call_to_local_get_route_blocks_cross_http_issue1459);
|
||||
RUN_TEST(cross_repo_caller_without_local_handler_keeps_cross_http_issue1459);
|
||||
RUN_TEST(cross_repo_angular_http_client_exact_verb_matching_issue2768);
|
||||
RUN_TEST(cross_repo_accepts_project_with_missed_shadow_row_issue1609);
|
||||
RUN_TEST(cross_repo_null_target_fails_without_dereference);
|
||||
RUN_TEST(cross_repo_wildcard_keeps_projects_containing_internal_tokens);
|
||||
|
||||
@@ -14939,6 +14939,99 @@ TEST(pipeline_typed_http_call_has_no_arg_url_twin_sequential) {
|
||||
return typed_http_no_url_twin_case(0);
|
||||
}
|
||||
|
||||
/* Issue #2768: Angular HttpClient calls (this.http.get, this.httpClient.post,
|
||||
* this.http.delete) emit HTTP_CALLS edges typed with the method verb, and are
|
||||
* never misclassified as route registrations. */
|
||||
static int angular_http_client_typed_case(int fillers) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_angular_http_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
write_temp_file(tmp, "src/item.service.ts",
|
||||
"import { Injectable } from '@angular/core';\n"
|
||||
"import { HttpClient } from '@angular/common/http';\n"
|
||||
"\n"
|
||||
"@Injectable({ providedIn: 'root' })\n"
|
||||
"export class ItemService {\n"
|
||||
" constructor(private http: HttpClient) {}\n"
|
||||
"\n"
|
||||
" getItems() {\n"
|
||||
" return this.http.get('/items/list');\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" getItem(id: string) {\n"
|
||||
" return this.http.get(`/items/${id}`);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" createItem(item: any) {\n"
|
||||
" return this.http.post('/items', item);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" deleteItem(id: string) {\n"
|
||||
" return this.http.delete(`/items/${id}`);\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
|
||||
for (int i = 0; i < fillers; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "src/filler%d.ts", i);
|
||||
snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i,
|
||||
i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/angular_http.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 exact HTTP_CALLS with proper methods */
|
||||
ASSERT_EQ(http_calls_to_route(s, project, "getItems", "__route__GET__/items/list"), 1);
|
||||
ASSERT_EQ(http_calls_to_route(s, project, "getItem", "__route__GET__/items/{}"), 1);
|
||||
ASSERT_EQ(http_calls_to_route(s, project, "createItem", "__route__POST__/items"), 1);
|
||||
ASSERT_EQ(http_calls_to_route(s, project, "deleteItem", "__route__DELETE__/items/{}"), 1);
|
||||
|
||||
/* Assert no ANY twins */
|
||||
ASSERT_EQ(http_calls_to_route(s, project, "getItems", "__route__ANY__"), 0);
|
||||
ASSERT_EQ(http_calls_to_route(s, project, "getItem", "__route__ANY__"), 0);
|
||||
ASSERT_EQ(http_calls_to_route(s, project, "createItem", "__route__ANY__"), 0);
|
||||
ASSERT_EQ(http_calls_to_route(s, project, "deleteItem", "__route__ANY__"), 0);
|
||||
|
||||
/* Angular service must NOT emit HANDLES edges (not a server route registration) */
|
||||
ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HANDLES"), 0);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_angular_http_client_typed_parallel) {
|
||||
return angular_http_client_typed_case(52);
|
||||
}
|
||||
|
||||
TEST(pipeline_angular_http_client_typed_sequential) {
|
||||
return angular_http_client_typed_case(0);
|
||||
}
|
||||
|
||||
/* #706 / #1147 end to end: a URL composed from module constants reaches the
|
||||
* HTTP classifier, so the client call forms an HTTP_CALLS edge. Python
|
||||
* `API_URL = API_BASE_URL + "/orders/create"` (env-derived base, literal path
|
||||
@@ -29383,6 +29476,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http);
|
||||
RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin);
|
||||
RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin_sequential);
|
||||
RUN_TEST(pipeline_angular_http_client_typed_parallel);
|
||||
RUN_TEST(pipeline_angular_http_client_typed_sequential);
|
||||
RUN_TEST(pipeline_url_constant_folding_http_calls_issue706_1147);
|
||||
RUN_TEST(pipeline_folded_topics_preserve_identity_sequential_parallel);
|
||||
RUN_TEST(pipeline_relative_folded_wrapper_url_candidates);
|
||||
|
||||
Reference in new issue
Block a user