Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 81 additions & 6 deletions rust/rubydex/src/model/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,14 +494,51 @@ impl Graph {
&self.names
}

/// Converts a `Resolved` `NameRef` back to `Unresolved`, preserving the original `Name` data.
/// Returns the `DeclarationId` it was previously resolved to, if any.
fn unresolve_name(&mut self, name_id: NameId) -> Option<DeclarationId> {
let name_ref = self.names.get(&name_id)?;

match name_ref {
NameRef::Resolved(resolved) => {
let declaration_id = *resolved.declaration_id();
let name = resolved.name().clone();
self.names.insert(name_id, NameRef::Unresolved(Box::new(name)));
Some(declaration_id)
}
NameRef::Unresolved(_) => None,
}
}

/// Unresolves a constant reference: removes it from the target declaration's reference set
/// and unresolves its underlying name.
fn unresolve_reference(&mut self, reference_id: ReferenceId) -> Option<DeclarationId> {
let constant_ref = self.constant_references.get(&reference_id)?;
let name_id = *constant_ref.name_id();

if let Some(old_decl_id) = self.unresolve_name(name_id) {
if let Some(declaration) = self.declarations.get_mut(&old_decl_id) {
declaration.remove_reference(&reference_id);
}
Some(old_decl_id)
} else {
None
}
}

/// Removes a name from the graph entirely.
fn remove_name(&mut self, name_id: NameId) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating this method as we'll also update name_dependents here in follow up PRs.

self.names.remove(&name_id);
}

/// Decrements the ref count for a name and removes it if the count reaches zero.
///
/// This does not recursively untrack `parent_scope` or `nesting` names.
pub fn untrack_name(&mut self, name_id: NameId) {
if let Some(name_ref) = self.names.get_mut(&name_id) {
let string_id = *name_ref.str();
if !name_ref.decrement_ref_count() {
self.names.remove(&name_id);
self.remove_name(name_id);
}
self.untrack_string(string_id);
}
Expand Down Expand Up @@ -714,12 +751,9 @@ impl Graph {
}

for ref_id in document.constant_references() {
self.unresolve_reference(*ref_id);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is okay for now, but be careful with mixing the removal and the invalidation of the data.

The fact that a constant reference was removed doesn't necessarily mean that the NameRef needs to become unresolved. For example:

# file1.rb
class Foo
  # NameRef(CONST, parent_scope: None, nesting: Foo)
  CONST
end

# Delete
# file2.rb
class Foo
  # NameRef(CONST, parent_scope: None, nesting: Foo)
  CONST
end

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My plan is for this and #589 to ALWAYS unresolve CONST when a Foo definition is removed to make sure this will be correct:

class Foo                                                                                                                                                               
  include Bar  # Bar defines CONST
  CONST        # resolves to Bar::CONST via ancestor lookup                                                                                                             
end           

And then we create a follow up PR to make this and other invalidation more efficient.


if let Some(constant_ref) = self.constant_references.remove(ref_id) {
if let Some(NameRef::Resolved(resolved)) = self.names.get(constant_ref.name_id())
&& let Some(declaration) = self.declarations.get_mut(resolved.declaration_id())
{
declaration.remove_reference(ref_id);
}
self.untrack_name(*constant_ref.name_id());
}
}
Expand Down Expand Up @@ -1743,4 +1777,45 @@ mod tests {

assert!(context.graph().resolve_alias(&DeclarationId::from("Foo")).is_none());
}

#[test]
fn deleting_sole_definition_removes_the_name_entirely() {
let mut context = GraphTest::new();

context.index_uri("file:///foo.rb", "module Foo; end\nBar");
context.index_uri("file:///bar.rb", "module Bar; end");
context.resolve();

// Bar declaration should have 1 reference (from foo.rb)
let bar_decl = context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap();
assert_eq!(bar_decl.references().len(), 1);

// Update foo.rb to remove the Bar reference
context.index_uri("file:///foo.rb", "module Foo; end");
context.resolve();

let bar_decl = context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap();
assert!(
bar_decl.references().is_empty(),
"Reference to Bar should be detached from declaration"
);

// Delete bar.rb — the Bar name should be fully removed
let bar_name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
context.index_uri("file:///bar.rb", "");
context.resolve();

assert!(
context
.graph()
.declarations()
.get(&DeclarationId::from("Bar"))
.is_none(),
"Bar declaration should be removed"
);
assert!(
context.graph().names().get(&bar_name_id).is_none(),
"Bar name should be removed from the names map"
);
}
}
Loading