mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 18:58:00 +00:00
Merge branch 'main' into fix/cypher-coalesce-where-874
This commit is contained in:
14 files changed
+618
-38
No files matched your search
+88
-29
@@ -1317,7 +1317,8 @@ static const char *annotation_route_method(const char *name) {
|
||||
}
|
||||
/* JAX-RS bare-verb annotations (@GET/@POST/...) — path comes from @Path. */
|
||||
if (strcmp(name, "GET") == 0 || strcmp(name, "POST") == 0 || strcmp(name, "PUT") == 0 ||
|
||||
strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0) {
|
||||
strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0 || strcmp(name, "HEAD") == 0 ||
|
||||
strcmp(name, "OPTIONS") == 0) {
|
||||
return name;
|
||||
}
|
||||
return NULL;
|
||||
@@ -1626,35 +1627,85 @@ static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Scan the annotation nodes nested in a JVM/C# `modifiers`/`attribute_list`
|
||||
* wrapper (and direct children) for a route-mapping annotation. Java/Kotlin
|
||||
* Spring annotations (@GetMapping, @RequestMapping, ...) live here rather than
|
||||
* as prev-siblings, so the prev-sibling decorator walk never sees them. */
|
||||
static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source,
|
||||
const CBMLangSpec *spec, const char **out_path,
|
||||
const char **out_method) {
|
||||
TSNode modifiers = find_jvm_modifiers(func_node, spec->language);
|
||||
/* Scan ALL annotation nodes nested in a JVM/C# `modifiers`/`attribute_list`
|
||||
* wrapper (and direct children), collecting route information from the whole
|
||||
* set instead of stopping at the first mapping annotation. Java/Kotlin Spring
|
||||
* annotations (@GetMapping, @RequestMapping, ...) live here rather than as
|
||||
* prev-siblings, so the prev-sibling decorator walk never sees them.
|
||||
*
|
||||
* JAX-RS splits the route across two annotations: the verb comes from a bare
|
||||
* @GET/@POST/... and the path from a sibling @Path("..."). Returning on the
|
||||
* first mapping annotation therefore dropped every method-level @Path
|
||||
* (the @GET matched first, defaulted the path to "/", and @Path was never
|
||||
* read), and class-level @Path prefixes were never recognized at all. */
|
||||
static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source,
|
||||
const CBMLangSpec *spec, const char **out_map_path,
|
||||
const char **out_method, const char **out_jax_path) {
|
||||
*out_map_path = NULL;
|
||||
*out_method = NULL;
|
||||
*out_jax_path = NULL;
|
||||
|
||||
TSNode wrappers[2];
|
||||
int wn = 0;
|
||||
TSNode modifiers = find_jvm_modifiers(owner, spec->language);
|
||||
if (!ts_node_is_null(modifiers)) {
|
||||
uint32_t mc = ts_node_child_count(modifiers);
|
||||
for (uint32_t mi = 0; mi < mc; mi++) {
|
||||
TSNode mchild = ts_node_child(modifiers, mi);
|
||||
if (cbm_kind_in_set(mchild, spec->decorator_node_types) &&
|
||||
try_route_from_annotation(a, mchild, source, out_path, out_method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
wrappers[wn++] = modifiers;
|
||||
}
|
||||
/* Direct-child annotations (some grammars attach the annotation as a child
|
||||
* of the method node rather than under `modifiers`). */
|
||||
uint32_t cc = ts_node_child_count(func_node);
|
||||
for (uint32_t ci = 0; ci < cc; ci++) {
|
||||
TSNode child = ts_node_child(func_node, ci);
|
||||
if (cbm_kind_in_set(child, spec->decorator_node_types) &&
|
||||
try_route_from_annotation(a, child, source, out_path, out_method)) {
|
||||
return true;
|
||||
wrappers[wn++] = owner;
|
||||
|
||||
for (int w = 0; w < wn; w++) {
|
||||
uint32_t cc = ts_node_child_count(wrappers[w]);
|
||||
for (uint32_t ci = 0; ci < cc; ci++) {
|
||||
TSNode child = ts_node_child(wrappers[w], ci);
|
||||
if (!cbm_kind_in_set(child, spec->decorator_node_types)) {
|
||||
continue;
|
||||
}
|
||||
TSNode name_node = annotation_name_node(child);
|
||||
if (ts_node_is_null(name_node)) {
|
||||
continue;
|
||||
}
|
||||
char *name = cbm_node_text(a, name_node, source);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
if (!*out_jax_path && strcmp(name, "Path") == 0) {
|
||||
TSNode args = annotation_args_node(child);
|
||||
if (!ts_node_is_null(args)) {
|
||||
*out_jax_path = extract_route_path_from_args(a, args, source);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!*out_method) {
|
||||
const char *method = annotation_route_method(name);
|
||||
if (method) {
|
||||
*out_method = method;
|
||||
TSNode args = annotation_args_node(child);
|
||||
if (!ts_node_is_null(args)) {
|
||||
*out_map_path = extract_route_path_from_args(a, args, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source,
|
||||
const CBMLangSpec *spec, const char **out_path,
|
||||
const char **out_method) {
|
||||
const char *map_path = NULL;
|
||||
const char *method = NULL;
|
||||
const char *jax_path = NULL;
|
||||
scan_route_annotations(a, func_node, source, spec, &map_path, &method, &jax_path);
|
||||
/* Method-level routes still require a verb/mapping annotation; a lone
|
||||
* @Path (JAX-RS sub-resource locator) is not an endpoint by itself. */
|
||||
if (!method) {
|
||||
return false;
|
||||
}
|
||||
*out_method = method;
|
||||
*out_path = map_path ? map_path : (jax_path ? jax_path : "/");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const char *source,
|
||||
@@ -1723,10 +1774,18 @@ static const char *join_route_paths(CBMArena *a, const char *prefix, const char
|
||||
|
||||
static const char *spring_class_route_prefix(CBMArena *a, TSNode class_node, const char *source,
|
||||
const CBMLangSpec *spec) {
|
||||
const char *prefix = NULL;
|
||||
const char *map_path = NULL;
|
||||
const char *method = NULL;
|
||||
if (extract_route_from_annotations(a, class_node, source, spec, &prefix, &method)) {
|
||||
return prefix;
|
||||
const char *jax_path = NULL;
|
||||
scan_route_annotations(a, class_node, source, spec, &map_path, &method, &jax_path);
|
||||
if (map_path) {
|
||||
return map_path; /* @RequestMapping("/api") and friends */
|
||||
}
|
||||
if (jax_path) {
|
||||
return jax_path; /* JAX-RS class-level @Path("/api") carries no verb */
|
||||
}
|
||||
if (method) {
|
||||
return "/"; /* mapping annotation without a path argument */
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -5901,8 +5960,8 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
|
||||
* For the nested case, the child has no "type" field directly. Detect by
|
||||
* walking named children for a variable_declaration. */
|
||||
TSNode type_node = ts_node_child_by_field_name(child, TS_FIELD("type"));
|
||||
TSNode name_node =
|
||||
ts_node_is_null(type_node) ? (TSNode){0} : resolve_field_name_node(child);
|
||||
TSNode empty_node = {0};
|
||||
TSNode name_node = ts_node_is_null(type_node) ? empty_node : resolve_field_name_node(child);
|
||||
|
||||
if (ts_node_is_null(type_node)) {
|
||||
uint32_t cnc = ts_node_named_child_count(child);
|
||||
|
||||
+17
-4
@@ -1793,10 +1793,23 @@ static bool db_internal_project_name(const char *full_path, char *name_out, size
|
||||
cbm_project_t *projs = NULL;
|
||||
int n = 0;
|
||||
bool ok = false;
|
||||
if (cbm_store_list_projects(st, &projs, &n) == CBM_STORE_OK && n == 1 && projs[0].name &&
|
||||
projs[0].name[0]) {
|
||||
snprintf(name_out, name_sz, "%s", projs[0].name);
|
||||
ok = true;
|
||||
if (cbm_store_list_projects(st, &projs, &n) == CBM_STORE_OK) {
|
||||
/* Ignore internal shadow projects ("<name>::missed" miss-graph rows):
|
||||
* they share the db with the primary project and must not make it
|
||||
* unresolvable — requiring n == 1 over ALL rows made every project
|
||||
* with a miss graph vanish from list_projects and the UI (#1044). */
|
||||
int primary = -1;
|
||||
int primary_count = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (projs[i].name && projs[i].name[0] && !strstr(projs[i].name, "::")) {
|
||||
primary = i;
|
||||
primary_count++;
|
||||
}
|
||||
}
|
||||
if (primary_count == 1) {
|
||||
snprintf(name_out, name_sz, "%s", projs[primary].name);
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
cbm_store_free_projects(projs, n);
|
||||
if (ok && out_store) {
|
||||
|
||||
+159
-2
@@ -426,8 +426,12 @@ static void parse_pyproject_toml(const char *source, int source_len, const char
|
||||
pkg_entries_push(entries, name, entry);
|
||||
|
||||
/* Also register <name>/__init__ as alternative (no src/ prefix) */
|
||||
snprintf(suffix, sizeof(suffix), "%s/__init__", name_copy);
|
||||
char *alt_entry = build_entry_path(rel_path, suffix);
|
||||
char *alt_entry = NULL;
|
||||
if (name_copy) {
|
||||
snprintf(suffix, sizeof(suffix), "%s/__init__", name_copy);
|
||||
alt_entry = build_entry_path(rel_path, suffix);
|
||||
}
|
||||
|
||||
if (name_copy && alt_entry) {
|
||||
pkg_entries_push(entries, name_copy, alt_entry);
|
||||
} else {
|
||||
@@ -1265,6 +1269,151 @@ static bool import_targetable_label(const char *label) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char *path_leaf(const char *path) {
|
||||
const char *leaf = path;
|
||||
for (const char *p = path; p && *p; p++) {
|
||||
if (*p == '/' || *p == '\\') {
|
||||
leaf = p + 1;
|
||||
}
|
||||
}
|
||||
return leaf;
|
||||
}
|
||||
|
||||
static bool is_header_include(const char *path) {
|
||||
if (!path || !path[0]) {
|
||||
return false;
|
||||
}
|
||||
static const char *header_exts[] = {".h", ".hh", ".hpp", ".hxx", ".inc", ".inl", ".ipp", NULL};
|
||||
for (const char **ext = header_exts; *ext; ext++) {
|
||||
size_t path_len = strlen(path);
|
||||
size_t ext_len = strlen(*ext);
|
||||
if (path_len > ext_len && strcmp(path + path_len - ext_len, *ext) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_c_family_source(const char *source_rel) {
|
||||
if (!source_rel || !source_rel[0]) {
|
||||
return false;
|
||||
}
|
||||
static const char *exts[] = {".c", ".cc", ".cpp", ".cxx", ".c++",
|
||||
".h", ".hh", ".hpp", ".hxx", NULL};
|
||||
for (const char **ext = exts; *ext; ext++) {
|
||||
size_t path_len = strlen(source_rel);
|
||||
size_t ext_len = strlen(*ext);
|
||||
if (path_len > ext_len && strcmp(source_rel + path_len - ext_len, *ext) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static const cbm_gbuf_node_t *resolve_exact_file_node(const cbm_pipeline_ctx_t *ctx,
|
||||
const char *file_path,
|
||||
const char *source_file_qn) {
|
||||
if (!ctx || !file_path || !file_path[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *leaf = path_leaf(file_path);
|
||||
if (!leaf || !leaf[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char stem[PKGMAP_PATH_BUF];
|
||||
snprintf(stem, sizeof(stem), "%s", leaf);
|
||||
char *last_dot = strrchr(stem, '.');
|
||||
if (last_dot && last_dot != stem) {
|
||||
*last_dot = '\0';
|
||||
}
|
||||
|
||||
const char *names[2] = {stem[0] ? stem : NULL, leaf};
|
||||
const cbm_gbuf_node_t *best = NULL;
|
||||
for (int ni = 0; ni < 2; ni++) {
|
||||
const char *name = names[ni];
|
||||
if (!name || !name[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cbm_gbuf_node_t **hits = NULL;
|
||||
int hit_count = 0;
|
||||
if (cbm_gbuf_find_by_name(ctx->gbuf, name, &hits, &hit_count) != 0 || !hits) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < hit_count; i++) {
|
||||
const cbm_gbuf_node_t *cand = hits[i];
|
||||
if (!cand || !cand->file_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Tie-breaker: Ensure the candidate's actual file_path ends with the
|
||||
* specific include path requested (e.g., 'a/util.h' instead of just 'util.h'). */
|
||||
if (!ends_with(cand->file_path, file_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cand->label || !import_targetable_label(cand->label)) {
|
||||
continue;
|
||||
}
|
||||
if (source_file_qn && cand->qualified_name &&
|
||||
strcmp(cand->qualified_name, source_file_qn) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(cand->label, "File") == 0) {
|
||||
return cand;
|
||||
}
|
||||
if (!best) {
|
||||
best = cand;
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
return best;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const cbm_gbuf_node_t *resolve_header_include(const cbm_pipeline_ctx_t *ctx,
|
||||
const char *source_rel,
|
||||
const char *source_file_qn,
|
||||
const char *module_path) {
|
||||
if (!is_c_family_source(source_rel) || !is_header_include(module_path)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *base = module_path;
|
||||
if (base[0] == '.' && base[1] == '/') {
|
||||
base += 2;
|
||||
}
|
||||
|
||||
const cbm_gbuf_node_t *exact = resolve_exact_file_node(ctx, base, source_file_qn);
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
|
||||
if (source_rel && source_rel[0]) {
|
||||
char *dir = path_dirname(source_rel);
|
||||
if (dir) {
|
||||
char candidate[PKGMAP_PATH_BUF];
|
||||
if (dir[0]) {
|
||||
snprintf(candidate, sizeof(candidate), "%s/%s", dir, base);
|
||||
} else {
|
||||
snprintf(candidate, sizeof(candidate), "%s", base);
|
||||
}
|
||||
free(dir);
|
||||
exact = resolve_exact_file_node(ctx, candidate, source_file_qn);
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Resolve a sibling-file import: a bare path/name (no leading "./") that names
|
||||
* a file relative to the importer's directory. This covers build/markup
|
||||
* grammars whose import string is a sibling filename or directory rather than a
|
||||
@@ -1352,6 +1501,14 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Prefer exact header-file nodes for C/C++ includes so same-stem source or
|
||||
* module nodes do not steal the edge target. */
|
||||
const cbm_gbuf_node_t *header_target =
|
||||
resolve_header_include(ctx, source_rel, source_file_qn, imp->module_path);
|
||||
if (header_target) {
|
||||
return header_target;
|
||||
}
|
||||
|
||||
/* Strategy 1: module-path resolution → existing node (Python/TS/Go).
|
||||
* No label filter here: directory-module languages (Go/Java packages)
|
||||
* legitimately resolve straight to a Folder node -- that's the intended,
|
||||
|
||||
@@ -885,8 +885,17 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
|
||||
for (int i = 0; i < ci; i++) {
|
||||
char *file_qn = cbm_pipeline_fqn_compute(project, changed_files[i].rel_path, "__file__");
|
||||
if (file_qn) {
|
||||
cbm_gbuf_upsert_node(existing, "File", changed_files[i].rel_path, file_qn,
|
||||
changed_files[i].rel_path, 0, 0, "{}");
|
||||
/* #994: the name must be the BASENAME with extension props,
|
||||
* mirroring the full build's File node (pipeline.c) — upserts
|
||||
* match by QN, so any other name here renames the node in place
|
||||
* and the incremental graph diverges from a full build. */
|
||||
const char *rel = changed_files[i].rel_path;
|
||||
const char *slash = strrchr(rel, '/');
|
||||
const char *basename = slash ? slash + SKIP_ONE : rel;
|
||||
char props[CBM_SZ_256];
|
||||
const char *ext = strrchr(basename, '.');
|
||||
snprintf(props, sizeof(props), "{\"extension\":\"%s\"}", ext ? ext : "");
|
||||
cbm_gbuf_upsert_node(existing, "File", basename, file_qn, rel, 0, 0, props);
|
||||
free(file_qn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "NodeController.h"
|
||||
|
||||
int node_controller_value() {
|
||||
return 1;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
struct NodeController {
|
||||
int value;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "SystemController.h"
|
||||
|
||||
int system_controller_value() {
|
||||
return 2;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
struct SystemController {
|
||||
int value;
|
||||
};
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
#include "NodeController.h"
|
||||
#include <SystemController.h>
|
||||
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
||||
@@ -72,6 +72,10 @@ typedef struct {
|
||||
const char *content;
|
||||
} EILangFile;
|
||||
|
||||
typedef struct {
|
||||
const char *name; /* fixture filename relative to a checked-in fixture root */
|
||||
} EILangFixtureFile;
|
||||
|
||||
static void ei_to_fwd_slashes(char *p) {
|
||||
for (; *p; p++) {
|
||||
if (*p == '\\') *p = '/';
|
||||
@@ -123,6 +127,89 @@ static cbm_store_t *ei_index_files(EILangProj *lp, const EILangFile *files, int
|
||||
return cbm_store_open_path(lp->dbpath);
|
||||
}
|
||||
|
||||
static char *ei_slurp_file(const char *path) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
return NULL;
|
||||
}
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
long size = ftell(f);
|
||||
if (size < 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
if (fseek(f, 0, SEEK_SET) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
char *buf = (char *)calloc((size_t)size + 1, 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
size_t nread = fread(buf, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
buf[nread] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
static cbm_store_t *ei_index_fixture_files(EILangProj *lp, const char *fixture_root,
|
||||
const EILangFixtureFile *files, int nfiles) {
|
||||
EILangFile *loaded = (EILangFile *)calloc((size_t)nfiles, sizeof(EILangFile));
|
||||
char **contents = (char **)calloc((size_t)nfiles, sizeof(char *));
|
||||
if (!loaded || !contents) {
|
||||
free(loaded);
|
||||
free(contents);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cbm_store_t *store = NULL;
|
||||
for (int i = 0; i < nfiles; i++) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/%s", fixture_root, files[i].name);
|
||||
contents[i] = ei_slurp_file(path);
|
||||
if (!contents[i]) {
|
||||
goto done;
|
||||
}
|
||||
loaded[i].name = files[i].name;
|
||||
loaded[i].content = contents[i];
|
||||
}
|
||||
|
||||
store = ei_index_files(lp, loaded, nfiles);
|
||||
|
||||
done:
|
||||
for (int i = 0; i < nfiles; i++) {
|
||||
free(contents[i]);
|
||||
}
|
||||
free(contents);
|
||||
free(loaded);
|
||||
return store;
|
||||
}
|
||||
|
||||
static int64_t ei_node_id_for_file_label(cbm_store_t *store, const char *project,
|
||||
const char *file_path, const char *label) {
|
||||
cbm_node_t *nodes = NULL;
|
||||
int count = 0;
|
||||
if (cbm_store_find_nodes_by_file(store, project, file_path, &nodes, &count) != CBM_STORE_OK) {
|
||||
return 0;
|
||||
}
|
||||
int64_t id = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (nodes[i].label && strcmp(nodes[i].label, label) == 0) {
|
||||
id = nodes[i].id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (id == 0 && count > 0) {
|
||||
id = nodes[0].id;
|
||||
}
|
||||
cbm_store_free_nodes(nodes, count);
|
||||
return id;
|
||||
}
|
||||
|
||||
static void ei_cleanup(EILangProj *lp, cbm_store_t *store) {
|
||||
if (store) cbm_store_close(store);
|
||||
if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; }
|
||||
@@ -409,6 +496,68 @@ TEST(ei_go_two_consumers_same_package) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* C++: header include should resolve to the header file node, not the same-stem
|
||||
* source node. Also exercises angle-bracket include resolution. */
|
||||
TEST(ei_cpp_header_include_targets_header_file) {
|
||||
static const EILangFixtureFile fixture_files[] = {
|
||||
{"main.cpp"},
|
||||
{"NodeController.h"},
|
||||
{"NodeController.cpp"},
|
||||
{"SystemController.h"},
|
||||
{"SystemController.cpp"},
|
||||
};
|
||||
|
||||
EILangProj lp;
|
||||
cbm_store_t *store = ei_index_fixture_files(&lp, "tests/fixtures/cpp_include",
|
||||
fixture_files,
|
||||
(int)(sizeof(fixture_files) / sizeof(fixture_files[0])));
|
||||
ASSERT_NOT_NULL(store);
|
||||
|
||||
int64_t main_id = ei_node_id_for_file_label(store, lp.project, "main.cpp", "File");
|
||||
int64_t node_source_id = ei_node_id_for_file_label(store, lp.project, "NodeController.cpp", "File");
|
||||
int64_t system_source_id = ei_node_id_for_file_label(store, lp.project, "SystemController.cpp", "File");
|
||||
|
||||
ASSERT_GT(main_id, 0);
|
||||
ASSERT_GT(node_source_id, 0);
|
||||
ASSERT_GT(system_source_id, 0);
|
||||
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
int rc = cbm_store_find_edges_by_source_type(store, main_id, "IMPORTS", &edges, &edge_count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_TRUE(edge_count >= 2);
|
||||
|
||||
bool saw_node_header = false;
|
||||
bool saw_system_header = false;
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
cbm_node_t *target = (cbm_node_t *)calloc(1, sizeof(cbm_node_t));
|
||||
|
||||
/* Pass target directly (no &) because it is already a pointer */
|
||||
ASSERT_EQ(cbm_store_find_node_by_id(store, edges[i].target_id, target), CBM_STORE_OK);
|
||||
ASSERT_EQ(edges[i].source_id, main_id);
|
||||
ASSERT_NEQ(edges[i].target_id, node_source_id);
|
||||
ASSERT_NEQ(edges[i].target_id, system_source_id);
|
||||
|
||||
/* Use -> instead of . to access fields on a pointer */
|
||||
if (target->file_path && strcmp(target->file_path, "NodeController.h") == 0) {
|
||||
saw_node_header = true;
|
||||
}
|
||||
if (target->file_path && strcmp(target->file_path, "SystemController.h") == 0) {
|
||||
saw_system_header = true;
|
||||
}
|
||||
|
||||
/* Free the node inside the loop */
|
||||
cbm_store_free_nodes(target, 1);
|
||||
}
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
|
||||
ASSERT_TRUE(saw_node_header);
|
||||
ASSERT_TRUE(saw_system_header);
|
||||
|
||||
ei_cleanup(&lp, store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* RED REPRODUCTION — Rust
|
||||
*
|
||||
@@ -868,6 +1017,7 @@ SUITE(edge_imports) {
|
||||
RUN_TEST(ei_go_subpackage_import);
|
||||
RUN_TEST(ei_go_blank_import);
|
||||
RUN_TEST(ei_go_two_consumers_same_package);
|
||||
RUN_TEST(ei_cpp_header_include_targets_header_file);
|
||||
|
||||
/* ── RED REPRODUCTIONS — Rust (expected to FAIL until pipeline fixed) ── */
|
||||
RUN_TEST(ei_rust_mod_plus_use);
|
||||
|
||||
@@ -381,6 +381,31 @@ TEST(handles_spring_kotlin) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* JAX-RS (Java): the verb (@GET) and the path (@Path) are sibling
|
||||
* annotations, and the class-level @Path must prefix method-level paths.
|
||||
* Reproduce-first (#1005): the first-mapping-annotation scan dropped every
|
||||
* method-level @Path, collapsing all methods onto a single "/" Route node;
|
||||
* only the exact Route-name set catches that through the emission dedup. */
|
||||
TEST(handles_jaxrs_java) {
|
||||
static const char *routes[] = {"/api/v1/widgets", "/api/v1/widgets/count", NULL};
|
||||
static const EtFile f[] = {
|
||||
{"WidgetResource.java",
|
||||
"package com.example;\n\n"
|
||||
"import jakarta.ws.rs.GET;\n"
|
||||
"import jakarta.ws.rs.Path;\n\n"
|
||||
"@Path(\"/api/v1/widgets\")\npublic class WidgetResource {\n"
|
||||
" @GET\n"
|
||||
" public String list() {\n"
|
||||
" return \"widgets\";\n }\n\n"
|
||||
" @GET\n"
|
||||
" @Path(\"/count\")\n"
|
||||
" public String count() {\n"
|
||||
" return \"42\";\n }\n}\n"}};
|
||||
ASSERT_TRUE(et_edge_present(f, 1, "HANDLES", 2));
|
||||
ASSERT_TRUE(et_routes_exact(f, 1, routes));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ASP.NET Minimal API (C#) — route registration via static MapGet/MapPost calls
|
||||
* with identifier handlers, under a Microsoft/AspNetCore path so the resolved
|
||||
* callee QN carries the "MapGet"/"Microsoft.AspNetCore" route-reg substrings.
|
||||
@@ -1488,7 +1513,7 @@ TEST(override_go_interface) {
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
SUITE(edge_types_probe) {
|
||||
/* HANDLES — route→handler across web frameworks (8 frameworks) */
|
||||
/* HANDLES — route→handler across web frameworks */
|
||||
RUN_TEST(handles_flask_python);
|
||||
RUN_TEST(handles_fastapi_python);
|
||||
RUN_TEST(handles_drf_action_python);
|
||||
@@ -1497,6 +1522,7 @@ SUITE(edge_types_probe) {
|
||||
RUN_TEST(handles_gin_go);
|
||||
RUN_TEST(handles_spring_java);
|
||||
RUN_TEST(handles_spring_kotlin);
|
||||
RUN_TEST(handles_jaxrs_java);
|
||||
RUN_TEST(handles_aspnet_csharp);
|
||||
RUN_TEST(handles_laravel_php);
|
||||
RUN_TEST(handles_laravel_facade_routes_issue952);
|
||||
|
||||
@@ -2738,6 +2738,38 @@ TEST(extract_java_method_annotations_issue382) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Issue #1005: JAX-RS splits a route across two annotations (@GET carries the
|
||||
* verb, a sibling @Path carries the path). Returning on the first mapping
|
||||
* annotation dropped every method-level @Path, and the class-level @Path
|
||||
* prefix was never recognized at all. */
|
||||
TEST(extract_java_jaxrs_path_composition_issue1005) {
|
||||
CBMFileResult *r = extract("import jakarta.ws.rs.GET;\n"
|
||||
"import jakarta.ws.rs.Path;\n"
|
||||
"@Path(\"/api/v1/widgets\")\n"
|
||||
"public class WidgetResource {\n"
|
||||
" @GET\n"
|
||||
" public String list() { return \"\"; }\n"
|
||||
" @GET\n"
|
||||
" @Path(\"/count\")\n"
|
||||
" public String count() { return \"\"; }\n"
|
||||
"}\n",
|
||||
CBM_LANG_JAVA, "t", "WidgetResource.java");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
const CBMDefinition *list = find_def_by_name(r, "list");
|
||||
ASSERT_NOT_NULL(list);
|
||||
ASSERT_NOT_NULL(list->route_path);
|
||||
ASSERT_STR_EQ(list->route_path, "/api/v1/widgets");
|
||||
ASSERT_STR_EQ(list->route_method, "GET");
|
||||
const CBMDefinition *count = find_def_by_name(r, "count");
|
||||
ASSERT_NOT_NULL(count);
|
||||
ASSERT_NOT_NULL(count->route_path);
|
||||
ASSERT_STR_EQ(count->route_path, "/api/v1/widgets/count");
|
||||
ASSERT_STR_EQ(count->route_method, "GET");
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Find an in-body call by its raw callee text; returns the call or NULL. */
|
||||
static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) {
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
@@ -4625,6 +4657,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(js_index_module_qn_not_collide_with_folder);
|
||||
RUN_TEST(python_regular_module_qn_unchanged);
|
||||
RUN_TEST(extract_java_method_annotations_issue382);
|
||||
RUN_TEST(extract_java_jaxrs_path_composition_issue1005);
|
||||
RUN_TEST(extract_java_no_double_class_qn);
|
||||
RUN_TEST(extract_go_no_filename_in_module_qn);
|
||||
RUN_TEST(extract_large_ts_has_functions_issue213);
|
||||
|
||||
@@ -964,6 +964,36 @@ static int resp_lacks_key(const char *resp, const char *key) {
|
||||
|
||||
/* ── list_projects ─────────────────────────────────────────────── */
|
||||
|
||||
TEST(incr_file_node_name_stays_basename) {
|
||||
/* #994: File nodes must keep their basename NAME through an incremental
|
||||
* re-index (the bug renamed touched files' nodes to rel_path, diverging
|
||||
* from a full build). Modify a file, re-index, assert no File node name
|
||||
* contains a path separator. */
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/fastapi/applications.py", g_repodir);
|
||||
FILE *f = fopen(path, "a");
|
||||
ASSERT(f != NULL);
|
||||
fprintf(f, "\n# incr_file_node_name_stays_basename touch\n");
|
||||
fclose(f);
|
||||
|
||||
char *resp = index_repo();
|
||||
ASSERT(resp != NULL);
|
||||
free(resp);
|
||||
|
||||
double ms;
|
||||
char *r = call_tool_timed("query_graph", &ms,
|
||||
"{\"project\":\"%s\",\"format\":\"json\","
|
||||
"\"query\":\"MATCH (n:File) WHERE n.name CONTAINS '/'"
|
||||
" RETURN n.name LIMIT 5\"}",
|
||||
g_project);
|
||||
TOOL_OK(r, ms);
|
||||
/* No File node may carry a path separator in its name; the query engine
|
||||
* returns an empty rows array when nothing matches. */
|
||||
ASSERT(strstr(r, "\"rows\":[]") != NULL || strstr(r, "\\\"rows\\\":[]") != NULL);
|
||||
free(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_list_projects_basic) {
|
||||
double ms;
|
||||
char *r = call_tool_timed("list_projects", &ms, "{}");
|
||||
@@ -2970,6 +3000,7 @@ SUITE(incremental) {
|
||||
|
||||
/* Phase 3: Incremental deltas */
|
||||
RUN_TEST(incr_modify_file);
|
||||
RUN_TEST(incr_file_node_name_stays_basename);
|
||||
RUN_TEST(incr_formatter_run);
|
||||
RUN_TEST(incr_add_file);
|
||||
RUN_TEST(incr_delete_file);
|
||||
|
||||
@@ -4780,6 +4780,81 @@ TEST(tool_resolve_store_by_internal_name_issue704) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── #1044: a "<name>::missed" shadow row must not hide the project ──
|
||||
*
|
||||
* The miss-graph pass inserts a second `projects` row ("<name>::missed") so
|
||||
* its nodes satisfy the FK on nodes.project. db_internal_project_name
|
||||
* required the projects table to hold EXACTLY ONE row, so any project with
|
||||
* a miss graph vanished from list_projects and the graph UI, and the
|
||||
* fallback-scan resolve path failed.
|
||||
*
|
||||
* RED on buggy code / GREEN on the fix:
|
||||
* A. list_projects still advertises "delta1044" while the shadow row exists.
|
||||
* B. the shadow name itself is never advertised.
|
||||
* C. search_graph(project="delta1044") still resolves and returns the node.
|
||||
*/
|
||||
TEST(tool_list_projects_ignores_missed_shadow_issue1044) {
|
||||
char cache[256];
|
||||
snprintf(cache, sizeof(cache), "/tmp/cbm-issue1044-XXXXXX");
|
||||
if (!cbm_mkdtemp(cache)) {
|
||||
PASS(); /* skip if mkdtemp fails — not a #1044 signal */
|
||||
}
|
||||
|
||||
const char *saved = getenv("CBM_CACHE_DIR");
|
||||
char *saved_copy = saved ? strdup(saved) : NULL;
|
||||
cbm_setenv("CBM_CACHE_DIR", cache, 1);
|
||||
|
||||
ASSERT_TRUE(issue704_make_db(cache, "delta1044.db", "delta1044", "deltaFunc1044"));
|
||||
|
||||
/* Add the shadow row exactly the way the miss-graph pass does. */
|
||||
char db_path[700];
|
||||
snprintf(db_path, sizeof(db_path), "%s/delta1044.db", cache);
|
||||
cbm_store_t *st = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(st);
|
||||
ASSERT_EQ(cbm_store_upsert_project(st, "delta1044::missed", ""), CBM_STORE_OK);
|
||||
cbm_store_close(st);
|
||||
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
|
||||
/* ── A + B: primary advertised, shadow hidden ─────────────────── */
|
||||
char *list =
|
||||
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"list_projects\",\"arguments\":{}}}");
|
||||
ASSERT_NOT_NULL(list);
|
||||
ASSERT_NOT_NULL(strstr(list, "delta1044")); /* RED before: db skipped as ghost */
|
||||
ASSERT_NULL(strstr(list, "::missed")); /* shadow never advertised */
|
||||
free(list);
|
||||
|
||||
/* ── C: the project still resolves and returns its node ───────── */
|
||||
char *q = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_graph\",\"arguments\":{"
|
||||
"\"project\":\"delta1044\",\"name_pattern\":\"deltaFunc1044\",\"limit\":5}}}");
|
||||
ASSERT_NOT_NULL(q);
|
||||
ASSERT_NOT_NULL(strstr(q, "deltaFunc1044"));
|
||||
ASSERT_NULL(strstr(q, "not found"));
|
||||
free(q);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
|
||||
/* ── cleanup ───────────────────────────────────────────────────── */
|
||||
if (saved_copy) {
|
||||
cbm_setenv("CBM_CACHE_DIR", saved_copy, 1);
|
||||
free(saved_copy);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_CACHE_DIR");
|
||||
}
|
||||
cbm_unlink(db_path);
|
||||
char side1044[740];
|
||||
snprintf(side1044, sizeof(side1044), "%s-wal", db_path);
|
||||
cbm_unlink(side1044);
|
||||
snprintf(side1044, sizeof(side1044), "%s-shm", db_path);
|
||||
cbm_unlink(side1044);
|
||||
cbm_rmdir(cache);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* QUERY STORE READ-ONLY (data-integrity reproductions)
|
||||
*
|
||||
@@ -6439,6 +6514,7 @@ SUITE(mcp) {
|
||||
RUN_TEST(tool_bad_project_name_no_overflow_issue235);
|
||||
RUN_TEST(tool_bad_project_error_valid_json_issue235);
|
||||
RUN_TEST(tool_resolve_store_by_internal_name_issue704);
|
||||
RUN_TEST(tool_list_projects_ignores_missed_shadow_issue1044);
|
||||
|
||||
/* auto_watch gate (distilled from PR #625) */
|
||||
RUN_TEST(mcp_auto_watch_default_registers_watcher_on_connect);
|
||||
|
||||
Reference in new issue
Block a user