Index private_class_method / public_class_method calls#780
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
85b078d to
16d134d
Compare
| call_name: &str, | ||
| ) { | ||
| match node.receiver() { | ||
| Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() { |
There was a problem hiding this comment.
Should the branches under this condition call visit_call_node_parts before returning/continuing?
There was a problem hiding this comment.
I think that's already happening. When we process the arguments below, if it's a string or a symbol, we consider it as a valid call, otherwise we visit the argument and add a diagnostic.
vinistock
left a comment
There was a problem hiding this comment.
General implementation is good, just a few fixes
| pub struct DefinitionFlags: u8 { | ||
| const DEPRECATED = 0b0001; | ||
| const PROMOTABLE = 0b0010; | ||
| const SINGLETON_METHOD_VISIBILITY = 0b0100; |
There was a problem hiding this comment.
Note for the future: we're using DefinitionFlags for all definitions, but the reality is that most flags are only applicable to certain types. For example, deprecated doesn't make much sense for these visibility related definitions, promotable is only applicable to constants and this new one is only applicable to visibility operations.
Right now, this is not a problem because we didn't run out of space yet, but it would be nice to specialize a bunch of different flags.
| } | ||
|
|
||
| let Some(arguments) = node.arguments() else { | ||
| return; |
There was a problem hiding this comment.
For a future PR, we can add a diagnostic here. Invoking private|public_class_method with no arguments does nothing.
| call_name: &str, | ||
| ) { | ||
| match node.receiver() { | ||
| Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() { |
There was a problem hiding this comment.
I think that's already happening. When we process the arguments below, if it's a string or a symbol, we consider it as a valid call, otherwise we visit the argument and add a diagnostic.
16d134d to
27da4dd
Compare
27da4dd to
ce07ec6
Compare
Merge activity
|
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

Part of #89
This PR adds indexer support for
private_class_methodandpublic_class_methodcalls. Each call becomes aMethodVisibilityDefinitionwith theSINGLETON_METHOD_VISIBILITYflag set, which the resolver applies to the singleton method declaration in a follow-up PR. This is the base of a Graphite stack.We need a definition because the indexer can't yet attach the visibility change to the singleton method:
The method declaration for
Foo::<Foo>#bar()only exists after resolution.Why a bit flag
I added a
DefinitionFlags::SINGLETON_METHOD_VISIBILITYbit on the existingMethodVisibilityDefinitionrather than introducing a parallelSingletonMethodVisibilityDefinitiontype. The two would have carried the same fields and the indexer treats instance and singleton visibility calls almost identically. A bit flag captures the singleton-vs-instance distinction without doubling the definition variants and the FFI plumbing that comes with them.Why no explicit receiver support
Foo.private_class_method :baris legal Ruby but rare in practice. Supporting it would require a new definition type to carry the receiver. Ignoring it lets us fold the singleton case into the existingMethodVisibilityDefinitionand get resolution-time differentiation from a single bit flag, which keeps the blast radius of the change small. Calls with an explicit receiver bypass the visibility logic, though the call is still visited so nested references stay indexed.In this PR
DefinitionFlags::SINGLETON_METHOD_VISIBILITYprivate_class_method/public_class_methodcalls with literal symbol or string targets, with an implicit receiver onlyresolve_method_visibilitiesso the new flag stays inert until the follow-up PR wires up resolutionNot in this PR
A few things are intentionally deferred to follow-up PRs in the stack:
handle_constant_visibilityprivate_class_method [:a, :b])private_class_method def self.foo; end)