@@ -1017,69 +1017,115 @@ impl Graph {
10171017 }
10181018
10191019 /// Removes raw document data (refs, defs, names, strings) from maps.
1020- /// Does not touch declarations or perform invalidation -- that is handled by `invalidate`.
1020+ ///
1021+ /// `invalidate` has already handled dependency invalidation and the normal
1022+ /// definition detachment path. This cleanup removes the raw map entries and
1023+ /// prunes declarations that become removable only after those entries are detached.
10211024 fn remove_document_data ( & mut self , document : & Document ) {
1025+ self . remove_document_method_references ( document) ;
1026+
1027+ let mut declarations_to_prune = self . remove_document_constant_references ( document) ;
1028+ declarations_to_prune. extend ( self . detach_unmapped_document_definitions ( document) ) ;
1029+ self . prune_removable_declarations ( declarations_to_prune) ;
1030+
1031+ self . remove_document_definitions ( document) ;
1032+ }
1033+
1034+ fn remove_document_method_references ( & mut self , document : & Document ) {
10221035 for ref_id in document. method_references ( ) {
10231036 if let Some ( method_ref) = self . method_references . remove ( ref_id) {
10241037 self . untrack_string ( * method_ref. str ( ) ) ;
10251038 }
10261039 }
1040+ }
10271041
1028- let mut emptied_by_ref_removal: Vec < InvalidationItem > = Vec :: new ( ) ;
1042+ fn remove_document_constant_references ( & mut self , document : & Document ) -> Vec < DeclarationId > {
1043+ let mut declarations_to_prune = Vec :: new ( ) ;
10291044 for ref_id in document. constant_references ( ) {
10301045 if let Some ( constant_ref) = self . constant_references . remove ( ref_id) {
10311046 // Detach from target declaration. References unresolved during invalidation
10321047 // were already detached; this catches the rest.
1033- if let Some ( NameRef :: Resolved ( resolved) ) = self . names . get ( constant_ref. name_id ( ) ) {
1034- let target_id = * resolved. declaration_id ( ) ;
1035- if let Some ( declaration) = self . declarations . get_mut ( & target_id) {
1036- declaration. remove_constant_reference ( ref_id) ;
1037- if declaration. is_removable ( ) {
1038- emptied_by_ref_removal. push ( InvalidationItem :: Declaration ( target_id) ) ;
1039- }
1040- }
1048+ if let Some ( target_id) =
1049+ self . detach_constant_reference_from_resolved_target ( * constant_ref. name_id ( ) , * ref_id)
1050+ {
1051+ declarations_to_prune. push ( target_id) ;
10411052 }
10421053
10431054 self . remove_name_dependent ( * constant_ref. name_id ( ) , NameDependent :: Reference ( * ref_id) ) ;
10441055 self . untrack_name ( * constant_ref. name_id ( ) ) ;
10451056 }
10461057 }
10471058
1048- if !emptied_by_ref_removal. is_empty ( ) {
1049- self . invalidate_graph ( emptied_by_ref_removal, IdentityHashMap :: default ( ) ) ;
1059+ declarations_to_prune
1060+ }
1061+
1062+ fn detach_constant_reference_from_resolved_target (
1063+ & mut self ,
1064+ name_id : NameId ,
1065+ ref_id : ConstantReferenceId ,
1066+ ) -> Option < DeclarationId > {
1067+ let target_id = match self . names . get ( & name_id) {
1068+ Some ( NameRef :: Resolved ( resolved) ) => * resolved. declaration_id ( ) ,
1069+ Some ( NameRef :: Unresolved ( _) ) | None => return None ,
1070+ } ;
1071+
1072+ let declaration = self . declarations . get_mut ( & target_id) ?;
1073+ declaration. remove_constant_reference ( & ref_id) ;
1074+ self . declaration_is_removable ( target_id) . then_some ( target_id)
1075+ }
1076+
1077+ fn detach_unmapped_document_definitions ( & mut self , document : & Document ) -> Vec < DeclarationId > {
1078+ let unmapped_definition_ids = self . unmapped_document_definition_ids ( document) ;
1079+
1080+ if unmapped_definition_ids. is_empty ( ) {
1081+ return Vec :: new ( ) ;
10501082 }
10511083
1052- // Detach removed definitions from their declarations.
1053- // Most definitions were already detached by invalidate_declaration via
1054- // pending_detachments. Definitions not handled by pending_detachments are
1055- // those where definition_to_declaration_id returns None, for example:
1056- // - methods inside `class << self` when <Foo> was unresolved by a prior deletion
1057- // - instance variables in class body (owned by singleton, but lookup resolves to class)
1058- // - definitions whose enclosing namespace name chain is broken
1059- // Detach those by scanning declarations for the remainder.
1060- let missed_def_ids: Vec < DefinitionId > = document
1084+ self . detach_definitions_from_any_declaration ( & unmapped_definition_ids)
1085+ }
1086+
1087+ /// Definitions that cannot be mapped through `definition_id_to_declaration_id`
1088+ /// are skipped by the normal invalidation detachment path. That happens when
1089+ /// the definition's name chain is already broken, or when the resolved owner
1090+ /// differs from the lexical owner used by lookup, such as singleton-owned
1091+ /// methods and instance variables.
1092+ fn unmapped_document_definition_ids ( & self , document : & Document ) -> Vec < DefinitionId > {
1093+ document
10611094 . definitions ( )
10621095 . iter ( )
10631096 . copied ( )
10641097 . filter ( |def_id| self . definition_id_to_declaration_id ( * def_id) . is_none ( ) )
1065- . collect ( ) ;
1098+ . collect ( )
1099+ }
10661100
1067- if !missed_def_ids . is_empty ( ) {
1068- let mut emptied : Vec < InvalidationItem > = Vec :: new ( ) ;
1069- for ( decl_id , declaration ) in & mut self . declarations {
1070- let had_definitions = !declaration . definitions ( ) . is_empty ( ) ;
1071- for def_id in & missed_def_ids {
1072- declaration . remove_definition ( def_id ) ;
1073- }
1074- if had_definitions && declaration . is_removable ( ) {
1075- emptied . push ( InvalidationItem :: Declaration ( * decl_id ) ) ;
1076- }
1101+ fn detach_definitions_from_any_declaration (
1102+ & mut self ,
1103+ document_definition_ids : & [ DefinitionId ] ,
1104+ ) -> Vec < DeclarationId > {
1105+ let mut touched_declarations = Vec :: new ( ) ;
1106+
1107+ for ( decl_id , declaration ) in & mut self . declarations {
1108+ let mut removed_document_definition = false ;
1109+ for def_id in document_definition_ids {
1110+ removed_document_definition |= declaration . remove_definition ( def_id ) ;
10771111 }
1078- if !emptied. is_empty ( ) {
1079- self . invalidate_graph ( emptied, IdentityHashMap :: default ( ) ) ;
1112+
1113+ // This is a fallback scan across every declaration. Only prune
1114+ // declarations that actually contained one of this document's
1115+ // definitions; otherwise unrelated already-removable declarations
1116+ // can be removed while pending resolution work still references them.
1117+ if removed_document_definition {
1118+ touched_declarations. push ( * decl_id) ;
10801119 }
10811120 }
10821121
1122+ touched_declarations
1123+ . into_iter ( )
1124+ . filter ( |decl_id| self . declaration_is_removable ( * decl_id) )
1125+ . collect ( )
1126+ }
1127+
1128+ fn remove_document_definitions ( & mut self , document : & Document ) {
10831129 for def_id in document. definitions ( ) {
10841130 let definition = self . definitions . remove ( def_id) . unwrap ( ) ;
10851131
@@ -1091,6 +1137,52 @@ impl Graph {
10911137 }
10921138 }
10931139
1140+ fn prune_removable_declarations ( & mut self , candidate_declaration_ids : Vec < DeclarationId > ) {
1141+ if candidate_declaration_ids. is_empty ( ) {
1142+ return ;
1143+ }
1144+
1145+ let prune_items: Vec < _ > = candidate_declaration_ids
1146+ . into_iter ( )
1147+ . filter ( |id| self . declaration_is_removable ( * id) )
1148+ . map ( InvalidationItem :: Declaration )
1149+ . collect ( ) ;
1150+
1151+ if prune_items. is_empty ( ) {
1152+ return ;
1153+ }
1154+
1155+ self . invalidate_graph ( prune_items, IdentityHashMap :: default ( ) ) ;
1156+ }
1157+
1158+ fn declaration_is_removable ( & self , declaration_id : DeclarationId ) -> bool {
1159+ let Some ( declaration) = self . declarations . get ( & declaration_id) else {
1160+ return false ;
1161+ } ;
1162+
1163+ if let Some ( singleton) = declaration. as_singleton_class ( )
1164+ && self . singleton_class_needed_for_extend ( singleton)
1165+ {
1166+ return false ;
1167+ }
1168+
1169+ declaration. is_removable ( )
1170+ }
1171+
1172+ fn singleton_class_needed_for_extend ( & self , singleton : & Namespace ) -> bool {
1173+ // `class Foo; extend Bar; end` records the extend on Foo's definition,
1174+ // but resolution installs Bar in Foo::<Foo>'s ancestor chain.
1175+ let Some ( attached_object) = self . declarations . get ( singleton. owner_id ( ) ) else {
1176+ return false ;
1177+ } ;
1178+
1179+ attached_object
1180+ . definitions ( )
1181+ . iter ( )
1182+ . filter_map ( |definition_id| self . definitions . get ( definition_id) )
1183+ . any ( Definition :: has_extend_mixin)
1184+ }
1185+
10941186 /// Unified invalidation worklist. Processes declaration and name items in a single loop,
10951187 /// where processing one item can push new items back onto the queue.
10961188 fn invalidate_graph (
@@ -1160,7 +1252,7 @@ impl Graph {
11601252 let Some ( decl) = self . declarations . get ( & decl_id) else {
11611253 return ;
11621254 } ;
1163- let should_remove = decl . is_removable ( ) || !self . declarations . contains_key ( decl. owner_id ( ) ) ;
1255+ let should_remove = self . declaration_is_removable ( decl_id ) || !self . declarations . contains_key ( decl. owner_id ( ) ) ;
11641256
11651257 if should_remove {
11661258 // Queue members + singleton for removal
@@ -1213,7 +1305,7 @@ impl Graph {
12131305 }
12141306 }
12151307
1216- if self . declarations . get ( & owner_id) . is_some_and ( Declaration :: is_removable ) {
1308+ if self . declaration_is_removable ( owner_id) {
12171309 queue. push ( InvalidationItem :: Declaration ( owner_id) ) ;
12181310 }
12191311 }
@@ -3894,6 +3986,22 @@ mod incremental_resolution_tests {
38943986 ) ;
38953987 }
38963988
3989+ #[ test]
3990+ fn deleting_singleton_method_preserves_singleton_class_needed_by_extend ( ) {
3991+ let mut context = GraphTest :: new ( ) ;
3992+ context. index_uri ( "file:///bar.rb" , "module Bar; end" ) ;
3993+ context. index_uri ( "file:///foo.rb" , "class Foo; extend Bar; end" ) ;
3994+ context. index_uri ( "file:///method.rb" , "def Foo.baz; end" ) ;
3995+ context. resolve ( ) ;
3996+
3997+ assert_declaration_exists ! ( context, "Foo::<Foo>" ) ;
3998+
3999+ context. delete_uri ( "file:///method.rb" ) ;
4000+ context. resolve ( ) ;
4001+
4002+ assert_declaration_exists ! ( context, "Foo::<Foo>" ) ;
4003+ }
4004+
38974005 #[ test]
38984006 fn no_duplicate_definition_on_identical_file_delete_readd ( ) {
38994007 let source = "class Foo; def self.run; end; def run; end; end" ;
0 commit comments