diff --git a/docs/architecture.md b/docs/architecture.md index 51560a8cd..7ce1dd33a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -82,6 +82,12 @@ Resolution combines the discovered definitions to build a semantic understanding - Assign semantic membership (which methods/constants belong to which class) - Create implicit singleton classes from `def self.method` patterns +Resolution may also create generated definitions for Ruby semantics that behave like copied methods at runtime. The main +case is retroactive `module_function :name`, which creates a public singleton method copy and may create a direct private +instance copy on the receiving module. These generated definitions preserve the source method's location/signature but +are reverse-indexed by their source definition, visibility trigger, alias dependencies, and ancestor dependencies so +incremental invalidation can remove or rebuild them. + ## Graph Structure Rubydex represents the codebase as a graph, where entities are nodes and relationships are edges. The visualization below shows the conceptual structure (implemented as an adjacency list using IDs). diff --git a/docs/ruby-behaviors.md b/docs/ruby-behaviors.md index ea55ccdd5..e559d70af 100644 --- a/docs/ruby-behaviors.md +++ b/docs/ruby-behaviors.md @@ -356,6 +356,10 @@ end Creates: `bar`, `bar=`, `baz`, `baz=` +Rubydex indexes `attr_accessor` as two method declarations: a reader using `AttrAccessorDefinition` with the +reader name (`foo`) and a writer using `AttrWriterDefinition` with the setter name (`foo=`). This mirrors Ruby's +two-method behavior and lets visibility changes target the reader and writer independently. + ### Receiver Context Attribute methods only work when called on `self` (implicit or explicit): @@ -964,6 +968,14 @@ class Foo; private :foo; end # works — retroactive across reopen Foo.private_instance_methods(false) # => [:foo] ``` +The method must already exist when the visibility call runs. Within one file, `private :foo` before `def foo` matches +Ruby's `NameError` behavior. Across files, Rubydex cannot know runtime load order, so it treats cross-file definitions +as potentially available. A definitely-prior same-file definition is preferred for the visibility snapshot; URI lexical +order is used only as a deterministic tie-breaker when multiple cross-file definitions could apply. + +Rubydex treats visibility calls inside method bodies or ordinary blocks as runtime calls. It does not apply those +visibility effects statically because Ruby only executes them if the containing method or block is called. + **`private :inherited_method` creates an implicit copy:** ```ruby @@ -1018,7 +1030,8 @@ Singleton.create # => # ### Retroactive `module_function :method_name` -`module_function` can be called with a method name to retroactively apply the module_function behavior, but only inside module bodies (or via `send` on a module). It is not available at the top level or inside class bodies: +`module_function` can be called with a method name, or with no arguments to affect later method definitions, but only +inside module bodies. It is not available at the top level, inside class bodies, or inside singleton class bodies: ```ruby module Foo @@ -1031,6 +1044,12 @@ Foo.private_instance_methods(false) # => [:foo] (instance becomes private) Foo.singleton_methods(false) # => [:foo] ``` +Ruby can invoke `module_function` indirectly with `send` on a module object. Rubydex indexes the direct call forms above; +dynamic `send` calls are treated as ordinary method calls rather than visibility operations. + +Like other visibility operations, Rubydex treats `module_function` calls inside method bodies or ordinary blocks as +runtime calls rather than static visibility operations. + **Creates a copy, not a reference:** ```ruby @@ -1042,6 +1061,66 @@ end Foo.foo # => "v1" (singleton is an independent copy of v1) ``` +The copied method may come from an ancestor. Ruby installs both a direct private instance method entry and a direct +singleton method entry on the receiving module, while preserving the source method's location and signature: + +```ruby +module A + def foo(x); "v1"; end +end + +module B + include A + module_function :foo +end + +B.method(:foo).source_location # => ["a.rb", 2] when A was loaded from a.rb +B.method(:foo).parameters # => [[:req, :x]] +B.private_instance_methods(false) # => [:foo] +B.singleton_methods(false) # => [:foo] +``` + +Later changes to `A#foo` do not mutate the already copied runtime method on `B`. A static indexer still needs to +invalidate and rebuild generated copies when the source file changes, because a fresh load of the current files would +copy the current source method at the `module_function :foo` call. + +When the source method and `module_function :foo` call are in different files, static indexing does not know runtime +load order. Rubydex treats cross-file source methods as potentially available and uses URI lexical order only as a +deterministic tie-breaker when multiple cross-file definitions could be copied. A definitely-prior same-file source +method is preferred over cross-file candidates. Within one file, the source method must +appear before the `module_function :foo` call, matching Ruby's `NameError` behavior for calls that appear before the +method definition. + +Method aliases are eligible targets. `module_function :bar` after `alias bar foo` copies the current `foo` method body +under the singleton name `bar`, while also making `bar` a private instance method on the module: + +```ruby +module Foo + def foo(x); x; end + alias bar foo + module_function :bar +end + +Foo.bar(1) # => 1 +Foo.private_instance_methods(false) # => [:bar] +Foo.singleton_methods(false) # => [:bar] +``` + +Attribute writers use the setter method name. `attr_writer :foo` defines `foo=`, not `foo`, so +`module_function :foo` fails while `module_function :foo=` creates a public singleton writer copy: + +```ruby +module Foo + attr_writer :foo + module_function :foo= # copies Foo#foo= to Foo.foo= +end + +module Foo + attr_writer :bar + module_function :bar # raises NameError +end +``` + ### Constant Visibility Ruby provides `private_constant` and `public_constant` to control constant visibility. @@ -1065,6 +1144,9 @@ Foo.constants # => [] (private constants hidden from .constants) - `Foo.const_defined?(:PRIV)` returns **true** even for private constants — visibility doesn't affect `const_defined?` - `Foo.const_get(:PRIV)` **bypasses** private constant visibility and returns the value — only the `::` operator enforces `private_constant` +Rubydex treats `private_constant`/`public_constant` calls inside method bodies or ordinary blocks as runtime calls rather +than static visibility operations. + #### `public_constant` Reverses `private_constant`: diff --git a/rust/rubydex-sys/src/graph_api.rs b/rust/rubydex-sys/src/graph_api.rs index 7b8ea4a4a..25caf37bc 100644 --- a/rust/rubydex-sys/src/graph_api.rs +++ b/rust/rubydex-sys/src/graph_api.rs @@ -980,7 +980,7 @@ pub unsafe extern "C" fn rdx_keyword_get(name: *const c_char) -> *const CKeyword } #[repr(u8)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CVisibility { Public = 0, Protected = 1, @@ -1001,13 +1001,16 @@ pub unsafe extern "C" fn rdx_graph_visibility(pointer: GraphPointer, declaration return ptr::null(); }; + debug_assert_ne!( + visibility, + Visibility::ModuleFunction, + "module_function visibility must be resolved before C API use" + ); + let c_visibility = match visibility { Visibility::Public => CVisibility::Public, Visibility::Protected => CVisibility::Protected, - Visibility::Private => CVisibility::Private, - Visibility::ModuleFunction => { - unimplemented!("module_function visibility translation is not implemented yet") - } + Visibility::Private | Visibility::ModuleFunction => CVisibility::Private, }; Box::into_raw(Box::new(c_visibility)).cast_const() @@ -1121,4 +1124,34 @@ mod tests { .ref_count() ); } + + #[test] + fn retroactive_module_function_exposes_private_instance_visibility() { + let mut indexer = RubyIndexer::new( + "file:///foo.rb".into(), + " + module Foo + def foo; end + module_function :foo + end + ", + ); + indexer.index(); + + let mut graph = Graph::new(); + graph.consume_document_changes(indexer.local_graph()); + let mut resolver = Resolver::new(&mut graph); + resolver.resolve(); + + let graph_ptr = Box::into_raw(Box::new(graph)) as GraphPointer; + let visibility = unsafe { rdx_graph_visibility(graph_ptr, DeclarationId::from("Foo#foo()").get()) }; + + assert!(!visibility.is_null()); + assert_eq!(unsafe { *visibility }, CVisibility::Private); + + unsafe { + free_c_visibility(visibility); + let _ = Box::from_raw(graph_ptr.cast::()); + } + } } diff --git a/rust/rubydex/src/indexing/local_graph.rs b/rust/rubydex/src/indexing/local_graph.rs index df29e6cc6..8c0180cc9 100644 --- a/rust/rubydex/src/indexing/local_graph.rs +++ b/rust/rubydex/src/indexing/local_graph.rs @@ -112,6 +112,15 @@ impl LocalGraph { string_id } + pub(crate) fn track_string(&mut self, string_id: StringId) { + let Some(string_ref) = self.strings.get_mut(&string_id) else { + debug_assert!(false, "Cannot track a string that has not been interned"); + return; + }; + + string_ref.increment_ref_count(1); + } + // Names #[must_use] diff --git a/rust/rubydex/src/indexing/ruby_indexer.rs b/rust/rubydex/src/indexing/ruby_indexer.rs index ddd3c6f1c..e24491876 100644 --- a/rust/rubydex/src/indexing/ruby_indexer.rs +++ b/rust/rubydex/src/indexing/ruby_indexer.rs @@ -88,6 +88,8 @@ pub struct RubyIndexer<'a> { comments: Vec, nesting_stack: Vec, visibility_stack: Vec, + runtime_block_depth: usize, + static_block_depth: usize, pending_decorator_offset: Option, } @@ -104,6 +106,8 @@ impl<'a> RubyIndexer<'a> { comments: Vec::new(), nesting_stack: Vec::new(), visibility_stack: vec![VisibilityModifier::new(Visibility::Private, false, Offset::new(0, 0))], + runtime_block_depth: 0, + static_block_depth: 0, pending_decorator_offset: None, } } @@ -725,7 +729,7 @@ impl<'a> RubyIndexer<'a> { self.nesting_stack.push(nesting_type(definition_id)); self.visibility_stack .push(VisibilityModifier::new(Visibility::Public, false, offset)); - self.visit(&body); + self.visit_static_body(&body); self.visibility_stack.pop(); self.nesting_stack.pop(); } @@ -785,13 +789,27 @@ impl<'a> RubyIndexer<'a> { self.nesting_stack.push(nesting_type(definition_id)); self.visibility_stack .push(VisibilityModifier::new(Visibility::Public, false, offset)); - self.visit(&body); + self.visit_static_body(&body); self.visibility_stack.pop(); self.nesting_stack.pop(); } } } + fn visit_static_body(&mut self, body: &ruby_prism::Node) { + if let Some(block) = body.as_block_node() { + self.visit_static_block(&block); + } else { + self.visit(body); + } + } + + fn visit_static_block(&mut self, block: &ruby_prism::BlockNode) { + self.static_block_depth += 1; + self.visit_block_node(block); + self.static_block_depth -= 1; + } + /// Handle dynamic class or module definitions, like `Module.new`, `Class.new`, `Data.define` and so on fn handle_dynamic_class_or_module(&mut self, node: &ruby_prism::Node, value: &ruby_prism::Node) -> bool { let Some(call_node) = value.as_call_node() else { @@ -835,15 +853,35 @@ impl<'a> RubyIndexer<'a> { }) } - fn current_nesting_is_module(&self) -> bool { - self.current_nesting_definition_id().is_some_and(|id| { + fn current_nesting_is_module_body(&self) -> bool { + self.nesting_stack.last().is_some_and(|nesting| { + let (Nesting::LexicalScope(id) | Nesting::Owner(id)) = nesting else { + return false; + }; + self.local_graph .definitions() - .get(&id) + .get(id) .is_some_and(|def| matches!(def, Definition::Module(_))) }) } + fn current_nesting_is_runtime_body(&self) -> bool { + matches!(self.nesting_stack.last(), Some(Nesting::Method(_))) || self.runtime_block_depth > 0 + } + + fn visit_runtime_call_node(&mut self, node: &ruby_prism::CallNode) { + if let Some(arguments) = node.arguments() { + self.visit_arguments_node(&arguments); + } + + if let Some(block) = node.block() { + self.visit(&block); + } + + self.index_method_reference_for_call(node); + } + /// Indexes the final constant target from a value node, unwrapping chained assignments. /// /// For `A = B = C`, when processing `A`, the value is `ConstantWriteNode(B)`. @@ -1192,6 +1230,11 @@ impl<'a> RubyIndexer<'a> { } fn handle_constant_visibility(&mut self, node: &ruby_prism::CallNode, visibility: Visibility) { + if self.current_nesting_is_runtime_body() { + self.visit_runtime_call_node(node); + return; + } + let receiver = node.receiver(); let receiver_name_id = match receiver { @@ -1199,10 +1242,6 @@ impl<'a> RubyIndexer<'a> { self.index_constant_reference(&receiver.unwrap(), true) } Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() { - Some(Nesting::Method(_)) => { - self.visit_call_node_parts(node); - return; - } None => { self.local_graph.add_diagnostic( Rule::InvalidPrivateConstant, @@ -1281,13 +1320,14 @@ impl<'a> RubyIndexer<'a> { visibility: Visibility, call_name: &str, ) { + if self.current_nesting_is_runtime_body() { + self.visit_runtime_call_node(node); + return; + } + match node.receiver() { - Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() { - Some(Nesting::Method(_)) => { - self.visit_call_node_parts(node); - return; - } - None => { + Some(ruby_prism::Node::SelfNode { .. }) | None => { + if self.nesting_stack.last().is_none() { self.local_graph.add_diagnostic( Rule::InvalidMethodVisibility, Offset::from_prism_location(&node.location()), @@ -1296,8 +1336,7 @@ impl<'a> RubyIndexer<'a> { self.visit_call_node_parts(node); return; } - _ => {} - }, + } _ => { self.visit_call_node_parts(node); return; @@ -1595,6 +1634,21 @@ impl Visit<'_> for RubyIndexer<'_> { ); } + fn visit_block_node(&mut self, node: &ruby_prism::BlockNode<'_>) { + if self.static_block_depth > 0 { + // Only the block passed to Module.new/Class.new is static. Its nested ordinary blocks still run later, + // so they must see static depth back at zero and be indexed as runtime bodies. + self.static_block_depth -= 1; + ruby_prism::visit_block_node(self, node); + self.static_block_depth += 1; + return; + } + + self.runtime_block_depth += 1; + ruby_prism::visit_block_node(self, node); + self.runtime_block_depth -= 1; + } + fn visit_singleton_class_node(&mut self, node: &ruby_prism::SingletonClassNode) { let expression = node.expression(); @@ -1856,6 +1910,12 @@ impl Visit<'_> for RubyIndexer<'_> { }; let definition_id = if receiver.is_none() && visibility == Visibility::ModuleFunction { + // The singleton copy shares interned strings with the private instance method. + self.local_graph.track_string(str_id); + for parameter in ¶meters { + self.local_graph.track_string(*parameter.inner().str()); + } + // module_function creates two method definitions: // 1. Public singleton method (class/module method) let method = Definition::Method(Box::new(MethodDefinition::new( @@ -1944,7 +2004,6 @@ impl Visit<'_> for RubyIndexer<'_> { .unwrap_or_else(|| offset_for_comments.start()); Self::each_string_or_symbol_arg(call, |name, location| { - let str_id = self.local_graph.intern_string(format!("{name}()")); let parent_nesting_id = self.parent_nesting_id(); let offset = Offset::from_prism_location(&location); @@ -1956,38 +2015,66 @@ impl Visit<'_> for RubyIndexer<'_> { v => v, }; - let definition = match kind { - AttrKind::Accessor => Definition::AttrAccessor(Box::new(AttrAccessorDefinition::new( - str_id, - self.uri_id, - offset, - comments, - flags, - parent_nesting_id, - visibility, - ))), - AttrKind::Reader => Definition::AttrReader(Box::new(AttrReaderDefinition::new( - str_id, - self.uri_id, - offset, - comments, - flags, - parent_nesting_id, - visibility, - ))), - AttrKind::Writer => Definition::AttrWriter(Box::new(AttrWriterDefinition::new( - str_id, - self.uri_id, - offset, - comments, - flags, - parent_nesting_id, - visibility, - ))), - }; + match kind { + AttrKind::Accessor => { + let reader_str_id = self.local_graph.intern_string(format!("{name}()")); + let writer_str_id = self.local_graph.intern_string(format!("{name}=()")); + + let reader = Definition::AttrAccessor(Box::new(AttrAccessorDefinition::new( + reader_str_id, + self.uri_id, + offset.clone(), + comments.clone(), + flags.clone(), + parent_nesting_id, + visibility, + ))); + let reader_id = self.local_graph.add_definition(reader); + self.add_member_to_current_owner(reader_id); - let definition_id = self.local_graph.add_definition(definition); - self.add_member_to_current_owner(definition_id); + let writer = Definition::AttrWriter(Box::new(AttrWriterDefinition::new( + writer_str_id, + self.uri_id, + offset, + comments, + flags, + parent_nesting_id, + visibility, + ))); + let writer_id = self.local_graph.add_definition(writer); + self.add_member_to_current_owner(writer_id); + } + AttrKind::Reader => { + let reader_str_id = self.local_graph.intern_string(format!("{name}()")); + + let definition = Definition::AttrReader(Box::new(AttrReaderDefinition::new( + reader_str_id, + self.uri_id, + offset, + comments, + flags, + parent_nesting_id, + visibility, + ))); + let definition_id = self.local_graph.add_definition(definition); + self.add_member_to_current_owner(definition_id); + } + AttrKind::Writer => { + let writer_str_id = self.local_graph.intern_string(format!("{name}=()")); + + let definition = Definition::AttrWriter(Box::new(AttrWriterDefinition::new( + writer_str_id, + self.uri_id, + offset, + comments, + flags, + parent_nesting_id, + visibility, + ))); + let definition_id = self.local_graph.add_definition(definition); + self.add_member_to_current_owner(definition_id); + } + } }); }; @@ -2116,6 +2203,13 @@ impl Visit<'_> for RubyIndexer<'_> { } } "private" | "protected" | "public" | "module_function" => { + let visibility = Visibility::from_string(message.as_str()); + + if self.current_nesting_is_runtime_body() { + self.visit_runtime_call_node(node); + return; + } + if node.receiver().is_some() { let offset = Offset::from_prism_location(&node.location()); self.local_graph.add_diagnostic( @@ -2127,20 +2221,22 @@ impl Visit<'_> for RubyIndexer<'_> { return; } - let visibility = Visibility::from_string(message.as_str()); let offset = Offset::from_prism_location(&node.location()); - if let Some(arguments) = node.arguments() { - if visibility == Visibility::ModuleFunction && !self.current_nesting_is_module() { - self.local_graph.add_diagnostic( - Rule::InvalidMethodVisibility, - offset, - "`module_function` can only be used in modules".to_string(), - ); + if visibility == Visibility::ModuleFunction && !self.current_nesting_is_module_body() { + self.local_graph.add_diagnostic( + Rule::InvalidMethodVisibility, + offset, + "`module_function` can only be used in modules".to_string(), + ); + if let Some(arguments) = node.arguments() { self.visit_arguments_node(&arguments); - } else { - self.handle_visibility_arguments(&arguments, visibility, &offset, message.as_str()); } + return; + } + + if let Some(arguments) = node.arguments() { + self.handle_visibility_arguments(&arguments, visibility, &offset, message.as_str()); } else { // Flag mode: `private` with no arguments // diff --git a/rust/rubydex/src/indexing/ruby_indexer_tests.rs b/rust/rubydex/src/indexing/ruby_indexer_tests.rs index db79b77a5..0bb7e23ca 100644 --- a/rust/rubydex/src/indexing/ruby_indexer_tests.rs +++ b/rust/rubydex/src/indexing/ruby_indexer_tests.rs @@ -4,7 +4,7 @@ use crate::{ assert_method_has_receiver, assert_name_path_eq, assert_no_local_diagnostics, assert_string_eq, model::{ definitions::{Definition, Parameter, Receiver, Signatures}, - ids::{StringId, UriId}, + ids::{DefinitionId, StringId, UriId}, visibility::Visibility, }, test_utils::LocalGraphTest, @@ -2048,6 +2048,184 @@ mod visibility_tests { } } + #[test] + fn index_no_arg_module_function_in_class_is_invalid() { + let context = index_source( + " + class Foo + module_function + def foo; end + end + ", + ); + + assert_local_diagnostics_eq!( + &context, + vec!["invalid-method-visibility: `module_function` can only be used in modules (2:3-2:18)"] + ); + + assert_definition_at!(&context, "3:3-3:15", Method, |def| { + assert_def_str_eq!(&context, def, "foo()"); + assert_eq!(def.visibility(), &Visibility::Public); + }); + + for def in context.graph().definitions().values() { + assert!( + !matches!(def, Definition::MethodVisibility(_)), + "should not create MethodVisibility for module_function in class" + ); + } + } + + #[test] + fn index_no_arg_module_function_at_top_level_is_invalid() { + let context = index_source( + " + module_function + def foo; end + ", + ); + + assert_local_diagnostics_eq!( + &context, + vec!["invalid-method-visibility: `module_function` can only be used in modules (1:1-1:16)"] + ); + + assert_definition_at!(&context, "2:1-2:13", Method, |def| { + assert_def_str_eq!(&context, def, "foo()"); + assert_eq!(def.visibility(), &Visibility::Private); + }); + + for def in context.graph().definitions().values() { + assert!( + !matches!(def, Definition::MethodVisibility(_)), + "should not create MethodVisibility for module_function at top level" + ); + } + } + + #[test] + fn index_no_arg_module_function_in_singleton_class_is_invalid() { + let context = index_source( + " + module Foo + class << self + module_function + def foo; end + end + end + ", + ); + + assert_local_diagnostics_eq!( + &context, + vec!["invalid-method-visibility: `module_function` can only be used in modules (3:5-3:20)"] + ); + + assert_definition_at!(&context, "4:5-4:17", Method, |def| { + assert_def_str_eq!(&context, def, "foo()"); + assert_eq!(def.visibility(), &Visibility::Public); + }); + + for def in context.graph().definitions().values() { + assert!( + !matches!(def, Definition::MethodVisibility(_)), + "should not create MethodVisibility for module_function in singleton class" + ); + } + } + + #[test] + fn index_module_function_inside_method_body_is_a_runtime_call() { + let context = index_source( + " + module Foo + def setup + module_function :bar + end + + def bar; end + end + ", + ); + + assert_no_local_diagnostics!(&context); + + for def in context.graph().definitions().values() { + assert!( + !matches!(def, Definition::MethodVisibility(_)), + "should not create MethodVisibility for module_function in a method body until runtime" + ); + } + } + + #[test] + fn index_module_function_inside_singleton_method_body_is_a_runtime_call() { + let context = index_source( + " + module Foo + def self.setup + module_function :bar + end + + def bar; end + end + ", + ); + + assert_no_local_diagnostics!(&context); + + for def in context.graph().definitions().values() { + assert!( + !matches!(def, Definition::MethodVisibility(_)), + "should not create MethodVisibility for module_function in a singleton method body until runtime" + ); + } + } + + #[test] + fn index_module_function_inside_block_body_is_a_runtime_call() { + let context = index_source( + " + module Foo + proc do + module_function :bar + end + + def bar; end + end + ", + ); + + assert_no_local_diagnostics!(&context); + + for def in context.graph().definitions().values() { + assert!( + !matches!(def, Definition::MethodVisibility(_)), + "should not create MethodVisibility for module_function in an ordinary block until runtime" + ); + } + } + + #[test] + fn index_module_function_inside_module_new_block_is_static_module_body() { + let context = index_source( + " + Foo = Module.new do + def bar; end + module_function :bar + end + ", + ); + + assert_no_local_diagnostics!(&context); + + assert_definition_at!(&context, "3:20-3:23", MethodVisibility, |def| { + assert_def_str_eq!(&context, def, "bar()"); + assert_eq!(def.visibility(), &Visibility::ModuleFunction); + }); + } + #[test] fn index_inline_visibility_mixed_with_retroactive() { let context = index_source( @@ -2469,6 +2647,42 @@ mod visibility_tests { mod attr_accessor_tests { use super::*; + fn assert_attr_accessor_pair( + context: &LocalGraphTest, + location: &str, + reader_name: &str, + writer_name: &str, + visibility: Visibility, + ) -> (DefinitionId, DefinitionId) { + let definitions = context.all_definitions_at(location); + assert_eq!( + definitions.len(), + 2, + "expected reader and writer definitions at {location}" + ); + + let mut reader_id = None; + let mut writer_id = None; + + for definition in definitions { + match definition { + Definition::AttrAccessor(def) => { + assert_def_str_eq!(context, def, reader_name); + assert_eq!(def.visibility(), &visibility); + reader_id = Some(def.id()); + } + Definition::AttrWriter(def) => { + assert_def_str_eq!(context, def, writer_name); + assert_eq!(def.visibility(), &visibility); + writer_id = Some(def.id()); + } + other => panic!("expected attr accessor pair, got {:?}", other.kind()), + } + } + + (reader_id.unwrap(), writer_id.unwrap()) + } + #[test] fn index_attr_accessor_definition() { let context = index_source({ @@ -2484,29 +2698,18 @@ mod attr_accessor_tests { }); assert_no_local_diagnostics!(&context); - assert_eq!(context.graph().definitions().len(), 4); - - assert_definition_at!(&context, "1:16-1:19", AttrAccessor, |def| { - assert_def_str_eq!(&context, def, "foo()"); - assert!(def.lexical_nesting_id().is_none()); - }); - - assert_definition_at!(&context, "4:18-4:21", AttrAccessor, |def| { - assert_def_str_eq!(&context, def, "bar()"); + assert_eq!(context.graph().definitions().len(), 7); - assert_definition_at!(&context, "3:1-5:4", Class, |parent_nesting| { - assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap()); - assert_eq!(parent_nesting.members()[0], def.id()); - }); - }); + assert_attr_accessor_pair(&context, "1:16-1:19", "foo()", "foo=()", Visibility::Private); - assert_definition_at!(&context, "4:24-4:27", AttrAccessor, |def| { - assert_def_str_eq!(&context, def, "baz()"); + let first_pair = assert_attr_accessor_pair(&context, "4:18-4:21", "bar()", "bar=()", Visibility::Public); + let second_pair = assert_attr_accessor_pair(&context, "4:24-4:27", "baz()", "baz=()", Visibility::Public); - assert_definition_at!(&context, "3:1-5:4", Class, |parent_nesting| { - assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap()); - assert_eq!(parent_nesting.members()[1], def.id()); - }); + assert_definition_at!(&context, "3:1-5:4", Class, |parent_nesting| { + assert_eq!( + parent_nesting.members(), + &[first_pair.0, first_pair.1, second_pair.0, second_pair.1] + ); }); } @@ -2565,12 +2768,12 @@ mod attr_accessor_tests { assert_eq!(context.graph().definitions().len(), 4); assert_definition_at!(&context, "1:14-1:17", AttrWriter, |def| { - assert_def_str_eq!(&context, def, "foo()"); + assert_def_str_eq!(&context, def, "foo=()"); assert!(def.lexical_nesting_id().is_none()); }); assert_definition_at!(&context, "4:16-4:19", AttrWriter, |def| { - assert_def_str_eq!(&context, def, "bar()"); + assert_def_str_eq!(&context, def, "bar=()"); assert_definition_at!(&context, "3:1-5:4", Class, |parent_nesting| { assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap()); @@ -2579,7 +2782,7 @@ mod attr_accessor_tests { }); assert_definition_at!(&context, "4:22-4:25", AttrWriter, |def| { - assert_def_str_eq!(&context, def, "baz()"); + assert_def_str_eq!(&context, def, "baz=()"); assert_definition_at!(&context, "3:1-5:4", Class, |parent_nesting| { assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap()); @@ -2603,7 +2806,7 @@ mod attr_accessor_tests { }); assert_no_local_diagnostics!(&context); - assert_eq!(context.graph().definitions().len(), 6); + assert_eq!(context.graph().definitions().len(), 7); assert_definition_at!(&context, "1:6-1:10", AttrReader, |def| { assert_def_str_eq!(&context, def, "a1()"); @@ -2615,21 +2818,15 @@ mod attr_accessor_tests { assert!(def.lexical_nesting_id().is_none()); }); - assert_definition_at!(&context, "4:8-4:12", AttrAccessor, |def| { - assert_def_str_eq!(&context, def, "a3()"); - - assert_definition_at!(&context, "3:1-7:4", Class, |parent_nesting| { - assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap()); - assert_eq!(parent_nesting.members()[0], def.id()); - }); - }); + let (a3_reader_id, a3_writer_id) = + assert_attr_accessor_pair(&context, "4:8-4:12", "a3()", "a3=()", Visibility::Public); assert_definition_at!(&context, "5:9-5:11", AttrReader, |def| { assert_def_str_eq!(&context, def, "a4()"); assert_definition_at!(&context, "3:1-7:4", Class, |parent_nesting| { assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap()); - assert_eq!(parent_nesting.members()[1], def.id()); + assert_eq!(parent_nesting.members()[2], def.id()); }); }); @@ -2637,9 +2834,14 @@ mod attr_accessor_tests { assert_def_str_eq!(&context, def, "a5()"); assert_definition_at!(&context, "3:1-7:4", Class, |parent_nesting| { assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap()); - assert_eq!(parent_nesting.members()[2], def.id()); + assert_eq!(parent_nesting.members()[3], def.id()); }); }); + + assert_definition_at!(&context, "3:1-7:4", Class, |parent_nesting| { + assert_eq!(parent_nesting.members()[0], a3_reader_id); + assert_eq!(parent_nesting.members()[1], a3_writer_id); + }); } #[test] @@ -2658,10 +2860,7 @@ mod attr_accessor_tests { assert_no_local_diagnostics!(&context); - assert_definition_at!(&context, "1:16-1:19", AttrAccessor, |def| { - assert_def_str_eq!(&context, def, "foo()"); - assert_eq!(def.visibility(), &Visibility::Private); - }); + assert_attr_accessor_pair(&context, "1:16-1:19", "foo()", "foo=()", Visibility::Private); assert_definition_at!(&context, "3:24-3:27", AttrReader, |def| { assert_def_str_eq!(&context, def, "bar()"); @@ -2669,7 +2868,7 @@ mod attr_accessor_tests { }); assert_definition_at!(&context, "7:14-7:17", AttrWriter, |def| { - assert_def_str_eq!(&context, def, "baz()"); + assert_def_str_eq!(&context, def, "baz=()"); assert_eq!(def.visibility(), &Visibility::Public); }); } @@ -2702,15 +2901,8 @@ mod attr_accessor_tests { assert_no_local_diagnostics!(&context); - assert_definition_at!(&context, "4:18-4:21", AttrAccessor, |def| { - assert_def_str_eq!(&context, def, "foo()"); - assert_eq!(def.visibility(), &Visibility::Public); - }); - - assert_definition_at!(&context, "9:20-9:23", AttrAccessor, |def| { - assert_def_str_eq!(&context, def, "bar()"); - assert_eq!(def.visibility(), &Visibility::Public); - }); + assert_attr_accessor_pair(&context, "4:18-4:21", "foo()", "foo=()", Visibility::Public); + assert_attr_accessor_pair(&context, "9:20-9:23", "bar()", "bar=()", Visibility::Public); assert_definition_at!(&context, "13:18-13:21", AttrReader, |def| { assert_def_str_eq!(&context, def, "baz()"); @@ -2718,7 +2910,7 @@ mod attr_accessor_tests { }); assert_definition_at!(&context, "18:16-18:19", AttrWriter, |def| { - assert_def_str_eq!(&context, def, "qux()"); + assert_def_str_eq!(&context, def, "qux=()"); assert_eq!(def.visibility(), &Visibility::Private); }); } @@ -4348,17 +4540,31 @@ mod comment_tests { assert_def_comments_eq!(&context, def, ["# Comment 1", "# Comment 2", "# Comment 3"]); }); - assert_definition_at!(&context, "13:18-13:21", AttrAccessor, |def| { - assert_def_comments_eq!(&context, def, ["# Comment 1", "# Comment 2", "# Comment 3"]); - }); - - assert_definition_at!(&context, "13:24-13:27", AttrAccessor, |def| { - assert_def_comments_eq!(&context, def, ["# Comment 1", "# Comment 2", "# Comment 3"]); - }); + for location in ["13:18-13:21", "13:24-13:27"] { + let definitions = context.all_definitions_at(location); + assert_eq!(definitions.len(), 2); + for definition in definitions { + match definition { + Definition::AttrAccessor(def) => { + assert_def_comments_eq!(&context, def, ["# Comment 1", "# Comment 2", "# Comment 3"]); + } + Definition::AttrWriter(def) => { + assert_def_comments_eq!(&context, def, ["# Comment 1", "# Comment 2", "# Comment 3"]); + } + other => panic!("expected attr accessor pair, got {:?}", other.kind()), + } + } + } - assert_definition_at!(&context, "16:9-16:13", AttrAccessor, |def| { - assert_def_comments_eq!(&context, def, ["# Comment"]); - }); + let definitions = context.all_definitions_at("16:9-16:13"); + assert_eq!(definitions.len(), 2); + for definition in definitions { + match definition { + Definition::AttrAccessor(def) => assert_def_comments_eq!(&context, def, ["# Comment"]), + Definition::AttrWriter(def) => assert_def_comments_eq!(&context, def, ["# Comment"]), + other => panic!("expected attr accessor pair, got {:?}", other.kind()), + } + } } #[test] diff --git a/rust/rubydex/src/model.rs b/rust/rubydex/src/model.rs index 7745db64c..fc2f5448a 100644 --- a/rust/rubydex/src/model.rs +++ b/rust/rubydex/src/model.rs @@ -5,6 +5,7 @@ pub mod definitions; pub mod document; pub mod encoding; pub mod graph; +pub(crate) mod graph_indexes; pub mod id; pub mod identity_maps; pub mod ids; diff --git a/rust/rubydex/src/model/declaration.rs b/rust/rubydex/src/model/declaration.rs index 91c775a76..303ca72f2 100644 --- a/rust/rubydex/src/model/declaration.rs +++ b/rust/rubydex/src/model/declaration.rs @@ -408,12 +408,23 @@ impl Declaration { }); } + pub fn insert_definition(&mut self, definition_id: DefinitionId, index: usize) { + all_declarations!(self, it => { + debug_assert!( + !it.definition_ids.contains(&definition_id), + "Cannot add the same exact definition to a declaration twice. Duplicate definition IDs" + ); + + it.definition_ids.insert(index.min(it.definition_ids.len()), definition_id); + }); + } + // Deletes a definition from this declaration pub fn remove_definition(&mut self, definition_id: &DefinitionId) -> bool { all_declarations!(self, it => { if let Some(pos) = it.definition_ids.iter().position(|id| id == definition_id) { - it.definition_ids.swap_remove(pos); - it.definition_ids.shrink_to_fit(); + // Definition order is semantic for retroactive visibility; keep it stable. + it.definition_ids.remove(pos); true } else { false @@ -452,6 +463,13 @@ impl Declaration { all_declarations!(self, it => it.diagnostics.clear()); } + pub fn retain_diagnostics(&mut self, mut f: F) + where + F: FnMut(&Diagnostic) -> bool, + { + all_declarations!(self, it => it.diagnostics.retain(|diagnostic| f(diagnostic))); + } + #[must_use] pub fn reference_count(&self) -> usize { all_declarations!(self, it => it.references.len()) @@ -684,6 +702,24 @@ mod tests { decl.add_definition(def_id); } + #[test] + fn removing_definition_preserves_remaining_order() { + let mut decl = Declaration::Method(Box::new(MethodDeclaration::new( + "Foo#bar()".to_string(), + DeclarationId::from("Foo"), + ))); + let first = DefinitionId::new(1); + let second = DefinitionId::new(2); + let third = DefinitionId::new(3); + + decl.add_definition(first); + decl.add_definition(second); + decl.add_definition(third); + + assert!(decl.remove_definition(&first)); + assert_eq!(decl.definitions(), [second, third]); + } + #[test] fn adding_and_removing_members() { let decl = Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new( diff --git a/rust/rubydex/src/model/definitions.rs b/rust/rubydex/src/model/definitions.rs index 06b572a18..d522dea44 100644 --- a/rust/rubydex/src/model/definitions.rs +++ b/rust/rubydex/src/model/definitions.rs @@ -41,6 +41,7 @@ bitflags! { const DEPRECATED = 0b0001; const PROMOTABLE = 0b0010; const SINGLETON_METHOD_VISIBILITY = 0b0100; + const GENERATED = 0b1000; } } @@ -59,6 +60,11 @@ impl DefinitionFlags { pub fn is_singleton_method_visibility(&self) -> bool { self.contains(Self::SINGLETON_METHOD_VISIBILITY) } + + #[must_use] + pub fn is_generated(&self) -> bool { + self.contains(Self::GENERATED) + } } #[derive(Debug)] @@ -189,6 +195,41 @@ impl Definition { pub fn is_deprecated(&self) -> bool { all_definitions!(self, it => it.flags().is_deprecated()) } + + #[must_use] + pub fn method_effective_visibility(&self) -> Option { + match self { + Definition::MethodVisibility(vis) => Some(match vis.visibility() { + Visibility::ModuleFunction => Visibility::Private, + visibility => *visibility, + }), + Definition::Method(method) => Some(*method.visibility()), + Definition::AttrAccessor(attr) => Some(*attr.visibility()), + Definition::AttrReader(attr) => Some(*attr.visibility()), + Definition::AttrWriter(attr) => Some(*attr.visibility()), + _ => None, + } + } + + #[must_use] + pub(crate) fn establishes_method_member(&self) -> bool { + matches!( + self, + Definition::Method(_) + | Definition::MethodAlias(_) + | Definition::AttrAccessor(_) + | Definition::AttrReader(_) + | Definition::AttrWriter(_) + ) + } + + #[must_use] + pub(crate) fn is_copyable_method_body(&self) -> bool { + matches!( + self, + Definition::Method(_) | Definition::AttrAccessor(_) | Definition::AttrReader(_) | Definition::AttrWriter(_) + ) + } } /// Represents a mixin: include, prepend, or extend. @@ -963,6 +1004,34 @@ impl MethodDefinition { } } + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn new_generated( + str_id: StringId, + uri_id: UriId, + offset: Offset, + comments: Box<[Comment]>, + mut flags: DefinitionFlags, + lexical_nesting_id: Option, + signatures: Signatures, + visibility: Visibility, + receiver: Option, + ) -> Self { + flags |= DefinitionFlags::GENERATED; + + Self { + str_id, + uri_id, + offset, + flags, + comments, + lexical_nesting_id, + signatures, + visibility, + receiver, + } + } + #[must_use] pub fn id(&self) -> DefinitionId { let mut formatted_id = format!("{}{}{}", *self.uri_id, self.offset.start(), *self.str_id); @@ -976,6 +1045,13 @@ impl MethodDefinition { } } + if self.flags.is_generated() + && let Some(lexical_nesting_id) = self.lexical_nesting_id + { + formatted_id.push('g'); + formatted_id.push_str(&lexical_nesting_id.to_string()); + } + DefinitionId::from(&formatted_id) } @@ -1080,7 +1156,12 @@ impl ParameterStruct { } } -/// An attr accessor definition +/// The reader-side method definition created by `attr_accessor`. +/// +/// The writer side is indexed separately as an `AttrWriterDefinition` for the +/// generated `foo=()` method. Keeping the reader and writer as separate method +/// declarations lets visibility changes and `module_function :foo=` target the +/// same methods Ruby creates. /// /// # Example /// ```ruby diff --git a/rust/rubydex/src/model/document.rs b/rust/rubydex/src/model/document.rs index 339a5ae5b..52debf829 100644 --- a/rust/rubydex/src/model/document.rs +++ b/rust/rubydex/src/model/document.rs @@ -82,6 +82,13 @@ impl Document { self.diagnostics.push(diagnostic); } + pub fn retain_diagnostics(&mut self, mut f: F) + where + F: FnMut(&Diagnostic) -> bool, + { + self.diagnostics.retain(|diagnostic| f(diagnostic)); + } + /// Computes the require path for this document given load paths. /// /// Returns `None` if: diff --git a/rust/rubydex/src/model/graph.rs b/rust/rubydex/src/model/graph.rs index 469f84c44..d8f3db366 100644 --- a/rust/rubydex/src/model/graph.rs +++ b/rust/rubydex/src/model/graph.rs @@ -1,7 +1,10 @@ +use std::cmp::Ordering; use std::collections::HashSet; use std::collections::hash_map::Entry; use std::path::PathBuf; +mod visibility; + use crate::diagnostic::Diagnostic; use crate::indexing::local_graph::LocalGraph; use crate::model::built_in::{OBJECT_ID, add_built_in_data}; @@ -9,12 +12,14 @@ use crate::model::declaration::{Ancestor, Declaration, Namespace}; use crate::model::definitions::{Definition, MethodVisibilityDefinition, Receiver}; use crate::model::document::Document; use crate::model::encoding::Encoding; +use crate::model::graph_indexes::{AppliedMethodVisibilities, ModuleFunctionCopies, UnresolvedMethodVisibilities}; use crate::model::identity_maps::{IdentityHashMap, IdentityHashSet}; use crate::model::ids::{ConstantReferenceId, DeclarationId, DefinitionId, MethodReferenceId, NameId, StringId, UriId}; use crate::model::name::{Name, NameRef, ParentScope, ResolvedName}; use crate::model::references::{ConstantReference, MethodRef}; use crate::model::string_ref::StringRef; use crate::model::visibility::Visibility; +use crate::offset::Offset; use crate::{query, stats}; /// An entity whose validity depends on a particular `NameId`. @@ -29,6 +34,35 @@ pub enum NameDependent { NestedName(NameId), } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DefinitionProgramOrderKey<'a> { + id: DefinitionId, + uri: &'a str, + offset: &'a Offset, +} + +impl<'a> DefinitionProgramOrderKey<'a> { + #[must_use] + pub(crate) fn new(id: DefinitionId, uri: &'a str, offset: &'a Offset) -> Self { + Self { id, uri, offset } + } +} + +impl Ord for DefinitionProgramOrderKey<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.uri + .cmp(other.uri) + .then_with(|| self.offset.cmp(other.offset)) + .then_with(|| self.id.cmp(&other.id)) + } +} + +impl PartialOrd for DefinitionProgramOrderKey<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + /// Items processed by the unified invalidation worklist. enum InvalidationItem { /// Ancestor chain is stale, or declaration has become empty and needs removal. @@ -81,6 +115,10 @@ pub struct Graph { /// Drained by `take_pending_work()` before resolution. pending_work: Vec, + module_function_copies: ModuleFunctionCopies, + unresolved_method_visibilities: UnresolvedMethodVisibilities, + applied_method_visibilities: AppliedMethodVisibilities, + /// Paths to exclude from file discovery during indexing. excluded_paths: HashSet, } @@ -99,6 +137,9 @@ impl Graph { position_encoding: Encoding::default(), name_dependents: IdentityHashMap::default(), pending_work: Vec::default(), + module_function_copies: ModuleFunctionCopies::default(), + unresolved_method_visibilities: UnresolvedMethodVisibilities::default(), + applied_method_visibilities: AppliedMethodVisibilities::default(), excluded_paths: HashSet::new(), }; @@ -186,6 +227,30 @@ impl Graph { declaration_id } + fn detach_definition_from_declaration(&mut self, declaration_id: DeclarationId, definition_id: DefinitionId) { + self.detach_applied_method_visibility(definition_id); + + let Some((owner_id, unqualified_str_id)) = (|| { + let declaration = self.declarations.get_mut(&declaration_id)?; + + if !declaration.remove_definition(&definition_id) || !declaration.has_no_definitions() { + return None; + } + + Some((*declaration.owner_id(), StringId::from(&declaration.unqualified_name()))) + })() else { + return; + }; + + self.declarations.remove(&declaration_id); + + if let Some(owner) = self.declarations.get_mut(&owner_id) + && let Some(namespace) = owner.as_namespace_mut() + { + namespace.remove_member(&unqualified_str_id); + } + } + /// Checks if all constant definitions for a declaration have the PROMOTABLE flag set. /// Used to determine whether a constant can be promoted to a namespace. #[must_use] @@ -232,6 +297,109 @@ impl Graph { &self.definitions } + /// Returns the static program-order key used when Rubydex needs a deterministic tie-breaker. + /// + /// Ruby load order is not known from a set of indexed documents, so cross-file ordering uses URI lexical order only + /// for deterministic selection/presentation. Same-file ordering uses source offsets. + pub(crate) fn definition_program_order_key(&self, definition_id: DefinitionId) -> Option<(&str, &Offset)> { + let definition = self.definitions.get(&definition_id)?; + let document = self.documents.get(definition.uri_id())?; + Some((document.uri(), definition.offset())) + } + + pub(crate) fn definition_effect_order_key(&self, definition_id: DefinitionId) -> Option<(&str, &Offset)> { + if let Some(copy) = self.module_function_copies.get(definition_id) { + return copy + .trigger_definition_ids + .iter() + .filter_map(|trigger_definition_id| { + let (trigger_uri, trigger_offset) = self.definition_program_order_key(*trigger_definition_id)?; + Some((*trigger_definition_id, trigger_uri, trigger_offset)) + }) + .min_by( + |(left_id, left_uri, left_offset), (right_id, right_uri, right_offset)| { + DefinitionProgramOrderKey::new(*left_id, left_uri, left_offset) + .cmp(&DefinitionProgramOrderKey::new(*right_id, right_uri, right_offset)) + }, + ) + .map(|(_, trigger_uri, trigger_offset)| (trigger_uri, trigger_offset)) + .or_else(|| self.definition_program_order_key(definition_id)); + } + + self.definition_program_order_key(definition_id) + } + + fn min_program_order_key<'a>( + current: Option<(DefinitionId, &'a str, &'a Offset)>, + candidate: (DefinitionId, &'a str, &'a Offset), + ) -> (DefinitionId, &'a str, &'a Offset) { + match current { + Some(current) + if DefinitionProgramOrderKey::new(current.0, current.1, current.2) + .cmp(&DefinitionProgramOrderKey::new(candidate.0, candidate.1, candidate.2)) + .is_le() => + { + current + } + _ => candidate, + } + } + + pub(crate) fn definition_order_key_before_position( + &self, + definition_id: DefinitionId, + uri_id: UriId, + offset: &Offset, + ) -> Option<(bool, &str, &Offset)> { + let position_uri = self.documents.get(&uri_id)?.uri(); + + if let Some(copy) = self.module_function_copies.get(definition_id) { + let mut same_file_key = None; + let mut cross_file_key = None; + + for trigger_definition_id in ©.trigger_definition_ids { + let Some((trigger_uri, trigger_offset)) = self.definition_program_order_key(*trigger_definition_id) + else { + continue; + }; + + if trigger_uri == position_uri { + if trigger_offset.start() < offset.start() { + same_file_key = Some(Self::min_program_order_key( + same_file_key, + (*trigger_definition_id, trigger_uri, trigger_offset), + )); + } + } else { + cross_file_key = Some(Self::min_program_order_key( + cross_file_key, + (*trigger_definition_id, trigger_uri, trigger_offset), + )); + } + } + + if let Some((_, uri, offset)) = same_file_key { + return Some((true, uri, offset)); + } + if let Some((_, uri, offset)) = cross_file_key { + return Some((false, uri, offset)); + } + + return None; + } + + let (definition_uri, definition_offset) = self.definition_program_order_key(definition_id)?; + if definition_uri == position_uri { + if definition_offset.start() < offset.start() { + return Some((true, definition_uri, definition_offset)); + } + + return None; + } + + Some((false, definition_uri, definition_offset)) + } + /// Returns the ID of the unqualified name of a definition /// /// # Panics @@ -504,6 +672,14 @@ impl Graph { string_id } + pub(crate) fn prepare_generated_string(&mut self, string: String) -> StringId { + let string_id = StringId::from(&string); + self.strings + .entry(string_id) + .or_insert_with(|| StringRef::new_pending(string)); + string_id + } + /// Registers a name in the graph unless already registered. In regular indexing, this only happens in the local /// graph. This method is only used to back the `Graph#resolve_constant` Ruby API because every name must be /// registered in the graph to properly resolve @@ -661,40 +837,42 @@ impl Graph { Some(Visibility::Public) } Declaration::Method(_) => { - let mut latest_alias: Option = None; - - for def_id in definitions.iter().rev() { - let Some(definition) = self.definitions.get(def_id) else { - continue; - }; - - let visibility = match definition { - Definition::MethodVisibility(vis) => Some(*vis.visibility()), - Definition::Method(method) => Some(*method.visibility()), - Definition::AttrAccessor(attr) => Some(*attr.visibility()), - Definition::AttrReader(attr) => Some(*attr.visibility()), - Definition::AttrWriter(attr) => Some(*attr.visibility()), - Definition::MethodAlias(_) => { - if latest_alias.is_none() { - latest_alias = Some(*def_id); - } - None - } - _ => None, - }; + if !definitions + .iter() + .any(|definition_id| self.module_function_copies.get(*definition_id).is_some()) + { + return self.method_visibility_from_definitions(definitions.iter().rev().copied()); + } - if visibility.is_some() { - return visibility; + let mut ordered_definitions = definitions + .iter() + .enumerate() + .filter(|(_, definition_id)| self.definitions.contains_key(definition_id)) + .map(|(index, definition_id)| (index, *definition_id)) + .collect::>(); + + ordered_definitions.sort_unstable_by(|(left_index, left_id), (right_index, right_id)| { + let left_generated = self.module_function_copies.get(*left_id).is_some(); + let right_generated = self.module_function_copies.get(*right_id).is_some(); + + // Keep declaration order across different files here. URI order is only a deterministic resolver + // approximation, not Ruby load order; using it for current visibility would let a cross-file + // generated copy override already-known declaration order. Same-file generated copies still use + // their trigger/effect offset so visibility calls before vs. after `module_function` behave correctly. + if (left_generated || right_generated) + && let (Some((left_uri, left_offset)), Some((right_uri, right_offset))) = ( + self.definition_effect_order_key(*left_id), + self.definition_effect_order_key(*right_id), + ) + && left_uri == right_uri + { + return left_offset.cmp(right_offset).then_with(|| left_index.cmp(right_index)); } - } - if let Some(alias_def_id) = latest_alias - && let Ok(target_id) = query::follow_method_alias(self, alias_def_id) - { - return self.visibility(&target_id); - } + left_index.cmp(right_index) + }); - Some(Visibility::Public) + self.method_visibility_from_definitions(ordered_definitions.into_iter().rev().map(|(_, def_id)| def_id)) } Declaration::Namespace(Namespace::SingletonClass(_)) | Declaration::GlobalVariable(_) @@ -703,11 +881,48 @@ impl Graph { } } + fn method_visibility_from_definitions( + &self, + definition_ids: impl IntoIterator, + ) -> Option { + let mut latest_alias: Option = None; + + for def_id in definition_ids { + let Some(definition) = self.definitions.get(&def_id) else { + continue; + }; + + if let Some(visibility) = definition.method_effective_visibility() { + return Some(visibility); + } + + if matches!(definition, Definition::MethodAlias(_)) && latest_alias.is_none() { + latest_alias = Some(def_id); + } + } + + if let Some(alias_def_id) = latest_alias + && let Ok(target_id) = query::follow_method_alias(self, alias_def_id) + { + return self.visibility(&target_id); + } + + Some(Visibility::Public) + } + /// Drains the accumulated work items, returning them for use by the resolver. pub fn take_pending_work(&mut self) -> Vec { std::mem::take(&mut self.pending_work) } + #[cfg(test)] + pub(crate) fn assert_method_visibility_indexes_consistent(&self) { + self.applied_method_visibilities + .assert_visibility_definitions_exist(&self.definitions); + self.unresolved_method_visibilities + .assert_visibility_definitions_exist(&self.definitions); + } + pub(crate) fn push_work(&mut self, unit: Unit) { self.pending_work.push(unit); } @@ -799,7 +1014,7 @@ impl Graph { } } - fn untrack_definition_strings(&mut self, definition: &Definition) { + fn for_each_definition_string(definition: &Definition, mut f: impl FnMut(StringId)) { match definition { Definition::Class(_) | Definition::SingletonClass(_) @@ -807,25 +1022,52 @@ impl Graph { | Definition::Constant(_) | Definition::ConstantAlias(_) | Definition::ConstantVisibility(_) => {} - Definition::MethodVisibility(d) => self.untrack_string(*d.str_id()), - Definition::Method(d) => self.untrack_string(*d.str_id()), - Definition::AttrAccessor(d) => self.untrack_string(*d.str_id()), - Definition::AttrReader(d) => self.untrack_string(*d.str_id()), - Definition::AttrWriter(d) => self.untrack_string(*d.str_id()), - Definition::GlobalVariable(d) => self.untrack_string(*d.str_id()), - Definition::InstanceVariable(d) => self.untrack_string(*d.str_id()), - Definition::ClassVariable(d) => self.untrack_string(*d.str_id()), + Definition::MethodVisibility(d) => f(*d.str_id()), + Definition::Method(d) => { + f(*d.str_id()); + for signature in d.signatures().as_slice() { + for parameter in signature { + f(*parameter.inner().str()); + } + } + } + Definition::AttrAccessor(d) => f(*d.str_id()), + Definition::AttrReader(d) => f(*d.str_id()), + Definition::AttrWriter(d) => f(*d.str_id()), + Definition::GlobalVariable(d) => f(*d.str_id()), + Definition::InstanceVariable(d) => f(*d.str_id()), + Definition::ClassVariable(d) => f(*d.str_id()), Definition::MethodAlias(d) => { - self.untrack_string(*d.new_name_str_id()); - self.untrack_string(*d.old_name_str_id()); + f(*d.new_name_str_id()); + f(*d.old_name_str_id()); } Definition::GlobalVariableAlias(d) => { - self.untrack_string(*d.new_name_str_id()); - self.untrack_string(*d.old_name_str_id()); + f(*d.new_name_str_id()); + f(*d.old_name_str_id()); } } } + fn untrack_definition_strings(&mut self, definition: &Definition) { + Self::for_each_definition_string(definition, |string_id| self.untrack_string(string_id)); + } + + fn track_definition_strings(&mut self, definition: &Definition) { + // Local-graph definitions arrive with their strings already tracked by the + // indexer. Generated definitions are inserted directly into the graph, so + // their strings must have been interned or prepared before this point. + Self::for_each_definition_string(definition, |string_id| self.track_string(string_id)); + } + + fn track_string(&mut self, string_id: StringId) { + let Some(string_ref) = self.strings.get_mut(&string_id) else { + debug_assert!(false, "Cannot track a string that has not been interned or prepared"); + return; + }; + + string_ref.increment_ref_count(1); + } + /// Decrements the ref count for a name and removes it if the count reaches zero. /// /// This recursively untracks `parent_scope` and `nesting` names. @@ -1038,11 +1280,24 @@ impl Graph { // Identify declarations affected by removed definitions if let Some(document) = old_document { + let mut module_function_visibility_ids = Vec::new(); for def_id in document.definitions() { + module_function_visibility_ids.extend(self.remove_module_function_copies_for_source(*def_id)); + module_function_visibility_ids.extend(self.remove_module_function_copies_for_alias(*def_id)); + module_function_visibility_ids.extend(self.remove_method_visibility_side_effects(*def_id)); + if let Some(declaration_id) = self.definition_id_to_declaration_id(*def_id).copied() { pending_detachments.entry(declaration_id).or_default().push(*def_id); } } + + for visibility_id in module_function_visibility_ids { + if let Some(declaration_id) = self.definition_id_to_declaration_id(visibility_id).copied() { + self.detach_definition_from_declaration(declaration_id, visibility_id); + } + self.push_work(Unit::Definition(visibility_id)); + } + for decl_id in pending_detachments.keys() { items.push(InvalidationItem::Declaration(*decl_id)); } @@ -1119,6 +1374,10 @@ impl Graph { .collect(); if !missed_def_ids.is_empty() { + for def_id in &missed_def_ids { + self.detach_applied_method_visibility(*def_id); + } + for declaration in self.declarations.values_mut() { for def_id in &missed_def_ids { declaration.remove_definition(def_id); @@ -1192,6 +1451,7 @@ impl Graph { ) { // Collect names before detaching — after detachment, definitions() may be empty let seed_names = self.names_for_declaration(decl_id); + let method_member_changes = self.method_member_changes_for_detach(decl_id, detach_def_ids); // Detach pending definitions before deciding the mode if let Some(decl) = self.declarations.get_mut(&decl_id) { @@ -1203,6 +1463,16 @@ impl Graph { } } + if !detach_def_ids.is_empty() && method_member_changes.is_empty() { + self.requeue_method_visibility_definitions_for_declaration(decl_id); + } + + if !detach_def_ids.is_empty() { + for (owner_id, member_str_id) in method_member_changes { + self.requeue_method_visibility_dependents_for_member_change(owner_id, member_str_id); + } + } + let Some(decl) = self.declarations.get(&decl_id) else { return; }; @@ -1293,6 +1563,9 @@ impl Graph { namespace.clear_descendants(); self.push_work(Unit::Ancestors(decl_id)); + self.requeue_unresolved_method_visibilities_for_owner(decl_id); + self.requeue_method_visibility_definitions_for_owner(decl_id); + self.requeue_module_function_copies_for_owner(decl_id); for seed_name_id in seed_names { self.queue_ancestor_triggered_invalidation(seed_name_id, queue); @@ -1300,6 +1573,39 @@ impl Graph { } } + fn method_member_changes_for_detach( + &self, + declaration_id: DeclarationId, + detach_def_ids: &[DefinitionId], + ) -> Vec<(DeclarationId, StringId)> { + let Some(declaration) = self.declarations.get(&declaration_id) else { + return Vec::new(); + }; + if !matches!(declaration, Declaration::Method(_)) { + return Vec::new(); + } + + let owner_id = *declaration.owner_id(); + let mut member_changes = Vec::new(); + let mut seen_str_ids = IdentityHashSet::default(); + + for definition_id in detach_def_ids { + let Some(definition) = self.definitions.get(definition_id) else { + continue; + }; + if !definition.establishes_method_member() { + continue; + } + + let str_id = self.definition_string_id(definition); + if seen_str_ids.insert(str_id) { + member_changes.push((owner_id, str_id)); + } + } + + member_changes + } + /// The name's structural dependency is broken (its nesting or parent scope was removed). /// Unresolves the name and cascades to all dependents — both references and definitions. fn unresolve_dependent_name(&mut self, name_id: NameId, queue: &mut Vec) { @@ -1317,6 +1623,7 @@ impl Graph { } NameDependent::Definition(def_id) => { self.push_work(Unit::Definition(*def_id)); + self.detach_applied_method_visibility(*def_id); if let Some(decl) = self.declarations.get_mut(&old_decl_id) { decl.remove_definition(def_id); @@ -1819,8 +2126,11 @@ mod tests { let strings = context.graph().strings(); assert_eq!(strings.get(&StringId::from("method_name()")).unwrap().ref_count(), 1); assert_eq!(strings.get(&StringId::from("accessor_name()")).unwrap().ref_count(), 1); + assert_eq!(strings.get(&StringId::from("accessor_name=()")).unwrap().ref_count(), 1); assert_eq!(strings.get(&StringId::from("reader_name()")).unwrap().ref_count(), 1); - assert_eq!(strings.get(&StringId::from("writer_name()")).unwrap().ref_count(), 1); + assert!(strings.get(&StringId::from("reader_name=()")).is_none()); + assert_eq!(strings.get(&StringId::from("writer_name=()")).unwrap().ref_count(), 1); + assert!(strings.get(&StringId::from("writer_name()")).is_none()); assert_eq!(strings.get(&StringId::from("$global_var")).unwrap().ref_count(), 1); assert_eq!(strings.get(&StringId::from("@@class_var")).unwrap().ref_count(), 1); assert_eq!(strings.get(&StringId::from("@instance_var")).unwrap().ref_count(), 1); @@ -2332,19 +2642,19 @@ mod tests { "; context.index_uri("file:///foo.rb", source); - assert_eq!(49, context.graph().definitions.len()); + assert_eq!(51, context.graph().definitions.len()); assert_eq!(13, context.graph().constant_references.len()); assert_eq!(2, context.graph().method_references.len()); assert_eq!(2, context.graph().documents.len()); assert_eq!(20, context.graph().names.len()); - assert_eq!(47, context.graph().strings.len()); + assert_eq!(49, context.graph().strings.len()); context.index_uri("file:///foo.rb", source); - assert_eq!(49, context.graph().definitions.len()); + assert_eq!(51, context.graph().definitions.len()); assert_eq!(13, context.graph().constant_references.len()); assert_eq!(2, context.graph().method_references.len()); assert_eq!(2, context.graph().documents.len()); assert_eq!(20, context.graph().names.len()); - assert_eq!(47, context.graph().strings.len()); + assert_eq!(49, context.graph().strings.len()); } #[test] diff --git a/rust/rubydex/src/model/graph/visibility.rs b/rust/rubydex/src/model/graph/visibility.rs new file mode 100644 index 000000000..fadea18a5 --- /dev/null +++ b/rust/rubydex/src/model/graph/visibility.rs @@ -0,0 +1,710 @@ +use std::collections::hash_map::Entry; + +use super::{Graph, Unit}; +use crate::diagnostic::{Diagnostic, Rule}; +use crate::model::declaration::Declaration; +use crate::model::definitions::Definition; +use crate::model::identity_maps::IdentityHashSet; +use crate::model::ids::{DeclarationId, DefinitionId, StringId}; + +#[derive(Default)] +struct VisibilityWork { + seen: IdentityHashSet, + ids: Vec, +} + +impl VisibilityWork { + fn push(&mut self, visibility_definition_id: DefinitionId) { + if self.seen.insert(visibility_definition_id) { + self.ids.push(visibility_definition_id); + } + } + + fn into_vec(self) -> Vec { + self.ids + } +} + +impl Graph { + pub(crate) fn add_method_visibility_declaration( + &mut self, + definition_id: DefinitionId, + owner_id: DeclarationId, + str_id: StringId, + fully_qualified_name: String, + constructor: F, + ) -> DeclarationId + where + F: FnOnce(String) -> Declaration, + { + let declaration_id = + self.add_ordered_declaration(definition_id, definition_id, fully_qualified_name, constructor); + self.applied_method_visibilities.track(owner_id, str_id, definition_id); + declaration_id + } + + pub(crate) fn add_module_function_copy_declaration( + &mut self, + definition_id: DefinitionId, + visibility_definition_id: DefinitionId, + fully_qualified_name: String, + constructor: F, + ) -> DeclarationId + where + F: FnOnce(String) -> Declaration, + { + self.add_ordered_declaration( + definition_id, + visibility_definition_id, + fully_qualified_name, + constructor, + ) + } + + fn add_ordered_declaration( + &mut self, + definition_id: DefinitionId, + ordering_visibility_definition_id: DefinitionId, + fully_qualified_name: String, + constructor: F, + ) -> DeclarationId + where + F: FnOnce(String) -> Declaration, + { + let declaration_id = DeclarationId::from(&fully_qualified_name); + let insert_index = self.method_visibility_insert_index(declaration_id, ordering_visibility_definition_id); + + match self.declarations.entry(declaration_id) { + Entry::Occupied(mut occupied_entry) => { + debug_assert!( + occupied_entry.get().name() == fully_qualified_name, + "DeclarationId collision in global graph" + ); + + if occupied_entry.get().definitions().contains(&definition_id) { + return declaration_id; + } + + if let Some(index) = insert_index { + occupied_entry.get_mut().insert_definition(definition_id, index); + } else { + occupied_entry.get_mut().add_definition(definition_id); + } + } + Entry::Vacant(vacant_entry) => { + let mut declaration = constructor(fully_qualified_name); + declaration.add_definition(definition_id); + vacant_entry.insert(declaration); + } + } + + declaration_id + } + + fn method_visibility_insert_index( + &self, + declaration_id: DeclarationId, + visibility_definition_id: DefinitionId, + ) -> Option { + let visibility_definition = self.definitions.get(&visibility_definition_id)?; + let Definition::MethodVisibility(_) = visibility_definition else { + return None; + }; + + let visibility_uri_id = *visibility_definition.uri_id(); + let visibility_uri = self.documents.get(&visibility_uri_id)?.uri(); + let visibility_offset = visibility_definition.offset(); + + self.declarations + .get(&declaration_id)? + .definitions() + .iter() + .position(|definition_id| { + self.definition_effect_order_key(*definition_id) + .is_some_and(|(definition_uri, definition_offset)| { + // Insert relative to later definitions in the same file only. Cross-file + // URI order is a deterministic resolver approximation, not Ruby load order, + // so declaration storage keeps existing cross-document insertion order. + definition_uri == visibility_uri && visibility_offset.start() < definition_offset.start() + }) + }) + } + + pub(crate) fn add_module_function_copy( + &mut self, + source_definition_id: DefinitionId, + visibility_definition_id: DefinitionId, + copy_declaration_id: DeclarationId, + definition: Definition, + ) -> DefinitionId { + let definition_id = definition.id(); + + if let Some(copy) = self.module_function_copies.get(definition_id) { + debug_assert_eq!( + copy.source_definition_id, source_definition_id, + "Module function copy ID collision with different source definition" + ); + debug_assert_eq!( + copy.declaration_id, copy_declaration_id, + "Module function copy ID collision with different declaration" + ); + self.attach_module_function_copy_visibility(definition_id, visibility_definition_id); + return definition_id; + } + + if self.definitions.contains_key(&definition_id) { + // `module_function def foo; end` already indexes the public singleton copy as + // a real document-owned definition. A later `module_function :foo` can compute + // the same copied method ID. Reuse that definition; its lifetime is still owned + // by the source document, not by the generated-copy tracker. + self.module_function_copies + .attach_document_owned_visibility(source_definition_id, visibility_definition_id); + return definition_id; + } + + self.track_definition_strings(&definition); + self.definitions.insert(definition_id, definition); + self.module_function_copies.insert( + definition_id, + source_definition_id, + visibility_definition_id, + copy_declaration_id, + ); + + definition_id + } + + pub(crate) fn find_module_function_copy( + &self, + source_definition_id: DefinitionId, + copy_declaration_id: DeclarationId, + ) -> Option { + self.module_function_copies + .find(source_definition_id, copy_declaration_id) + } + + pub(crate) fn attach_module_function_copy_visibility( + &mut self, + copy_definition_id: DefinitionId, + visibility_definition_id: DefinitionId, + ) { + self.module_function_copies + .attach_visibility(copy_definition_id, visibility_definition_id); + } + + pub(crate) fn track_module_function_alias_dependency( + &mut self, + alias_definition_id: DefinitionId, + visibility_definition_id: DefinitionId, + ) { + self.module_function_copies + .track_alias_dependent_visibility(alias_definition_id, visibility_definition_id); + } + + pub(crate) fn track_module_function_ancestor_dependency( + &mut self, + owner_id: DeclarationId, + str_id: StringId, + visibility_definition_id: DefinitionId, + ) { + self.module_function_copies + .track_ancestor_dependent_visibility(owner_id, str_id, visibility_definition_id); + } + + pub(crate) fn track_unresolved_method_visibility( + &mut self, + owner_id: DeclarationId, + str_id: StringId, + visibility_definition_id: DefinitionId, + ) { + if let Some(old_owner_id) = + self.unresolved_method_visibilities + .track(owner_id, str_id, visibility_definition_id) + { + self.remove_unresolved_method_visibility_diagnostic(old_owner_id, visibility_definition_id); + } + } + + pub(crate) fn add_unresolved_method_visibility_diagnostic( + &mut self, + owner_id: DeclarationId, + visibility_definition_id: DefinitionId, + diagnostic: Diagnostic, + ) { + self.remove_unresolved_method_visibility_diagnostic(owner_id, visibility_definition_id); + + let Some(Definition::MethodVisibility(visibility_definition)) = self.definitions.get(&visibility_definition_id) + else { + return; + }; + + if visibility_definition.flags().is_singleton_method_visibility() { + // Document-scoped: the singleton class may be synthetic (created by this + // visibility resolution) and won't be cleaned up on file delete, so attaching + // the diagnostic to the declaration would leave it orphaned. + self.add_document_diagnostic(*visibility_definition.uri_id(), diagnostic); + } else if let Some(declaration) = self.declarations.get_mut(&owner_id) { + declaration.add_diagnostic(diagnostic); + } + } + + pub(crate) fn mark_method_visibility_resolved(&mut self, visibility_definition_id: DefinitionId) { + if let Some(owner_id) = self.remove_unresolved_method_visibility(visibility_definition_id) { + self.remove_unresolved_method_visibility_diagnostic(owner_id, visibility_definition_id); + } + } + + pub(crate) fn detach_method_visibility_declaration(&mut self, visibility_definition_id: DefinitionId) { + let Some(Definition::MethodVisibility(_)) = self.definitions.get(&visibility_definition_id) else { + return; + }; + + self.detach_applied_method_visibility(visibility_definition_id); + + if let Some(declaration_id) = self.definition_id_to_declaration_id(visibility_definition_id).copied() { + self.detach_definition_from_declaration(declaration_id, visibility_definition_id); + } + } + + pub(super) fn detach_applied_method_visibility(&mut self, visibility_definition_id: DefinitionId) { + if matches!( + self.definitions.get(&visibility_definition_id), + Some(Definition::MethodVisibility(_)) + ) { + self.applied_method_visibilities.detach(visibility_definition_id); + } + } + + pub(super) fn remove_method_visibility_side_effects( + &mut self, + visibility_definition_id: DefinitionId, + ) -> Vec { + let Some(Definition::MethodVisibility(_)) = self.definitions.get(&visibility_definition_id) else { + return Vec::new(); + }; + + self.applied_method_visibilities.detach(visibility_definition_id); + if let Some(owner_id) = self.remove_unresolved_method_visibility(visibility_definition_id) { + self.remove_unresolved_method_visibility_diagnostic(owner_id, visibility_definition_id); + } + + self.remove_module_function_copy_for_visibility(visibility_definition_id) + } + + pub(super) fn remove_unresolved_method_visibility( + &mut self, + visibility_definition_id: DefinitionId, + ) -> Option { + self.unresolved_method_visibilities.remove(visibility_definition_id) + } + + pub(crate) fn take_unresolved_method_visibilities_for_owner( + &mut self, + owner_id: DeclarationId, + ) -> Vec { + let visibility_ids = self.unresolved_method_visibilities.take_for_owner(owner_id); + + for visibility_id in &visibility_ids { + self.remove_unresolved_method_visibility_diagnostic(owner_id, *visibility_id); + } + + visibility_ids + } + + pub(crate) fn take_unresolved_method_visibilities_for_owner_and_member( + &mut self, + owner_id: DeclarationId, + member_str_id: StringId, + ) -> Vec { + let visibility_ids = self + .unresolved_method_visibilities + .take_for_owner_and_member(owner_id, member_str_id); + + for visibility_id in &visibility_ids { + self.remove_unresolved_method_visibility_diagnostic(owner_id, *visibility_id); + } + + visibility_ids + } + + pub(crate) fn invalidate_method_visibility_dependents_for_member_change( + &mut self, + owner_id: DeclarationId, + member_str_id: StringId, + ) -> Vec { + if !self.has_method_visibility_member_change_dependents() { + return Vec::new(); + } + + let mut visibility_ids = VisibilityWork::default(); + let owner_ids = self.method_visibility_owner_dependents(owner_id); + + self.collect_unresolved_visibility_work_for_member_change(&owner_ids, member_str_id, &mut visibility_ids); + self.collect_applied_visibility_work_for_member_change(&owner_ids, member_str_id, &mut visibility_ids); + self.collect_module_function_copy_work_for_member_change(owner_ids, member_str_id, &mut visibility_ids); + + visibility_ids.into_vec() + } + + fn collect_unresolved_visibility_work_for_member_change( + &mut self, + owner_ids: &[DeclarationId], + member_str_id: StringId, + visibility_ids: &mut VisibilityWork, + ) { + for owner_id in owner_ids { + let unresolved_visibility_ids = + self.take_unresolved_method_visibilities_for_owner_and_member(*owner_id, member_str_id); + self.push_unique_visibility_work(unresolved_visibility_ids, visibility_ids); + } + } + + fn collect_applied_visibility_work_for_member_change( + &mut self, + owner_ids: &[DeclarationId], + member_str_id: StringId, + visibility_ids: &mut VisibilityWork, + ) { + for owner_id in owner_ids { + let member_visibility_ids = self + .applied_method_visibilities + .take_for_owner_and_member(*owner_id, member_str_id); + for visibility_id in member_visibility_ids { + self.detach_method_visibility_declaration(visibility_id); + self.push_existing_visibility_work(visibility_id, visibility_ids); + } + } + } + + fn collect_module_function_copy_work_for_member_change( + &mut self, + owner_ids: Vec, + member_str_id: StringId, + visibility_ids: &mut VisibilityWork, + ) { + for owner_id in owner_ids { + let module_function_visibility_ids = + self.take_module_function_copy_work_for_owner_and_member(owner_id, member_str_id); + self.push_unique_visibility_work(module_function_visibility_ids, visibility_ids); + } + } + + fn has_method_visibility_member_change_dependents(&self) -> bool { + !self.unresolved_method_visibilities.is_empty() + || !self.applied_method_visibilities.is_empty() + || self.module_function_copies.has_ancestor_dependent_visibilities() + } + + pub(super) fn requeue_unresolved_method_visibilities_for_owner(&mut self, owner_id: DeclarationId) { + for visibility_id in self.take_unresolved_method_visibilities_for_owner(owner_id) { + self.push_work(Unit::Definition(visibility_id)); + } + } + + pub(super) fn remove_unresolved_method_visibility_diagnostic( + &mut self, + owner_id: DeclarationId, + visibility_definition_id: DefinitionId, + ) { + let Some(Definition::MethodVisibility(visibility_definition)) = self.definitions.get(&visibility_definition_id) + else { + return; + }; + let uri_id = *visibility_definition.uri_id(); + let offset = visibility_definition.offset().clone(); + + if visibility_definition.flags().is_singleton_method_visibility() { + if let Some(document) = self.documents.get_mut(&uri_id) { + document.retain_diagnostics(|diagnostic| { + diagnostic.rule() != &Rule::UndefinedMethodVisibilityTarget + || diagnostic.uri_id() != &uri_id + || diagnostic.offset() != &offset + }); + } + return; + } + + if let Some(declaration) = self.declarations.get_mut(&owner_id) { + declaration.retain_diagnostics(|diagnostic| { + diagnostic.rule() != &Rule::UndefinedMethodVisibilityTarget + || diagnostic.uri_id() != &uri_id + || diagnostic.offset() != &offset + }); + } + } + + pub(super) fn requeue_method_visibility_definitions_for_declaration(&mut self, declaration_id: DeclarationId) { + let visibility_ids = self.method_visibility_definition_ids_for_declaration(declaration_id); + + for visibility_id in visibility_ids { + self.detach_method_visibility_declaration(visibility_id); + self.push_work(Unit::Definition(visibility_id)); + } + } + + pub(super) fn requeue_method_visibility_definitions_for_owner(&mut self, owner_id: DeclarationId) { + let member_ids = self + .declarations + .get(&owner_id) + .and_then(Declaration::as_namespace) + .map(|namespace| namespace.members().values().copied().collect::>()) + .unwrap_or_default(); + + for member_id in member_ids { + if matches!(self.declarations.get(&member_id), Some(Declaration::Method(_))) { + self.requeue_method_visibility_definitions_for_declaration(member_id); + } + } + } + + pub(super) fn requeue_method_visibility_dependents_for_member_change( + &mut self, + owner_id: DeclarationId, + member_str_id: StringId, + ) { + for visibility_id in self.invalidate_method_visibility_dependents_for_member_change(owner_id, member_str_id) { + self.push_work(Unit::Definition(visibility_id)); + } + } + + fn method_visibility_definition_ids_for_declaration(&self, declaration_id: DeclarationId) -> Vec { + self.declarations + .get(&declaration_id) + .map(|declaration| { + declaration + .definitions() + .iter() + .copied() + .filter(|definition_id| { + matches!( + self.definitions.get(definition_id), + Some(Definition::MethodVisibility(_)) + ) + }) + .collect::>() + }) + .unwrap_or_default() + } + + fn method_visibility_owner_dependents(&self, owner_id: DeclarationId) -> Vec { + let mut seen_owner_ids = IdentityHashSet::default(); + let mut owner_ids = Vec::new(); + Self::push_unique_owner(owner_id, &mut seen_owner_ids, &mut owner_ids); + + // Descendants are updated whenever ancestor linearization changes. During a method-member change, they are the + // namespaces whose inherited visibility declarations and generated module_function copies may depend on this + // owner. + if let Some(namespace) = self.declarations.get(&owner_id).and_then(Declaration::as_namespace) { + for descendant_id in namespace.descendants() { + Self::push_unique_owner(*descendant_id, &mut seen_owner_ids, &mut owner_ids); + } + } + + owner_ids + } + + fn push_unique_owner( + owner_id: DeclarationId, + seen_owner_ids: &mut IdentityHashSet, + owner_ids: &mut Vec, + ) { + if seen_owner_ids.insert(owner_id) { + owner_ids.push(owner_id); + } + } + + fn push_unique_visibility_work( + &self, + source_visibility_ids: Vec, + visibility_ids: &mut VisibilityWork, + ) { + for visibility_id in source_visibility_ids { + self.push_existing_visibility_work(visibility_id, visibility_ids); + } + } + + pub(super) fn remove_module_function_copies_for_source( + &mut self, + source_definition_id: DefinitionId, + ) -> Vec { + let mut visibility_definition_ids = VisibilityWork::default(); + for visibility_id in self + .module_function_copies + .take_document_owned_visibility_ids_for_source(source_definition_id) + { + self.push_existing_visibility_work(visibility_id, &mut visibility_definition_ids); + } + let copy_ids = self.module_function_copies.copy_ids_for_source(source_definition_id); + let mut visited_copy_ids = IdentityHashSet::default(); + + for copy_id in copy_ids { + self.remove_module_function_copy_and_collect_visibility_work( + copy_id, + &mut visibility_definition_ids, + &mut visited_copy_ids, + ); + } + + visibility_definition_ids.into_vec() + } + + pub(super) fn remove_module_function_copies_for_alias( + &mut self, + alias_definition_id: DefinitionId, + ) -> Vec { + let mut visibility_ids = VisibilityWork::default(); + let alias_visibility_ids = self + .module_function_copies + .take_alias_dependent_visibility_ids_for_alias(alias_definition_id); + for visibility_id in &alias_visibility_ids { + self.push_existing_visibility_work(*visibility_id, &mut visibility_ids); + } + + for visibility_id in alias_visibility_ids { + for dependent_visibility_id in self.remove_module_function_copy_for_visibility(visibility_id) { + self.push_existing_visibility_work(dependent_visibility_id, &mut visibility_ids); + } + } + + visibility_ids.into_vec() + } + + pub(super) fn remove_module_function_copy_for_visibility( + &mut self, + visibility_definition_id: DefinitionId, + ) -> Vec { + let mut visibility_definition_ids = VisibilityWork::default(); + let affected_copy_definition_ids = self + .module_function_copies + .copy_ids_for_visibility(visibility_definition_id); + + self.module_function_copies + .detach_document_owned_visibility(visibility_definition_id); + self.module_function_copies + .detach_alias_dependent_visibility(visibility_definition_id); + self.module_function_copies + .detach_ancestor_dependent_visibility(visibility_definition_id); + + let mut visited_copy_ids = IdentityHashSet::default(); + for copy_definition_id in self.module_function_copies.detach_visibility(visibility_definition_id) { + self.remove_module_function_copy_and_collect_visibility_work( + copy_definition_id, + &mut visibility_definition_ids, + &mut visited_copy_ids, + ); + } + + let affected_declaration_ids = affected_copy_definition_ids + .into_iter() + .filter(|copy_definition_id| self.definitions.contains_key(copy_definition_id)) + .filter_map(|copy_definition_id| self.definition_id_to_declaration_id(copy_definition_id).copied()) + .collect::>(); + + for declaration_id in affected_declaration_ids { + self.requeue_method_visibility_definitions_for_declaration(declaration_id); + } + + visibility_definition_ids.into_vec() + } + + pub(super) fn requeue_module_function_copies_for_owner(&mut self, owner_id: DeclarationId) { + for visibility_id in self.take_module_function_copy_work_for_owner(owner_id) { + self.push_work(Unit::Definition(visibility_id)); + } + } + + fn take_module_function_copy_work_for_owner(&mut self, owner_id: DeclarationId) -> Vec { + let visibility_ids = self + .module_function_copies + .take_ancestor_dependent_visibility_ids_for_owner(owner_id); + self.take_module_function_copy_work_for_visibilities(visibility_ids) + } + + fn take_module_function_copy_work_for_owner_and_member( + &mut self, + owner_id: DeclarationId, + member_str_id: StringId, + ) -> Vec { + let visibility_ids = self + .module_function_copies + .take_ancestor_dependent_visibility_ids_for_owner_and_member(owner_id, member_str_id); + + self.take_module_function_copy_work_for_visibilities(visibility_ids) + } + + fn take_module_function_copy_work_for_visibilities( + &mut self, + visibility_ids: Vec, + ) -> Vec { + let mut visibility_ids_to_requeue = VisibilityWork::default(); + + for visibility_id in visibility_ids { + self.module_function_copies + .detach_alias_dependent_visibility(visibility_id); + let mut visited_copy_ids = IdentityHashSet::default(); + for copy_definition_id in self.module_function_copies.detach_visibility(visibility_id) { + self.remove_module_function_copy_and_collect_visibility_work( + copy_definition_id, + &mut visibility_ids_to_requeue, + &mut visited_copy_ids, + ); + } + + self.push_existing_visibility_work(visibility_id, &mut visibility_ids_to_requeue); + } + + visibility_ids_to_requeue.into_vec() + } + + fn remove_module_function_copy_and_collect_visibility_work( + &mut self, + copy_definition_id: DefinitionId, + visibility_definition_ids: &mut VisibilityWork, + visited_copy_ids: &mut IdentityHashSet, + ) { + if !visited_copy_ids.insert(copy_definition_id) { + return; + } + + let dependent_copy_ids = self.module_function_copies.copy_ids_for_source(copy_definition_id); + for dependent_copy_id in dependent_copy_ids { + self.remove_module_function_copy_and_collect_visibility_work( + dependent_copy_id, + visibility_definition_ids, + visited_copy_ids, + ); + } + + for visibility_id in self.remove_module_function_copy(copy_definition_id) { + self.module_function_copies + .detach_alias_dependent_visibility(visibility_id); + self.module_function_copies + .detach_ancestor_dependent_visibility(visibility_id); + self.push_existing_visibility_work(visibility_id, visibility_definition_ids); + } + } + + fn push_existing_visibility_work( + &self, + visibility_definition_id: DefinitionId, + visibility_definition_ids: &mut VisibilityWork, + ) { + if self.definitions.contains_key(&visibility_definition_id) { + visibility_definition_ids.push(visibility_definition_id); + } + } + + fn remove_module_function_copy(&mut self, copy_definition_id: DefinitionId) -> Vec { + let Some(copy) = self.module_function_copies.remove(copy_definition_id) else { + return Vec::new(); + }; + + self.detach_definition_from_declaration(copy.declaration_id, copy_definition_id); + + if let Some(definition) = self.definitions.remove(©_definition_id) { + self.untrack_definition_strings(&definition); + } + + copy.trigger_definition_ids + } +} diff --git a/rust/rubydex/src/model/graph_indexes.rs b/rust/rubydex/src/model/graph_indexes.rs new file mode 100644 index 000000000..a5bff6556 --- /dev/null +++ b/rust/rubydex/src/model/graph_indexes.rs @@ -0,0 +1,695 @@ +use std::{ + collections::hash_map::Entry, + hash::{Hash, Hasher}, +}; + +#[cfg(test)] +use crate::model::definitions::Definition; +use crate::model::identity_maps::IdentityHashMap; +use crate::model::ids::{DeclarationId, DefinitionId, StringId}; + +fn push_unique(values: &mut Vec, value: T) { + if !values.contains(&value) { + values.push(value); + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct MethodVisibilityDependencyKey { + owner_id: DeclarationId, + str_id: StringId, +} + +impl MethodVisibilityDependencyKey { + fn new(owner_id: DeclarationId, str_id: StringId) -> Self { + Self { owner_id, str_id } + } +} + +impl Hash for MethodVisibilityDependencyKey { + fn hash(&self, state: &mut H) { + let owner = self.owner_id.get(); + let name = self.str_id.get(); + state.write_u64(owner.rotate_left(17) ^ name.rotate_right(11)); + } +} + +/// Small bidirectional multi-map for feature-local reverse indexes. +/// +/// Values are stored in `Vec`s because the expected fanout is tiny: a method visibility usually depends on one alias, +/// one owner, or one source method. If these relationships become high-cardinality, switch the lists to sets. +/// +/// This uses `IdentityHashMap`, so keys and values must hash through a single `write_u64`/`write_u32` path. The current +/// users are graph IDs and `MethodVisibilityDependencyKey`, which implements that explicitly. +#[derive(Debug)] +struct BiMultiMap { + forward: IdentityHashMap>, + reverse: IdentityHashMap>, +} + +impl Default for BiMultiMap { + fn default() -> Self { + Self { + forward: IdentityHashMap::default(), + reverse: IdentityHashMap::default(), + } + } +} + +impl BiMultiMap +where + K: Copy + Eq + Hash, + V: Copy + Eq + Hash, +{ + fn insert(&mut self, key: K, value: V) { + push_unique(self.forward.entry(key).or_default(), value); + push_unique(self.reverse.entry(value).or_default(), key); + } + + fn remove_key(&mut self, key: K) -> Vec { + let values = self.forward.remove(&key).unwrap_or_default(); + + for value in &values { + if let Some(keys) = self.reverse.get_mut(value) { + keys.retain(|id| *id != key); + if keys.is_empty() { + self.reverse.remove(value); + } + } + } + + values + } + + fn remove_value(&mut self, value: V) -> Vec { + let keys = self.reverse.remove(&value).unwrap_or_default(); + + for key in &keys { + if let Some(values) = self.forward.get_mut(key) { + values.retain(|id| *id != value); + if values.is_empty() { + self.forward.remove(key); + } + } + } + + keys + } + + fn is_empty(&self) -> bool { + self.forward.is_empty() + } + + fn debug_assert_consistent(&self) { + #[cfg(debug_assertions)] + { + for (key, values) in &self.forward { + for value in values { + debug_assert!( + self.reverse.get(value).is_some_and(|keys| keys.contains(key)), + "BiMultiMap missing reverse edge" + ); + } + } + + for (value, keys) in &self.reverse { + for key in keys { + debug_assert!( + self.forward.get(key).is_some_and(|values| values.contains(value)), + "BiMultiMap missing forward edge" + ); + } + } + } + } + + #[cfg(test)] + fn values(&self) -> impl Iterator + '_ { + self.reverse.keys().copied() + } +} + +#[derive(Debug)] +pub(crate) struct GeneratedMethodCopy { + pub(crate) source_definition_id: DefinitionId, + pub(crate) trigger_definition_ids: Vec, + pub(crate) declaration_id: DeclarationId, +} + +/// Generated method copies keyed by generated definition id, source definition id, and trigger definition id. +/// +/// This is generic enough for copied-method lifecycle tracking, but currently only `module_function :name` uses it. +#[derive(Default, Debug)] +struct GeneratedMethodCopies { + definitions: IdentityHashMap, + by_source: IdentityHashMap>, + by_trigger: IdentityHashMap>, +} + +impl GeneratedMethodCopies { + fn get(&self, generated_definition_id: DefinitionId) -> Option<&GeneratedMethodCopy> { + self.definitions.get(&generated_definition_id) + } + + fn find(&self, source_definition_id: DefinitionId, declaration_id: DeclarationId) -> Option { + self.by_source + .get(&source_definition_id)? + .iter() + .copied() + .find(|generated_definition_id| { + self.definitions + .get(generated_definition_id) + .is_some_and(|definition| definition.declaration_id == declaration_id) + }) + } + + fn insert( + &mut self, + generated_definition_id: DefinitionId, + source_definition_id: DefinitionId, + trigger_definition_id: DefinitionId, + declaration_id: DeclarationId, + ) { + match self.definitions.entry(generated_definition_id) { + Entry::Occupied(_) => panic!("generated definition inserted twice"), + Entry::Vacant(entry) => { + entry.insert(GeneratedMethodCopy { + source_definition_id, + trigger_definition_ids: Vec::new(), + declaration_id, + }); + } + } + + let generated_ids = self.by_source.entry(source_definition_id).or_default(); + push_unique(generated_ids, generated_definition_id); + + self.attach_trigger(generated_definition_id, trigger_definition_id); + } + + fn attach_trigger(&mut self, generated_definition_id: DefinitionId, trigger_definition_id: DefinitionId) { + let definition = self + .definitions + .get_mut(&generated_definition_id) + .expect("generated definition should exist before attaching trigger"); + + push_unique(&mut definition.trigger_definition_ids, trigger_definition_id); + + let generated_ids = self.by_trigger.entry(trigger_definition_id).or_default(); + push_unique(generated_ids, generated_definition_id); + } + + fn ids_for_source(&self, source_definition_id: DefinitionId) -> Vec { + self.by_source.get(&source_definition_id).cloned().unwrap_or_default() + } + + fn ids_for_trigger(&self, trigger_definition_id: DefinitionId) -> Vec { + self.by_trigger.get(&trigger_definition_id).cloned().unwrap_or_default() + } + + fn detach_trigger(&mut self, trigger_definition_id: DefinitionId) -> Vec { + let Some(generated_definition_ids) = self.by_trigger.remove(&trigger_definition_id) else { + return Vec::new(); + }; + + generated_definition_ids + .into_iter() + .filter(|generated_definition_id| { + if let Some(definition) = self.definitions.get_mut(generated_definition_id) { + definition + .trigger_definition_ids + .retain(|id| *id != trigger_definition_id); + definition.trigger_definition_ids.is_empty() + } else { + false + } + }) + .collect() + } + + fn remove(&mut self, generated_definition_id: DefinitionId) -> Option { + let definition = self.definitions.remove(&generated_definition_id)?; + + for trigger_id in &definition.trigger_definition_ids { + if let Some(generated_ids) = self.by_trigger.get_mut(trigger_id) { + generated_ids.retain(|id| *id != generated_definition_id); + if generated_ids.is_empty() { + self.by_trigger.remove(trigger_id); + } + } + } + + let remove_source_entry = if let Some(generated_ids) = self.by_source.get_mut(&definition.source_definition_id) + { + generated_ids.retain(|id| *id != generated_definition_id); + generated_ids.is_empty() + } else { + false + }; + if remove_source_entry { + self.by_source.remove(&definition.source_definition_id); + } + + Some(definition) + } + + fn debug_assert_consistent(&self) { + #[cfg(debug_assertions)] + { + for (generated_id, definition) in &self.definitions { + debug_assert!( + self.by_source + .get(&definition.source_definition_id) + .is_some_and(|ids| ids.contains(generated_id)), + "generated definition missing source reverse edge" + ); + + for trigger_id in &definition.trigger_definition_ids { + debug_assert!( + self.by_trigger + .get(trigger_id) + .is_some_and(|ids| ids.contains(generated_id)), + "generated definition missing trigger reverse edge" + ); + } + } + + for (source_id, generated_ids) in &self.by_source { + for (idx, generated_id) in generated_ids.iter().enumerate() { + debug_assert!( + !generated_ids[..idx].contains(generated_id), + "source reverse edge contains duplicate generated definition" + ); + debug_assert!( + self.definitions + .get(generated_id) + .is_some_and(|definition| definition.source_definition_id == *source_id), + "source reverse edge points to missing or mismatched generated definition" + ); + } + } + + for (trigger_id, generated_ids) in &self.by_trigger { + for (idx, generated_id) in generated_ids.iter().enumerate() { + debug_assert!( + !generated_ids[..idx].contains(generated_id), + "trigger reverse edge contains duplicate generated definition" + ); + debug_assert!( + self.definitions + .get(generated_id) + .is_some_and(|definition| definition.trigger_definition_ids.contains(trigger_id)), + "trigger reverse edge points to missing or mismatched generated definition" + ); + } + } + } + } +} + +/// Reverse indexes for generated methods created by retroactive `module_function :name`. +/// +/// The generated definition's source location belongs to the copied source method, while its lifetime depends on both +/// the source definition and the visibility definition that requested the copy. +#[derive(Default, Debug)] +pub(crate) struct ModuleFunctionCopies { + generated: GeneratedMethodCopies, + alias_dependent_visibilities: BiMultiMap, + ancestor_dependent_visibilities: BiMultiMap, + ancestor_dependent_visibilities_by_member: BiMultiMap, + document_owned_visibilities: BiMultiMap, +} + +impl ModuleFunctionCopies { + pub(crate) fn get(&self, copy_definition_id: DefinitionId) -> Option<&GeneratedMethodCopy> { + self.generated.get(copy_definition_id) + } + + pub(crate) fn find( + &self, + source_definition_id: DefinitionId, + copy_declaration_id: DeclarationId, + ) -> Option { + self.generated.find(source_definition_id, copy_declaration_id) + } + + pub(crate) fn insert( + &mut self, + copy_definition_id: DefinitionId, + source_definition_id: DefinitionId, + visibility_definition_id: DefinitionId, + copy_declaration_id: DeclarationId, + ) { + self.generated.insert( + copy_definition_id, + source_definition_id, + visibility_definition_id, + copy_declaration_id, + ); + } + + pub(crate) fn attach_visibility( + &mut self, + copy_definition_id: DefinitionId, + visibility_definition_id: DefinitionId, + ) { + self.generated + .attach_trigger(copy_definition_id, visibility_definition_id); + } + + pub(crate) fn track_alias_dependent_visibility( + &mut self, + alias_definition_id: DefinitionId, + visibility_definition_id: DefinitionId, + ) { + self.alias_dependent_visibilities + .insert(alias_definition_id, visibility_definition_id); + self.debug_assert_consistent(); + } + + pub(crate) fn take_alias_dependent_visibility_ids_for_alias( + &mut self, + alias_definition_id: DefinitionId, + ) -> Vec { + let visibility_ids = self.alias_dependent_visibilities.remove_key(alias_definition_id); + self.debug_assert_consistent(); + visibility_ids + } + + pub(crate) fn detach_alias_dependent_visibility(&mut self, visibility_definition_id: DefinitionId) { + self.alias_dependent_visibilities.remove_value(visibility_definition_id); + self.debug_assert_consistent(); + } + + pub(crate) fn track_ancestor_dependent_visibility( + &mut self, + owner_id: DeclarationId, + str_id: StringId, + visibility_definition_id: DefinitionId, + ) { + self.ancestor_dependent_visibilities + .insert(owner_id, visibility_definition_id); + self.ancestor_dependent_visibilities_by_member.insert( + MethodVisibilityDependencyKey::new(owner_id, str_id), + visibility_definition_id, + ); + self.debug_assert_consistent(); + } + + pub(crate) fn take_ancestor_dependent_visibility_ids_for_owner( + &mut self, + owner_id: DeclarationId, + ) -> Vec { + let visibility_ids = self.ancestor_dependent_visibilities.remove_key(owner_id); + for visibility_id in &visibility_ids { + self.ancestor_dependent_visibilities_by_member + .remove_value(*visibility_id); + } + self.debug_assert_consistent(); + visibility_ids + } + + pub(crate) fn take_ancestor_dependent_visibility_ids_for_owner_and_member( + &mut self, + owner_id: DeclarationId, + str_id: StringId, + ) -> Vec { + let visibility_ids = self + .ancestor_dependent_visibilities_by_member + .remove_key(MethodVisibilityDependencyKey::new(owner_id, str_id)); + for visibility_id in &visibility_ids { + self.ancestor_dependent_visibilities.remove_value(*visibility_id); + } + self.debug_assert_consistent(); + visibility_ids + } + + pub(crate) fn detach_ancestor_dependent_visibility(&mut self, visibility_definition_id: DefinitionId) { + self.ancestor_dependent_visibilities + .remove_value(visibility_definition_id); + self.ancestor_dependent_visibilities_by_member + .remove_value(visibility_definition_id); + self.debug_assert_consistent(); + } + + pub(crate) fn attach_document_owned_visibility( + &mut self, + source_definition_id: DefinitionId, + visibility_definition_id: DefinitionId, + ) { + self.document_owned_visibilities + .insert(source_definition_id, visibility_definition_id); + self.debug_assert_consistent(); + } + + pub(crate) fn copy_ids_for_source(&self, source_definition_id: DefinitionId) -> Vec { + self.generated.ids_for_source(source_definition_id) + } + + pub(crate) fn copy_ids_for_visibility(&self, visibility_definition_id: DefinitionId) -> Vec { + self.generated.ids_for_trigger(visibility_definition_id) + } + + pub(crate) fn take_document_owned_visibility_ids_for_source( + &mut self, + source_definition_id: DefinitionId, + ) -> Vec { + let visibility_ids = self.document_owned_visibilities.remove_key(source_definition_id); + self.debug_assert_consistent(); + visibility_ids + } + + pub(crate) fn detach_visibility(&mut self, visibility_definition_id: DefinitionId) -> Vec { + let copy_definition_ids = self.generated.detach_trigger(visibility_definition_id); + self.debug_assert_consistent(); + copy_definition_ids + } + + pub(crate) fn detach_document_owned_visibility(&mut self, visibility_definition_id: DefinitionId) { + self.document_owned_visibilities.remove_value(visibility_definition_id); + self.debug_assert_consistent(); + } + + pub(crate) fn remove(&mut self, copy_definition_id: DefinitionId) -> Option { + let definition = self.generated.remove(copy_definition_id); + self.debug_assert_consistent(); + definition + } + + pub(crate) fn has_ancestor_dependent_visibilities(&self) -> bool { + !self.ancestor_dependent_visibilities.is_empty() + } + + fn debug_assert_consistent(&self) { + self.generated.debug_assert_consistent(); + self.alias_dependent_visibilities.debug_assert_consistent(); + self.ancestor_dependent_visibilities.debug_assert_consistent(); + self.ancestor_dependent_visibilities_by_member.debug_assert_consistent(); + self.document_owned_visibilities.debug_assert_consistent(); + } +} + +#[derive(Default, Debug)] +pub(crate) struct AppliedMethodVisibilities { + by_owner_and_member: BiMultiMap, +} + +impl AppliedMethodVisibilities { + pub(crate) fn track(&mut self, owner_id: DeclarationId, str_id: StringId, visibility_definition_id: DefinitionId) { + self.by_owner_and_member.insert( + MethodVisibilityDependencyKey::new(owner_id, str_id), + visibility_definition_id, + ); + self.debug_assert_consistent(); + } + + pub(crate) fn take_for_owner_and_member(&mut self, owner_id: DeclarationId, str_id: StringId) -> Vec { + let visibility_ids = self + .by_owner_and_member + .remove_key(MethodVisibilityDependencyKey::new(owner_id, str_id)); + self.debug_assert_consistent(); + visibility_ids + } + + pub(crate) fn detach(&mut self, visibility_definition_id: DefinitionId) { + self.by_owner_and_member.remove_value(visibility_definition_id); + self.debug_assert_consistent(); + } + + pub(crate) fn is_empty(&self) -> bool { + self.by_owner_and_member.is_empty() + } + + fn debug_assert_consistent(&self) { + self.by_owner_and_member.debug_assert_consistent(); + } + + #[cfg(test)] + pub(crate) fn assert_visibility_definitions_exist(&self, definitions: &IdentityHashMap) { + for visibility_id in self.by_owner_and_member.values() { + assert!( + matches!(definitions.get(&visibility_id), Some(Definition::MethodVisibility(_))), + "applied method visibility index references a missing or non-visibility definition" + ); + } + } +} + +/// Reverse index for method visibility definitions that failed because their target did not exist yet. +/// +/// These are retried when the owning namespace or one of its ancestors changes, because a previously missing inherited +/// method can become available incrementally. +#[derive(Default, Debug)] +pub(crate) struct UnresolvedMethodVisibilities { + by_owner: IdentityHashMap>, + by_owner_and_member: IdentityHashMap>, + owner_by_visibility: IdentityHashMap, +} + +impl UnresolvedMethodVisibilities { + pub(crate) fn track( + &mut self, + owner_id: DeclarationId, + str_id: StringId, + visibility_definition_id: DefinitionId, + ) -> Option { + let key = MethodVisibilityDependencyKey::new(owner_id, str_id); + let old_key = self + .owner_by_visibility + .insert(visibility_definition_id, key) + .filter(|old_key| *old_key != key); + + if let Some(old_key) = old_key { + Self::remove_from_owner_map(&mut self.by_owner, old_key.owner_id, visibility_definition_id); + Self::remove_from_owner_and_member_map(&mut self.by_owner_and_member, old_key, visibility_definition_id); + } + + let visibility_ids = self.by_owner.entry(owner_id).or_default(); + push_unique(visibility_ids, visibility_definition_id); + + let visibility_ids = self.by_owner_and_member.entry(key).or_default(); + push_unique(visibility_ids, visibility_definition_id); + + self.debug_assert_consistent(); + old_key.map(|key| key.owner_id) + } + + pub(crate) fn remove(&mut self, visibility_definition_id: DefinitionId) -> Option { + let key = self.owner_by_visibility.remove(&visibility_definition_id)?; + + Self::remove_from_owner_map(&mut self.by_owner, key.owner_id, visibility_definition_id); + Self::remove_from_owner_and_member_map(&mut self.by_owner_and_member, key, visibility_definition_id); + + self.debug_assert_consistent(); + Some(key.owner_id) + } + + pub(crate) fn take_for_owner(&mut self, owner_id: DeclarationId) -> Vec { + let visibility_ids = self.by_owner.remove(&owner_id).unwrap_or_default(); + + for visibility_id in &visibility_ids { + if let Some(key) = self.owner_by_visibility.remove(visibility_id) { + Self::remove_from_owner_and_member_map(&mut self.by_owner_and_member, key, *visibility_id); + } + } + + self.debug_assert_consistent(); + visibility_ids + } + + pub(crate) fn take_for_owner_and_member(&mut self, owner_id: DeclarationId, str_id: StringId) -> Vec { + let key = MethodVisibilityDependencyKey::new(owner_id, str_id); + let visibility_ids = self.by_owner_and_member.remove(&key).unwrap_or_default(); + + for visibility_id in &visibility_ids { + if self.owner_by_visibility.remove(visibility_id).is_some() { + Self::remove_from_owner_map(&mut self.by_owner, owner_id, *visibility_id); + } + } + + self.debug_assert_consistent(); + visibility_ids + } + + fn remove_from_owner_map( + by_owner: &mut IdentityHashMap>, + owner_id: DeclarationId, + visibility_definition_id: DefinitionId, + ) { + if let Some(visibility_ids) = by_owner.get_mut(&owner_id) { + visibility_ids.retain(|id| *id != visibility_definition_id); + if visibility_ids.is_empty() { + by_owner.remove(&owner_id); + } + } + } + + fn remove_from_owner_and_member_map( + by_owner_and_member: &mut IdentityHashMap>, + key: MethodVisibilityDependencyKey, + visibility_definition_id: DefinitionId, + ) { + if let Some(visibility_ids) = by_owner_and_member.get_mut(&key) { + visibility_ids.retain(|id| *id != visibility_definition_id); + if visibility_ids.is_empty() { + by_owner_and_member.remove(&key); + } + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.by_owner.is_empty() + } + + fn debug_assert_consistent(&self) { + #[cfg(debug_assertions)] + { + for (owner_id, visibility_ids) in &self.by_owner { + for visibility_id in visibility_ids { + debug_assert!( + self.owner_by_visibility + .get(visibility_id) + .is_some_and(|key| key.owner_id == *owner_id), + "unresolved visibility missing owner reverse edge" + ); + } + } + + for (key, visibility_ids) in &self.by_owner_and_member { + for visibility_id in visibility_ids { + debug_assert!( + self.owner_by_visibility + .get(visibility_id) + .is_some_and(|stored_key| stored_key == key), + "unresolved visibility missing owner/member reverse edge" + ); + } + } + + for (visibility_id, key) in &self.owner_by_visibility { + debug_assert!( + self.by_owner + .get(&key.owner_id) + .is_some_and(|visibility_ids| visibility_ids.contains(visibility_id)), + "unresolved visibility missing owner forward edge" + ); + debug_assert!( + self.by_owner_and_member + .get(key) + .is_some_and(|visibility_ids| visibility_ids.contains(visibility_id)), + "unresolved visibility missing owner/member forward edge" + ); + } + } + } + + #[cfg(test)] + pub(crate) fn assert_visibility_definitions_exist(&self, definitions: &IdentityHashMap) { + for visibility_id in self.owner_by_visibility.keys() { + assert!( + matches!(definitions.get(visibility_id), Some(Definition::MethodVisibility(_))), + "unresolved method visibility index references a missing or non-visibility definition" + ); + } + } +} diff --git a/rust/rubydex/src/model/string_ref.rs b/rust/rubydex/src/model/string_ref.rs index b93c0a609..9a1b91245 100644 --- a/rust/rubydex/src/model/string_ref.rs +++ b/rust/rubydex/src/model/string_ref.rs @@ -18,6 +18,14 @@ impl StringRef { Self { value, ref_count: 1 } } + /// Creates a string entry that has been interned but is not yet owned by a tracked graph object. + /// + /// Callers must increment the ref count when the object that uses this string is inserted into the graph. + #[must_use] + pub fn new_pending(value: String) -> Self { + Self { value, ref_count: 0 } + } + #[must_use] pub fn ref_count(&self) -> u32 { self.ref_count diff --git a/rust/rubydex/src/resolution.rs b/rust/rubydex/src/resolution.rs index 47015b068..bd6f24e46 100644 --- a/rust/rubydex/src/resolution.rs +++ b/rust/rubydex/src/resolution.rs @@ -18,6 +18,8 @@ use crate::model::{ name::{Name, NameRef, ParentScope}, }; +mod method_visibility; + enum Outcome { /// The constant was successfully resolved to the given declaration ID. The second optional tuple element is a /// declaration that still needs to have its ancestors linearized @@ -620,107 +622,6 @@ impl<'a> Resolver<'a> { self.resolve_method_visibilities(method_visibility_ids); } - /// Resolves retroactive method visibility changes (`private :foo`, `protected :foo`, `public :foo`, - /// `private_class_method :foo`, `public_class_method :foo`). - /// - /// Runs as a second pass after all methods/attrs are declared, so `private :bar` works - /// regardless of whether `def bar` appeared before or after it in source. - fn resolve_method_visibilities(&mut self, visibility_ids: Vec) { - let mut pending_work = Vec::new(); - - for id in visibility_ids { - let Definition::MethodVisibility(method_visibility) = self.graph.definitions().get(&id).unwrap() else { - unreachable!() - }; - - let str_id = *method_visibility.str_id(); - let uri_id = *method_visibility.uri_id(); - let offset = method_visibility.offset().clone(); - let lexical_nesting_id = *method_visibility.lexical_nesting_id(); - let is_singleton = method_visibility.flags().is_singleton_method_visibility(); - - let Some(lexical_owner_id) = self.resolve_lexical_owner(lexical_nesting_id, id) else { - continue; - }; - - let owner_id = if is_singleton { - let Some(singleton_id) = self.get_or_create_singleton_class(lexical_owner_id, true) else { - continue; - }; - singleton_id - } else { - lexical_owner_id - }; - - let Some(Declaration::Namespace(namespace)) = self.graph.declarations().get(&owner_id) else { - continue; - }; - - let mut visibility_applied = false; - let mut has_partial = false; - - for ancestor in namespace.ancestors() { - match ancestor { - Ancestor::Complete(ancestor_id) => { - let has_member = self - .graph - .declarations() - .get(ancestor_id) - .and_then(|decl| decl.as_namespace()) - .and_then(|ns| ns.member(&str_id)) - .is_some(); - - if has_member { - // Direct member: `create_declaration`'s fully qualified name dedup attaches - // this visibility definition to the existing method declaration. - // Inherited: a new child-owned declaration is created. - self.create_declaration(str_id, id, owner_id, |name| { - Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id))) - }); - visibility_applied = true; - break; - } - } - Ancestor::Partial(_) => has_partial = true, - } - } - - if visibility_applied { - continue; - } - - if has_partial { - // Method might exist on an unresolved ancestor — requeue for retry. - pending_work.push(Unit::Definition(id)); - } else { - // Ancestors are fully resolved — method definitively doesn't exist. - let method_name = self.graph.strings().get(&str_id).unwrap().as_str().to_string(); - let owner_name = self.graph.declarations().get(&owner_id).unwrap().name().to_string(); - let diagnostic = Diagnostic::new( - Rule::UndefinedMethodVisibilityTarget, - uri_id, - offset, - format!("undefined method `{owner_name}#{method_name}` for visibility change"), - ); - if is_singleton { - // Document-scoped: the singleton class may be synthetic (created by this - // visibility resolution) and won't be cleaned up on file delete, so attaching - // the diagnostic to the declaration would leave it orphaned. - self.graph.add_document_diagnostic(uri_id, diagnostic); - } else { - self.graph - .declarations_mut() - .get_mut(&owner_id) - .unwrap() - .add_diagnostic(diagnostic); - } - } - } - - // Must extend work here so incremental resolution can resolve previously unresolved visibility operations - self.graph.extend_work(pending_work); - } - /// Resolves a constant receiver for `handle_remaining_definitions`. /// If the receiver name is unresolved, preserve the definition for a later /// resolve cycle instead of dropping work during an incremental delete/re-add gap. @@ -743,16 +644,33 @@ impl<'a> Resolver<'a> { ) where F: FnOnce(String) -> Declaration, { - let fully_qualified_name = { - let owner = self.graph.declarations().get(&owner_id).unwrap(); - let name_str = self.graph.strings().get(&str_id).unwrap(); - format!("{}#{}", owner.name(), name_str.as_str()) - }; + let fully_qualified_name = self.member_fully_qualified_name(owner_id, str_id); let declaration_id = self .graph .add_declaration(definition_id, fully_qualified_name, declaration_builder); self.graph.add_member(&owner_id, declaration_id, str_id); + + if matches!( + self.graph.declarations().get(&declaration_id), + Some(Declaration::Method(_)) + ) { + // A newly-created method can satisfy unresolved visibility calls or replace an inherited + // `module_function` copy on descendant namespaces. Resolve those dependents immediately + // so diagnostics and generated copies converge in the same resolution cycle. + let visibility_ids = self + .graph + .invalidate_method_visibility_dependents_for_member_change(owner_id, str_id); + if !visibility_ids.is_empty() { + self.resolve_method_visibilities(visibility_ids); + } + } + } + + fn member_fully_qualified_name(&self, owner_id: DeclarationId, str_id: StringId) -> String { + let owner = self.graph.declarations().get(&owner_id).unwrap(); + let name_str = self.graph.strings().get(&str_id).unwrap(); + format!("{}#{}", owner.name(), name_str.as_str()) } /// Resolves owner for class variables, bypassing singleton classes. Returns `None` if the owner can't be diff --git a/rust/rubydex/src/resolution/method_visibility.rs b/rust/rubydex/src/resolution/method_visibility.rs new file mode 100644 index 000000000..e70ff42c0 --- /dev/null +++ b/rust/rubydex/src/resolution/method_visibility.rs @@ -0,0 +1,242 @@ +//! Resolver-side semantic effects for Ruby method visibility calls. +//! +//! The indexer records source constructs first. This pass then applies Ruby's +//! retroactive visibility operations (`private :foo`, `module_function :foo`, +//! and class-method variants) to declarations, including the generated method +//! copies required by `module_function`. + +mod module_function; +mod order; + +use module_function::ModuleFunctionCopySource; +use order::DefinitionOrderCache; + +use super::Resolver; +use crate::diagnostic::{Diagnostic, Rule}; +use crate::model::{ + declaration::{Ancestor, Declaration, MethodDeclaration}, + definitions::Definition, + graph::{DefinitionProgramOrderKey, Unit}, + identity_maps::IdentityHashSet, + ids::{DeclarationId, DefinitionId, StringId}, + visibility::Visibility, +}; + +enum MethodVisibilityResolution { + ApplyExisting, + ApplyModuleFunctionCopy(ModuleFunctionCopySource), + RetryPartial, + Missing, + Skip, +} + +impl Resolver<'_> { + /// Resolves retroactive method visibility changes (`private :foo`, `protected :foo`, `public :foo`, + /// `private_class_method :foo`, `public_class_method :foo`). + /// + /// Runs as a second pass after all methods/attrs are declared. Visibility calls with method-name arguments require + /// a method declaration that already exists at the call site. `module_function :bar` also copies that method + /// snapshot into a public singleton method. + pub(super) fn resolve_method_visibilities(&mut self, mut visibility_ids: Vec) { + let mut pending_work = Vec::new(); + // This cache is intentionally scoped to one visibility-resolution pass. + // Visibility definitions are processed in static program order, and each + // lookup asks for the current visibility site or an earlier alias site. + // That means a successful application cannot invalidate a previously + // cached future-position lookup during this pass. + let mut definition_order_cache = DefinitionOrderCache::default(); + let ordered_visibility_ids = { + let mut seen_visibility_ids = IdentityHashSet::default(); + let mut ordered_visibility_ids = visibility_ids + .drain(..) + .filter(|id| seen_visibility_ids.insert(*id)) + .filter_map(|id| { + let (uri, offset) = self.graph.definition_program_order_key(id)?; + Some((id, uri, offset)) + }) + .collect::>(); + + ordered_visibility_ids.sort_unstable_by( + |(left_id, left_uri, left_offset), (right_id, right_uri, right_offset)| { + DefinitionProgramOrderKey::new(*left_id, left_uri, left_offset) + .cmp(&DefinitionProgramOrderKey::new(*right_id, right_uri, right_offset)) + }, + ); + + ordered_visibility_ids + .into_iter() + .map(|(id, _, _)| id) + .collect::>() + }; + + for id in ordered_visibility_ids { + let Definition::MethodVisibility(method_visibility) = self.graph.definitions().get(&id).unwrap() else { + unreachable!() + }; + + let str_id = *method_visibility.str_id(); + let uri_id = *method_visibility.uri_id(); + let offset = method_visibility.offset().clone(); + let lexical_nesting_id = *method_visibility.lexical_nesting_id(); + let is_singleton = method_visibility.flags().is_singleton_method_visibility(); + let visibility = *method_visibility.visibility(); + + let Some(lexical_owner_id) = self.resolve_lexical_owner(lexical_nesting_id, id) else { + continue; + }; + + let owner_id = if is_singleton { + let Some(singleton_id) = self.get_or_create_singleton_class(lexical_owner_id, true) else { + continue; + }; + singleton_id + } else { + lexical_owner_id + }; + + let resolution = + self.method_visibility_resolution(owner_id, str_id, visibility, id, &mut definition_order_cache); + match resolution { + MethodVisibilityResolution::ApplyExisting => { + self.graph.mark_method_visibility_resolved(id); + self.create_method_visibility_declaration_for_owner(str_id, id, owner_id); + continue; + } + MethodVisibilityResolution::ApplyModuleFunctionCopy(source) => { + if self.apply_module_function(str_id, id, owner_id, source) { + self.graph.mark_method_visibility_resolved(id); + continue; + } + } + MethodVisibilityResolution::RetryPartial => { + self.graph.detach_method_visibility_declaration(id); + // Method might exist on an unresolved ancestor — requeue for retry. + pending_work.push(Unit::Definition(id)); + continue; + } + MethodVisibilityResolution::Missing => {} + MethodVisibilityResolution::Skip => continue, + } + + // Ancestors are fully resolved — method definitively doesn't exist. + self.graph.detach_method_visibility_declaration(id); + self.graph.track_unresolved_method_visibility(owner_id, str_id, id); + + let method_name = self.graph.strings().get(&str_id).unwrap().as_str().to_string(); + let owner_name = self.graph.declarations().get(&owner_id).unwrap().name().to_string(); + let diagnostic = Diagnostic::new( + Rule::UndefinedMethodVisibilityTarget, + uri_id, + offset, + format!("undefined method `{owner_name}#{method_name}` for visibility change"), + ); + self.graph + .add_unresolved_method_visibility_diagnostic(owner_id, id, diagnostic); + } + + // Must extend work here so incremental resolution can resolve previously unresolved visibility operations + self.graph.extend_work(pending_work); + } + + pub(in crate::resolution::method_visibility) fn create_method_visibility_declaration_for_owner( + &mut self, + str_id: StringId, + visibility_id: DefinitionId, + owner_id: DeclarationId, + ) { + // Direct member: `create_declaration`'s fully qualified name dedup attaches + // this visibility definition to the existing method declaration. + // Inherited: a new child-owned declaration is created. + self.create_method_visibility_declaration(str_id, visibility_id, owner_id, |name| { + Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id))) + }); + } + + fn method_visibility_resolution( + &self, + owner_id: DeclarationId, + str_id: StringId, + visibility: Visibility, + visibility_id: DefinitionId, + definition_order_cache: &mut DefinitionOrderCache, + ) -> MethodVisibilityResolution { + let Some(Declaration::Namespace(namespace)) = self.graph.declarations().get(&owner_id) else { + return MethodVisibilityResolution::Skip; + }; + + let mut has_partial = false; + + for ancestor in namespace.ancestors() { + match ancestor { + Ancestor::Complete(ancestor_id) => { + let member_id = self + .graph + .declarations() + .get(ancestor_id) + .and_then(|decl| decl.as_namespace()) + .and_then(|ns| ns.member(&str_id)) + .copied(); + + if let Some(member_id) = member_id { + if visibility == Visibility::ModuleFunction { + let Some(target_visibility) = self.method_visibility_before_visibility_definition( + member_id, + visibility_id, + definition_order_cache, + ) else { + continue; + }; + let Some(source) = self.module_function_copy_source( + member_id, + visibility_id, + owner_id, + target_visibility, + definition_order_cache, + ) else { + continue; + }; + + return MethodVisibilityResolution::ApplyModuleFunctionCopy(source); + } + + if self.method_has_definition_before_visibility( + member_id, + visibility_id, + definition_order_cache, + ) { + return MethodVisibilityResolution::ApplyExisting; + } + } + } + Ancestor::Partial(_) => has_partial = true, + } + } + + if has_partial { + MethodVisibilityResolution::RetryPartial + } else { + MethodVisibilityResolution::Missing + } + } + + fn create_method_visibility_declaration( + &mut self, + str_id: StringId, + definition_id: DefinitionId, + owner_id: DeclarationId, + declaration_builder: F, + ) where + F: FnOnce(String) -> Declaration, + { + let fully_qualified_name = self.member_fully_qualified_name(owner_id, str_id); + + let declaration_id = self.graph.add_method_visibility_declaration( + definition_id, + owner_id, + str_id, + fully_qualified_name, + declaration_builder, + ); + self.graph.add_member(&owner_id, declaration_id, str_id); + } +} diff --git a/rust/rubydex/src/resolution/method_visibility/module_function.rs b/rust/rubydex/src/resolution/method_visibility/module_function.rs new file mode 100644 index 000000000..7222e57a3 --- /dev/null +++ b/rust/rubydex/src/resolution/method_visibility/module_function.rs @@ -0,0 +1,529 @@ +use super::{DefinitionOrderCache, Resolver}; +use crate::model::{ + comment::Comment, + declaration::{Declaration, MethodDeclaration}, + definitions::{Definition, DefinitionFlags, MethodDefinition, Parameter, ParameterStruct, Receiver, Signatures}, + identity_maps::IdentityHashSet, + ids::{DeclarationId, DefinitionId, StringId, UriId}, + visibility::Visibility, +}; +use crate::offset::Offset; + +pub(in crate::resolution::method_visibility) struct ModuleFunctionCopySource { + definition_id: DefinitionId, + target_visibility: Visibility, + source_kind: ModuleFunctionSourceKind, + alias_definition_ids: Vec, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ModuleFunctionSourceKind { + DirectMethod, + DirectAlias, + InheritedMethod, + InheritedAlias, +} + +impl ModuleFunctionSourceKind { + fn new(target_is_direct: bool, uses_alias: bool) -> Self { + match (target_is_direct, uses_alias) { + (true, false) => Self::DirectMethod, + (true, true) => Self::DirectAlias, + (false, false) => Self::InheritedMethod, + (false, true) => Self::InheritedAlias, + } + } + + fn should_create_instance_copy(self, target_visibility: Visibility) -> bool { + match self { + Self::DirectMethod => false, + Self::DirectAlias => true, + Self::InheritedMethod | Self::InheritedAlias => target_visibility != Visibility::Private, + } + } + + fn is_direct_method(self) -> bool { + self == Self::DirectMethod + } + + fn should_track_ancestor_dependency(self, source_declared_on_owner: bool) -> bool { + !source_declared_on_owner && !self.is_direct_method() + } +} + +struct ModuleFunctionCopyRequest { + str_id: StringId, + visibility_id: DefinitionId, + source_definition_id: DefinitionId, + owner_id: DeclarationId, + copy_declaration_id: DeclarationId, + fully_qualified_name: String, +} + +struct MethodBodySnapshot { + uri_id: UriId, + offset: Offset, + comments: Box<[Comment]>, + flags: DefinitionFlags, + signatures: Signatures, +} + +struct ModuleFunctionCopyDefinitionRequest { + str_id: StringId, + owner_definition_id: DefinitionId, + visibility: Visibility, + receiver: Option, + generated: bool, +} + +impl Resolver<'_> { + pub(in crate::resolution::method_visibility) fn apply_module_function( + &mut self, + str_id: StringId, + visibility_id: DefinitionId, + owner_id: DeclarationId, + source: ModuleFunctionCopySource, + ) -> bool { + let Some(owner_definition_id) = self + .graph + .definitions() + .get(&visibility_id) + .and_then(|definition| *definition.lexical_nesting_id()) + else { + return false; + }; + + let source_definition_id = source.definition_id; + let source_declared_on_owner = self.source_definition_declared_on_owner(owner_id, source_definition_id); + // Ruby only installs a direct instance method copy for inherited module_function + // sources when the inherited method was public/protected at the call site. Direct + // methods are handled by the MethodVisibilityDefinition, and direct aliases need + // their own private copy because the alias body can come from another declaration. + // Inherited private sources keep using the ancestor method while still getting a + // singleton copy. + if source.source_kind.should_create_instance_copy(source.target_visibility) + && !self.create_module_function_instance_copy( + str_id, + visibility_id, + owner_id, + source_definition_id, + owner_definition_id, + ) + { + return false; + } + + if !self.create_module_function_singleton_copy( + str_id, + visibility_id, + owner_id, + source_definition_id, + owner_definition_id, + ) { + return false; + } + + if source.source_kind.is_direct_method() { + self.create_method_visibility_declaration_for_owner(str_id, visibility_id, owner_id); + } else if source + .source_kind + .should_track_ancestor_dependency(source_declared_on_owner) + { + self.graph + .track_module_function_ancestor_dependency(owner_id, str_id, visibility_id); + } + + for alias_id in source.alias_definition_ids { + self.graph + .track_module_function_alias_dependency(alias_id, visibility_id); + } + + true + } + + fn source_definition_declared_on_owner(&self, owner_id: DeclarationId, source_definition_id: DefinitionId) -> bool { + self.graph + .definition_id_to_declaration_id(source_definition_id) + .and_then(|declaration_id| self.graph.declarations().get(declaration_id)) + .is_some_and(|declaration| *declaration.owner_id() == owner_id) + } + + fn create_module_function_instance_copy( + &mut self, + str_id: StringId, + visibility_id: DefinitionId, + owner_id: DeclarationId, + source_definition_id: DefinitionId, + owner_definition_id: DefinitionId, + ) -> bool { + let fully_qualified_name = self.member_fully_qualified_name(owner_id, str_id); + let copy_declaration_id = DeclarationId::from(&fully_qualified_name); + self.create_module_function_copy( + ModuleFunctionCopyRequest { + str_id, + visibility_id, + source_definition_id, + owner_id, + copy_declaration_id, + fully_qualified_name, + }, + |resolver| { + resolver.build_module_function_copy_definition( + str_id, + source_definition_id, + owner_definition_id, + Visibility::Private, + None, + true, + ) + }, + ) + } + + fn create_module_function_singleton_copy( + &mut self, + str_id: StringId, + visibility_id: DefinitionId, + owner_id: DeclarationId, + source_definition_id: DefinitionId, + owner_definition_id: DefinitionId, + ) -> bool { + let Some(singleton_id) = self.get_or_create_singleton_class(owner_id, true) else { + return false; + }; + + let fully_qualified_name = self.member_fully_qualified_name(singleton_id, str_id); + let copy_declaration_id = DeclarationId::from(&fully_qualified_name); + + self.create_module_function_copy( + ModuleFunctionCopyRequest { + str_id, + visibility_id, + source_definition_id, + owner_id: singleton_id, + copy_declaration_id, + fully_qualified_name, + }, + |resolver| { + resolver.build_module_function_copy_definition( + str_id, + source_definition_id, + owner_definition_id, + Visibility::Public, + Some(Receiver::SelfReceiver(owner_definition_id)), + false, + ) + }, + ) + } + + fn create_module_function_copy(&mut self, request: ModuleFunctionCopyRequest, build_copy_definition: F) -> bool + where + F: FnOnce(&mut Self) -> Option, + { + let ModuleFunctionCopyRequest { + str_id, + visibility_id, + source_definition_id, + owner_id, + copy_declaration_id, + fully_qualified_name, + } = request; + + let copy_definition_id = if let Some(copy_definition_id) = self + .graph + .find_module_function_copy(source_definition_id, copy_declaration_id) + { + self.graph + .attach_module_function_copy_visibility(copy_definition_id, visibility_id); + copy_definition_id + } else { + let Some(copy_definition) = build_copy_definition(self) else { + return false; + }; + + self.graph.add_module_function_copy( + source_definition_id, + visibility_id, + copy_declaration_id, + copy_definition, + ) + }; + + if self + .graph + .declarations() + .get(©_declaration_id) + .is_some_and(|declaration| declaration.definitions().contains(©_definition_id)) + { + self.graph.add_member(&owner_id, copy_declaration_id, str_id); + return true; + } + + let copy_declaration_id = self.graph.add_module_function_copy_declaration( + copy_definition_id, + visibility_id, + fully_qualified_name, + |name| Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id))), + ); + self.graph.add_member(&owner_id, copy_declaration_id, str_id); + + true + } + + pub(in crate::resolution::method_visibility) fn module_function_copy_source( + &self, + source_method_id: DeclarationId, + visibility_id: DefinitionId, + owner_id: DeclarationId, + target_visibility: Visibility, + definition_order_cache: &mut DefinitionOrderCache, + ) -> Option { + let visibility_definition = self.graph.definitions().get(&visibility_id)?; + let visibility_uri_id = *visibility_definition.uri_id(); + let visibility_offset = visibility_definition.offset(); + + let mut seen_method_ids = IdentityHashSet::default(); + let mut source = self.module_function_copy_source_before( + source_method_id, + visibility_uri_id, + visibility_offset, + target_visibility, + &mut seen_method_ids, + definition_order_cache, + )?; + let target_is_direct = self.method_declaration_declared_on_owner(source_method_id, owner_id); + source.source_kind = ModuleFunctionSourceKind::new(target_is_direct, !source.alias_definition_ids.is_empty()); + Some(source) + } + + fn module_function_copy_source_before( + &self, + source_method_id: DeclarationId, + uri_id: UriId, + offset: &Offset, + target_visibility: Visibility, + seen_method_ids: &mut IdentityHashSet, + definition_order_cache: &mut DefinitionOrderCache, + ) -> Option { + if !seen_method_ids.insert(source_method_id) { + return None; + } + + self.definition_ids_before_visibility(source_method_id, uri_id, offset, definition_order_cache)? + .iter() + .copied() + .rev() + .find_map(|definition_id| { + let definition = self.graph.definitions().get(&definition_id)?; + + if definition.is_copyable_method_body() { + return Some(ModuleFunctionCopySource { + definition_id, + target_visibility, + source_kind: ModuleFunctionSourceKind::InheritedMethod, + alias_definition_ids: Vec::new(), + }); + } + + let Definition::MethodAlias(alias) = definition else { + return None; + }; + + let owner_id = self + .graph + .definition_id_to_declaration_id(definition_id) + .and_then(|decl_id| self.graph.declarations().get(decl_id)) + .map(|declaration| *declaration.owner_id())?; + for target_id in self.method_member_ids_in_ancestors_before( + owner_id, + *alias.old_name_str_id(), + *definition.uri_id(), + definition.offset(), + definition_order_cache, + ) { + let Some(mut source) = self.module_function_copy_source_before( + target_id, + *definition.uri_id(), + definition.offset(), + target_visibility, + seen_method_ids, + definition_order_cache, + ) else { + continue; + }; + source.alias_definition_ids.push(definition_id); + return Some(source); + } + + None + }) + } + + fn method_declaration_declared_on_owner(&self, method_id: DeclarationId, owner_id: DeclarationId) -> bool { + self.graph + .declarations() + .get(&method_id) + .is_some_and(|declaration| *declaration.owner_id() == owner_id) + } + + fn build_module_function_copy_definition( + &mut self, + copy_str_id: StringId, + source_definition_id: DefinitionId, + owner_definition_id: DefinitionId, + visibility: Visibility, + receiver: Option, + generated: bool, + ) -> Option { + let snapshot = self.module_function_body_snapshot(source_definition_id)?; + + Some(Self::module_function_copy_from_snapshot( + ModuleFunctionCopyDefinitionRequest { + str_id: copy_str_id, + owner_definition_id, + visibility, + receiver, + generated, + }, + snapshot, + )) + } + + fn module_function_body_snapshot(&mut self, source_definition_id: DefinitionId) -> Option { + let source_definition = self.graph.definitions().get(&source_definition_id)?; + + match source_definition { + Definition::Method(method) => Some(MethodBodySnapshot { + uri_id: *method.uri_id(), + offset: method.offset().clone(), + comments: method.comments().to_vec().into_boxed_slice(), + flags: method.flags().clone(), + signatures: method.signatures().clone(), + }), + Definition::AttrAccessor(attr) => Some(Self::module_function_attr_body_snapshot( + *attr.uri_id(), + attr.offset(), + attr.comments(), + attr.flags(), + )), + Definition::AttrReader(attr) => Some(Self::module_function_attr_body_snapshot( + *attr.uri_id(), + attr.offset(), + attr.comments(), + attr.flags(), + )), + Definition::AttrWriter(attr) => { + let uri_id = *attr.uri_id(); + let offset = attr.offset().clone(); + let comments = attr.comments().to_vec().into_boxed_slice(); + let flags = attr.flags().clone(); + let arg_str_id = self.graph.prepare_generated_string("__rubydex_arg0".to_string()); + Some(Self::module_function_attr_body_snapshot_from_parts( + uri_id, + offset, + comments, + flags, + Some(arg_str_id), + )) + } + _ => None, + } + } + + fn module_function_attr_body_snapshot( + uri_id: UriId, + offset: &Offset, + comments: &[Comment], + flags: &DefinitionFlags, + ) -> MethodBodySnapshot { + Self::module_function_attr_body_snapshot_from_parts( + uri_id, + offset.clone(), + comments.to_vec().into_boxed_slice(), + flags.clone(), + None, + ) + } + + fn module_function_attr_body_snapshot_from_parts( + uri_id: UriId, + offset: Offset, + comments: Box<[Comment]>, + flags: DefinitionFlags, + writer_arg_str_id: Option, + ) -> MethodBodySnapshot { + let signatures = if let Some(arg_str_id) = writer_arg_str_id { + Signatures::Simple( + vec![Parameter::RequiredPositional(ParameterStruct::new( + offset.clone(), + arg_str_id, + ))] + .into_boxed_slice(), + ) + } else { + Signatures::Simple(Box::default()) + }; + + MethodBodySnapshot { + uri_id, + offset, + comments, + flags, + signatures, + } + } + + fn module_function_copy_from_snapshot( + request: ModuleFunctionCopyDefinitionRequest, + snapshot: MethodBodySnapshot, + ) -> Definition { + let ModuleFunctionCopyDefinitionRequest { + str_id, + owner_definition_id, + visibility, + receiver, + generated, + } = request; + let MethodBodySnapshot { + uri_id, + offset, + comments, + mut flags, + signatures, + } = snapshot; + + // A generated source copy may be copied again. Recompute GENERATED from + // this copy's role so singleton copies can still collide with inline + // `module_function def` definitions while instance copies stay distinct. + flags.remove(DefinitionFlags::GENERATED); + + let method = if generated { + MethodDefinition::new_generated( + str_id, + uri_id, + offset, + comments, + flags, + Some(owner_definition_id), + signatures, + visibility, + receiver, + ) + } else { + MethodDefinition::new( + str_id, + uri_id, + offset, + comments, + flags, + Some(owner_definition_id), + signatures, + visibility, + receiver, + ) + }; + + Definition::Method(Box::new(method)) + } +} diff --git a/rust/rubydex/src/resolution/method_visibility/order.rs b/rust/rubydex/src/resolution/method_visibility/order.rs new file mode 100644 index 000000000..2cb03700e --- /dev/null +++ b/rust/rubydex/src/resolution/method_visibility/order.rs @@ -0,0 +1,233 @@ +use std::{collections::HashMap, rc::Rc}; + +use super::Resolver; +use crate::model::{ + declaration::{Ancestor, Declaration}, + definitions::Definition, + graph::DefinitionProgramOrderKey, + identity_maps::IdentityHashSet, + ids::{DeclarationId, DefinitionId, StringId, UriId}, + visibility::Visibility, +}; +use crate::offset::Offset; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct DefinitionOrderKey { + method_id: DeclarationId, + uri_id: UriId, + // Same-file "before" checks use the visibility start offset only; the + // end offset does not affect membership in this cache bucket. + offset_start: u32, +} + +#[derive(Default)] +pub(in crate::resolution::method_visibility) struct DefinitionOrderCache { + ids_by_position: HashMap>, +} + +impl Resolver<'_> { + pub(in crate::resolution::method_visibility) fn method_visibility_before_visibility_definition( + &self, + method_id: DeclarationId, + visibility_id: DefinitionId, + definition_order_cache: &mut DefinitionOrderCache, + ) -> Option { + let visibility_definition = self.graph.definitions().get(&visibility_id)?; + let mut seen_method_ids = IdentityHashSet::default(); + self.method_visibility_before( + method_id, + *visibility_definition.uri_id(), + visibility_definition.offset(), + &mut seen_method_ids, + definition_order_cache, + ) + } + + fn method_visibility_before( + &self, + method_id: DeclarationId, + uri_id: UriId, + offset: &Offset, + seen_method_ids: &mut IdentityHashSet, + definition_order_cache: &mut DefinitionOrderCache, + ) -> Option { + if !seen_method_ids.insert(method_id) { + return None; + } + + for definition_id in self + .definition_ids_before_visibility(method_id, uri_id, offset, definition_order_cache)? + .iter() + .copied() + .rev() + { + let definition = self.graph.definitions().get(&definition_id).unwrap(); + + if let Some(visibility) = definition.method_effective_visibility() { + return Some(visibility); + } + + let Definition::MethodAlias(alias) = definition else { + continue; + }; + + let owner_id = self + .graph + .definition_id_to_declaration_id(definition_id) + .and_then(|decl_id| self.graph.declarations().get(decl_id)) + .map(|declaration| *declaration.owner_id())?; + for target_id in self.method_member_ids_in_ancestors_before( + owner_id, + *alias.old_name_str_id(), + *definition.uri_id(), + definition.offset(), + definition_order_cache, + ) { + let Some(visibility) = self.method_visibility_before( + target_id, + *definition.uri_id(), + definition.offset(), + seen_method_ids, + definition_order_cache, + ) else { + continue; + }; + return Some(visibility); + } + } + + Some(Visibility::Public) + } + + pub(in crate::resolution::method_visibility) fn method_has_definition_before_visibility( + &self, + method_id: DeclarationId, + visibility_id: DefinitionId, + definition_order_cache: &mut DefinitionOrderCache, + ) -> bool { + let Some(visibility_definition) = self.graph.definitions().get(&visibility_id) else { + return false; + }; + + self.method_has_definition_before_position( + method_id, + *visibility_definition.uri_id(), + visibility_definition.offset(), + definition_order_cache, + ) + } + + fn method_has_definition_before_position( + &self, + method_id: DeclarationId, + uri_id: UriId, + offset: &Offset, + definition_order_cache: &mut DefinitionOrderCache, + ) -> bool { + self.definition_ids_before_visibility(method_id, uri_id, offset, definition_order_cache) + .is_some_and(|definition_ids| { + definition_ids.iter().copied().any(|definition_id| { + let Some(definition) = self.graph.definitions().get(&definition_id) else { + return false; + }; + + definition.establishes_method_member() + }) + }) + } + + pub(in crate::resolution::method_visibility) fn method_member_ids_in_ancestors_before( + &self, + owner_id: DeclarationId, + str_id: StringId, + uri_id: UriId, + offset: &Offset, + definition_order_cache: &mut DefinitionOrderCache, + ) -> Vec { + let Some(namespace) = self + .graph + .declarations() + .get(&owner_id) + .and_then(Declaration::as_namespace) + else { + return Vec::new(); + }; + + namespace + .ancestors() + .iter() + .filter_map(|ancestor| { + let Ancestor::Complete(ancestor_id) = ancestor else { + return None; + }; + let member_id = self + .graph + .declarations() + .get(ancestor_id)? + .as_namespace()? + .member(&str_id) + .copied()?; + + if !matches!(self.graph.declarations().get(&member_id), Some(Declaration::Method(_))) { + return None; + } + + self.method_has_definition_before_position(member_id, uri_id, offset, definition_order_cache) + .then_some(member_id) + }) + .collect() + } + + pub(in crate::resolution::method_visibility) fn definition_ids_before_visibility( + &self, + method_id: DeclarationId, + visibility_uri_id: UriId, + visibility_offset: &Offset, + cache: &mut DefinitionOrderCache, + ) -> Option> { + let key = DefinitionOrderKey { + method_id, + uri_id: visibility_uri_id, + offset_start: visibility_offset.start(), + }; + if let Some(definitions) = cache.ids_by_position.get(&key) { + return Some(Rc::clone(definitions)); + } + + let mut definitions = self + .graph + .declarations() + .get(&method_id)? + .definitions() + .iter() + .copied() + .filter_map(|definition_id| { + let (same_uri, uri, offset) = self.graph.definition_order_key_before_position( + definition_id, + visibility_uri_id, + visibility_offset, + )?; + Some((definition_id, same_uri, uri, offset)) + }) + .collect::>(); + + definitions.sort_unstable_by( + |(left_id, left_same_uri, left_uri, left_offset), (right_id, right_same_uri, right_uri, right_offset)| { + // Cross-file candidates sort before same-file candidates so the reverse scans below prefer + // definitions known to be before the visibility call in the same file. + left_same_uri.cmp(right_same_uri).then_with(|| { + DefinitionProgramOrderKey::new(*left_id, left_uri, left_offset) + .cmp(&DefinitionProgramOrderKey::new(*right_id, right_uri, right_offset)) + }) + }, + ); + + let definitions = definitions + .into_iter() + .map(|(definition_id, _, _, _)| definition_id) + .collect::>(); + let definitions = Rc::<[DefinitionId]>::from(definitions.into_boxed_slice()); + cache.ids_by_position.insert(key, Rc::clone(&definitions)); + Some(definitions) + } +} diff --git a/rust/rubydex/src/resolution_tests.rs b/rust/rubydex/src/resolution_tests.rs index 1ddc528d2..c31e0c284 100644 --- a/rust/rubydex/src/resolution_tests.rs +++ b/rust/rubydex/src/resolution_tests.rs @@ -2606,7 +2606,11 @@ mod method_tests { assert_members_eq!(context, "Foo::", ["setup()"]); // All attr_* should be owned by Foo, not by setup - assert_members_eq!(context, "Foo", ["accessor_attr()", "reader_attr()", "writer_attr()"]); + assert_members_eq!( + context, + "Foo", + ["accessor_attr()", "accessor_attr=()", "reader_attr()", "writer_attr=()"] + ); } } @@ -3642,11 +3646,9 @@ mod fqn_and_naming_tests { assert_declaration_exists!(context, "Foo::Bar::CONST"); assert_declaration_exists!(context, "Foo::Bar::#@class_ivar"); assert_declaration_exists!(context, "Foo::Bar#baz()"); - // TODO: needs the fix for attributes - // assert_declaration_exists!(context, "Foo::Bar#qux=()"); + assert_declaration_exists!(context, "Foo::Bar#qux=()"); assert_declaration_exists!(context, "Foo::Bar#zip()"); - // TODO: needs the fix for attributes - // assert_declaration_exists!(context, "Foo::Bar#zip=()"); + assert_declaration_exists!(context, "Foo::Bar#zip=()"); assert_declaration_exists!(context, "Foo::Bar#instance_m()"); assert_declaration_exists!(context, "Foo::Bar#@@class_var"); assert_declaration_exists!(context, "Foo::Bar::#singleton_m()"); @@ -4909,7 +4911,7 @@ mod rbs_tests { mod visibility_resolution_tests { use super::*; - use crate::model::visibility::Visibility; + use crate::model::{definitions::Definition, definitions::Parameter, visibility::Visibility}; macro_rules! assert_visibility_eq { ($context:expr, $declaration_name:expr, $expected_visibility:expr) => { @@ -4926,6 +4928,78 @@ mod visibility_resolution_tests { }; } + fn method_required_parameter_count(context: &GraphTest, declaration_name: &str) -> usize { + method_definition(context, declaration_name).signatures().as_slice()[0].len() + } + + fn latest_method_required_parameter_count(context: &GraphTest, declaration_name: &str) -> usize { + latest_method_definition(context, declaration_name) + .signatures() + .as_slice()[0] + .len() + } + + fn method_required_parameter_names(context: &GraphTest, declaration_name: &str) -> Vec { + method_definition(context, declaration_name).signatures().as_slice()[0] + .iter() + .filter_map(|parameter| match parameter { + Parameter::RequiredPositional(parameter) => Some( + context + .graph() + .strings() + .get(parameter.str()) + .unwrap() + .as_str() + .to_string(), + ), + _ => None, + }) + .collect() + } + + fn method_definition<'a>( + context: &'a GraphTest, + declaration_name: &str, + ) -> &'a crate::model::definitions::MethodDefinition { + let declaration = context + .graph() + .declarations() + .get(&DeclarationId::from(declaration_name)) + .unwrap(); + declaration + .definitions() + .iter() + .find_map( + |definition_id| match context.graph().definitions().get(definition_id).unwrap() { + Definition::Method(method) => Some(method), + _ => None, + }, + ) + .unwrap() + } + + fn latest_method_definition<'a>( + context: &'a GraphTest, + declaration_name: &str, + ) -> &'a crate::model::definitions::MethodDefinition { + let declaration = context + .graph() + .declarations() + .get(&DeclarationId::from(declaration_name)) + .unwrap(); + declaration + .definitions() + .iter() + .rev() + .find_map( + |definition_id| match context.graph().definitions().get(definition_id).unwrap() { + Definition::Method(method) => Some(method), + _ => None, + }, + ) + .unwrap() + } + #[test] fn retroactive_visibility_override_applies_in_source_order() { let mut context = GraphTest::new(); @@ -4972,246 +5046,227 @@ mod visibility_resolution_tests { } #[test] - fn retroactive_visibility_on_attr_methods() { + fn retroactive_module_function_creates_private_instance_and_public_singleton_copy() { let mut context = GraphTest::new(); context.index_uri( "file:///foo.rb", r" - class Foo - attr_reader :reader_method - private :reader_method - - attr_writer :writer_method - protected :writer_method - - attr_accessor :accessor_method - private :accessor_method + module Foo + def bar; end + module_function :bar end ", ); context.resolve(); assert_no_diagnostics!(&context); - assert_visibility_eq!(context, "Foo#reader_method()", Visibility::Private); - assert_visibility_eq!(context, "Foo#writer_method()", Visibility::Protected); - assert_visibility_eq!(context, "Foo#accessor_method()", Visibility::Private); + assert_declaration_exists!(context, "Foo#bar()"); + assert_declaration_exists!(context, "Foo::#bar()"); + assert_members_eq!(context, "Foo", ["bar()"]); + assert_members_eq!(context, "Foo::", ["bar()"]); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); } #[test] - fn retroactive_visibility_on_inherited_method() { + fn retroactive_module_function_copies_the_method_at_the_call_site() { let mut context = GraphTest::new(); context.index_uri( "file:///foo.rb", r" - class Parent - def foo; end - end - - class Child < Parent - private :foo + module Foo + def bar(first); end + module_function :bar + def bar(first, second); end end ", ); context.resolve(); assert_no_diagnostics!(&context); - assert_declaration_exists!(context, "Child#foo()"); - assert_members_eq!(context, "Child", ["foo()"]); - assert_owner_eq!(context, "Child#foo()", "Child"); - assert_visibility_eq!(context, "Child#foo()", Visibility::Private); - assert_visibility_eq!(context, "Parent#foo()", Visibility::Public); + + assert_visibility_eq!(context, "Foo#bar()", Visibility::Public); + + let singleton = context + .graph() + .declarations() + .get(&DeclarationId::from("Foo::#bar()")) + .unwrap(); + let method = singleton + .definitions() + .iter() + .find_map( + |definition_id| match context.graph().definitions().get(definition_id).unwrap() { + Definition::Method(method) => Some(method), + _ => None, + }, + ) + .expect("module_function should create a copied singleton method definition"); + + assert_eq!(method.signatures().as_slice().len(), 1); + assert_eq!(method.signatures().as_slice()[0].len(), 1); } #[test] - fn retroactive_visibility_on_grandparent_method() { + fn retroactive_module_function_uses_uri_order_to_tie_break_cross_file_source_selection() { let mut context = GraphTest::new(); context.index_uri( - "file:///foo.rb", + "file:///a_source.rb", r" - class GrandParent - def greet; end - end - - class Parent < GrandParent; end - - class Child < Parent - private :greet + module Foo + def bar(first); end end ", ); - context.resolve(); - - assert_no_diagnostics!(&context); - assert_owner_eq!(context, "Child#greet()", "Child"); - assert_visibility_eq!(context, "Child#greet()", Visibility::Private); - } - - #[test] - fn retroactive_visibility_on_included_module_method() { - let mut context = GraphTest::new(); context.index_uri( - "file:///foo.rb", + "file:///z_source.rb", r" - module Greetable - def greet; end - end - - class Foo - include Greetable - private :greet + module Foo + def bar(first, second); end end ", ); - context.resolve(); - - assert_no_diagnostics!(&context); - assert_owner_eq!(context, "Foo#greet()", "Foo"); - assert_visibility_eq!(context, "Foo#greet()", Visibility::Private); - } - - #[test] - fn retroactive_visibility_on_undefined_method_emits_diagnostic() { - let mut context = GraphTest::new(); context.index_uri( - "file:///foo.rb", + "file:///m_visibility.rb", r" - class Foo - private :nonexistent + module Foo + module_function :bar end ", ); context.resolve(); - assert_diagnostics_eq!( - context, - &[ - "undefined-method-visibility-target: undefined method `Foo#nonexistent()` for visibility change (2:12-2:23)" - ] + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 2); + assert_eq!( + method_required_parameter_names(&context, "Foo::#bar()"), + ["first", "second"] ); } #[test] - fn retroactive_visibility_across_reopened_class() { + fn retroactive_module_function_prefers_same_file_source_over_cross_file_tie_breaker() { let mut context = GraphTest::new(); context.index_uri( - "file:///a.rb", + "file:///z_source.rb", r" - class Foo - def bar; end + module Foo + def bar(first, second); end end ", ); context.index_uri( - "file:///b.rb", + "file:///m_visibility.rb", r" - class Foo - private :bar + module Foo + def bar(first); end + module_function :bar end ", ); context.resolve(); assert_no_diagnostics!(&context); - assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 1); + assert_eq!(method_required_parameter_names(&context, "Foo::#bar()"), ["first"]); } #[test] - fn retroactive_visibility_resolves_when_ancestor_discovered_incrementally() { + fn retroactive_module_function_allows_cross_file_source_after_visibility_uri() { let mut context = GraphTest::new(); context.index_uri( - "file:///child.rb", + "file:///z_source.rb", r" - class Child - include M - private :foo + module Foo + def bar(first); end + end + ", + ); + context.index_uri( + "file:///m_visibility.rb", + r" + module Foo + module_function :bar end ", ); context.resolve(); assert_no_diagnostics!(&context); - assert_declaration_does_not_exist!(context, "Child#foo()"); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 1); + } + #[test] + fn singleton_visibility_after_module_function_sees_generated_copy() { + let mut context = GraphTest::new(); context.index_uri( - "file:///module.rb", + "file:///foo.rb", r" module M def foo; end + module_function :foo + private_class_method :foo end ", ); context.resolve(); assert_no_diagnostics!(&context); - assert_declaration_exists!(context, "Child#foo()"); - assert_owner_eq!(context, "Child#foo()", "Child"); - assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + assert_visibility_eq!(context, "M#foo()", Visibility::Private); + assert_visibility_eq!(context, "M::#foo()", Visibility::Private); } #[test] - fn retroactive_constant_visibility_on_direct_member() { + fn singleton_visibility_before_module_function_does_not_see_generated_copy() { let mut context = GraphTest::new(); context.index_uri( "file:///foo.rb", r" - class Foo - BAR = 1 - private_constant :BAR - - BAZ = 2 - public_constant :BAZ - - QUX = 3 - - class Inner; end - private_constant :Inner - - module InnerMod; end - private_constant :InnerMod + module M + def foo; end + private_class_method :foo + module_function :foo end ", ); context.resolve(); - assert_no_diagnostics!(&context); - assert_visibility_eq!(context, "Foo::BAR", Visibility::Private); - assert_visibility_eq!(context, "Foo::BAZ", Visibility::Public); - assert_visibility_eq!(context, "Foo::QUX", Visibility::Public); - assert_visibility_eq!(context, "Foo::Inner", Visibility::Private); - assert_visibility_eq!(context, "Foo::InnerMod", Visibility::Private); + assert_diagnostics_eq!( + context, + &["undefined-method-visibility-target: undefined method `M::#foo()` for visibility change (3:25-3:28)"] + ); + assert_visibility_eq!(context, "M#foo()", Visibility::Private); + assert_visibility_eq!(context, "M::#foo()", Visibility::Public); } #[test] - fn retroactive_constant_visibility_via_qualified_receiver() { + fn singleton_visibility_before_module_function_stays_unresolved_after_incremental_reorder() { let mut context = GraphTest::new(); context.index_uri( "file:///foo.rb", r" - class Foo - BAR = 1 - BAZ = 2 + module M + def foo; end + module_function :foo + private_class_method :foo end - - ALIAS = Foo - Foo.private_constant :BAR - ALIAS.private_constant :BAZ ", ); context.resolve(); assert_no_diagnostics!(&context); - assert_visibility_eq!(context, "Foo::BAR", Visibility::Private); - assert_visibility_eq!(context, "Foo::BAZ", Visibility::Private); - } + assert_visibility_eq!(context, "M::#foo()", Visibility::Private); - #[test] - fn retroactive_constant_visibility_multi_arg_undefined_emits_per_name_diagnostic() { - let mut context = GraphTest::new(); context.index_uri( "file:///foo.rb", r" - class Foo - private_constant :NOPE_ONE, :NOPE_TWO + module M + def foo; end + private_class_method :foo + module_function :foo end ", ); @@ -5219,97 +5274,2104 @@ mod visibility_resolution_tests { assert_diagnostics_eq!( context, - &[ - "undefined-constant-visibility-target: undefined constant `NOPE_ONE` for visibility change in `Foo` (2:21-2:29)", - "undefined-constant-visibility-target: undefined constant `NOPE_TWO` for visibility change in `Foo` (2:32-2:40)", - ] + &["undefined-method-visibility-target: undefined method `M::#foo()` for visibility change (3:25-3:28)"] ); + assert_visibility_eq!(context, "M::#foo()", Visibility::Public); } #[test] - fn retroactive_constant_visibility_inherited_constant_emits_diagnostic() { + fn singleton_visibility_requeues_when_generated_copy_loses_cross_file_trigger() { let mut context = GraphTest::new(); context.index_uri( - "file:///foo.rb", + "file:///a_source.rb", r" - class Parent - CONST = 1 + module M + def foo; end end - - class Child < Parent - private_constant :CONST + ", + ); + context.index_uri( + "file:///a_trigger.rb", + r" + module M + module_function :foo + end + ", + ); + context.index_uri( + "file:///b_visibility.rb", + r" + module M + private_class_method :foo + module_function :foo end ", ); context.resolve(); + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "M::#foo()", Visibility::Private); + + context.delete_uri("file:///a_trigger.rb"); + context.resolve(); + assert_diagnostics_eq!( context, - &[ - "undefined-constant-visibility-target: undefined constant `CONST` for visibility change in `Child` (6:21-6:26)" - ] + &["undefined-method-visibility-target: undefined method `M::#foo()` for visibility change (2:25-2:28)"] ); - assert_visibility_eq!(context, "Parent::CONST", Visibility::Public); + assert_visibility_eq!(context, "M#foo()", Visibility::Private); + assert_visibility_eq!(context, "M::#foo()", Visibility::Public); } #[test] - fn retroactive_constant_visibility_clears_when_call_removed() { + fn inherited_retroactive_module_function_uses_uri_order_to_tie_break_cross_file_visibility_snapshot() { let mut context = GraphTest::new(); context.index_uri( - "file:///foo.rb", + "file:///a_source.rb", r" - class Foo - BAR = 1 + module A + def foo; end + private :foo end ", ); context.index_uri( - "file:///vis.rb", + "file:///z_source.rb", r" - Foo.private_constant :BAR + module A + def foo(x); end + end + ", + ); + context.index_uri( + "file:///m_visibility.rb", + r" + module B + include A + module_function :foo + end ", ); context.resolve(); assert_no_diagnostics!(&context); - assert_visibility_eq!(context, "Foo::BAR", Visibility::Private); - - context.delete_uri("file:///vis.rb"); - context.resolve(); - - assert_no_diagnostics!(&context); - assert_visibility_eq!(context, "Foo::BAR", Visibility::Public); + assert_visibility_eq!(context, "B#foo()", Visibility::Private); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 1); } #[test] - fn retroactive_constant_visibility_inside_singleton_class_body() { + fn retroactive_module_function_instance_copy_does_not_override_later_alias_method_definition() { let mut context = GraphTest::new(); context.index_uri( "file:///foo.rb", r" - class Foo - class << self - BAR = 1 - private_constant :BAR - end + module Foo + def original(first); end + alias copied original + module_function :copied + def copied(first, second); end end ", ); context.resolve(); assert_no_diagnostics!(&context); - assert_visibility_eq!(context, "Foo::::BAR", Visibility::Private); + assert_visibility_eq!(context, "Foo#copied()", Visibility::Public); + assert_visibility_eq!(context, "Foo::#copied()", Visibility::Public); + assert_eq!(latest_method_required_parameter_count(&context, "Foo#copied()"), 2); + assert_eq!( + latest_method_required_parameter_count(&context, "Foo::#copied()"), + 1 + ); } #[test] - fn retroactive_constant_visibility_persists_across_reopened_class() { + fn inherited_retroactive_module_function_instance_copy_does_not_override_later_direct_method() { let mut context = GraphTest::new(); context.index_uri( "file:///a.rb", r" - class Foo - BAR = 1 - private_constant :BAR + module A + def foo(x); end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module B + include A + module_function :foo + def foo(x, y); end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "B#foo()", Visibility::Public); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(latest_method_required_parameter_count(&context, "B#foo()"), 2); + assert_eq!(latest_method_required_parameter_count(&context, "B::#foo()"), 1); + } + + #[test] + fn retroactive_module_function_singleton_copy_does_not_override_later_singleton_method_definition() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + def bar(first); end + module_function :bar + def self.bar(first, second); end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(latest_method_required_parameter_count(&context, "Foo::#bar()"), 2); + } + + #[test] + fn retroactive_module_function_copies_method_alias_target() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + def original(first); end + alias copied original + module_function :copied + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Foo#copied()"); + assert_declaration_exists!(context, "Foo::#copied()"); + assert_visibility_eq!(context, "Foo#copied()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#copied()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo#copied()"), 1); + assert_eq!(method_required_parameter_names(&context, "Foo#copied()"), ["first"]); + assert_eq!(method_required_parameter_count(&context, "Foo::#copied()"), 1); + assert_eq!( + method_required_parameter_names(&context, "Foo::#copied()"), + ["first"] + ); + } + + #[test] + fn retroactive_module_function_direct_alias_to_private_target_stays_private_when_target_later_public() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + def original; end + private :original + alias copied original + public :original + module_function :copied + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Foo#copied()"); + assert_declaration_exists!(context, "Foo::#copied()"); + assert_visibility_eq!(context, "Foo#original()", Visibility::Public); + assert_visibility_eq!(context, "Foo#copied()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#copied()", Visibility::Public); + } + + #[test] + fn retroactive_module_function_alias_uses_target_available_at_alias_time() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Parent + def original(first); end + end + + module Foo + include Parent + alias copied original + def original(first, second); end + module_function :copied + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Foo#copied()"); + assert_declaration_exists!(context, "Foo::#copied()"); + assert_visibility_eq!(context, "Foo#copied()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#copied()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#copied()"), 1); + assert_eq!( + method_required_parameter_names(&context, "Foo::#copied()"), + ["first"] + ); + } + + #[test] + fn retroactive_module_function_can_copy_multiple_aliases_to_the_same_method() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + def original(first); end + alias copied original + alias also_copied original + module_function :copied + module_function :also_copied + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Foo#copied()"); + assert_declaration_exists!(context, "Foo#also_copied()"); + assert_declaration_exists!(context, "Foo::#copied()"); + assert_declaration_exists!(context, "Foo::#also_copied()"); + assert_eq!(method_required_parameter_count(&context, "Foo#copied()"), 1); + assert_eq!(method_required_parameter_count(&context, "Foo#also_copied()"), 1); + assert_eq!(method_required_parameter_count(&context, "Foo::#copied()"), 1); + assert_eq!(method_required_parameter_count(&context, "Foo::#also_copied()"), 1); + } + + #[test] + fn retroactive_module_function_copy_updates_when_intermediate_alias_changes() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + def original(first); end + def replacement(first, second); end + alias middle original + alias copied middle + module_function :copied + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_eq!(method_required_parameter_count(&context, "Foo#copied()"), 1); + assert_eq!(method_required_parameter_count(&context, "Foo::#copied()"), 1); + + context.index_uri( + "file:///foo.rb", + r" + module Foo + def original(first); end + def replacement(first, second); end + alias middle replacement + alias copied middle + module_function :copied + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#copied()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#copied()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo#copied()"), 2); + assert_eq!(method_required_parameter_count(&context, "Foo::#copied()"), 2); + } + + #[test] + fn retroactive_module_function_copy_clears_when_alias_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + def original(first); end + alias copied original + module_function :copied + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#copied()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#copied()", Visibility::Public); + + context.index_uri( + "file:///foo.rb", + r" + module Foo + def original(first); end + module_function :copied + end + ", + ); + context.resolve(); + + assert_declaration_does_not_exist!(context, "Foo#copied()"); + assert_declaration_does_not_exist!(context, "Foo::#copied()"); + assert_diagnostics_eq!( + &context, + vec![ + "undefined-method-visibility-target: undefined method `Foo#copied()` for visibility change (3:20-3:26)" + ] + ); + } + + #[test] + fn repeated_retroactive_module_function_reuses_the_existing_copy() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + def bar; end + module_function :bar + module_function :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + + let singleton = context + .graph() + .declarations() + .get(&DeclarationId::from("Foo::#bar()")) + .unwrap(); + let copied_method_count = singleton + .definitions() + .iter() + .filter(|definition_id| { + matches!( + context.graph().definitions().get(definition_id).unwrap(), + Definition::Method(_) + ) + }) + .count(); + + assert_eq!(copied_method_count, 1); + } + + #[test] + fn retroactive_module_function_copy_survives_when_one_of_multiple_visibility_triggers_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///source.rb", + r" + module Foo + def bar(first); end + end + ", + ); + context.index_uri( + "file:///visibility_a.rb", + r" + module Foo + module_function :bar + end + ", + ); + context.index_uri( + "file:///visibility_b.rb", + r" + module Foo + module_function :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 1); + + context.delete_uri("file:///visibility_a.rb"); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 1); + + context.delete_uri("file:///visibility_b.rb"); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "Foo::#bar()"); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Public); + } + + #[test] + fn retroactive_module_function_reuses_inline_module_function_copy() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + module_function def bar(first); end + module_function :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 1); + + let singleton = context + .graph() + .declarations() + .get(&DeclarationId::from("Foo::#bar()")) + .unwrap(); + let copied_method_count = singleton + .definitions() + .iter() + .filter(|definition_id| { + matches!( + context.graph().definitions().get(definition_id).unwrap(), + Definition::Method(_) + ) + }) + .count(); + + assert_eq!(copied_method_count, 1); + } + + #[test] + fn retroactive_module_function_reusing_cross_file_inline_copy_clears_when_source_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module Foo + module_function def bar(first); end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module Foo + module_function :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 1); + + context.index_uri( + "file:///a.rb", + r" + module Foo + end + ", + ); + context.resolve(); + + assert_declaration_does_not_exist!(context, "Foo#bar()"); + assert_declaration_does_not_exist!(context, "Foo::#bar()"); + assert_diagnostics_eq!( + &context, + vec!["undefined-method-visibility-target: undefined method `Foo#bar()` for visibility change (2:20-2:23)"] + ); + } + + #[test] + fn retroactive_module_function_reusing_cross_file_inline_copy_updates_when_source_changes() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module Foo + module_function def bar(first); end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module Foo + module_function :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 1); + + context.index_uri( + "file:///a.rb", + r" + module Foo + module_function def bar(first, second); end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 2); + } + + #[test] + fn inherited_retroactive_module_function_creates_direct_private_instance_and_singleton_copy() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x); end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module B + include A + module_function :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "B#foo()"); + assert_declaration_exists!(context, "B::#foo()"); + assert_members_eq!(context, "B", ["foo()"]); + assert_members_eq!(context, "B::", ["foo()"]); + assert_visibility_eq!(context, "B#foo()", Visibility::Private); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B#foo()"), 1); + assert_eq!(method_required_parameter_names(&context, "B#foo()"), ["x"]); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 1); + assert_eq!(method_required_parameter_names(&context, "B::#foo()"), ["x"]); + } + + #[test] + fn inherited_private_retroactive_module_function_creates_only_singleton_copy() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x); end + private :foo + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module B + include A + module_function :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "B#foo()"); + assert_declaration_exists!(context, "B::#foo()"); + assert_visibility_eq!(context, "A#foo()", Visibility::Private); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 1); + assert_eq!(method_required_parameter_names(&context, "B::#foo()"), ["x"]); + } + + #[test] + fn inherited_retroactive_module_function_uses_private_visibility_at_call_site() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module A + def foo; end + private :foo + end + + module B + include A + module_function :foo + end + + module A + def foo(x); end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "B#foo()"); + assert_declaration_exists!(context, "B::#foo()"); + assert_visibility_eq!(context, "A#foo()", Visibility::Public); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(latest_method_required_parameter_count(&context, "A#foo()"), 1); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 0); + } + + #[test] + fn inherited_retroactive_module_function_uses_public_visibility_at_call_site() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module A + def foo(x); end + end + + module B + include A + module_function :foo + end + + module A + private :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "B#foo()"); + assert_declaration_exists!(context, "B::#foo()"); + assert_visibility_eq!(context, "A#foo()", Visibility::Private); + assert_visibility_eq!(context, "B#foo()", Visibility::Private); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B#foo()"), 1); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 1); + } + + #[test] + fn inherited_retroactive_module_function_uses_alias_visibility_snapshot() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module A + def foo(x); end + private :foo + alias bar foo + public :foo + end + + module B + include A + module_function :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "B#bar()"); + assert_declaration_exists!(context, "B::#bar()"); + assert_visibility_eq!(context, "A#foo()", Visibility::Public); + assert_visibility_eq!(context, "B::#bar()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B::#bar()"), 1); + assert_eq!(method_required_parameter_names(&context, "B::#bar()"), ["x"]); + } + + #[test] + fn inherited_retroactive_module_function_copy_updates_when_source_method_changes() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x); end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module B + include A + module_function :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_eq!(method_required_parameter_count(&context, "B#foo()"), 1); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 1); + + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x, y); end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "B#foo()", Visibility::Private); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B#foo()"), 2); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 2); + } + + #[test] + fn chained_inherited_retroactive_module_function_copy_updates_when_root_source_changes() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x); end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module B + include A + module_function :foo + end + ", + ); + context.index_uri( + "file:///c.rb", + r" + module C + include B + module_function :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "B#foo()"); + assert_declaration_does_not_exist!(context, "C#foo()"); + assert_eq!(method_required_parameter_count(&context, "B#foo()"), 1); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 1); + assert_eq!(method_required_parameter_count(&context, "C::#foo()"), 1); + + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x, y); end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "B#foo()", Visibility::Private); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_declaration_does_not_exist!(context, "C#foo()"); + assert_visibility_eq!(context, "C::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B#foo()"), 2); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 2); + assert_eq!(method_required_parameter_count(&context, "C::#foo()"), 2); + } + + #[test] + fn inherited_retroactive_module_function_copy_updates_when_ancestor_chain_changes() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x); end + end + ", + ); + context.index_uri( + "file:///c.rb", + r" + module C + def foo(x, y); end + end + ", + ); + context.index_uri( + "file:///m.rb", + r" + module M + include A + end + ", + ); + context.index_uri( + "file:///z_b.rb", + r" + module B + include M + module_function :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_eq!(method_required_parameter_count(&context, "B#foo()"), 1); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 1); + + context.index_uri( + "file:///m.rb", + r" + module M + include A + include C + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "B#foo()", Visibility::Private); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B#foo()"), 2); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 2); + } + + #[test] + fn inherited_retroactive_module_function_copy_updates_when_higher_priority_ancestor_method_is_added() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///low.rb", + r" + module Low + def foo(x); end + end + ", + ); + context.index_uri( + "file:///high.rb", + r" + module High + include Low + end + ", + ); + context.index_uri( + "file:///child.rb", + r" + module Child + include High + module_function :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_eq!(method_required_parameter_count(&context, "Child#foo()"), 1); + assert_eq!(method_required_parameter_count(&context, "Child::#foo()"), 1); + + context.index_uri( + "file:///high.rb", + r" + module High + include Low + def foo(x, y); end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + assert_visibility_eq!(context, "Child::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Child#foo()"), 2); + assert_eq!(method_required_parameter_count(&context, "Child::#foo()"), 2); + } + + #[test] + fn inherited_retroactive_module_function_copy_clears_when_source_method_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x); end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module B + include A + module_function :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "B#foo()"); + assert_declaration_exists!(context, "B::#foo()"); + + context.index_uri( + "file:///a.rb", + r" + module A + end + ", + ); + context.resolve(); + + assert_declaration_does_not_exist!(context, "B#foo()"); + assert_declaration_does_not_exist!(context, "B::#foo()"); + assert_diagnostics_eq!( + &context, + vec!["undefined-method-visibility-target: undefined method `B#foo()` for visibility change (3:20-3:23)"] + ); + } + + #[test] + fn inherited_retroactive_module_function_copy_appears_when_source_method_is_added() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module A + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module B + include A + module_function :foo + end + ", + ); + context.resolve(); + + assert_declaration_does_not_exist!(context, "B#foo()"); + assert_declaration_does_not_exist!(context, "B::#foo()"); + assert_diagnostics_eq!( + &context, + vec!["undefined-method-visibility-target: undefined method `B#foo()` for visibility change (3:20-3:23)"] + ); + + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x); end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "B#foo()"); + assert_declaration_exists!(context, "B::#foo()"); + assert_visibility_eq!(context, "B#foo()", Visibility::Private); + assert_visibility_eq!(context, "B::#foo()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "B::#foo()"), 1); + } + + #[test] + fn inherited_retroactive_module_function_copy_clears_when_visibility_call_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + module A + def foo(x); end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + module B + include A + module_function :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "B#foo()"); + assert_declaration_exists!(context, "B::#foo()"); + + context.index_uri( + "file:///b.rb", + r" + module B + include A + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "B#foo()"); + assert_declaration_does_not_exist!(context, "B::#foo()"); + } + + #[test] + fn retroactive_module_function_before_method_definition_is_invalid() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + module_function :bar + def bar; end + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + &context, + vec!["undefined-method-visibility-target: undefined method `Foo#bar()` for visibility change (2:20-2:23)"] + ); + assert_declaration_does_not_exist!(context, "Foo::#bar()"); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Public); + } + + #[test] + fn retroactive_module_function_attr_writer_requires_setter_name() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + attr_writer :bar + module_function :bar + module_function :bar= + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + &context, + vec!["undefined-method-visibility-target: undefined method `Foo#bar()` for visibility change (3:20-3:23)"] + ); + assert_declaration_does_not_exist!(context, "Foo::#bar()"); + assert_declaration_exists!(context, "Foo#bar=()"); + assert_declaration_exists!(context, "Foo::#bar=()"); + assert_visibility_eq!(context, "Foo#bar=()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar=()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar=()"), 1); + assert_eq!( + method_required_parameter_names(&context, "Foo::#bar=()"), + vec!["__rubydex_arg0".to_string()] + ); + } + + #[test] + fn retroactive_module_function_attr_accessor_can_copy_reader_and_setter() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + attr_accessor :bar + module_function :bar + module_function :bar= + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Foo::#bar()"); + assert_declaration_exists!(context, "Foo::#bar=()"); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + assert_visibility_eq!(context, "Foo#bar=()", Visibility::Private); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + assert_visibility_eq!(context, "Foo::#bar=()", Visibility::Public); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar()"), 0); + assert_eq!(method_required_parameter_count(&context, "Foo::#bar=()"), 1); + assert_eq!( + method_required_parameter_names(&context, "Foo::#bar=()"), + vec!["__rubydex_arg0".to_string()] + ); + } + + #[test] + fn retroactive_module_function_copy_clears_when_call_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Foo + def bar; end + module_function :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Foo::#bar()"); + + context.index_uri( + "file:///foo.rb", + r" + module Foo + def bar; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "Foo::#bar()"); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Public); + } + + #[test] + fn retroactive_visibility_on_attr_methods() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + attr_reader :reader_method + private :reader_method + + attr_writer :writer_method + protected :writer_method= + + attr_accessor :accessor_method + private :accessor_method + private :accessor_method= + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#reader_method()", Visibility::Private); + assert_visibility_eq!(context, "Foo#writer_method=()", Visibility::Protected); + assert_visibility_eq!(context, "Foo#accessor_method()", Visibility::Private); + assert_visibility_eq!(context, "Foo#accessor_method=()", Visibility::Private); + } + + #[test] + fn retroactive_visibility_on_inherited_method() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Parent + def foo; end + end + + class Child < Parent + private :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Child#foo()"); + assert_members_eq!(context, "Child", ["foo()"]); + assert_owner_eq!(context, "Child#foo()", "Child"); + assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + assert_visibility_eq!(context, "Parent#foo()", Visibility::Public); + } + + #[test] + fn retroactive_visibility_on_grandparent_method() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class GrandParent + def greet; end + end + + class Parent < GrandParent; end + + class Child < Parent + private :greet + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_owner_eq!(context, "Child#greet()", "Child"); + assert_visibility_eq!(context, "Child#greet()", Visibility::Private); + } + + #[test] + fn retroactive_visibility_on_included_module_method() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + module Greetable + def greet; end + end + + class Foo + include Greetable + private :greet + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_owner_eq!(context, "Foo#greet()", "Foo"); + assert_visibility_eq!(context, "Foo#greet()", Visibility::Private); + } + + #[test] + fn retroactive_visibility_on_undefined_method_emits_diagnostic() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + private :nonexistent + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &[ + "undefined-method-visibility-target: undefined method `Foo#nonexistent()` for visibility change (2:12-2:23)" + ] + ); + } + + #[test] + fn retroactive_visibility_before_method_definition_emits_diagnostic() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + private :bar + def bar; end + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &["undefined-method-visibility-target: undefined method `Foo#bar()` for visibility change (2:12-2:15)"] + ); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Public); + } + + #[test] + fn runtime_method_visibility_call_in_method_body_does_not_apply_statically() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + def toggle + private :bar + end + + def bar; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Public); + } + + #[test] + fn runtime_method_visibility_call_in_block_does_not_apply_statically() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + proc do + private :bar + end + + def bar; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Public); + } + + #[test] + fn retroactive_visibility_detaches_when_direct_source_method_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a_foo.rb", + r" + class Foo + def bar; end + end + ", + ); + context.index_uri( + "file:///b_visibility.rb", + r" + class Foo + private :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + + context.index_uri( + "file:///a_foo.rb", + r" + class Foo + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &["undefined-method-visibility-target: undefined method `Foo#bar()` for visibility change (2:12-2:15)"] + ); + assert_declaration_does_not_exist!(context, "Foo#bar()"); + } + + #[test] + fn inherited_retroactive_visibility_resolves_when_ancestor_method_is_added() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///m.rb", + r" + module M + end + ", + ); + context.index_uri( + "file:///child.rb", + r" + module Child + include M + private :foo + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &["undefined-method-visibility-target: undefined method `Child#foo()` for visibility change (3:12-3:15)"] + ); + + context.index_uri( + "file:///m.rb", + r" + module M + def foo; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + } + + #[test] + fn inherited_retroactive_visibility_requeues_when_ancestor_method_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///m.rb", + r" + module M + def foo; end + end + ", + ); + context.index_uri( + "file:///child.rb", + r" + module Child + include M + private :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + + context.index_uri( + "file:///m.rb", + r" + module M + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &["undefined-method-visibility-target: undefined method `Child#foo()` for visibility change (3:12-3:15)"] + ); + assert_declaration_does_not_exist!(context, "Child#foo()"); + } + + #[test] + fn retroactive_visibility_undefined_target_diagnostic_clears_when_visibility_file_deleted() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///bad.rb", + r" + private :missing + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &[ + "undefined-method-visibility-target: undefined method `Object#missing()` for visibility change (1:10-1:17)" + ] + ); + + context.delete_uri("file:///bad.rb"); + context.resolve(); + + assert_no_diagnostics!(&context); + } + + #[test] + fn retroactive_visibility_across_reopened_class() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + class Foo + def bar; end + end + ", + ); + context.index_uri( + "file:///b.rb", + r" + class Foo + private :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + } + + #[test] + fn retroactive_visibility_resolves_when_ancestor_discovered_incrementally() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///child.rb", + r" + class Child + include M + private :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "Child#foo()"); + + context.index_uri( + "file:///a_module.rb", + r" + module M + def foo; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Child#foo()"); + assert_owner_eq!(context, "Child#foo()", "Child"); + assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + } + + #[test] + fn retroactive_visibility_resolves_when_known_ancestor_method_is_added_incrementally() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a_parent.rb", + r" + class Parent + end + ", + ); + context.index_uri( + "file:///b_child.rb", + r" + class Child < Parent + private :foo + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &["undefined-method-visibility-target: undefined method `Child#foo()` for visibility change (2:12-2:15)"] + ); + assert_declaration_does_not_exist!(context, "Child#foo()"); + + context.index_uri( + "file:///a_parent.rb", + r" + class Parent + def foo; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Child#foo()"); + assert_owner_eq!(context, "Child#foo()", "Child"); + assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + } + + #[test] + fn retroactive_visibility_allows_cross_file_source_after_visibility_uri() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///z_source.rb", + r" + class Foo + def bar; end + end + ", + ); + context.index_uri( + "file:///a_visibility.rb", + r" + class Foo + private :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo#bar()", Visibility::Private); + } + + #[test] + fn retroactive_visibility_snapshot_prefers_same_file_visibility_over_cross_file_tie_breaker() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///z_source.rb", + r" + module A + def bar; end + public :bar + end + ", + ); + context.index_uri( + "file:///m_visibility.rb", + r" + module A + def bar; end + private :bar + end + + module B + include A + module_function :bar + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "B#bar()"); + assert_visibility_eq!(context, "B::#bar()", Visibility::Public); + } + + #[test] + fn retroactive_visibility_detaches_when_inherited_source_method_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a_parent.rb", + r" + class Parent + def foo; end + end + ", + ); + context.index_uri( + "file:///b_child.rb", + r" + class Child < Parent + private :foo + public :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Child#foo()"); + assert_owner_eq!(context, "Child#foo()", "Child"); + assert_visibility_eq!(context, "Child#foo()", Visibility::Public); + + context.index_uri( + "file:///a_parent.rb", + r" + class Parent + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &[ + "undefined-method-visibility-target: undefined method `Child#foo()` for visibility change (2:12-2:15)", + "undefined-method-visibility-target: undefined method `Child#foo()` for visibility change (3:11-3:14)", + ] + ); + assert_declaration_does_not_exist!(context, "Child#foo()"); + } + + #[test] + fn applied_visibility_index_is_cleaned_when_visibility_document_is_reindexed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///visibility.rb", + r" + class Old + def foo; end + private :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Old#foo()", Visibility::Private); + context.graph().assert_method_visibility_indexes_consistent(); + + context.index_uri( + "file:///visibility.rb", + r" + class New + def foo; end + private :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_does_not_exist!(context, "Old#foo()"); + assert_visibility_eq!(context, "New#foo()", Visibility::Private); + context.graph().assert_method_visibility_indexes_consistent(); + + context.index_uri( + "file:///old.rb", + r" + class Old + def foo; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Old#foo()", Visibility::Public); + assert_visibility_eq!(context, "New#foo()", Visibility::Private); + context.graph().assert_method_visibility_indexes_consistent(); + } + + #[test] + fn retroactive_visibility_detaches_when_inherited_alias_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a_mixin.rb", + r" + module Mixin + def original; end + alias copied original + end + ", + ); + context.index_uri( + "file:///b_child.rb", + r" + class Child + include Mixin + private :copied + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Child#copied()"); + assert_owner_eq!(context, "Child#copied()", "Child"); + assert_visibility_eq!(context, "Child#copied()", Visibility::Private); + + context.index_uri( + "file:///a_mixin.rb", + r" + module Mixin + def original; end + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &[ + "undefined-method-visibility-target: undefined method `Child#copied()` for visibility change (3:12-3:18)" + ] + ); + assert_declaration_does_not_exist!(context, "Child#copied()"); + } + + #[test] + fn retroactive_visibility_detaches_when_included_source_method_is_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a_mixin.rb", + r" + module Mixin + def foo; end + end + ", + ); + context.index_uri( + "file:///b_child.rb", + r" + class Child + include Mixin + private :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Child#foo()"); + assert_owner_eq!(context, "Child#foo()", "Child"); + assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + + context.index_uri( + "file:///a_mixin.rb", + r" + module Mixin + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &["undefined-method-visibility-target: undefined method `Child#foo()` for visibility change (3:12-3:15)"] + ); + assert_declaration_does_not_exist!(context, "Child#foo()"); + } + + #[test] + fn retroactive_visibility_detaches_when_include_chain_stops_providing_method() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a_sources.rb", + r" + module Source + def foo; end + end + + module Empty + end + ", + ); + context.index_uri( + "file:///b_mixin.rb", + r" + module Mixin + include Source + end + ", + ); + context.index_uri( + "file:///c_child.rb", + r" + class Child + include Mixin + private :foo + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_declaration_exists!(context, "Child#foo()"); + assert_owner_eq!(context, "Child#foo()", "Child"); + assert_visibility_eq!(context, "Child#foo()", Visibility::Private); + + context.index_uri( + "file:///b_mixin.rb", + r" + module Mixin + include Empty + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &["undefined-method-visibility-target: undefined method `Child#foo()` for visibility change (3:12-3:15)"] + ); + assert_declaration_does_not_exist!(context, "Child#foo()"); + } + + #[test] + fn retroactive_constant_visibility_on_direct_member() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + BAR = 1 + private_constant :BAR + + BAZ = 2 + public_constant :BAZ + + QUX = 3 + + class Inner; end + private_constant :Inner + + module InnerMod; end + private_constant :InnerMod + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::BAR", Visibility::Private); + assert_visibility_eq!(context, "Foo::BAZ", Visibility::Public); + assert_visibility_eq!(context, "Foo::QUX", Visibility::Public); + assert_visibility_eq!(context, "Foo::Inner", Visibility::Private); + assert_visibility_eq!(context, "Foo::InnerMod", Visibility::Private); + } + + #[test] + fn runtime_constant_visibility_call_in_block_does_not_apply_statically() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + BAR = 1 + + proc do + private_constant :BAR + end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::BAR", Visibility::Public); + } + + #[test] + fn retroactive_constant_visibility_via_qualified_receiver() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + BAR = 1 + BAZ = 2 + end + + ALIAS = Foo + Foo.private_constant :BAR + ALIAS.private_constant :BAZ + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::BAR", Visibility::Private); + assert_visibility_eq!(context, "Foo::BAZ", Visibility::Private); + } + + #[test] + fn retroactive_constant_visibility_multi_arg_undefined_emits_per_name_diagnostic() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + private_constant :NOPE_ONE, :NOPE_TWO + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &[ + "undefined-constant-visibility-target: undefined constant `NOPE_ONE` for visibility change in `Foo` (2:21-2:29)", + "undefined-constant-visibility-target: undefined constant `NOPE_TWO` for visibility change in `Foo` (2:32-2:40)", + ] + ); + } + + #[test] + fn retroactive_constant_visibility_inherited_constant_emits_diagnostic() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Parent + CONST = 1 + end + + class Child < Parent + private_constant :CONST + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &[ + "undefined-constant-visibility-target: undefined constant `CONST` for visibility change in `Child` (6:21-6:26)" + ] + ); + assert_visibility_eq!(context, "Parent::CONST", Visibility::Public); + } + + #[test] + fn retroactive_constant_visibility_clears_when_call_removed() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + BAR = 1 + end + ", + ); + context.index_uri( + "file:///vis.rb", + r" + Foo.private_constant :BAR + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::BAR", Visibility::Private); + + context.delete_uri("file:///vis.rb"); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::BAR", Visibility::Public); + } + + #[test] + fn retroactive_constant_visibility_inside_singleton_class_body() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + class << self + BAR = 1 + private_constant :BAR + end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::::BAR", Visibility::Private); + } + + #[test] + fn retroactive_constant_visibility_persists_across_reopened_class() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///a.rb", + r" + class Foo + BAR = 1 + private_constant :BAR end ", ); @@ -5393,6 +7455,29 @@ mod visibility_resolution_tests { ); } + #[test] + fn retroactive_singleton_method_visibility_before_method_definition_emits_diagnostic() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + private_class_method :bar + def self.bar; end + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &[ + "undefined-method-visibility-target: undefined method `Foo::#bar()` for visibility change (2:25-2:28)" + ] + ); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + } + #[test] fn retroactive_singleton_method_visibility_undefined_target_diagnostic_clears_when_file_deleted() { let mut context = GraphTest::new(); @@ -5461,6 +7546,82 @@ mod visibility_resolution_tests { assert_visibility_eq!(context, "Foo::#missing()", Visibility::Private); } + #[test] + fn retroactive_singleton_method_visibility_resolves_when_target_added_incrementally() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///b_visibility.rb", + r" + module Foo + private_class_method :bar + end + ", + ); + context.resolve(); + + assert_diagnostics_eq!( + context, + &[ + "undefined-method-visibility-target: undefined method `Foo::#bar()` for visibility change (2:25-2:28)" + ] + ); + + context.index_uri( + "file:///a_method.rb", + r" + module Foo + def self.bar; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Private); + } + + #[test] + fn runtime_singleton_method_visibility_call_in_method_body_does_not_apply_statically() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + def toggle + private_class_method :bar + end + + def self.bar; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + } + + #[test] + fn runtime_singleton_method_visibility_call_in_block_does_not_apply_statically() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + r" + class Foo + proc do + private_class_method :bar + end + + def self.bar; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_visibility_eq!(context, "Foo::#bar()", Visibility::Public); + } + #[test] fn retroactive_singleton_method_visibility_inline_def() { let mut context = GraphTest::new(); diff --git a/test/declaration_test.rb b/test/declaration_test.rb index d93562391..813687e2b 100644 --- a/test/declaration_test.rb +++ b/test/declaration_test.rb @@ -994,6 +994,24 @@ def bar; end end end + def test_retroactive_module_function_visibility + with_context do |context| + context.write!("file1.rb", <<~RUBY) + module Foo + def bar; end + module_function :bar + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + assert_equal(:private, graph["Foo#bar()"].visibility) + assert_equal(:public, graph["Foo::#bar()"].visibility) + end + end + def test_visibility_predicates with_context do |context| context.write!("file1.rb", <<~RUBY) diff --git a/test/definition_test.rb b/test/definition_test.rb index 565c5b0b5..40ee82745 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -58,16 +58,17 @@ def bar; end assert_instance_of(Rubydex::ClassDefinition, defs[0]) assert_instance_of(Rubydex::ClassVariableDefinition, defs[1]) assert_instance_of(Rubydex::AttrAccessorDefinition, defs[2]) - assert_instance_of(Rubydex::AttrReaderDefinition, defs[3]) - assert_instance_of(Rubydex::AttrWriterDefinition, defs[4]) - assert_instance_of(Rubydex::ModuleDefinition, defs[5]) - assert_instance_of(Rubydex::ConstantAliasDefinition, defs[6]) - assert_instance_of(Rubydex::ConstantDefinition, defs[7]) - assert_instance_of(Rubydex::MethodDefinition, defs[8]) - assert_instance_of(Rubydex::GlobalVariableDefinition, defs[9]) - assert_instance_of(Rubydex::InstanceVariableDefinition, defs[10]) - assert_instance_of(Rubydex::MethodAliasDefinition, defs[11]) - assert_instance_of(Rubydex::GlobalVariableAliasDefinition, defs[12]) + assert_instance_of(Rubydex::AttrWriterDefinition, defs[3]) + assert_instance_of(Rubydex::AttrReaderDefinition, defs[4]) + assert_instance_of(Rubydex::AttrWriterDefinition, defs[5]) + assert_instance_of(Rubydex::ModuleDefinition, defs[6]) + assert_instance_of(Rubydex::ConstantAliasDefinition, defs[7]) + assert_instance_of(Rubydex::ConstantDefinition, defs[8]) + assert_instance_of(Rubydex::MethodDefinition, defs[9]) + assert_instance_of(Rubydex::GlobalVariableDefinition, defs[10]) + assert_instance_of(Rubydex::InstanceVariableDefinition, defs[11]) + assert_instance_of(Rubydex::MethodAliasDefinition, defs[12]) + assert_instance_of(Rubydex::GlobalVariableAliasDefinition, defs[13]) end end @@ -466,6 +467,58 @@ def self.bar(...); end end end + def test_module_function_attr_writer_signature_uses_synthetic_parameter_name + with_context do |context| + context.write!("file1.rb", <<~RUBY) + module Foo + attr_writer :bar + module_function :bar= + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + sig = graph["Foo::#bar=()"].definitions.first.signatures[0] + assert_equal(1, sig.parameters.length) + + sig.parameters[0].tap do |param| + assert_instance_of(Rubydex::Signature::PositionalParameter, param) + assert_equal(:__rubydex_arg0, param.name) + end + end + end + + def test_inherited_module_function_instance_and_singleton_signatures + with_context do |context| + context.write!("a.rb", <<~RUBY) + module A + def foo(x); end + end + RUBY + context.write!("b.rb", <<~RUBY) + module B + include A + module_function :foo + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + instance_def = graph["B#foo()"].definitions.find { |definition| definition.is_a?(Rubydex::MethodDefinition) } + singleton_def = graph["B::#foo()"].definitions.find { |definition| definition.is_a?(Rubydex::MethodDefinition) } + + refute_nil(instance_def) + refute_nil(singleton_def) + + assert_equal([:x], instance_def.signatures[0].parameters.map(&:name)) + assert_equal([:x], singleton_def.signatures[0].parameters.map(&:name)) + end + end + def test_method_definition_signatures_rbs with_context do |context| context.write!("foo.rbs", <<~RBS)