From 2bd71e5cb8ce92f50a2922c35f69575f744bd8c2 Mon Sep 17 00:00:00 2001 From: Thomas Dyar Date: Fri, 12 Jun 2026 14:55:57 -0400 Subject: [PATCH] fix(extract): Class.lines always 0 for all languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extract_class_def set start_line and end_line from tree-sitter node positions but left def.lines = 0 (zeroed by memset). push_method_def correctly computes lines = end_line - start_line + 1 — the same one-liner was simply missing from the class extraction path. Affects all languages: any tool call or Cypher query returning Class nodes with a lines property always received 0, making it impossible to sort classes by size or filter large classes. Regression test: tool_qg_class_lines_nonzero in test_incremental.c. Signed-off-by: Thomas Dyar --- internal/cbm/extract_defs.c | 1 + tests/test_incremental.c | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 913268d8..2d3ab45f 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -3267,6 +3267,7 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; + def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = cbm_is_exported(name, ctx->language); def.base_classes = extract_base_classes(a, node, ctx->source, ctx->language); def.decorators = extract_decorators(a, node, ctx->source, ctx->language, spec); diff --git a/tests/test_incremental.c b/tests/test_incremental.c index c210d543..5fae5d87 100644 --- a/tests/test_incremental.c +++ b/tests/test_incremental.c @@ -932,6 +932,52 @@ TEST(tool_list_projects_basic) { PASS(); } +TEST(tool_qg_defines_method_more_than_10) { + write_file_at("fastapi/big_class.py", + "class BigClass:\n" + " def m1(self): pass\n" + " def m2(self): pass\n" + " def m3(self): pass\n" + " def m4(self): pass\n" + " def m5(self): pass\n" + " def m6(self): pass\n" + " def m7(self): pass\n" + " def m8(self): pass\n" + " def m9(self): pass\n" + " def m10(self): pass\n" + " def m11(self): pass\n" + " def m12(self): pass\n" + " def m13(self): pass\n" + " def m14(self): pass\n" + " def m15(self): pass\n"); + char *idx = index_repo(); + ASSERT(idx != NULL); + free(idx); + double ms; + char *r = call_tool_timed("query_graph", &ms, + "{\"project\":\"%s\"," + "\"query\":\"MATCH (c:Class)-[:DEFINES_METHOD]->(m:Method)" + " WHERE c.name = 'BigClass' RETURN count(m) AS n\"}", + g_project); + TOOL_OK(r, ms); + ASSERT(strstr(r, "\"15\"") != NULL || strstr(r, "\\\"15\\\"") != NULL); + free(r); + PASS(); +} + +TEST(tool_qg_class_lines_nonzero) { + double ms; + char *r = call_tool_timed("query_graph", &ms, + "{\"project\":\"%s\"," + "\"query\":\"MATCH (c:Class) WHERE c.lines > 0 RETURN count(c) AS n\"}", + g_project); + TOOL_OK(r, ms); + ASSERT(strstr(r, "\"0\"") == NULL); + free(r); + PASS(); +} + + TEST(tool_list_projects_has_current) { double ms; char *r = call_tool_timed("list_projects", &ms, "{}"); @@ -3042,6 +3088,8 @@ SUITE(incremental) { RUN_TEST(tool_qg_configures); RUN_TEST(tool_qg_handles); RUN_TEST(tool_qg_defines_method); + RUN_TEST(tool_qg_defines_method_more_than_10); + RUN_TEST(tool_qg_class_lines_nonzero); RUN_TEST(tool_qg_no_limit); RUN_TEST(tool_qg_empty_result);