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
335 changes: 288 additions & 47 deletions src/mcp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
}

Expand Down Expand Up @@ -2645,33 +2643,82 @@ 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 {
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;
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 (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)) {
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;
continue;
}
return false;
}
if (stringLiteralEnd(line, i)) |end| {

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 Preserve Go raw strings ending in backslash

For Go, raw backtick strings do not use backslash escapes, but this branch uses stringLiteralEnd (which skips \\ plus the next byte) to locate every non-template backtick literal. On a valid Go line like s := `\`; renderX() the closing backtick after the backslash is skipped, so the scanner takes the unclosed-literal path and codedb_callers drops the real call that follows.

Useful? React with 👍 / 👎.

i = end + 1;
continue;
}
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;
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;
Expand All @@ -2683,45 +2730,239 @@ 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 => &.{"//"},
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 => &.{"#"},
.hcl => &.{ "#", "//" },
.sql => &.{"--"},
.fortran => &.{"!"},
.llvm_ir => &.{";"},
else => return line,
else => &.{},
};
var i: usize = 0;
}

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 false;
}

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 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, template_depth: usize) TemplateScan {
var i = open + 1;
while (i < line.len) {
if (line[i] == '"' or line[i] == '\'') {
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 (hasWholeWordMatchInCodeDepth(line[expression_start..end], needle, language, template_depth + 1)) {
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 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 (!javascriptRegexCanFollow(line, previous)) 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 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;
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;
}
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 (line[i] == '/') {
if (javascriptRegexEndAt(line, i)) |end| {
i = end;
continue;
}
}
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,
};
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..], "<!--")) {
return .{ .end = delimitedCommentEnd(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 line;
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
Expand Down
Loading