@@ -14,6 +14,18 @@ use crate::model::references::{ConstantReference, MethodRef};
1414use crate :: model:: string_ref:: StringRef ;
1515use crate :: stats;
1616
17+ /// An entity whose validity depends on a particular `NameId`.
18+ /// Used as the value type in the `name_dependents` reverse index.
19+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
20+ pub enum NameDependent {
21+ Definition ( DefinitionId ) ,
22+ Reference ( ReferenceId ) ,
23+ /// This name's `parent_scope` is the key name — structural dependency.
24+ ChildName ( NameId ) ,
25+ /// This name's `nesting` is the key name — reference-only dependency.
26+ NestedName ( NameId ) ,
27+ }
28+
1729pub static BASIC_OBJECT_ID : LazyLock < DeclarationId > = LazyLock :: new ( || DeclarationId :: from ( "BasicObject" ) ) ;
1830pub static OBJECT_ID : LazyLock < DeclarationId > = LazyLock :: new ( || DeclarationId :: from ( "Object" ) ) ;
1931pub static MODULE_ID : LazyLock < DeclarationId > = LazyLock :: new ( || DeclarationId :: from ( "Module" ) ) ;
@@ -41,6 +53,10 @@ pub struct Graph {
4153
4254 /// The position encoding used for LSP line/column locations. Not related to the actual encoding of the file
4355 position_encoding : Encoding ,
56+
57+ /// Reverse index: for each `NameId`, which definitions, references, and child/nested names depend on it.
58+ /// Used during invalidation to efficiently find affected entities without scanning the full graph.
59+ name_dependents : IdentityHashMap < NameId , Vec < NameDependent > > ,
4460}
4561
4662impl Graph {
@@ -55,6 +71,7 @@ impl Graph {
5571 constant_references : IdentityHashMap :: default ( ) ,
5672 method_references : IdentityHashMap :: default ( ) ,
5773 position_encoding : Encoding :: default ( ) ,
74+ name_dependents : IdentityHashMap :: default ( ) ,
5875 }
5976 }
6077
@@ -501,6 +518,11 @@ impl Graph {
501518 & self . names
502519 }
503520
521+ #[ must_use]
522+ pub fn name_dependents ( & self ) -> & IdentityHashMap < NameId , Vec < NameDependent > > {
523+ & self . name_dependents
524+ }
525+
504526 /// Converts a `Resolved` `NameRef` back to `Unresolved`, preserving the original `Name` data.
505527 /// Returns the `DeclarationId` it was previously resolved to, if any.
506528 fn unresolve_name ( & mut self , name_id : NameId ) -> Option < DeclarationId > {
@@ -533,11 +555,34 @@ impl Graph {
533555 }
534556 }
535557
536- /// Removes a name from the graph entirely .
558+ /// Removes a name from the graph and cleans up its name-to-name edges from parent names .
537559 fn remove_name ( & mut self , name_id : NameId ) {
560+ if let Some ( name_ref) = self . names . get ( & name_id) {
561+ let parent_scope = name_ref. parent_scope ( ) . as_ref ( ) . copied ( ) ;
562+ let nesting = name_ref. nesting ( ) . as_ref ( ) . copied ( ) ;
563+
564+ if let Some ( ps_id) = parent_scope {
565+ self . remove_name_dependent ( ps_id, NameDependent :: ChildName ( name_id) ) ;
566+ }
567+ if let Some ( nesting_id) = nesting {
568+ self . remove_name_dependent ( nesting_id, NameDependent :: NestedName ( name_id) ) ;
569+ }
570+ }
571+ self . name_dependents . remove ( & name_id) ;
538572 self . names . remove ( & name_id) ;
539573 }
540574
575+ /// Removes a specific dependent from the `name_dependents` entry for `name_id`,
576+ /// cleaning up the entry if no dependents remain.
577+ fn remove_name_dependent ( & mut self , name_id : NameId , dependent : NameDependent ) {
578+ if let Some ( deps) = self . name_dependents . get_mut ( & name_id) {
579+ deps. retain ( |d| * d != dependent) ;
580+ if deps. is_empty ( ) {
581+ self . name_dependents . remove ( & name_id) ;
582+ }
583+ }
584+ }
585+
541586 /// Decrements the ref count for a name and removes it if the count reaches zero.
542587 ///
543588 /// This does not recursively untrack `parent_scope` or `nesting` names.
@@ -687,7 +732,7 @@ impl Graph {
687732 /// Merges everything in `other` into this Graph. This method is meant to merge all graph representations from
688733 /// different threads, but not meant to handle updates to the existing global representation
689734 pub fn extend ( & mut self , local_graph : LocalGraph ) {
690- let ( uri_id, document, definitions, strings, names, constant_references, method_references) =
735+ let ( uri_id, document, definitions, strings, names, constant_references, method_references, name_dependents ) =
691736 local_graph. into_parts ( ) ;
692737
693738 if self . documents . insert ( uri_id, document) . is_some ( ) {
@@ -735,6 +780,15 @@ impl Graph {
735780 debug_assert ! ( false , "Method ReferenceId collision in global graph" ) ;
736781 }
737782 }
783+
784+ for ( name_id, deps) in name_dependents {
785+ let global_deps = self . name_dependents . entry ( name_id) . or_default ( ) ;
786+ for dep in deps {
787+ if !global_deps. contains ( & dep) {
788+ global_deps. push ( dep) ;
789+ }
790+ }
791+ }
738792 }
739793
740794 /// Updates the global representation with the information contained in `other`, handling deletions, insertions and
@@ -765,6 +819,7 @@ impl Graph {
765819 self . unresolve_reference ( * ref_id) ;
766820
767821 if let Some ( constant_ref) = self . constant_references . remove ( ref_id) {
822+ self . remove_name_dependent ( * constant_ref. name_id ( ) , NameDependent :: Reference ( * ref_id) ) ;
768823 self . untrack_name ( * constant_ref. name_id ( ) ) ;
769824 }
770825 }
@@ -800,8 +855,9 @@ impl Graph {
800855 }
801856 }
802857
803- if let Some ( name_id) = self . definitions . get ( def_id) . unwrap ( ) . name_id ( ) {
804- self . untrack_name ( * name_id) ;
858+ if let Some ( name_id) = self . definitions . get ( def_id) . unwrap ( ) . name_id ( ) . copied ( ) {
859+ self . remove_name_dependent ( name_id, NameDependent :: Definition ( * def_id) ) ;
860+ self . untrack_name ( name_id) ;
805861 }
806862 }
807863
@@ -999,7 +1055,7 @@ mod tests {
9991055 use crate :: model:: comment:: Comment ;
10001056 use crate :: model:: declaration:: Ancestors ;
10011057 use crate :: test_utils:: GraphTest ;
1002- use crate :: { assert_descendants, assert_members_eq, assert_no_diagnostics, assert_no_members} ;
1058+ use crate :: { assert_dependents , assert_descendants, assert_members_eq, assert_no_diagnostics, assert_no_members} ;
10031059
10041060 #[ test]
10051061 fn deleting_a_uri ( ) {
@@ -1021,6 +1077,55 @@ mod tests {
10211077 ) ;
10221078 }
10231079
1080+ #[ test]
1081+ fn deleting_file_triggers_name_dependent_cleanup ( ) {
1082+ let mut context = GraphTest :: new ( ) ;
1083+
1084+ context. index_uri (
1085+ "file:///foo.rb" ,
1086+ "
1087+ module Foo
1088+ CONST
1089+ end
1090+ " ,
1091+ ) ;
1092+ context. index_uri (
1093+ "file:///bar.rb" ,
1094+ "
1095+ module Foo
1096+ class Bar; end
1097+ end
1098+ " ,
1099+ ) ;
1100+ context. resolve ( ) ;
1101+
1102+ assert_dependents ! (
1103+ & context,
1104+ "Foo" ,
1105+ [
1106+ Definition ( "Foo" ) ,
1107+ Definition ( "Foo" ) ,
1108+ NestedName ( "Bar" ) ,
1109+ NestedName ( "CONST" ) ,
1110+ ]
1111+ ) ;
1112+
1113+ // Deleting bar.rb removes Bar's name (and its NestedName edge from Foo)
1114+ // and one Definition dependent (bar.rb's `module Foo` definition).
1115+ context. delete_uri ( "file:///bar.rb" ) ;
1116+ assert_dependents ! ( & context, "Foo" , [ Definition ( "Foo" ) , NestedName ( "CONST" ) ] ) ;
1117+
1118+ // Deleting foo.rb cleans up everything
1119+ context. delete_uri ( "file:///foo.rb" ) ;
1120+ let foo_ids = context
1121+ . graph ( )
1122+ . names ( )
1123+ . iter ( )
1124+ . filter ( |( _, n) | * n. str ( ) == StringId :: from ( "Foo" ) )
1125+ . count ( ) ;
1126+ assert_eq ! ( foo_ids, 0 , "Foo name should be removed after deleting both files" ) ;
1127+ }
1128+
10241129 #[ test]
10251130 fn updating_index_with_deleted_definitions ( ) {
10261131 let mut context = GraphTest :: new ( ) ;
0 commit comments