diff --git a/crates/codegraph-core/src/extractors/javascript.rs b/crates/codegraph-core/src/extractors/javascript.rs index 4b6a2efdf..74bd45d96 100644 --- a/crates/codegraph-core/src/extractors/javascript.rs +++ b/crates/codegraph-core/src/extractors/javascript.rs @@ -3161,6 +3161,7 @@ fn handle_export_declaration(decl: &Node, source: &[u8], symbols: &mut FileSymbo "class_declaration" | "abstract_class_declaration" => ("class", "name"), "interface_declaration" => ("interface", "name"), "type_alias_declaration" => ("type", "name"), + "enum_declaration" => ("enum", "name"), "lexical_declaration" | "variable_declaration" => { collect_exported_var_declarations(decl, source, symbols); return; @@ -10741,6 +10742,46 @@ mod tests { ); } + #[test] + fn exports_an_enum_declaration_with_kind_enum() { + // Regression guard for #2560: enum_declaration had no arm in + // handle_export_declaration's match, so `export enum Foo {}` was + // extracted as a Definition (via handle_enum_decl) but never marked + // exported. + let s = parse_ts("export enum Color { Red, Green, Blue }"); + assert!( + s.definitions + .iter() + .any(|d| d.name == "Color" && d.kind == "enum" && d.line == 1), + "expected 'Color' defined as enum at line 1; got: {:?}", + s.definitions + ); + assert!( + s.exports + .iter() + .any(|e| e.name == "Color" && e.kind == "enum" && e.line == 1), + "expected 'Color' exported as enum at line 1; got: {:?}", + s.exports + ); + } + + #[test] + fn does_not_export_a_non_exported_enum() { + let s = parse_ts("enum Internal { A, B }"); + assert!( + s.definitions + .iter() + .any(|d| d.name == "Internal" && d.kind == "enum"), + "expected 'Internal' defined as enum; got: {:?}", + s.definitions + ); + assert!( + !s.exports.iter().any(|e| e.name == "Internal"), + "did not expect 'Internal' to be exported; got: {:?}", + s.exports + ); + } + #[test] fn skips_a_comment_between_the_export_keyword_and_the_declaration() { let s = parse_js("export\n// why is this exported\nconst withComment = 1;"); diff --git a/src/extractors/javascript.ts b/src/extractors/javascript.ts index daac7a103..e33f4c5c7 100644 --- a/src/extractors/javascript.ts +++ b/src/extractors/javascript.ts @@ -210,6 +210,7 @@ const EXPORT_DECL_KIND: Record = { abstract_class_declaration: 'class', interface_declaration: 'interface', type_alias_declaration: 'type', + enum_declaration: 'enum', }; /** @@ -218,8 +219,8 @@ const EXPORT_DECL_KIND: Record = { * walk-based `handleExportStmt`) so they can't drift apart on what counts as * an export — see the "two code paths" gotcha for this extractor. * - * Named function/class/interface/type declarations carry their own `name` - * field. `export const/let/var …` has no such field — each declarator's value + * Named function/class/interface/type/enum declarations carry their own + * `name` field. `export const/let/var …` has no such field — each declarator's value * is classified the same way `handleVariableDeclarator` classifies it when * building the matching Definition (function-valued → kind 'function'; any * other `const` initializer shape → kind 'constant', regardless of complexity — diff --git a/tests/parsers/javascript.test.ts b/tests/parsers/javascript.test.ts index 624a3f6b2..4ecd2e70d 100644 --- a/tests/parsers/javascript.test.ts +++ b/tests/parsers/javascript.test.ts @@ -3973,6 +3973,37 @@ function runDemo(reporter: Reporter, users: string[]): void { }); }); + describe('export enum declarations produce an Export record (#2560)', () => { + // Regression guard for #2560: `enum_declaration` was extracted as a + // Definition (via handleEnumDecl) but had no entry in EXPORT_DECL_KIND, + // so collectExportedDeclarations silently no-op'd for it — the enum + // itself was never marked exported, even though real TS/JS semantics say + // `export enum Foo {}` genuinely exports `Foo`. + function parseTS(code) { + const parser = parsers.get('typescript'); + const tree = parser.parse(code); + return extractSymbols(tree, 'test.ts'); + } + + it('lists an exported enum with kind "enum"', () => { + const symbols = parseTS(`export enum Color { Red, Green, Blue }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Color', kind: 'enum', line: 1 }), + ); + expect(symbols.exports).toContainEqual( + expect.objectContaining({ name: 'Color', kind: 'enum', line: 1 }), + ); + }); + + it('does not list a non-exported enum', () => { + const symbols = parseTS(`enum Internal { A, B }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Internal', kind: 'enum' }), + ); + expect(symbols.exports.some((e) => e.name === 'Internal')).toBe(false); + }); + }); + describe('export line matches the declaration, not the `export` keyword (#2293)', () => { // Regression guard for #2293: collectExportedDeclarations computed a single // `exportLine` from the wrapping `export_statement` node and applied it to