Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions src/mcp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2578,11 +2578,12 @@ fn handleCallers(alloc: std.mem.Allocator, args: *const std.json.ObjectMap, out:
}
}
if (is_def) continue;
// A whole-word match that only ever lands inside a string literal is a
// mention, not an invocation — a `("name", "file")` table row or a
// `test "…name…" {` declaration. Real calls in the same file (and even
// in the same test body) sit outside the quotes and survive.
if (!hasWholeWordMatchOutsideStrings(r.line_text, name)) continue;
// A whole-word match that only ever lands inside a string literal or a
// trailing comment is a mention, not an invocation — a `("name",
// "file")` table row, a `test "…name…" {` declaration, or
// `init(); // then name() …`. Real calls sit on the code prefix of the
// line, outside the quotes, and survive both cuts. (#682)
if (!hasWholeWordMatchOutsideStrings(lineCodePrefix(r.line_text, lang), name)) continue;
kept.append(alloc, r_idx) catch {};
}

Expand Down Expand Up @@ -2682,6 +2683,47 @@ fn hasWholeWordMatchOutsideStrings(line: []const u8, needle: []const u8) bool {
return false;
}

/// The slice of `line` before its first line-comment marker outside string
/// literals, per `language` — the part of the line that can actually contain a
/// call. Lines with no marker, and languages without a line-comment syntax
/// (OCaml's `(* *)` is block-only), come back whole. (#682)
///
/// Same line-local scanning discipline as `hasWholeWordMatchOutsideStrings`:
/// quoted spans are skipped (so `"https://…"` never truncates), an unclosed
/// quote is ordinary code. Mid-line block-comment openers are left alone — a
/// false negative re-admits a mention; truncating at `/*` could drop the real
/// call in `foo(); /* … */ bar()`.
fn lineCodePrefix(line: []const u8, language: explore_mod.Language) []const u8 {
const markers: []const []const u8 = switch (language) {
.zig, .rust, .go_lang, .javascript, .typescript, .c, .cpp, .dart, .java, .kotlin, .mlir, .tablegen, .rescript, .svelte, .vue, .astro => &.{"//"},
.python, .ruby, .r, .shell => &.{"#"},
.hcl => &.{ "#", "//" },
.sql => &.{"--"},
.fortran => &.{"!"},
.llvm_ir => &.{";"},
else => return line,
};
var i: usize = 0;
while (i < line.len) {
if (line[i] == '"' or line[i] == '\'') {
if (stringLiteralEnd(line, i)) |end| {
i = end + 1;
continue;
Comment on lines +2708 to +2711

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track raw literals before cutting line comments

When // occurs outside single- or double-quoted strings without opening a line comment, this truncates valid code—for example, const s = https://x`; renderX()in JavaScript/Go orfoo(); /* https://x */ renderX();in C/C++. The prefix ends beforerenderX, so codedb_callers` drops a genuine call site that was reported before this commit; account for backtick/raw/template literals and block-comment spans before accepting a line-comment marker.

Useful? React with 👍 / 👎.

}
}
for (markers) |m| {
if (std.mem.startsWith(u8, line[i..], m)) {
// Shell: `#` only opens a comment at the start of a word —
// `${#var}` and `$#` are live syntax, not comments.
if (language == .shell and i != 0 and line[i - 1] != ' ' and line[i - 1] != '\t') break;
return line[0..i];
}
}
i += 1;
}
return line;
}

/// Index of the quote closing the literal opened at `open`, or null when the
/// literal never closes on this line.
fn stringLiteralEnd(line: []const u8, open: usize) ?usize {
Expand Down
36 changes: 36 additions & 0 deletions src/test_mcp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3785,6 +3785,42 @@ test "issue: codedb_callers keeps real calls inside and outside test bodies" {
try testing.expect(std.mem.indexOf(u8, out.items, "body.zig:1") == null);
}

test "issue-682: codedb_callers excludes a symbol mentioned only in a trailing comment" {
// `init(); // then renderX() draws the frame` — the mention lives in the
// trailing comment. The comment/blank filter passes the line (it starts
// with code) and the string filter sees no quotes, so the line is
// reported as a call site even though nothing on it invokes the symbol.
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
var out: std.ArrayList(u8) = .empty;
defer out.deinit(testing.allocator);

try renderCallersFixture(arena.allocator(), &.{
.{ "trail.zig", "pub fn callerA() void {\n init(); // then renderX() draws the frame\n}\n" },
.{ "call.zig", "pub fn callerB() void {\n renderX();\n}\n" },
}, "renderX", &out);

try testing.expect(std.mem.indexOf(u8, out.items, "1 call sites for 'renderX'") != null);
try testing.expect(std.mem.indexOf(u8, out.items, "trail.zig:2") == null);
try testing.expect(std.mem.indexOf(u8, out.items, "call.zig:2") != null);
}

test "issue-682: codedb_callers keeps a call after a string containing the comment marker" {
// `fetch("https://x", renderX);` — the `//` sits inside the string literal,
// so the comment cut must not swallow the real reference after it.
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
var out: std.ArrayList(u8) = .empty;
defer out.deinit(testing.allocator);

try renderCallersFixture(arena.allocator(), &.{
.{ "url.zig", "pub fn callerA() void {\n fetch(\"https://x\", renderX);\n}\n" },
}, "renderX", &out);

try testing.expect(std.mem.indexOf(u8, out.items, "1 call sites for 'renderX'") != null);
try testing.expect(std.mem.indexOf(u8, out.items, "url.zig:2") != null);
}

test "issue: codedb_callers keeps a symbol used outside a string on a line that has strings" {
// `foo("msg", renderX)` passes the function by reference next to a string
// literal — the mention is real code and must survive the filter.
Expand Down