From 8efc9241714d955137de76b8f0ab6e890d60789b Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:21:30 +0800 Subject: [PATCH 1/8] test(#682): expose remaining comment scanner edge cases Add failing coverage for PHP and Swift trailing comments, shell comments after operators, and real calls following marker-like text in backtick, raw-string, and inline block-comment spans. These cases show why the merged prefix scanner is not yet safe to promote unchanged. Co-Authored-By: Codegraff --- src/test_mcp.zig | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/test_mcp.zig b/src/test_mcp.zig index 536684e9..aadfdf5d 100644 --- a/src/test_mcp.zig +++ b/src/test_mcp.zig @@ -3821,6 +3821,62 @@ test "issue-682: codedb_callers keeps a call after a string containing the comme try testing.expect(std.mem.indexOf(u8, out.items, "url.zig:2") != null); } +test "issue-682: codedb_callers recognizes PHP and Swift trailing comments" { + 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.php", " Date: Wed, 5 Aug 2026 10:27:35 +0800 Subject: [PATCH 2/8] fix(#682): harden callers comment-prefix scanning Prevent line-comment markers inside backtick/raw literals and complete inline block comments from truncating away real callers later on the line. Cover PHP and Swift markers, preserve PHP 8 attributes, and recognize shell comments after unescaped operators while keeping parameter-expansion hashes live. This keeps the scan line-local and allocation-free, preserving the existing conservative behavior for block-comment mentions while removing regressions introduced by the initial trailing-comment filter. Co-Authored-By: Codegraff --- src/mcp.zig | 130 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 117 insertions(+), 13 deletions(-) diff --git a/src/mcp.zig b/src/mcp.zig index fe0d13b1..7a14287e 100644 --- a/src/mcp.zig +++ b/src/mcp.zig @@ -2688,14 +2688,15 @@ fn hasWholeWordMatchOutsideStrings(line: []const u8, needle: []const u8) bool { /// 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()`. +/// This remains deliberately line-local, but skips complete quoted, raw, +/// backtick, and block-comment spans while looking for a line-comment opener. +/// The spans remain in the returned prefix so the caller matcher keeps its +/// existing behavior for references inside them; skipping them here only +/// prevents marker-like text from hiding real code later on the same line. 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 => &.{"//"}, + .zig, .rust, .go_lang, .javascript, .typescript, .c, .cpp, .dart, .java, .kotlin, .swift, .mlir, .tablegen, .rescript, .svelte, .vue, .astro => &.{"//"}, + .php => &.{ "//", "#" }, .python, .ruby, .r, .shell => &.{"#"}, .hcl => &.{ "#", "//" }, .sql => &.{"--"}, @@ -2705,25 +2706,128 @@ fn lineCodePrefix(line: []const u8, language: explore_mod.Language) []const u8 { }; var i: usize = 0; while (i < line.len) { - if (line[i] == '"' or line[i] == '\'') { + if (language == .rust) { + if (rustRawLiteralAt(line, i)) |raw| { + if (raw.end) |end| { + i = end; + continue; + } + return line; + } + } + if (line[i] == '"' or line[i] == '\'' or (line[i] == '`' and hasBacktickLiterals(language))) { if (stringLiteralEnd(line, i)) |end| { i = end + 1; continue; } } - 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]; + if (blockCommentAt(line, i, language)) |block| { + if (block.end) |end| { + i = end; + continue; } + return line; + } + for (markers) |m| { + if (!std.mem.startsWith(u8, line[i..], m)) continue; + // `#[` starts a PHP 8 attribute, not a hash comment. + if (language == .php and m[0] == '#' and i + 1 < line.len and line[i + 1] == '[') break; + // Shell: `#` opens a comment only when it begins a word. Operators + // are word boundaries too; `${#var}` and `$#` remain live syntax. + if (language == .shell and !isShellCommentStart(line, i)) break; + return line[0..i]; } i += 1; } return line; } +const LineSpan = struct { end: ?usize }; + +fn rustRawLiteralAt(line: []const u8, start: usize) ?LineSpan { + if (start != 0 and isIdentChar(line[start - 1])) return null; + var i = start; + if (line[i] == 'b') { + if (i + 1 >= line.len or line[i + 1] != 'r') return null; + i += 2; + } else if (line[i] == 'r') { + i += 1; + } else return null; + + var hashes: usize = 0; + while (i < line.len and line[i] == '#') : (i += 1) hashes += 1; + if (i >= line.len or line[i] != '"') return null; + + i += 1; + while (i < line.len) : (i += 1) { + if (line[i] != '"' or i + 1 + hashes > line.len) continue; + var matched: usize = 0; + while (matched < hashes and line[i + 1 + matched] == '#') : (matched += 1) {} + if (matched == hashes) return .{ .end = i + 1 + hashes }; + } + return .{ .end = null }; +} + +fn hasBacktickLiterals(language: explore_mod.Language) bool { + return switch (language) { + .go_lang, .javascript, .typescript, .php, .ruby, .r, .svelte, .vue, .astro => true, + else => false, + }; +} + +fn blockCommentAt(line: []const u8, start: usize, language: explore_mod.Language) ?LineSpan { + const slash_block = switch (language) { + .rust, .go_lang, .javascript, .typescript, .c, .cpp, .php, .hcl, .dart, .java, .kotlin, .swift, .svelte, .vue, .astro, .sql, .mlir, .tablegen, .rescript => true, + else => false, + }; + if (slash_block and std.mem.startsWith(u8, line[start..], "/*")) { + return .{ .end = delimitedCommentEnd(line, start, "/*", "*/", language == .rust or language == .swift) }; + } + const html_block = switch (language) { + .svelte, .vue, .astro => true, + else => false, + }; + if (html_block and std.mem.startsWith(u8, line[start..], "", false) }; + } + return null; +} + +fn delimitedCommentEnd(line: []const u8, start: usize, open: []const u8, close: []const u8, nested: bool) ?usize { + var depth: usize = 1; + var i = start + open.len; + while (i < line.len) { + if (nested and std.mem.startsWith(u8, line[i..], open)) { + depth += 1; + i += open.len; + continue; + } + if (std.mem.startsWith(u8, line[i..], close)) { + depth -= 1; + i += close.len; + if (depth == 0) return i; + continue; + } + i += 1; + } + return null; +} + +fn isShellCommentStart(line: []const u8, hash: usize) bool { + if (hash == 0) return true; + const boundary = hash - 1; + const c = line[boundary]; + if (c != ' ' and c != '\t' and std.mem.indexOfScalar(u8, "|&;()<>", c) == null) return false; + + var slash_count: usize = 0; + var i = boundary; + while (i > 0 and line[i - 1] == '\\') { + slash_count += 1; + i -= 1; + } + return slash_count % 2 == 0; +} + /// 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 { From 7e25eaba39d7f86a6a705343cc31d20303368bee Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:33:41 +0800 Subject: [PATCH 3/8] test(#682): guard literal and block-comment mentions Cover negative-only raw, backtick, and inline block-comment spans so scanner fixes cannot trade false negatives for false callers. Preserve JavaScript template-expression calls and exercise unquoted shell parameter expansion. Co-Authored-By: Codegraff --- src/test_mcp.zig | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/test_mcp.zig b/src/test_mcp.zig index aadfdf5d..8c790902 100644 --- a/src/test_mcp.zig +++ b/src/test_mcp.zig @@ -3861,6 +3861,28 @@ test "issue-682: codedb_callers keeps calls after markers inside literals and bl try testing.expect(std.mem.indexOf(u8, out.items, "raw.rs:2") != null); } +test "issue-682: codedb_callers excludes mentions inside raw literals and block comments" { + 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(), &.{ + .{ "mention.go", "func notCallerA() {\n text := `https://example.test/renderX`\n}\n" }, + .{ "mention.js", "function notCallerB() {\n const text = `https://example.test/renderX`;\n}\n" }, + .{ "mention.c", "void not_caller_c(void) {\n init(); /* see https://example.test/renderX */\n}\n" }, + .{ "mention.rs", "fn not_caller_d() {\n let text = r#\"say \"https://example.test/renderX\"\"#;\n}\n" }, + .{ "template.js", "function callerE() {\n const text = `value: ${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, "mention.go") == null); + try testing.expect(std.mem.indexOf(u8, out.items, "mention.js") == null); + try testing.expect(std.mem.indexOf(u8, out.items, "mention.c") == null); + try testing.expect(std.mem.indexOf(u8, out.items, "mention.rs") == null); + try testing.expect(std.mem.indexOf(u8, out.items, "template.js:2") != null); +} + test "issue-682: codedb_callers recognizes shell comments after operators" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); @@ -3869,7 +3891,7 @@ test "issue-682: codedb_callers recognizes shell comments after operators" { try renderCallersFixture(arena.allocator(), &.{ .{ "trail.sh", "init;# renderX() is documented here\n" }, - .{ "call.sh", "items=abc\nprintf '%s' \"${#items}\"; renderX\n" }, + .{ "call.sh", "items=abc\ncount=${#items}; renderX\n" }, }, "renderX", &out); try testing.expect(std.mem.indexOf(u8, out.items, "1 call sites for 'renderX'") != null); From 49a31a35b23b9d0abafe0f4c5065233ea3f081a4 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:43:29 +0800 Subject: [PATCH 4/8] fix(#682): match callers only in lexical code Unify literal and comment handling in the whole-word caller scan so marker-like text cannot hide later code or re-admit mentions from skipped spans. Raw strings, backticks, complete inline block comments, and line comments are scanned allocation-free; JavaScript template text is ignored while ${...} expressions remain searchable. This replaces the prefix-then-match split that handled the same spans inconsistently and traded false negatives for false positives. Co-Authored-By: Codegraff --- src/mcp.zig | 203 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 131 insertions(+), 72 deletions(-) diff --git a/src/mcp.zig b/src/mcp.zig index 7a14287e..223af7da 100644 --- a/src/mcp.zig +++ b/src/mcp.zig @@ -2578,12 +2578,10 @@ 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 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; + // A whole-word match that only lands inside a literal or comment is a + // mention, not an invocation — a `("name", "file")` table row, a + // `test "…name…" {` declaration, or `init(); // then name() …`. (#682) + if (!hasWholeWordMatchInCode(r.line_text, name, lang)) continue; kept.append(alloc, r_idx) catch {}; } @@ -2645,33 +2643,55 @@ fn isIdentChar(c: u8) bool { c == '_'; } -/// Returns true iff `needle` occurs in `line` as a whole-word identifier -/// (non-identifier characters or line boundaries on both sides) at a position -/// that is NOT inside a string literal. -/// -/// An occurrence that only ever appears between quotes is a mention, not a -/// call: the Python table row `("renderPlainSearch", "src/explore.zig"),` and -/// the Zig declaration `test "def-first: renderPlainSearch surfaces …" {` both -/// name the symbol without invoking it, yet passed every other filter here. -/// -/// Deliberately line-local and pragmatic, in the spirit of `isCommentOrBlank`: -/// - tracks '…' and "…" spans, honouring backslash escapes -/// - a quote with no closing partner on the line (an apostrophe in a -/// trailing comment, a Rust lifetime `&'a`, an open multi-line literal) is -/// treated as ordinary code, so it can never swallow the rest of the line -/// - backticks are not treated as quotes: Go raw strings and JS template -/// literals routinely wrap real calls (`${render()}`) -fn hasWholeWordMatchOutsideStrings(line: []const u8, needle: []const u8) bool { +/// Returns true when `needle` occurs as a whole-word identifier in code rather +/// than in a string, raw/template literal, or comment. This is deliberately a +/// line-local lexical scan: it handles complete spans on the current line and +/// conservatively treats an unclosed block/raw span as consuming the remainder. +fn hasWholeWordMatchInCode(line: []const u8, needle: []const u8, language: explore_mod.Language) bool { if (needle.len == 0 or line.len < needle.len) return false; + const markers = lineCommentMarkers(language); var i: usize = 0; while (i < line.len) { + if (language == .rust) { + if (rustRawLiteralAt(line, i)) |raw| { + if (raw.end) |end| { + i = end; + continue; + } + return false; + } + } if (line[i] == '"' or line[i] == '\'') { if (stringLiteralEnd(line, i)) |end| { i = end + 1; continue; } } - // `i` is a code byte — try to anchor a whole-word match on it. + if (line[i] == '`' and hasBacktickLiterals(language)) { + if (hasTemplateLiterals(language)) { + const template = scanTemplateLiteral(line, i, needle, language); + if (template.matched) return true; + if (template.end) |end| { + i = end; + continue; + } + return false; + } + if (stringLiteralEnd(line, i)) |end| { + i = end + 1; + continue; + } + return false; + } + if (blockCommentAt(line, i, language)) |block| { + if (block.end) |end| { + i = end; + continue; + } + return false; + } + if (isLineCommentAt(line, i, language, markers)) return false; + if (line.len - i >= needle.len and std.mem.eql(u8, line[i..][0..needle.len], needle)) { const before_ok = i == 0 or !isIdentChar(line[i - 1]); const after = i + needle.len; @@ -2683,18 +2703,8 @@ 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) -/// -/// This remains deliberately line-local, but skips complete quoted, raw, -/// backtick, and block-comment spans while looking for a line-comment opener. -/// The spans remain in the returned prefix so the caller matcher keeps its -/// existing behavior for references inside them; skipping them here only -/// prevents marker-like text from hiding real code later on the same line. -fn lineCodePrefix(line: []const u8, language: explore_mod.Language) []const u8 { - const markers: []const []const u8 = switch (language) { +fn lineCommentMarkers(language: explore_mod.Language) []const []const u8 { + return switch (language) { .zig, .rust, .go_lang, .javascript, .typescript, .c, .cpp, .dart, .java, .kotlin, .swift, .mlir, .tablegen, .rescript, .svelte, .vue, .astro => &.{"//"}, .php => &.{ "//", "#" }, .python, .ruby, .r, .shell => &.{"#"}, @@ -2702,44 +2712,21 @@ fn lineCodePrefix(line: []const u8, language: explore_mod.Language) []const u8 { .sql => &.{"--"}, .fortran => &.{"!"}, .llvm_ir => &.{";"}, - else => return line, + else => &.{}, }; - var i: usize = 0; - while (i < line.len) { - if (language == .rust) { - if (rustRawLiteralAt(line, i)) |raw| { - if (raw.end) |end| { - i = end; - continue; - } - return line; - } - } - if (line[i] == '"' or line[i] == '\'' or (line[i] == '`' and hasBacktickLiterals(language))) { - if (stringLiteralEnd(line, i)) |end| { - i = end + 1; - continue; - } - } - if (blockCommentAt(line, i, language)) |block| { - if (block.end) |end| { - i = end; - continue; - } - return line; - } - for (markers) |m| { - if (!std.mem.startsWith(u8, line[i..], m)) continue; - // `#[` starts a PHP 8 attribute, not a hash comment. - if (language == .php and m[0] == '#' and i + 1 < line.len and line[i + 1] == '[') break; - // Shell: `#` opens a comment only when it begins a word. Operators - // are word boundaries too; `${#var}` and `$#` remain live syntax. - if (language == .shell and !isShellCommentStart(line, i)) break; - return line[0..i]; - } - i += 1; +} + +fn isLineCommentAt(line: []const u8, start: usize, language: explore_mod.Language, markers: []const []const u8) bool { + for (markers) |marker| { + if (!std.mem.startsWith(u8, line[start..], marker)) continue; + // `#[` starts a PHP 8 attribute, not a hash comment. + if (language == .php and marker[0] == '#' and start + 1 < line.len and line[start + 1] == '[') return false; + // Shell: `#` opens a comment only when it begins a word. Operators are + // word boundaries too; `${#var}` and `$#` remain live syntax. + if (language == .shell and !isShellCommentStart(line, start)) return false; + return true; } - return line; + return false; } const LineSpan = struct { end: ?usize }; @@ -2775,7 +2762,79 @@ fn hasBacktickLiterals(language: explore_mod.Language) bool { }; } +fn hasTemplateLiterals(language: explore_mod.Language) bool { + return switch (language) { + .javascript, .typescript, .svelte, .vue, .astro => true, + else => false, + }; +} + +const TemplateScan = struct { + end: ?usize, + matched: bool, +}; + +fn scanTemplateLiteral(line: []const u8, open: usize, needle: []const u8, language: explore_mod.Language) TemplateScan { + var i = open + 1; + while (i < line.len) { + if (line[i] == '\\') { + i += if (line.len - i > 1) 2 else 1; + continue; + } + if (line[i] == '`') return .{ .end = i + 1, .matched = false }; + if (line[i] == '$' and i + 1 < line.len and line[i + 1] == '{') { + const expression_start = i + 2; + const expression_end = templateExpressionEnd(line, expression_start, language); + const end = expression_end orelse line.len; + if (hasWholeWordMatchInCode(line[expression_start..end], needle, language)) { + return .{ .end = expression_end, .matched = true }; + } + if (expression_end) |close| { + i = close + 1; + continue; + } + return .{ .end = null, .matched = false }; + } + i += 1; + } + return .{ .end = null, .matched = false }; +} + +fn templateExpressionEnd(line: []const u8, start: usize, language: explore_mod.Language) ?usize { + const markers = lineCommentMarkers(language); + var depth: usize = 1; + var i = start; + while (i < line.len) { + if (line[i] == '"' or line[i] == '\'' or line[i] == '`') { + if (stringLiteralEnd(line, i)) |end| { + i = end + 1; + continue; + } + return null; + } + if (blockCommentAt(line, i, language)) |block| { + if (block.end) |end| { + i = end; + continue; + } + return null; + } + if (isLineCommentAt(line, i, language, markers)) return null; + if (line[i] == '{') { + depth += 1; + } else if (line[i] == '}') { + depth -= 1; + if (depth == 0) return i; + } + i += 1; + } + return null; +} + fn blockCommentAt(line: []const u8, start: usize, language: explore_mod.Language) ?LineSpan { + if (language == .ocaml and std.mem.startsWith(u8, line[start..], "(*")) { + return .{ .end = delimitedCommentEnd(line, start, "(*", "*)", true) }; + } const slash_block = switch (language) { .rust, .go_lang, .javascript, .typescript, .c, .cpp, .php, .hcl, .dart, .java, .kotlin, .swift, .svelte, .vue, .astro, .sql, .mlir, .tablegen, .rescript => true, else => false, From 7e2b4c9b440c0f866d2c424fa07cf26f3145bf27 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:44:50 +0800 Subject: [PATCH 5/8] test(#682): cover template regexes and R backticks Add failing caller coverage for braces inside JavaScript regex literals, callable R backtick identifiers, and hash-bearing R identifiers before a real call. Co-Authored-By: Codegraff --- src/test_mcp.zig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test_mcp.zig b/src/test_mcp.zig index 8c790902..479944fe 100644 --- a/src/test_mcp.zig +++ b/src/test_mcp.zig @@ -3883,6 +3883,24 @@ test "issue-682: codedb_callers excludes mentions inside raw literals and block try testing.expect(std.mem.indexOf(u8, out.items, "template.js:2") != null); } +test "issue-682: codedb_callers keeps template-regex and R backtick calls" { + 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(), &.{ + .{ "regex.js", "function callerA(value) {\n return `${/[}]/.test(value) ? renderX() : value}`;\n}\n" }, + .{ "backtick-call.r", "caller_b <- function() {\n `renderX`(1)\n}\n" }, + .{ "backtick-marker.r", "caller_c <- function() {\n `label#value`; renderX()\n}\n" }, + }, "renderX", &out); + + try testing.expect(std.mem.indexOf(u8, out.items, "3 call sites for 'renderX'") != null); + try testing.expect(std.mem.indexOf(u8, out.items, "regex.js:2") != null); + try testing.expect(std.mem.indexOf(u8, out.items, "backtick-call.r:2") != null); + try testing.expect(std.mem.indexOf(u8, out.items, "backtick-marker.r:2") != null); +} + test "issue-682: codedb_callers recognizes shell comments after operators" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); From 154fb86bf60a912dc2315b6dd8924f3c84467a03 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:49:28 +0800 Subject: [PATCH 6/8] fix(#682): bound template scanning edge cases Cap recursive JavaScript template analysis to bound stack and worst-case rescanning on hostile single-line input. Skip regex literals while locating interpolation braces, and preserve R backtick identifiers as callable symbols without treating embedded hashes as comments. Co-Authored-By: Codegraff --- src/mcp.zig | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/src/mcp.zig b/src/mcp.zig index 223af7da..378cd619 100644 --- a/src/mcp.zig +++ b/src/mcp.zig @@ -2648,6 +2648,12 @@ fn isIdentChar(c: u8) bool { /// line-local lexical scan: it handles complete spans on the current line and /// conservatively treats an unclosed block/raw span as consuming the remainder. fn hasWholeWordMatchInCode(line: []const u8, needle: []const u8, language: explore_mod.Language) bool { + return hasWholeWordMatchInCodeDepth(line, needle, language, 0); +} + +const max_template_nesting = 16; + +fn hasWholeWordMatchInCodeDepth(line: []const u8, needle: []const u8, language: explore_mod.Language, template_depth: usize) bool { if (needle.len == 0 or line.len < needle.len) return false; const markers = lineCommentMarkers(language); var i: usize = 0; @@ -2668,8 +2674,23 @@ fn hasWholeWordMatchInCode(line: []const u8, needle: []const u8, language: explo } } if (line[i] == '`' and hasBacktickLiterals(language)) { + if (language == .r) { + if (stringLiteralEnd(line, i)) |end| { + if (std.mem.eql(u8, line[i + 1 .. end], needle)) return true; + i = end + 1; + continue; + } + return false; + } if (hasTemplateLiterals(language)) { - const template = scanTemplateLiteral(line, i, needle, language); + if (template_depth >= max_template_nesting) { + if (stringLiteralEnd(line, i)) |end| { + i = end + 1; + continue; + } + return false; + } + const template = scanTemplateLiteral(line, i, needle, language, template_depth); if (template.matched) return true; if (template.end) |end| { i = end; @@ -2683,6 +2704,12 @@ fn hasWholeWordMatchInCode(line: []const u8, needle: []const u8, language: explo } return false; } + if (hasTemplateLiterals(language) and line[i] == '/') { + if (javascriptRegexEndAt(line, i)) |end| { + i = end; + continue; + } + } if (blockCommentAt(line, i, language)) |block| { if (block.end) |end| { i = end; @@ -2774,7 +2801,7 @@ const TemplateScan = struct { matched: bool, }; -fn scanTemplateLiteral(line: []const u8, open: usize, needle: []const u8, language: explore_mod.Language) TemplateScan { +fn scanTemplateLiteral(line: []const u8, open: usize, needle: []const u8, language: explore_mod.Language, template_depth: usize) TemplateScan { var i = open + 1; while (i < line.len) { if (line[i] == '\\') { @@ -2786,7 +2813,7 @@ fn scanTemplateLiteral(line: []const u8, open: usize, needle: []const u8, langua const expression_start = i + 2; const expression_end = templateExpressionEnd(line, expression_start, language); const end = expression_end orelse line.len; - if (hasWholeWordMatchInCode(line[expression_start..end], needle, language)) { + if (hasWholeWordMatchInCodeDepth(line[expression_start..end], needle, language, template_depth + 1)) { return .{ .end = expression_end, .matched = true }; } if (expression_end) |close| { @@ -2800,6 +2827,34 @@ fn scanTemplateLiteral(line: []const u8, open: usize, needle: []const u8, langua return .{ .end = null, .matched = false }; } +fn javascriptRegexEndAt(line: []const u8, start: usize) ?usize { + if (start + 1 >= line.len or line[start] != '/' or line[start + 1] == '/' or line[start + 1] == '*') return null; + + var previous = start; + while (previous > 0 and (line[previous - 1] == ' ' or line[previous - 1] == '\t')) previous -= 1; + if (previous > 0 and std.mem.indexOfScalar(u8, "([{:;,=!?&|+-*%^~<>", line[previous - 1]) == null) return null; + + var in_class = false; + var i = start + 1; + while (i < line.len) { + if (line[i] == '\\') { + i += if (line.len - i > 1) 2 else 1; + continue; + } + if (line[i] == '[') { + in_class = true; + } else if (line[i] == ']') { + in_class = false; + } else if (line[i] == '/' and !in_class) { + i += 1; + while (i < line.len and std.ascii.isAlphabetic(line[i])) i += 1; + return i; + } + i += 1; + } + return null; +} + fn templateExpressionEnd(line: []const u8, start: usize, language: explore_mod.Language) ?usize { const markers = lineCommentMarkers(language); var depth: usize = 1; @@ -2812,6 +2867,12 @@ fn templateExpressionEnd(line: []const u8, start: usize, language: explore_mod.L } return null; } + if (line[i] == '/') { + if (javascriptRegexEndAt(line, i)) |end| { + i = end; + continue; + } + } if (blockCommentAt(line, i, language)) |block| { if (block.end) |end| { i = end; From 2f14ea021bb980fbae3d1628ceaf28f186c2e946 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:52:20 +0800 Subject: [PATCH 7/8] test(#682): exclude keyword-led regex mentions Add failing coverage for a symbol appearing only in a JavaScript regex literal after return, where punctuation-only regex context detection scans the pattern as code. Co-Authored-By: Codegraff --- src/test_mcp.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test_mcp.zig b/src/test_mcp.zig index 479944fe..0356f3f5 100644 --- a/src/test_mcp.zig +++ b/src/test_mcp.zig @@ -3872,7 +3872,8 @@ test "issue-682: codedb_callers excludes mentions inside raw literals and block .{ "mention.js", "function notCallerB() {\n const text = `https://example.test/renderX`;\n}\n" }, .{ "mention.c", "void not_caller_c(void) {\n init(); /* see https://example.test/renderX */\n}\n" }, .{ "mention.rs", "fn not_caller_d() {\n let text = r#\"say \"https://example.test/renderX\"\"#;\n}\n" }, - .{ "template.js", "function callerE() {\n const text = `value: ${renderX()}`;\n}\n" }, + .{ "mention-regex.js", "function notCallerE() {\n return /renderX/;\n}\n" }, + .{ "template.js", "function callerF() {\n const text = `value: ${renderX()}`;\n}\n" }, }, "renderX", &out); try testing.expect(std.mem.indexOf(u8, out.items, "1 call sites for 'renderX'") != null); @@ -3880,6 +3881,7 @@ test "issue-682: codedb_callers excludes mentions inside raw literals and block try testing.expect(std.mem.indexOf(u8, out.items, "mention.js") == null); try testing.expect(std.mem.indexOf(u8, out.items, "mention.c") == null); try testing.expect(std.mem.indexOf(u8, out.items, "mention.rs") == null); + try testing.expect(std.mem.indexOf(u8, out.items, "mention-regex.js") == null); try testing.expect(std.mem.indexOf(u8, out.items, "template.js:2") != null); } From 09b4e26040d88362b7b8f3348e2c83cdc71a14cf Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:53:39 +0800 Subject: [PATCH 8/8] fix(#682): recognize keyword-led JavaScript regexes Treat regex literals after expression-leading JavaScript keywords such as return, throw, yield, and await as non-code spans, preventing pattern text from becoming false caller matches. Co-Authored-By: Codegraff --- src/mcp.zig | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/mcp.zig b/src/mcp.zig index 378cd619..4d325cfa 100644 --- a/src/mcp.zig +++ b/src/mcp.zig @@ -2832,7 +2832,7 @@ fn javascriptRegexEndAt(line: []const u8, start: usize) ?usize { var previous = start; while (previous > 0 and (line[previous - 1] == ' ' or line[previous - 1] == '\t')) previous -= 1; - if (previous > 0 and std.mem.indexOfScalar(u8, "([{:;,=!?&|+-*%^~<>", line[previous - 1]) == null) return null; + if (!javascriptRegexCanFollow(line, previous)) return null; var in_class = false; var i = start + 1; @@ -2855,6 +2855,23 @@ fn javascriptRegexEndAt(line: []const u8, start: usize) ?usize { return null; } +fn javascriptRegexCanFollow(line: []const u8, end: usize) bool { + if (end == 0) return true; + if (std.mem.indexOfScalar(u8, "([{:;,=!?&|+-*%^~<>", line[end - 1]) != null) return true; + + var start = end; + while (start > 0 and (std.ascii.isAlphanumeric(line[start - 1]) or line[start - 1] == '_')) start -= 1; + const word = line[start..end]; + const keywords = [_][]const u8{ + "await", "case", "delete", "do", "else", "in", "instanceof", + "new", "of", "return", "throw", "typeof", "void", "yield", + }; + for (keywords) |keyword| { + if (std.mem.eql(u8, word, keyword)) return true; + } + return false; +} + fn templateExpressionEnd(line: []const u8, start: usize, language: explore_mod.Language) ?usize { const markers = lineCommentMarkers(language); var depth: usize = 1;