From c81407a506f5daf551fd780cd5be6d2fc1511bd9 Mon Sep 17 00:00:00 2001 From: bompus Date: Mon, 2 Nov 2026 17:05:06 -0700 Subject: [PATCH 1/5] fix(resolution): a comment before an anonymous export default stays anonymous The classifier read `export default /* c */ () => 2` as an expression, so the default_fn_class fallback could name the module's first exported function. Skip whitespace and JS block/line comments between `export default` and the head before classifying. Adds a kernel unit test (red before) and an extraction regression fixture. --- __tests__/default-export-declaration.test.ts | 9 +++++ codegraph-kernel/src/resolve/query.rs | 37 ++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/__tests__/default-export-declaration.test.ts b/__tests__/default-export-declaration.test.ts index ac7671031..1d3c67755 100644 --- a/__tests__/default-export-declaration.test.ts +++ b/__tests__/default-export-declaration.test.ts @@ -278,6 +278,9 @@ export default Main export default ( value ) => value; +`, + 'src/gen/commented.ts': `export function helper() { return 1; } +export default /* comment */ () => 2; `, 'src/gen/use.ts': `import Issue from "./issue" import Render from "./render" @@ -286,6 +289,7 @@ import Persisted from "./model" import Plain from "./plain" import Unbalanced from "./unbalanced" import Multi from "./multi" +import Commented from "./commented" export function generate() { Issue() @@ -295,6 +299,7 @@ export function generate() { Plain() Unbalanced() Multi() + Commented() } `, 'src/use.tsx': `import handler from "./api/vans" @@ -453,6 +458,10 @@ describe('an `export default` line inside a template literal', () => { expect(generated()).not.toContain('src/gen/multi.ts:helper'); }); + it('binds a comment before an anonymous default to nothing, not to a function beside it', () => { + expect(generated()).not.toContain('src/gen/commented.ts:helper'); + }); + it('leaves regexes, strings and division alone, and a scan it cannot finish', () => { const targets = generated(); expect(targets).toContain('src/gen/plain.ts:Main'); diff --git a/codegraph-kernel/src/resolve/query.rs b/codegraph-kernel/src/resolve/query.rs index fd41d19ed..0a96791d7 100644 --- a/codegraph-kernel/src/resolve/query.rs +++ b/codegraph-kernel/src/resolve/query.rs @@ -937,13 +937,38 @@ fn is_js_member_node(n: &KNode) -> bool { /// function, class or arrow with no name. `export default memo(Card)` or /// `new Service()` is an expression and answers `false`; a head the line /// doesn't begin conservatively answers `true`. +/// Skip whitespace, block comments and line comments between `export default` +/// and the expression that follows it. +fn skip_js_trivia(mut s: &str) -> &str { + loop { + let t = s.trim_start(); + if let Some(rest) = t.strip_prefix("/*") { + if let Some(end) = rest.find("*/") { + s = &rest[end + 2..]; + continue; + } + return t; + } + if let Some(rest) = t.strip_prefix("//") { + match rest.find('\n') { + Some(end) => { + s = &rest[end + 1..]; + continue; + } + None => return "", + } + } + return t; + } +} + fn export_default_head_is_anonymous(line: &str) -> bool { let Some(m) = re!(r"^[ \t]*export[ \t]+default[ \t]+").find(line) else { return true; }; - let mut rest = &line[m.end()..]; + let mut rest = skip_js_trivia(&line[m.end()..]); if let Some(after) = rest.strip_prefix("async") { - rest = after.trim_start(); + rest = skip_js_trivia(after); } if re!(r"^(?:function|class)").is_match(rest) { return true; @@ -973,6 +998,14 @@ fn export_default_head_is_anonymous(line: &str) -> bool { mod tests { use super::*; + #[test] + fn export_default_head_skips_comments() { + assert!(export_default_head_is_anonymous("export default /* c */ () => 2")); + assert!(export_default_head_is_anonymous("export default // c\n() => 2")); + assert!(export_default_head_is_anonymous("export default /* c */ function () {}")); + assert!(!export_default_head_is_anonymous("export default /* c */ memo(Card)")); + } + #[test] fn read_source_refuses_directories_and_oversized_files() { let dir = std::env::temp_dir().join(format!("cg-read-source-{}", std::process::id())); From fe29676d9d28ca135866f25d6786cd1d40d108dd Mon Sep 17 00:00:00 2001 From: bompus Date: Mon, 2 Nov 2026 17:14:09 -0700 Subject: [PATCH 2/5] test(installer): compare realpaths so the JSON-target block runs on macOS The installer returns realpath'd file paths; the test compared the raw mkdtemp paths, so every find and array equality failed on macOS (/var -> /private/var). Realpath the temp base before joining the documented file. Docker/linux suite: 641 passed, 8 skipped. --- __tests__/installer-targets.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index de5aae3c8..06f39d6d3 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -4081,9 +4081,10 @@ describe('Installer targets — agents with a JSON mcpServers file', () => { const locations = (['global', 'local'] as const).filter((l) => c[l] !== null); for (const location of locations) { - const file = () => path.join(location === 'global' ? dirs.home() : dirs.cwd(), ...c[location]!); + const baseDir = () => fs.realpathSync(location === 'global' ? dirs.home() : dirs.cwd()); + const file = () => path.join(baseDir(), ...c[location]!); const instructions = location === 'global' ? c.globalInstructions : c.localInstructions; - const instructionsFile = () => path.join(location === 'global' ? dirs.home() : dirs.cwd(), ...instructions!); + const instructionsFile = () => path.join(baseDir(), ...instructions!); describe(`${c.id} (${location})`, () => { it('writes the documented entry to the documented file, and nothing else for the MCP config', () => { From 308737e0da8810a67f7ce5e35beeac2ac689bb65 Mon Sep 17 00:00:00 2001 From: bompus Date: Mon, 2 Nov 2026 17:19:46 -0700 Subject: [PATCH 3/5] fix(resolution): harden the export-default comment scan Addresses the three review findings: the function/class keyword needs a word boundary after the comment skip; a comment inside the parenthesized head is skipped while balancing so its ) is not a closer; and a line comment ends at every JavaScript line terminator (LF, CR, U+2028, U+2029), not just LF. The unit test covers all three; golden dumps unchanged; precision gin/json held. --- codegraph-kernel/src/resolve/query.rs | 46 ++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/codegraph-kernel/src/resolve/query.rs b/codegraph-kernel/src/resolve/query.rs index 0a96791d7..f82de54fa 100644 --- a/codegraph-kernel/src/resolve/query.rs +++ b/codegraph-kernel/src/resolve/query.rs @@ -937,6 +937,15 @@ fn is_js_member_node(n: &KNode) -> bool { /// function, class or arrow with no name. `export default memo(Card)` or /// `new Service()` is an expression and answers `false`; a head the line /// doesn't begin conservatively answers `true`. +/// The byte index of the first JavaScript line terminator at or after `from`. +fn line_comment_end(s: &str, from: usize) -> usize { + s[from..] + .char_indices() + .find(|(_, c)| matches!(c, '\n' | '\r' | '\u{2028}' | '\u{2029}')) + .map(|(i, _)| from + i) + .unwrap_or(s.len()) +} + /// Skip whitespace, block comments and line comments between `export default` /// and the expression that follows it. fn skip_js_trivia(mut s: &str) -> &str { @@ -950,13 +959,12 @@ fn skip_js_trivia(mut s: &str) -> &str { return t; } if let Some(rest) = t.strip_prefix("//") { - match rest.find('\n') { - Some(end) => { - s = &rest[end + 1..]; - continue; - } - None => return "", + let end = line_comment_end(rest, 0); + if end == rest.len() { + return ""; } + s = &rest[end..]; + continue; } return t; } @@ -970,7 +978,7 @@ fn export_default_head_is_anonymous(line: &str) -> bool { if let Some(after) = rest.strip_prefix("async") { rest = skip_js_trivia(after); } - if re!(r"^(?:function|class)").is_match(rest) { + if re!(r"^(?:function|class)(?-u:\b)").is_match(rest) { return true; } // `x =>` is anonymous; `(…)` is an arrow's parameters when `=>` or a @@ -980,7 +988,23 @@ fn export_default_head_is_anonymous(line: &str) -> bool { } if rest.starts_with('(') { let mut depth = 0i32; - for (i, c) in rest.char_indices() { + let mut i = 0; + while i < rest.len() { + let c = rest[i..].chars().next().unwrap(); + // A comment inside the parameters is not a closer. + if c == '/' { + if let Some(next) = rest[i + 1..].chars().next() { + if next == '/' { + i = line_comment_end(rest, i + 2); + continue; + } + if next == '*' { + let Some(end) = rest[i + 2..].find("*/") else { return false }; + i = i + 2 + end + 2; + continue; + } + } + } if c == '(' { depth += 1; } else if c == ')' { @@ -989,6 +1013,7 @@ fn export_default_head_is_anonymous(line: &str) -> bool { return re!(r"^[ \t]*(?:=>|:)").is_match(&rest[i + 1..]); } } + i += c.len_utf8(); } } false @@ -1002,8 +1027,13 @@ mod tests { fn export_default_head_skips_comments() { assert!(export_default_head_is_anonymous("export default /* c */ () => 2")); assert!(export_default_head_is_anonymous("export default // c\n() => 2")); + assert!(export_default_head_is_anonymous("export default // c\r() => 2")); + assert!(export_default_head_is_anonymous("export default // c\u{2028}() => 2")); assert!(export_default_head_is_anonymous("export default /* c */ function () {}")); + assert!(export_default_head_is_anonymous("export default /* c */ (/* ) */ x) => x")); assert!(!export_default_head_is_anonymous("export default /* c */ memo(Card)")); + assert!(!export_default_head_is_anonymous("export default /* c */ functionality")); + assert!(!export_default_head_is_anonymous("export default /* c */ classy")); } #[test] From cd9e2cf0e30df77250caa28a2d0aa3ee415abe8d Mon Sep 17 00:00:00 2001 From: bompus Date: Mon, 2 Nov 2026 17:24:16 -0700 Subject: [PATCH 4/5] test(installer): realpath the test HOME in useTmpDirs Global targets build paths from HOME as spelled; the JSON-target expectations already used the real path, so a symlinked temp root (or macOS /var) disagreed. Set HOME to the real temp path. Installer suite: 641 passed, 8 skipped. --- __tests__/installer-targets.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 06f39d6d3..c7030e8bd 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -4061,7 +4061,9 @@ function useTmpDirs(label: string): { home: () => string; cwd: () => string } { tmpCwd = mkTmpDir(`${label}-cwd`); origCwd = process.cwd(); process.chdir(tmpCwd); - homeRestore = setHome(tmpHome); + // Global targets build paths from HOME as spelled; use the real path so a + // symlinked temp root or macOS /var matches the expected real paths. + homeRestore = setHome(fs.realpathSync(tmpHome)); }); afterEach(() => { From 4464aae4880792c5de23d4f81d8bdfe055c2ccbe Mon Sep 17 00:00:00 2001 From: bompus Date: Mon, 2 Nov 2026 17:36:13 -0700 Subject: [PATCH 5/5] test(installer): realpath mkTmpDir so every target block agrees with macOS paths The remaining macOS failures were the other target blocks: mkTmpDir returned a symlinked /tmp spelling while the targets return real paths. Realpath the mkdtemp result once for every helper; the JSON-target HOME realpath stays as a no-op. Installer suite: 641 passed, 8 skipped. --- __tests__/installer-targets.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index c7030e8bd..8314e5791 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -33,7 +33,9 @@ import { } from '../src/installer/instructions-template'; function mkTmpDir(label: string): string { - return fs.mkdtempSync(path.join(os.tmpdir(), `cg-targets-${label}-`)); + // Realpath so the target's real paths and the test's expected paths agree + // through a symlinked temp root (macOS /var, or a TMPDIR symlink). + return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), `cg-targets-${label}-`))); } describe('standalone installer Windows shell guidance (#1294)', () => {