Propagate singleton class creation to descendants - #937
Conversation
|
@vinistock This looks good overall, but the reported For reference, my previous PR (#893), which I believe should produce a broadly comparable graph shape, added |
|
|
||
| assert_declaration_exists!(context, "NewSub::<NewSub>"); | ||
| assert_descendants!(context, "Bar::<Bar>", ["NewSub::<NewSub>"]); | ||
| } |
There was a problem hiding this comment.
# resolution 1
class Bar
def self.foo; end
end
class Other
def self.foo; end
end
class Child < Bar; end# resolution 2
class Child < Other; endDo we update the parent of Child::<Child>?
|
|
||
| for descendant in descendants { | ||
| if descendant == attached_id { | ||
| continue; |
There was a problem hiding this comment.
Can we add a test like this:
# First resolution
class Child; end
# Later resolution
def Object.foo; end
I'm not sure we're properly updating the Child::<Child> parent.
| let (picked_parent, unresolved_parent) = self.get_parent_class(definition_ids); | ||
| let mut result = self.linearize_ancestors(picked_parent, context); | ||
|
|
||
| self.ensure_matching_singleton_class_depth(declaration_id, picked_parent); |
There was a problem hiding this comment.
Can you try this:
# resolution 1
# parents.rb
class OldParent
def self.old_method; end
end
class NewParent
def self.new_method; end
end
# child_a.rb
class Child < OldParent; end
# child_b.rb
class Child < OldParent; endthen
# resolution 2
# child_a.rb
class Child < NewParent; end
# child_b.rb
class Child < NewParent; endIs the parent of Child::<Child> updated?
| .copied() | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| for descendant in descendants { |
There was a problem hiding this comment.
Since descendants is already transitive, aren't we repeating a lot of work here? Creating the singleton for A visits all of its descendants, and then each singleton we enqueue does another overlapping scan when it is linearized. For a deep hierarchy, this looks quadratic.
Could we track the singleton depth already propagated for each declaration during this resolution and skip these repeated scans?
Background
We were creating broken descendant chains for singleton classes. The essence of the issue is that when a parent singleton class was created, it did not propagate the creation of singletons to its descendants, resulting in missing data in the graph and non-sensical results.
For example:
That would result in
graph["Parent"].descendantsto returnIntermediateandChild(correct), whilegraph["Parent::<Parent>"].descendantswould return nothing, which is incorrect and inconsistent.The fix
When we are linearizing singleton ancestors, we need to figure out what is the parent class of the singleton, for which we have a dedicated method
singleton_parent_id. That method recurses and determines the parent class based on the entity we are looking at. Eventually, we reach the ultimate attached object, read its parent class and use it as the ancestor.That is the point where we need to grab the attached descendants and ensure that we're creating singletons for all of them, matching all depth levels so that
Parent::<Parent>::<<Parent>>(and any other level) is also handled correctly and lazily (without creating unnecessary singleton classes).The major gotcha that makes this change require context to apply is that simply propagating to descendants immediately results in infinite recursion. The reason is not very trivial to trace, but I tried to document it as best as I could in a comment (please let me know if it's not clear).
Finally, to ensure proper behaviour in incremental resolution scenarios, we also need a tiny modification to the parent ancestor linearization helper, so that when a class appears later, we correctly create all necessary singleton levels.
Impact
Naturally, this change is not 100% free. We were "avoiding" work by building an inconsistent graph, which is faster, but not correct. That said, the impact is acceptable and we need the correctness (+ ~2s resolution + ~200MB graph). I'm sure we can shave this off with more optimizations.