@@ -545,3 +545,151 @@ def test_find_generic_function(self, ts_analyzer):
545545
546546 assert len (functions ) == 1
547547 assert functions [0 ].name == "identity"
548+
549+
550+ class TestExportConstArrowFunctions :
551+ """Tests for export const arrow function pattern - Issue #10.
552+
553+ Modern TypeScript codebases commonly use:
554+ - export const slugify = (str: string) => { return s; }
555+ - export const uniqueBy = <T>(array: T[]) => { ... }
556+
557+ These must be correctly recognized as optimizable functions.
558+ """
559+
560+ @pytest .fixture
561+ def ts_analyzer (self ):
562+ """Create a TypeScript analyzer."""
563+ return TreeSitterAnalyzer (TreeSitterLanguage .TYPESCRIPT )
564+
565+ def test_export_const_arrow_function_basic (self , ts_analyzer ):
566+ """Test finding export const arrow function (basic pattern)."""
567+ code = """export const slugify = (str: string) => {
568+ return str.toLowerCase();
569+ };"""
570+ functions = ts_analyzer .find_functions (code )
571+
572+ assert len (functions ) == 1
573+ assert functions [0 ].name == "slugify"
574+ assert functions [0 ].is_arrow is True
575+ assert ts_analyzer .has_return_statement (functions [0 ], code ) is True
576+
577+ def test_export_const_arrow_function_optional_param (self , ts_analyzer ):
578+ """Test finding export const arrow function with optional parameter."""
579+ code = """export const slugify = (str: string, forDisplayingInput?: boolean) => {
580+ if (!str) {
581+ return "";
582+ }
583+ const s = str.toLowerCase();
584+ return forDisplayingInput ? s : s.replace(/-+$/, "");
585+ };"""
586+ functions = ts_analyzer .find_functions (code )
587+
588+ assert len (functions ) == 1
589+ assert functions [0 ].name == "slugify"
590+ assert functions [0 ].is_arrow is True
591+ assert ts_analyzer .has_return_statement (functions [0 ], code ) is True
592+
593+ def test_export_const_generic_arrow_function (self , ts_analyzer ):
594+ """Test finding export const arrow function with generics."""
595+ code = """export const uniqueBy = <T extends { [key: string]: unknown }>(array: T[], keys: (keyof T)[]) => {
596+ return array.filter(
597+ (item, index, self) => index === self.findIndex((t) => keys.every((key) => t[key] === item[key]))
598+ );
599+ };"""
600+ functions = ts_analyzer .find_functions (code )
601+
602+ # Should find uniqueBy, and possibly the inner arrow functions
603+ uniqueBy = next ((f for f in functions if f .name == "uniqueBy" ), None )
604+ assert uniqueBy is not None
605+ assert uniqueBy .is_arrow is True
606+ assert ts_analyzer .has_return_statement (uniqueBy , code ) is True
607+
608+ def test_export_const_arrow_function_is_exported (self , ts_analyzer ):
609+ """Test that export const arrow functions are recognized as exported."""
610+ code = """export const slugify = (str: string) => {
611+ return str.toLowerCase();
612+ };"""
613+
614+ # Check is_function_exported
615+ is_exported , export_name = ts_analyzer .is_function_exported (code , "slugify" )
616+ assert is_exported is True
617+ assert export_name == "slugify"
618+
619+ def test_export_const_with_default_export (self , ts_analyzer ):
620+ """Test export const with separate default export."""
621+ code = """export const slugify = (str: string) => {
622+ return str.toLowerCase();
623+ };
624+
625+ export default slugify;"""
626+
627+ functions = ts_analyzer .find_functions (code )
628+ assert len (functions ) == 1
629+ assert functions [0 ].name == "slugify"
630+
631+ # Should be exported both ways
632+ is_named , named_name = ts_analyzer .is_function_exported (code , "slugify" )
633+ assert is_named is True
634+
635+ def test_multiple_export_const_functions (self , ts_analyzer ):
636+ """Test multiple export const arrow functions in same file."""
637+ code = """export const notUndefined = <T>(val: T | undefined): val is T => Boolean(val);
638+
639+ export const uniqueBy = <T extends { [key: string]: unknown }>(array: T[], keys: (keyof T)[]) => {
640+ return array.filter(
641+ (item, index, self) => index === self.findIndex((t) => keys.every((key) => t[key] === item[key]))
642+ );
643+ };"""
644+
645+ functions = ts_analyzer .find_functions (code )
646+
647+ # Find the top-level exported functions
648+ names = {f .name for f in functions if f .parent_function is None }
649+ assert "notUndefined" in names
650+ assert "uniqueBy" in names
651+
652+ def test_export_const_arrow_with_implicit_return (self , ts_analyzer ):
653+ """Test export const arrow function with implicit return."""
654+ code = """export const double = (n: number) => n * 2;"""
655+
656+ functions = ts_analyzer .find_functions (code )
657+
658+ assert len (functions ) == 1
659+ assert functions [0 ].name == "double"
660+ assert functions [0 ].is_arrow is True
661+ assert ts_analyzer .has_return_statement (functions [0 ], code ) is True
662+
663+ def test_export_const_async_arrow_function (self , ts_analyzer ):
664+ """Test export const async arrow function."""
665+ code = """export const fetchData = async (url: string) => {
666+ const response = await fetch(url);
667+ return response.json();
668+ };"""
669+
670+ functions = ts_analyzer .find_functions (code )
671+
672+ assert len (functions ) == 1
673+ assert functions [0 ].name == "fetchData"
674+ assert functions [0 ].is_arrow is True
675+ assert functions [0 ].is_async is True
676+ assert ts_analyzer .has_return_statement (functions [0 ], code ) is True
677+
678+ def test_non_exported_const_not_exported (self , ts_analyzer ):
679+ """Test that non-exported const functions are not marked as exported."""
680+ code = """const privateFunc = (x: number) => {
681+ return x * 2;
682+ };
683+
684+ export const publicFunc = (x: number) => {
685+ return privateFunc(x);
686+ };"""
687+
688+ # privateFunc should not be exported
689+ is_private_exported , _ = ts_analyzer .is_function_exported (code , "privateFunc" )
690+ assert is_private_exported is False
691+
692+ # publicFunc should be exported
693+ is_public_exported , name = ts_analyzer .is_function_exported (code , "publicFunc" )
694+ assert is_public_exported is True
695+ assert name == "publicFunc"
0 commit comments