Skip to content

Commit 82a6ddf

Browse files
authored
Merge branch 'main' into fix/510-enclosing-gitignore-not-inherited
2 parents 65fc0bb + 66d9c2b commit 82a6ddf

24 files changed

Lines changed: 1093 additions & 155 deletions

‎internal/cbm/extract_calls.c‎

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ static const char *strip_quotes(CBMArena *a, const char *text) {
7979

8080
// Callee suffixes for IRIS Python interop string-dispatch. Kept at file scope
8181
// (not inside the function) to satisfy cppcheck variableScope.
82-
static const char *s_py_dispatch_suffixes[] = {".classMethodValue", ".classMethodVoid",
83-
".classMethodBoolean", ".classMethodObject", NULL};
82+
static const char *s_py_dispatch_suffixes[] = {".classMethodValue", ".classMethodVoid",
83+
".classMethodBoolean", ".classMethodObject",
84+
".invokeClassMethod", NULL};
8485

8586
// Per-language callee-suffix dispatch table — returns a NULL-terminated list of
8687
// method-name suffixes whose calls should be resolved by extracting class+method
@@ -2039,6 +2040,49 @@ static const char *extract_nth_string_arg(CBMExtractCtx *ctx, TSNode args, uint3
20392040
return NULL;
20402041
}
20412042

2043+
// IRIS embedded Python names the target class in the receiver:
2044+
// iris.cls("Pkg.Class").Method(...) calls ClassMethod Pkg.Class.Method.
2045+
// Returns "Pkg.Class.Method", or NULL unless the receiver is iris.cls() with
2046+
// a string-literal first argument. A non-literal class names nothing, so it
2047+
// gets no class-qualified callee rather than a guessed one.
2048+
static const char *python_iris_cls_callee(CBMExtractCtx *ctx, TSNode call_node) {
2049+
TSNode fn = ts_node_child_by_field_name(call_node, TS_FIELD("function"));
2050+
if (ts_node_is_null(fn) || strcmp(ts_node_type(fn), "attribute") != 0) {
2051+
return NULL;
2052+
}
2053+
TSNode recv = ts_node_child_by_field_name(fn, TS_FIELD("object"));
2054+
TSNode meth = ts_node_child_by_field_name(fn, TS_FIELD("attribute"));
2055+
if (ts_node_is_null(recv) || ts_node_is_null(meth) || strcmp(ts_node_type(recv), "call") != 0) {
2056+
return NULL;
2057+
}
2058+
TSNode recv_fn = ts_node_child_by_field_name(recv, TS_FIELD("function"));
2059+
TSNode recv_args = ts_node_child_by_field_name(recv, TS_FIELD("arguments"));
2060+
if (ts_node_is_null(recv_fn) || ts_node_is_null(recv_args) ||
2061+
ts_node_named_child_count(recv_args) == 0) {
2062+
return NULL;
2063+
}
2064+
const char *recv_text = cbm_node_text(ctx->arena, recv_fn, ctx->source);
2065+
if (!recv_text || strcmp(recv_text, "iris.cls") != 0) {
2066+
return NULL;
2067+
}
2068+
TSNode cls_arg = ts_node_named_child(recv_args, 0);
2069+
if (!is_string_like(ts_node_type(cls_arg))) {
2070+
return NULL;
2071+
}
2072+
// Plain quoted literals only: an f-string or other prefixed string is a
2073+
// "string" node too, but its text is not a class name.
2074+
char *cls_text = cbm_node_text(ctx->arena, cls_arg, ctx->source);
2075+
if (!cls_text || (cls_text[0] != '"' && cls_text[0] != '\'')) {
2076+
return NULL;
2077+
}
2078+
const char *cls = strip_and_validate_string_arg(ctx->arena, cls_text);
2079+
const char *mth = cbm_node_text(ctx->arena, meth, ctx->source);
2080+
if (!cls || !cls[0] || !mth || !mth[0]) {
2081+
return NULL;
2082+
}
2083+
return cbm_arena_sprintf(ctx->arena, "%s.%s", cls, mth);
2084+
}
2085+
20422086
// --- Unified handler: called once per node by the cursor walk ---
20432087

20442088
// Process a keyword argument (keyword_argument or pair node).
@@ -3887,6 +3931,16 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
38873931
}
38883932
}
38893933

3934+
// #1260: iris.cls("Pkg.X").M() names its class, so the qualified
3935+
// callee replaces the bare one. Keeping both would let the bare
3936+
// name weak-match an unrelated same-named method.
3937+
if (ctx->language == CBM_LANG_PYTHON) {
3938+
const char *iris_callee = python_iris_cls_callee(ctx, node);
3939+
if (iris_callee) {
3940+
call.callee_name = iris_callee;
3941+
call.is_method = false;
3942+
}
3943+
}
38903944
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
38913945
invocation = describe_emitted_primary_call(node, &callee);
38923946

‎internal/cbm/extract_defs.c‎

Lines changed: 88 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
#include "helpers.h"
44
#include "lang_specs.h"
55
#include "foundation/constants.h"
6-
#include "foundation/platform.h" // safe_realloc (frees old on failure)
7-
#include "foundation/log.h" // cbm_log_warn
6+
#include "foundation/log.h" // cbm_log_error
7+
#include "foundation/mem_core.h" // cbm_realloc/cbm_free -- walk_defs stack
88
#include "extract_node_stack.h"
99
#include "simhash/minhash.h"
1010
#include "semantic/ast_profile.h"
1111
#include "tree_sitter/api.h" // TSNode, ts_node_*
12-
#include <stdint.h> // uint32_t
12+
#include <limits.h> // INT_MAX
13+
#include <stdint.h> // uint32_t, SIZE_MAX
1314
#include <stdio.h> // snprintf (ObjectScript storage/trigger sidecars)
14-
#include <stdlib.h> // getenv, atoi
15+
#include <stdlib.h> // malloc/free (child-collection scratch)
1516
#include <string.h>
1617
#include <ctype.h>
1718

@@ -1416,7 +1417,8 @@ static bool is_route_string_kind(const char *kind) {
14161417
strcmp(kind, "interpreted_string_literal") == 0;
14171418
}
14181419

1419-
static const char *route_path_from_string_node(CBMArena *a, TSNode node, const char *source) {
1420+
static const char *route_path_from_string_node(CBMArena *a, TSNode node, const char *source,
1421+
bool allow_relative) {
14201422
if (!is_route_string_kind(ts_node_type(node))) {
14211423
return NULL;
14221424
}
@@ -1428,30 +1430,41 @@ static const char *route_path_from_string_node(CBMArena *a, TSNode node, const c
14281430
if (plen >= PAIR_CHARS && (path[0] == '"' || path[0] == '\'')) {
14291431
path = cbm_arena_strndup(a, path + SKIP_CHAR, (size_t)(plen - PAIR_CHARS));
14301432
}
1431-
return (path && path[0] == '/') ? path : NULL;
1433+
if (!path || path[0] == '/' || !allow_relative) {
1434+
return (path && path[0] == '/') ? path : NULL;
1435+
}
1436+
/* JAX-RS @Path values are relative URI templates; a leading slash is
1437+
* optional and ignored by the framework. Route nodes use absolute-looking
1438+
* paths consistently, so normalize a non-empty relative value here. An
1439+
* empty @Path("") means "the class path itself": leave it unset so the
1440+
* caller falls back exactly as it does for a method without @Path. */
1441+
return path[0] ? cbm_arena_sprintf(a, "/%s", path) : NULL;
14321442
}
14331443

14341444
static const char *find_route_path_literal(CBMArena *a, TSNode node, const char *source,
1435-
int max_depth) {
1445+
int max_depth, bool allow_relative) {
14361446
if (ts_node_is_null(node) || max_depth < 0) {
14371447
return NULL;
14381448
}
1439-
const char *path = route_path_from_string_node(a, node, source);
1449+
const char *path = route_path_from_string_node(a, node, source, allow_relative);
14401450
if (path || max_depth == 0) {
14411451
return path;
14421452
}
14431453
uint32_t nc = ts_node_named_child_count(node);
14441454
for (uint32_t i = 0; i < nc && i < DECORATOR_SCAN_LIMIT; i++) {
1445-
path = find_route_path_literal(a, ts_node_named_child(node, i), source, max_depth - 1);
1455+
path = find_route_path_literal(a, ts_node_named_child(node, i), source, max_depth - 1,
1456+
allow_relative);
14461457
if (path) {
14471458
return path;
14481459
}
14491460
}
14501461
return NULL;
14511462
}
14521463

1453-
// Extract route path from decorator arguments (first string that starts with /).
1454-
static const char *extract_route_path_from_args(CBMArena *a, TSNode args, const char *source) {
1464+
// Extract route path from decorator arguments. Generic mappings keep only
1465+
// slash-prefixed strings; JAX-RS @Path additionally accepts relative templates.
1466+
static const char *extract_route_path_from_args(CBMArena *a, TSNode args, const char *source,
1467+
bool allow_relative) {
14551468
/* Every argument is checked. Java and Kotlin put no order on annotation
14561469
* attributes, so `path` can sit anywhere in the list. Stopping early left
14571470
* a real route unread and formed no Route node. Each argument's own
@@ -1464,7 +1477,8 @@ static const char *extract_route_path_from_args(CBMArena *a, TSNode args, const
14641477
* @GetMapping(path = {"/orders"})
14651478
* Walk a bounded subtree and keep the first string literal that is
14661479
* path-shaped, while ignoring non-route literals such as media types. */
1467-
const char *path = find_route_path_literal(a, arg, source, CBM_DESCENDANT_MAX_DEPTH);
1480+
const char *path =
1481+
find_route_path_literal(a, arg, source, CBM_DESCENDANT_MAX_DEPTH, allow_relative);
14681482
if (path) {
14691483
return path;
14701484
}
@@ -1610,7 +1624,7 @@ static bool try_route_from_decorator_call(CBMArena *a, TSNode dchild, const char
16101624

16111625
TSNode args = find_decorator_args(dchild);
16121626
if (!ts_node_is_null(args)) {
1613-
const char *path = extract_route_path_from_args(a, args, source);
1627+
const char *path = extract_route_path_from_args(a, args, source, false);
16141628
if (path) {
16151629
*out_path = path;
16161630
*out_method = method;
@@ -1690,7 +1704,7 @@ static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char
16901704
TSNode args = annotation_args_node(annotation);
16911705
const char *path = NULL;
16921706
if (!ts_node_is_null(args)) {
1693-
path = extract_route_path_from_args(a, args, source);
1707+
path = extract_route_path_from_args(a, args, source, false);
16941708
}
16951709
*out_path = path ? path : "/";
16961710
*out_method = method;
@@ -1740,7 +1754,7 @@ static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source
17401754
if (!*out_jax_path && strcmp(name, "Path") == 0) {
17411755
TSNode args = annotation_args_node(child);
17421756
if (!ts_node_is_null(args)) {
1743-
*out_jax_path = extract_route_path_from_args(a, args, source);
1757+
*out_jax_path = extract_route_path_from_args(a, args, source, true);
17441758
}
17451759
continue;
17461760
}
@@ -1750,7 +1764,7 @@ static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source
17501764
*out_method = method;
17511765
TSNode args = annotation_args_node(child);
17521766
if (!ts_node_is_null(args)) {
1753-
*out_map_path = extract_route_path_from_args(a, args, source);
1767+
*out_map_path = extract_route_path_from_args(a, args, source, false);
17541768
}
17551769
}
17561770
}
@@ -5147,7 +5161,8 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
51475161

51485162
def.decorators = extract_decorators(a, child, ctx->source, ctx->language, spec);
51495163
extract_route_from_decorators(a, child, ctx->source, spec, &def.route_path, &def.route_method);
5150-
if (def.route_path && (ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN)) {
5164+
if (def.route_path && (ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN ||
5165+
ctx->language == CBM_LANG_SCALA)) {
51515166
const char *prefix = spring_class_route_prefix(a, class_node, ctx->source, spec);
51525167
def.route_path = join_route_paths(a, prefix, def.route_path);
51535168
}
@@ -7195,74 +7210,75 @@ typedef struct {
71957210
* single ~160 KB C-stack frame. That overflowed small thread stacks (the
71967211
* pre-2026-03 Windows 1 MB main thread) on the definitions pass, and its
71977212
* `top < 4096` push guards SILENTLY DROPPED every top-level definition past
7198-
* 4096. Use a growable heap stack instead: a tiny initial footprint that doubles
7199-
* on demand, bounded by a generous, env-configurable ceiling that WARNs (once)
7200-
* rather than dropping — so a file with thousands of top-level defs is fully
7201-
* extracted, and a pathological one degrades to a warned, bounded skip instead
7202-
* of an OOM or a stack overflow. */
7213+
* 4096. It became a growable stack bounded by an 8M-frame ceiling
7214+
* (env CBM_WALK_DEFS_MAX) that still stopped pushing once reached — and since
7215+
* children are pushed last-to-first, a file wider than the ceiling lost its
7216+
* FIRST definitions. No work cap may decide graph content, so there is no
7217+
* ceiling: the stack doubles on demand, and only an allocation failure stops
7218+
* the walk. That failure is loud — logged at error level and reported as a
7219+
* per-file extract error (result->has_error) — never a silent partial file. */
72037220
typedef struct {
72047221
walk_defs_frame_t *data;
72057222
int top;
72067223
int cap;
7207-
const char *path; // for the WARN when the ceiling is hit (may be NULL)
7208-
bool warned;
7224+
const char *path; // for the error log on allocation failure (may be NULL)
7225+
bool failed; // growth failed: pending frames dropped, walk drains
72097226
/* The per-file traversal scratch (ctx->scratch) when there is one: frames
72107227
* then come from memory the thread reuses file after file, where a malloc
72117228
* of 256 frames per file was 28 k allocations and 255 MB never written on
72127229
* the Go corpus (waste sanitizer, 2026-09-17). Growth copies into a
72137230
* doubled buffer and abandons the old one to the arena, like TSNodeStack.
7214-
* NULL: the heap, freed by the walk. */
7231+
* NULL: the memory core (class EXTRACT), released by the walk. */
72157232
CBMArena *arena;
72167233
} wd_stack_t;
72177234

7218-
// Generous safety ceiling (frames), env-overridable via CBM_WALK_DEFS_MAX.
7219-
// Realistic files never approach this; it only bounds a pathological/adversarial
7220-
// file so extraction degrades to a warned skip rather than unbounded memory.
7221-
static int wd_stack_max(void) {
7222-
const char *e = getenv("CBM_WALK_DEFS_MAX");
7223-
if (e) {
7224-
int v = atoi(e);
7225-
if (v > 0) {
7226-
return v;
7227-
}
7228-
}
7229-
return 8 * 1024 * 1024; // 8M frames (~320 MB) default
7230-
}
7231-
7232-
static void wd_push(wd_stack_t *s, TSNode node, const char *enclosing_qn) {
7233-
if (s->top >= s->cap) {
7234-
int ncap = s->cap ? s->cap * 2 : 256;
7235-
if (ncap > wd_stack_max()) {
7236-
if (!s->warned) {
7237-
char lim[24];
7238-
snprintf(lim, sizeof(lim), "%d", wd_stack_max());
7239-
cbm_log_warn("extract.walk_defs_capped", "limit", lim, "path",
7240-
s->path ? s->path : "");
7241-
s->warned = true;
7242-
}
7243-
return; // bounded: stop growing (warned, not silent)
7244-
}
7245-
walk_defs_frame_t *nd = NULL;
7235+
enum { WD_STACK_INITIAL = 256 };
7236+
7237+
/* Double `s` (or give it its first WD_STACK_INITIAL frames). Returns false,
7238+
* after logging, when the doubled size is not representable or cannot be
7239+
* allocated; the old buffer is left intact so the walk can still release it. */
7240+
static bool wd_grow(wd_stack_t *s) {
7241+
walk_defs_frame_t *nd = NULL;
7242+
int ncap = WD_STACK_INITIAL;
7243+
bool fits = true;
7244+
if (s->cap > 0) {
7245+
fits = s->cap <= INT_MAX / 2;
7246+
ncap = fits ? s->cap * 2 : s->cap;
7247+
}
7248+
fits = fits && (size_t)ncap <= SIZE_MAX / sizeof(walk_defs_frame_t);
7249+
if (fits) {
7250+
size_t bytes = (size_t)ncap * sizeof(walk_defs_frame_t);
72467251
if (s->arena) {
7247-
nd = (walk_defs_frame_t *)cbm_arena_alloc(s->arena,
7248-
(size_t)ncap * sizeof(walk_defs_frame_t));
7252+
nd = (walk_defs_frame_t *)cbm_arena_alloc(s->arena, bytes);
72497253
if (nd && s->top > 0) {
72507254
memcpy(nd, s->data, (size_t)s->top * sizeof(walk_defs_frame_t));
72517255
}
72527256
} else {
7253-
nd = safe_realloc(s->data, (size_t)ncap * sizeof(walk_defs_frame_t));
7254-
}
7255-
if (!nd) {
7256-
/* OOM — safe_realloc already freed the old buffer. Bail cleanly: drop
7257-
* pending frames so the walk_defs loop drains and exits without a NULL
7258-
* deref; extraction keeps whatever was already emitted. */
7259-
s->data = NULL;
7260-
s->cap = 0;
7261-
s->top = 0;
7262-
return;
7257+
nd = (walk_defs_frame_t *)cbm_realloc(CBM_MEM_CLASS_EXTRACT, s->data, bytes);
72637258
}
7264-
s->data = nd;
7265-
s->cap = ncap;
7259+
}
7260+
if (!nd) {
7261+
char pending[24];
7262+
snprintf(pending, sizeof(pending), "%d", s->top);
7263+
cbm_log_error("extract.walk_stack_alloc_failed", "walker", "walk_defs", "pending", pending,
7264+
"path", s->path ? s->path : "");
7265+
return false;
7266+
}
7267+
s->data = nd;
7268+
s->cap = ncap;
7269+
return true;
7270+
}
7271+
7272+
static void wd_push(wd_stack_t *s, TSNode node, const char *enclosing_qn) {
7273+
if (s->failed) {
7274+
return;
7275+
}
7276+
if (s->top >= s->cap && !wd_grow(s)) {
7277+
/* Drop every pending frame so the walk_defs loop drains and exits;
7278+
* walk_defs then marks the file's result as an extract error. */
7279+
s->failed = true;
7280+
s->top = 0;
7281+
return;
72667282
}
72677283
s->data[s->top++] = (walk_defs_frame_t){node, enclosing_qn};
72687284
}
@@ -8070,7 +8086,11 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
80708086
wd_push_children_reverse(&s, node, frame.enclosing_class_qn);
80718087
}
80728088
if (!s.arena) {
8073-
free(s.data);
8089+
cbm_free(CBM_MEM_CLASS_EXTRACT, s.data);
8090+
}
8091+
if (s.failed) {
8092+
ctx->result->has_error = true;
8093+
ctx->result->error_msg = "definitions walk: stack allocation failed";
80748094
}
80758095
}
80768096

‎pkg/scoop/codebase-memory-mcp.json‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
{
2-
"version": "0.8.1",
3-
"description": "Fast code intelligence engine — native executable with verified runtime assets",
4-
"homepage": "https://github.com/DeusData/codebase-memory-mcp",
2+
"version": "0.11.0",
3+
"description": "Code intelligence knowledge graph MCP server for AI coding agents",
4+
"homepage": "https://deusdata.github.io/codebase-memory-mcp/",
55
"license": "MIT",
6+
"notes": "To register this MCP server with your clients, run `codebase-memory-mcp install` after install.",
67
"architecture": {
78
"64bit": {
8-
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.8.1/codebase-memory-mcp-windows-amd64.zip",
9-
"hash": "a602ad090ed3f49d86c55472f73f27ad7055222806a82358f2e08513e027f00f",
10-
"bin": "codebase-memory-mcp.exe"
9+
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.11.0/codebase-memory-mcp-windows-amd64.zip",
10+
"hash": "6eb6beaf261b19e419766e78baf93cbc3cf1c6338cff8fb7c0234859f96d1685"
11+
},
12+
"arm64": {
13+
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.11.0/codebase-memory-mcp-windows-arm64.zip",
14+
"hash": "52b29881214fce47d529e098308b1de77c40f6812e20a34d588ee4c25b84fd1a"
1115
}
1216
},
17+
"bin": "codebase-memory-mcp.exe",
1318
"checkver": {
1419
"github": "https://github.com/DeusData/codebase-memory-mcp"
1520
},
@@ -18,8 +23,15 @@
1823
"64bit": {
1924
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v$version/codebase-memory-mcp-windows-amd64.zip",
2025
"hash": {
21-
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v$version/checksums.txt",
22-
"find": "([a-f0-9]{64})\\s+codebase-memory-mcp-windows-amd64\\.zip"
26+
"url": "$baseurl/checksums.txt",
27+
"find": "([a-f0-9]{64})\\s+codebase-memory-mcp-windows-amd64\\.zip$"
28+
}
29+
},
30+
"arm64": {
31+
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v$version/codebase-memory-mcp-windows-arm64.zip",
32+
"hash": {
33+
"url": "$baseurl/checksums.txt",
34+
"find": "([a-f0-9]{64})\\s+codebase-memory-mcp-windows-arm64\\.zip$"
2335
}
2436
}
2537
}

0 commit comments

Comments
 (0)