Resolve retroactive method visibility changes#738
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
e8ff8a0 to
815966b
Compare
fe9f274 to
2dead23
Compare
041f4a5 to
22d309e
Compare
|
|
||
| let str_id = *method_visibility.str_id(); | ||
| let uri_id = *method_visibility.uri_id(); | ||
| let offset = method_visibility.offset().clone(); |
There was a problem hiding this comment.
I Derived Copy on Offset, as a new commit since it touches other areas of the codebase, and we no longer need to clone
There was a problem hiding this comment.
Sorry, I was meaning to understand why we need to clone the offset in this case. I don't think we should derive Copy in Offset because that makes Rust implicitly create new copies of the structure when needed, which may end up creating unnecessary offset copies.
I'm okay with the explicit clone, I just wanted to understand why we needed it.
There was a problem hiding this comment.
Ah, fair... I jumped the gun too quickly. Reverted the derive Copy.
We still need to clone here due to method_visibility being a reference borrowed from self.graph.definitions() which holds a shared borrow on self.graph, and keeping it as a reference would extend the borrow past the &mut self calls downstream, effectively blocking them
There was a problem hiding this comment.
I see. However, we only need the offset in the failed case: when there's a diagnostic to create and we end up cloning the offset every time, even when there's no diagnostic.
Would it be possible to move it down and avoid cloning if there's no diagnostic?
There was a problem hiding this comment.
Moving the clone down would keep method_visibility borrowed past the &mut self calls. We could refetch self.graph.definitions().get(&id) in the else branch, but I am not sure that's better than cloning here
ce1865b to
564d600
Compare
2dead23 to
92c611d
Compare
564d600 to
b555e38
Compare
|
|
||
| let str_id = *method_visibility.str_id(); | ||
| let uri_id = *method_visibility.uri_id(); | ||
| let offset = method_visibility.offset().clone(); |
There was a problem hiding this comment.
I see. However, we only need the offset in the failed case: when there's a diagnostic to create and we end up cloning the offset every time, even when there's no diagnostic.
Would it be possible to move it down and avoid cloning if there's no diagnostic?
b555e38 to
09fa346
Compare
Continues #89 and follows #506, which indexed `ConstantVisibilityDefinition` and left the `Definition::ConstantVisibility` arm in resolution as a TODO. This PR resolves retroactive `private_constant` and `public_constant` calls by reusing the second-pass mechanism from #738: visibility definitions are processed in `handle_remaining_definitions` after the ancestors linearization loop terminates, so we can resolve the owner and look up the named constant reliably before we apply the visibility. Visibility is applied only to direct members of the owner. Inherited constants are rejected because both `private_constant` and `public_constant` raise `NameError` on inherited names in Ruby. When the target is missing, an `UndefinedConstantVisibilityTarget` diagnostic is emitted. `Graph::visibility()` was changed to treat constant visibility as sticky once set: reassigning a private constant in a reopened class doesn't flip it back to public. Note: we don't handle the case when an owner isn't yet in the graph or is indexed in a later resolve(). A follow-up PR will handle incremental resolution properly
Part of #89 and follows #780, which added the `SINGLETON_METHOD_VISIBILITY` bit flag and indexed singleton-flagged `MethodVisibilityDefinition`s. This PR routes singleton-flagged defs through the singleton class and folds the singleton path into the existing `resolve_method_visibilities` pass. Visibility definitions are processed after the main convergence loop, so the target method declaration is guaranteed to exist by the time we attach visibility. For inherited targets like `private_class_method :foo` where `foo` comes from a parent class's singleton, we create a child-owned `MethodDeclaration` on the child's singleton class. This matches what #738 did for instance methods, and what Ruby reports when you ask `Child.singleton_class.instance_method(:foo).owner`: ```ruby class Parent def self.foo; end end class Child < Parent private_class_method :foo end Child.singleton_class.instance_method(:foo).owner # => #<Class:Child> (Ruby copies the method to Child when visibility changes) Parent.singleton_class.instance_method(:foo).owner # => #<Class:Parent> (untouched on the parent) ``` ### Why document-scoped diagnostics for the singleton path When a singleton target doesn't resolve, the diagnostic attaches to the document, not to the singleton declaration. Walking ancestors via `get_or_create_singleton_class(Foo, ...)` can synthesize `Foo::<Foo>` as a side effect, even when it has no real members. For `class Foo; private_class_method :missing; end` where `Foo` has no other singleton methods, that synthetic singleton class only exists to host the diagnostic. Attaching to it would leak the declaration on file delete. Document-scoped clears with the file. Instance-method diagnostics keep their existing declaration scope: their owner can't be synthetic. ### Source-order processing of visibility defs Ruby applies visibility statements in the order they appear in source, so the later one wins: ```ruby class Foo def bar; end private :bar public :bar # Ruby: bar is Public end ``` This was already broken on `main` for instance methods; surfaced while wiring up the singleton path, fixed here for both. `prepare_units` sorted `definitions` and `const_refs` by `(uri_id, offset)` for determinism but left `others` (which holds the visibility defs queued by `handle_remaining_definitions`) in `IdentityHashMap` iteration order, which is bucket-hash order. So `Foo#bar` could end up `Private` because the resolver happened to apply `public` first and `private` second. Same sort applied to `others` so the override-order invariant holds for both instance and singleton paths. ### In this PR - branch on `is_singleton_method_visibility()` inside `resolve_method_visibilities`: resolve through `get_or_create_singleton_class` for the singleton path, render `"class method"` vs `"method"` in the diagnostic, attach document-scoped (singleton) vs declaration-scoped (instance) - add `Graph::find_singleton_method_visibility_declaration` and route through it from the existing `Definition::MethodVisibility` arm in `definition_to_declaration_id` when the flag is set - sort `others` by `(uri_id, offset)` in `prepare_units` so visibility overrides apply in source order

This PR continues the work in #89 and follows #695, which added indexing for
MethodVisibilityDefinitionand left theDefinition::MethodVisibilityarm in resolution as a TODO.This change resolves retroactive
private,protected, andpublicvisibility changes by deferring visibility definitions to a second pass insidehandle_remaining_definitions, so methods and attrs are always declared before visibility is applied.For inherited targets such as
private :foowherefoocomes from a parent class or included module, the resolver creates a child-ownedMethodDeclaration, matching CRuby's behaviour whereChild.instance_method(:foo).ownerisChild. When ancestor resolution is partial, unresolved visibility changes are requeued for a later resolve pass.We also add diagnostics for undefined visibility targets and introduce
Graph::visibility()as a helper for querying a method declaration's resolved visibility.Note: This PR intentionally covers retroactive
private,protected, andpublicresolution only.module_functionwill come in a follow-up PR because its dual instance/singleton behaviour needs separate handling.