@@ -727,4 +727,292 @@ mod tests {
727727 "should not offer conflicting import"
728728 ) ;
729729 }
730+
731+ // ── No-namespace file tests ─────────────────────────────────────────
732+
733+ #[ test]
734+ fn import_action_offered_in_no_namespace_file_for_new_expression ( ) {
735+ let backend = crate :: Backend :: new_test ( ) ;
736+ let uri = "file:///test.php" ;
737+ // File has NO namespace declaration.
738+ let content = "<?php\n \n new Request();\n " ;
739+
740+ backend. update_ast ( uri, content) ;
741+
742+ {
743+ let mut cmap = backend. classmap . write ( ) ;
744+ cmap. insert (
745+ "Illuminate\\ Http\\ Request" . to_string ( ) ,
746+ "/vendor/laravel/framework/src/Illuminate/Http/Request.php" . into ( ) ,
747+ ) ;
748+ }
749+
750+ // Range covering "Request" on line 2.
751+ let params = CodeActionParams {
752+ text_document : TextDocumentIdentifier {
753+ uri : uri. parse ( ) . unwrap ( ) ,
754+ } ,
755+ range : Range {
756+ start : Position :: new ( 2 , 4 ) ,
757+ end : Position :: new ( 2 , 11 ) ,
758+ } ,
759+ context : CodeActionContext {
760+ diagnostics : vec ! [ ] ,
761+ only : None ,
762+ trigger_kind : None ,
763+ } ,
764+ work_done_progress_params : Default :: default ( ) ,
765+ partial_result_params : Default :: default ( ) ,
766+ } ;
767+
768+ let actions = backend. handle_code_action ( uri, content, & params) ;
769+ assert ! (
770+ actions. iter( ) . any( |a| {
771+ if let CodeActionOrCommand :: CodeAction ( ca) = a {
772+ ca. title. contains( "Illuminate\\ Http\\ Request" )
773+ } else {
774+ false
775+ }
776+ } ) ,
777+ "expected an import action for Illuminate\\ Http\\ Request in no-namespace file, got: {:?}" ,
778+ actions
779+ . iter( )
780+ . map( |a| match a {
781+ CodeActionOrCommand :: CodeAction ( ca) => ca. title. clone( ) ,
782+ CodeActionOrCommand :: Command ( c) => c. title. clone( ) ,
783+ } )
784+ . collect:: <Vec <_>>( )
785+ ) ;
786+ }
787+
788+ #[ test]
789+ fn import_action_offered_in_no_namespace_file_for_static_call ( ) {
790+ let backend = crate :: Backend :: new_test ( ) ;
791+ let uri = "file:///test.php" ;
792+ // File has NO namespace — reproduces issue #59.
793+ let content = "<?php\n \n function () {\n return Carbon::now();\n };\n " ;
794+
795+ backend. update_ast ( uri, content) ;
796+
797+ {
798+ let mut cmap = backend. classmap . write ( ) ;
799+ cmap. insert (
800+ "Carbon\\ Carbon" . to_string ( ) ,
801+ "/vendor/nesbot/carbon/src/Carbon/Carbon.php" . into ( ) ,
802+ ) ;
803+ }
804+
805+ // Range covering "Carbon" on line 3 (the class name in Carbon::now()).
806+ let params = CodeActionParams {
807+ text_document : TextDocumentIdentifier {
808+ uri : uri. parse ( ) . unwrap ( ) ,
809+ } ,
810+ range : Range {
811+ start : Position :: new ( 3 , 11 ) ,
812+ end : Position :: new ( 3 , 17 ) ,
813+ } ,
814+ context : CodeActionContext {
815+ diagnostics : vec ! [ ] ,
816+ only : None ,
817+ trigger_kind : None ,
818+ } ,
819+ work_done_progress_params : Default :: default ( ) ,
820+ partial_result_params : Default :: default ( ) ,
821+ } ;
822+
823+ let actions = backend. handle_code_action ( uri, content, & params) ;
824+ assert ! (
825+ actions. iter( ) . any( |a| {
826+ if let CodeActionOrCommand :: CodeAction ( ca) = a {
827+ ca. title. contains( "Carbon\\ Carbon" )
828+ } else {
829+ false
830+ }
831+ } ) ,
832+ "expected an import action for Carbon\\ Carbon in no-namespace file, got: {:?}" ,
833+ actions
834+ . iter( )
835+ . map( |a| match a {
836+ CodeActionOrCommand :: CodeAction ( ca) => ca. title. clone( ) ,
837+ CodeActionOrCommand :: Command ( c) => c. title. clone( ) ,
838+ } )
839+ . collect:: <Vec <_>>( )
840+ ) ;
841+ }
842+
843+ #[ test]
844+ fn import_action_inserts_use_after_php_open_in_no_namespace_file ( ) {
845+ let backend = crate :: Backend :: new_test ( ) ;
846+ let uri = "file:///test.php" ;
847+ let content = "<?php\n \n new Request();\n " ;
848+
849+ backend. update_ast ( uri, content) ;
850+
851+ {
852+ let mut cmap = backend. classmap . write ( ) ;
853+ cmap. insert (
854+ "Illuminate\\ Http\\ Request" . to_string ( ) ,
855+ "/vendor/laravel/framework/src/Illuminate/Http/Request.php" . into ( ) ,
856+ ) ;
857+ }
858+
859+ let params = CodeActionParams {
860+ text_document : TextDocumentIdentifier {
861+ uri : uri. parse ( ) . unwrap ( ) ,
862+ } ,
863+ range : Range {
864+ start : Position :: new ( 2 , 4 ) ,
865+ end : Position :: new ( 2 , 11 ) ,
866+ } ,
867+ context : CodeActionContext {
868+ diagnostics : vec ! [ ] ,
869+ only : None ,
870+ trigger_kind : None ,
871+ } ,
872+ work_done_progress_params : Default :: default ( ) ,
873+ partial_result_params : Default :: default ( ) ,
874+ } ;
875+
876+ let actions = backend. handle_code_action ( uri, content, & params) ;
877+ let action = actions
878+ . iter ( )
879+ . find_map ( |a| match a {
880+ CodeActionOrCommand :: CodeAction ( ca)
881+ if ca. title . contains ( "Illuminate\\ Http\\ Request" ) =>
882+ {
883+ Some ( ca)
884+ }
885+ _ => None ,
886+ } )
887+ . expect ( "expected import action" ) ;
888+
889+ let edit = action. edit . as_ref ( ) . expect ( "expected workspace edit" ) ;
890+ let changes = edit. changes . as_ref ( ) . expect ( "expected changes" ) ;
891+ let file_edits = changes
892+ . get ( & uri. parse :: < Url > ( ) . unwrap ( ) )
893+ . expect ( "expected edits for the file" ) ;
894+ assert_eq ! ( file_edits. len( ) , 1 ) ;
895+ assert_eq ! ( file_edits[ 0 ] . new_text, "use Illuminate\\ Http\\ Request;\n " ) ;
896+ // Should insert after `<?php` (line 1), not line 0.
897+ assert_eq ! ( file_edits[ 0 ] . range. start. line, 1 ) ;
898+ }
899+
900+ #[ test]
901+ fn no_import_action_for_known_global_class_in_no_namespace_file ( ) {
902+ let backend = crate :: Backend :: new_test ( ) ;
903+ let uri_dep = "file:///dep.php" ;
904+ let content_dep = "<?php\n class Helper {}\n " ;
905+ backend. update_ast ( uri_dep, content_dep) ;
906+
907+ {
908+ let mut idx = backend. class_index . write ( ) ;
909+ idx. insert ( "Helper" . to_string ( ) , uri_dep. to_string ( ) ) ;
910+ }
911+
912+ let uri = "file:///test.php" ;
913+ let content = "<?php\n \n new Helper();\n " ;
914+ backend. update_ast ( uri, content) ;
915+
916+ let params = CodeActionParams {
917+ text_document : TextDocumentIdentifier {
918+ uri : uri. parse ( ) . unwrap ( ) ,
919+ } ,
920+ range : Range {
921+ start : Position :: new ( 2 , 4 ) ,
922+ end : Position :: new ( 2 , 10 ) ,
923+ } ,
924+ context : CodeActionContext {
925+ diagnostics : vec ! [ ] ,
926+ only : None ,
927+ trigger_kind : None ,
928+ } ,
929+ work_done_progress_params : Default :: default ( ) ,
930+ partial_result_params : Default :: default ( ) ,
931+ } ;
932+
933+ let actions = backend. handle_code_action ( uri, content, & params) ;
934+ let import_actions: Vec < _ > = actions
935+ . iter ( )
936+ . filter ( |a| match a {
937+ CodeActionOrCommand :: CodeAction ( ca) => ca. title . starts_with ( "Import" ) ,
938+ _ => false ,
939+ } )
940+ . collect ( ) ;
941+ assert ! (
942+ import_actions. is_empty( ) ,
943+ "should not offer import for a known global class in no-namespace file, got: {:?}" ,
944+ import_actions
945+ . iter( )
946+ . map( |a| match a {
947+ CodeActionOrCommand :: CodeAction ( ca) => ca. title. clone( ) ,
948+ _ => String :: new( ) ,
949+ } )
950+ . collect:: <Vec <_>>( )
951+ ) ;
952+ }
953+
954+ #[ test]
955+ fn import_action_offered_when_namespaced_class_in_ast_map ( ) {
956+ // Reproduces issue #59: when a namespaced class like `Carbon\Carbon`
957+ // is already parsed and in the ast_map, `find_or_load_class("Carbon")`
958+ // must NOT match it — the bare name `"Carbon"` is a global-scope
959+ // lookup and should not resolve to `Carbon\Carbon`.
960+ //
961+ // Without the fix, `find_class_in_ast_map("Carbon")` ignores the
962+ // namespace filter when `expected_ns` is `None`, so ANY class with
963+ // short name `Carbon` matches. The import action then skips it
964+ // thinking "this class resolves in global scope".
965+ let backend = crate :: Backend :: new_test ( ) ;
966+
967+ // Parse the dependency file so Carbon\Carbon is in the ast_map.
968+ let uri_dep = "file:///vendor/carbon.php" ;
969+ let content_dep = "<?php\n namespace Carbon;\n \n class Carbon {}\n " ;
970+ backend. update_ast ( uri_dep, content_dep) ;
971+ {
972+ let mut idx = backend. class_index . write ( ) ;
973+ idx. insert ( "Carbon\\ Carbon" . to_string ( ) , uri_dep. to_string ( ) ) ;
974+ }
975+
976+ // The file under edit has NO namespace.
977+ let uri = "file:///test.php" ;
978+ let content = "<?php\n \n function () {\n return Carbon::now();\n };\n " ;
979+ backend. update_ast ( uri, content) ;
980+
981+ // Range covering "Carbon" on line 3.
982+ let params = CodeActionParams {
983+ text_document : TextDocumentIdentifier {
984+ uri : uri. parse ( ) . unwrap ( ) ,
985+ } ,
986+ range : Range {
987+ start : Position :: new ( 3 , 11 ) ,
988+ end : Position :: new ( 3 , 17 ) ,
989+ } ,
990+ context : CodeActionContext {
991+ diagnostics : vec ! [ ] ,
992+ only : None ,
993+ trigger_kind : None ,
994+ } ,
995+ work_done_progress_params : Default :: default ( ) ,
996+ partial_result_params : Default :: default ( ) ,
997+ } ;
998+
999+ let actions = backend. handle_code_action ( uri, content, & params) ;
1000+ assert ! (
1001+ actions. iter( ) . any( |a| {
1002+ if let CodeActionOrCommand :: CodeAction ( ca) = a {
1003+ ca. title. contains( "Carbon\\ Carbon" )
1004+ } else {
1005+ false
1006+ }
1007+ } ) ,
1008+ "expected an import action for Carbon\\ Carbon when the namespaced class is in ast_map, got: {:?}" ,
1009+ actions
1010+ . iter( )
1011+ . map( |a| match a {
1012+ CodeActionOrCommand :: CodeAction ( ca) => ca. title. clone( ) ,
1013+ CodeActionOrCommand :: Command ( c) => c. title. clone( ) ,
1014+ } )
1015+ . collect:: <Vec <_>>( )
1016+ ) ;
1017+ }
7301018}
0 commit comments