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/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index de5aae3c8..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)', () => { @@ -4061,7 +4063,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(() => { @@ -4081,9 +4085,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', () => { diff --git a/codegraph-kernel/src/resolve/query.rs b/codegraph-kernel/src/resolve/query.rs index fd41d19ed..f82de54fa 100644 --- a/codegraph-kernel/src/resolve/query.rs +++ b/codegraph-kernel/src/resolve/query.rs @@ -937,15 +937,48 @@ 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 { + 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("//") { + let end = line_comment_end(rest, 0); + if end == rest.len() { + return ""; + } + s = &rest[end..]; + continue; + } + 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) { + if re!(r"^(?:function|class)(?-u:\b)").is_match(rest) { return true; } // `x =>` is anonymous; `(…)` is an arrow's parameters when `=>` or a @@ -955,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 == ')' { @@ -964,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 @@ -973,6 +1023,19 @@ 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\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] fn read_source_refuses_directories_and_oversized_files() { let dir = std::env::temp_dir().join(format!("cg-read-source-{}", std::process::id()));