diff --git a/rust/rubydex-mcp/src/server.rs b/rust/rubydex-mcp/src/server.rs index 239aed38..c533c69a 100644 --- a/rust/rubydex-mcp/src/server.rs +++ b/rust/rubydex-mcp/src/server.rs @@ -54,7 +54,11 @@ impl RubydexServer { } let mut graph = Graph::new(); - let errors = rubydex::indexing::index_files(&mut graph, file_paths); + let errors = rubydex::indexing::index_files( + &mut graph, + file_paths, + rubydex::indexing::IndexerBackend::RubyIndexer, + ); for error in &errors { eprintln!("Indexing error: {error}"); } diff --git a/rust/rubydex-sys/src/graph_api.rs b/rust/rubydex-sys/src/graph_api.rs index 7b8ea4a4..7ec12451 100644 --- a/rust/rubydex-sys/src/graph_api.rs +++ b/rust/rubydex-sys/src/graph_api.rs @@ -233,7 +233,7 @@ pub unsafe extern "C" fn rdx_index_all( with_mut_graph(pointer, |graph| { let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, graph.excluded_paths()); - let indexing_errors = indexing::index_files(graph, file_paths); + let indexing_errors = indexing::index_files(graph, file_paths, indexing::IndexerBackend::RubyIndexer); let all_errors: Vec = listing_errors .into_iter() diff --git a/rust/rubydex/src/indexing.rs b/rust/rubydex/src/indexing.rs index c9e362cf..cfac3e81 100644 --- a/rust/rubydex/src/indexing.rs +++ b/rust/rubydex/src/indexing.rs @@ -3,6 +3,7 @@ use crate::{ indexing::{local_graph::LocalGraph, rbs_indexer::RBSIndexer, ruby_indexer::RubyIndexer}, job_queue::{Job, JobQueue}, model::graph::Graph, + operation::ruby_builder::RubyOperationBuilder, }; use crossbeam_channel::{Sender, unbounded}; use std::{ffi::OsStr, fs, path::PathBuf, sync::Arc}; @@ -12,6 +13,15 @@ pub mod local_graph; pub mod rbs_indexer; pub mod ruby_indexer; +/// Which backend to use for indexing Ruby files. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IndexerBackend { + /// The original tree-walking indexer. + RubyIndexer, + /// The two-phase operation builder + applier pipeline. + OperationBuilder, +} + /// The language of a source document, used to dispatch to the appropriate indexer pub enum LanguageId { Ruby, @@ -42,15 +52,22 @@ impl LanguageId { /// Job that indexes a single file pub struct IndexingJob { path: PathBuf, + backend: IndexerBackend, local_graph_tx: Sender, errors_tx: Sender, } impl IndexingJob { #[must_use] - pub fn new(path: PathBuf, local_graph_tx: Sender, errors_tx: Sender) -> Self { + pub fn new( + path: PathBuf, + backend: IndexerBackend, + local_graph_tx: Sender, + errors_tx: Sender, + ) -> Self { Self { path, + backend, local_graph_tx, errors_tx, } @@ -84,7 +101,7 @@ impl Job for IndexingJob { }; let language = self.path.extension().map_or(LanguageId::Ruby, LanguageId::from); - let local_graph = build_local_graph(url.to_string(), &source, &language); + let local_graph = build_local_graph(url.to_string(), &source, &language, self.backend); self.local_graph_tx .send(local_graph) @@ -94,7 +111,7 @@ impl Job for IndexingJob { /// Indexes a single source string in memory, dispatching to the appropriate indexer based on `language_id`. pub fn index_source(graph: &mut Graph, uri: &str, source: &str, language_id: &LanguageId) { - let local_graph = build_local_graph(uri.to_string(), source, language_id); + let local_graph = build_local_graph(uri.to_string(), source, language_id, IndexerBackend::RubyIndexer); graph.consume_document_changes(local_graph); } @@ -103,7 +120,7 @@ pub fn index_source(graph: &mut Graph, uri: &str, source: &str, language_id: &La /// # Panics /// /// Will panic if the graph cannot be wrapped in an Arc> -pub fn index_files(graph: &mut Graph, paths: Vec) -> Vec { +pub fn index_files(graph: &mut Graph, paths: Vec, backend: IndexerBackend) -> Vec { let queue = Arc::new(JobQueue::new()); let (local_graphs_tx, local_graphs_rx) = unbounded(); let (errors_tx, errors_rx) = unbounded(); @@ -111,6 +128,7 @@ pub fn index_files(graph: &mut Graph, paths: Vec) -> Vec { for path in paths { queue.push(Box::new(IndexingJob::new( path, + backend, local_graphs_tx.clone(), errors_tx.clone(), ))); @@ -134,13 +152,21 @@ pub fn index_files(graph: &mut Graph, paths: Vec) -> Vec { } /// Indexes a source string using the appropriate indexer for the given language. -fn build_local_graph(uri: String, source: &str, language: &LanguageId) -> LocalGraph { +#[must_use] +pub fn build_local_graph(uri: String, source: &str, language: &LanguageId, backend: IndexerBackend) -> LocalGraph { match language { - LanguageId::Ruby => { - let mut indexer = RubyIndexer::new(uri, source); - indexer.index(); - indexer.local_graph() - } + LanguageId::Ruby => match backend { + IndexerBackend::RubyIndexer => { + let mut indexer = RubyIndexer::new(uri, source); + indexer.index(); + indexer.local_graph() + } + IndexerBackend::OperationBuilder => { + let builder = RubyOperationBuilder::new(uri, source); + let result = builder.build(); + crate::operation::applier::apply_operations(result) + } + }, LanguageId::Rbs => { let mut indexer = RBSIndexer::new(uri, source); indexer.index(); @@ -175,7 +201,7 @@ mod tests { let relative_to_pwd = &dots.join(absolute_path); let mut graph = Graph::new(); - let errors = index_files(&mut graph, vec![relative_to_pwd.clone()]); + let errors = index_files(&mut graph, vec![relative_to_pwd.clone()], IndexerBackend::RubyIndexer); assert!(errors.is_empty()); assert_eq!(graph.documents().len(), 2); @@ -196,7 +222,7 @@ mod tests { let uri = Url::from_file_path(&path).unwrap().to_string(); let mut graph = Graph::new(); - let errors = index_files(&mut graph, vec![path]); + let errors = index_files(&mut graph, vec![path], IndexerBackend::RubyIndexer); assert!(errors.is_empty(), "Expected no errors, got: {errors:#?}"); assert_eq!(6, graph.definitions().len()); diff --git a/rust/rubydex/src/indexing/local_graph.rs b/rust/rubydex/src/indexing/local_graph.rs index df29e6cc..7b652b33 100644 --- a/rust/rubydex/src/indexing/local_graph.rs +++ b/rust/rubydex/src/indexing/local_graph.rs @@ -206,6 +206,44 @@ impl LocalGraph { &self.name_dependents } + /// Creates a `LocalGraph` from pre-built parts (used by the operation applier pipeline). + #[must_use] + pub fn from_parts( + uri_id: UriId, + document: Document, + strings: IdentityHashMap, + names: IdentityHashMap, + ) -> Self { + let mut name_dependents: IdentityHashMap> = IdentityHashMap::default(); + for (name_id, name_ref) in &names { + if let NameRef::Unresolved(name) = name_ref { + if let Some(&parent_scope) = name.parent_scope().as_ref() { + name_dependents + .entry(parent_scope) + .or_default() + .push(NameDependent::ChildName(*name_id)); + } + if let Some(&nesting_id) = name.nesting().as_ref() { + name_dependents + .entry(nesting_id) + .or_default() + .push(NameDependent::NestedName(*name_id)); + } + } + } + + Self { + uri_id, + document, + definitions: IdentityHashMap::default(), + strings, + names, + constant_references: IdentityHashMap::default(), + method_references: IdentityHashMap::default(), + name_dependents, + } + } + // Into parts #[must_use] diff --git a/rust/rubydex/src/indexing/ruby_indexer.rs b/rust/rubydex/src/indexing/ruby_indexer.rs index ddd3c6f1..9562c1b8 100644 --- a/rust/rubydex/src/indexing/ruby_indexer.rs +++ b/rust/rubydex/src/indexing/ruby_indexer.rs @@ -2481,5 +2481,11 @@ impl Visit<'_> for RubyIndexer<'_> { } #[cfg(test)] +fn backend() -> super::IndexerBackend { + super::IndexerBackend::RubyIndexer +} + +#[cfg(test)] +#[allow(clippy::duplicate_mod)] #[path = "ruby_indexer_tests.rs"] mod tests; diff --git a/rust/rubydex/src/indexing/ruby_indexer_tests.rs b/rust/rubydex/src/indexing/ruby_indexer_tests.rs index db79b77a..58d71c3a 100644 --- a/rust/rubydex/src/indexing/ruby_indexer_tests.rs +++ b/rust/rubydex/src/indexing/ruby_indexer_tests.rs @@ -1,3 +1,7 @@ +// This file is included via #[path] by both ruby_indexer.rs and operation/applier.rs +// to run the same tests against both indexing backends. Each parent module provides +// a `backend()` function that `index_source` calls via `super::backend()`. + use crate::{ assert_def_comments_eq, assert_def_mixins_eq, assert_def_name_eq, assert_def_name_offset_eq, assert_def_str_eq, assert_def_superclass_ref_eq, assert_definition_at, assert_dependents, assert_local_diagnostics_eq, @@ -86,7 +90,7 @@ macro_rules! assert_method_references_eq { } fn index_source(source: &str) -> LocalGraphTest { - LocalGraphTest::new("file:///foo.rb", source) + LocalGraphTest::new_with_backend("file:///foo.rb", source, super::backend()) } mod constant_tests { diff --git a/rust/rubydex/src/lib.rs b/rust/rubydex/src/lib.rs index 3a468ef5..52275e30 100644 --- a/rust/rubydex/src/lib.rs +++ b/rust/rubydex/src/lib.rs @@ -7,6 +7,7 @@ pub mod job_queue; pub mod listing; pub mod model; pub mod offset; +pub mod operation; pub mod position; pub mod query; pub mod resolution; diff --git a/rust/rubydex/src/main.rs b/rust/rubydex/src/main.rs index 468a4aaf..82ab5b2e 100644 --- a/rust/rubydex/src/main.rs +++ b/rust/rubydex/src/main.rs @@ -2,7 +2,8 @@ use clap::{Parser, ValueEnum}; use std::{collections::HashSet, mem}; use rubydex::{ - indexing, integrity, listing, + indexing::{self, IndexerBackend}, + integrity, listing, model::graph::Graph, resolution::Resolver, stats::{ @@ -31,6 +32,14 @@ struct Args { #[arg(long = "check-integrity", help = "Check the integrity of the graph after resolution")] check_integrity: bool, + #[arg( + long = "indexer", + value_enum, + default_value = "ruby-indexer", + help = "Which indexer backend to use for Ruby files" + )] + indexer: Indexer, + #[arg( long = "report-orphans", value_name = "PATH", @@ -49,6 +58,21 @@ enum StopAfter { Resolution, } +#[derive(Debug, Clone, ValueEnum)] +enum Indexer { + RubyIndexer, + OperationBuilder, +} + +impl From<&Indexer> for IndexerBackend { + fn from(indexer: &Indexer) -> Self { + match indexer { + Indexer::RubyIndexer => IndexerBackend::RubyIndexer, + Indexer::OperationBuilder => IndexerBackend::OperationBuilder, + } + } +} + fn exit(print_stats: bool) { if print_stats { Timer::print_breakdown(); @@ -80,7 +104,8 @@ fn main() { // Indexing let mut graph = Graph::new(); - let errors = time_it!(indexing, { indexing::index_files(&mut graph, file_paths) }); + let backend = IndexerBackend::from(&args.indexer); + let errors = time_it!(indexing, { indexing::index_files(&mut graph, file_paths, backend) }); for error in errors { eprintln!("{error}"); diff --git a/rust/rubydex/src/model/definitions.rs b/rust/rubydex/src/model/definitions.rs index 06b572a1..d4cb5d7c 100644 --- a/rust/rubydex/src/model/definitions.rs +++ b/rust/rubydex/src/model/definitions.rs @@ -29,7 +29,7 @@ use crate::{ assert_mem_size, model::{ comment::Comment, - ids::{ConstantReferenceId, DefinitionId, NameId, StringId, UriId}, + ids::{self, ConstantReferenceId, DefinitionId, NameId, StringId, UriId}, visibility::Visibility, }, offset::Offset, @@ -289,7 +289,7 @@ impl ClassDefinition { #[must_use] pub fn id(&self) -> DefinitionId { - DefinitionId::from(&format!("{}{}{}", *self.uri_id, self.offset.start(), *self.name_id)) + ids::namespace_definition_id(self.uri_id, &self.offset, self.name_id) } #[must_use] @@ -411,7 +411,7 @@ impl SingletonClassDefinition { #[must_use] pub fn id(&self) -> DefinitionId { - DefinitionId::from(&format!("{}{}{}", *self.uri_id, self.offset.start(), *self.name_id)) + ids::namespace_definition_id(self.uri_id, &self.offset, self.name_id) } #[must_use] @@ -515,7 +515,7 @@ impl ModuleDefinition { #[must_use] pub fn id(&self) -> DefinitionId { - DefinitionId::from(&format!("{}{}{}", *self.uri_id, self.offset.start(), *self.name_id)) + ids::namespace_definition_id(self.uri_id, &self.offset, self.name_id) } #[must_use] @@ -611,7 +611,7 @@ impl ConstantDefinition { #[must_use] pub fn id(&self) -> DefinitionId { - DefinitionId::from(&format!("{}{}{}", *self.uri_id, self.offset.start(), *self.name_id)) + ids::namespace_definition_id(self.uri_id, &self.offset, self.name_id) } #[must_use] @@ -926,7 +926,7 @@ pub struct MethodDefinition { assert_mem_size!(MethodDefinition, 96); /// The receiver of a singleton method definition. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Receiver { /// `def self.foo` - receiver is the enclosing definition (class, module, singleton class or DSL) SelfReceiver(DefinitionId), @@ -965,18 +965,7 @@ impl MethodDefinition { #[must_use] pub fn id(&self) -> DefinitionId { - let mut formatted_id = format!("{}{}{}", *self.uri_id, self.offset.start(), *self.str_id); - - if let Some(receiver) = &self.receiver { - match receiver { - Receiver::SelfReceiver(def_id) => formatted_id.push_str(&def_id.to_string()), - Receiver::ConstantReceiver(name_id) => { - formatted_id.push_str(&name_id.to_string()); - } - } - } - - DefinitionId::from(&formatted_id) + ids::method_definition_id(self.uri_id, &self.offset, self.str_id, self.receiver.as_ref()) } #[must_use] diff --git a/rust/rubydex/src/model/ids.rs b/rust/rubydex/src/model/ids.rs index 98b0c15b..f782794b 100644 --- a/rust/rubydex/src/model/ids.rs +++ b/rust/rubydex/src/model/ids.rs @@ -1,4 +1,8 @@ -use crate::{assert_mem_size, model::id::Id}; +use crate::{ + assert_mem_size, + model::{definitions::Receiver, id::Id}, + offset::Offset, +}; #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] pub struct DeclarationMarker; @@ -12,6 +16,28 @@ pub struct DefinitionMarker; pub type DefinitionId = Id; assert_mem_size!(DefinitionId, 8); +#[must_use] +pub fn namespace_definition_id(uri_id: UriId, offset: &Offset, name_id: NameId) -> DefinitionId { + DefinitionId::from(&format!("{}{}{}", *uri_id, offset.start(), *name_id)) +} + +#[must_use] +pub fn method_definition_id( + uri_id: UriId, + offset: &Offset, + str_id: StringId, + receiver: Option<&Receiver>, +) -> DefinitionId { + let mut formatted_id = format!("{}{}{}", *uri_id, offset.start(), *str_id); + if let Some(receiver) = receiver { + match receiver { + Receiver::SelfReceiver(def_id) => formatted_id.push_str(&def_id.to_string()), + Receiver::ConstantReceiver(name_id) => formatted_id.push_str(&name_id.to_string()), + } + } + DefinitionId::from(&formatted_id) +} + #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] pub struct UriMarker; // UriId represents the ID of a URI, which is the unique identifier for a document diff --git a/rust/rubydex/src/operation/applier.rs b/rust/rubydex/src/operation/applier.rs new file mode 100644 index 00000000..bf299772 --- /dev/null +++ b/rust/rubydex/src/operation/applier.rs @@ -0,0 +1,519 @@ +//! Converts an `OperationBuilderResult` into a `LocalGraph` by walking operations and creating definitions. +//! +//! This is the second phase of the two-phase operation pipeline: +//! 1. `RubyOperationBuilder` parses source → produces ordered operations +//! 2. `apply_operations` walks operations → creates definitions in a `LocalGraph` +//! +//! The applier maintains its own scope stack to derive `lexical_nesting_id` for definitions. +//! Operations carry only their own data; scope context comes from Enter/Exit scope operations. + +use std::collections::HashMap; + +use crate::indexing::local_graph::LocalGraph; +use crate::model::definitions::{ + AttrAccessorDefinition, AttrReaderDefinition, AttrWriterDefinition, ClassDefinition, ClassVariableDefinition, + ConstantAliasDefinition, ConstantDefinition, ConstantVisibilityDefinition, Definition, ExtendDefinition, + GlobalVariableAliasDefinition, GlobalVariableDefinition, IncludeDefinition, InstanceVariableDefinition, + MethodAliasDefinition, MethodDefinition, MethodVisibilityDefinition, Mixin, ModuleDefinition, PrependDefinition, + Receiver, SingletonClassDefinition, +}; +use crate::model::ids::{ConstantReferenceId, DefinitionId, NameId}; +use crate::model::references::{ConstantReference, MethodRef}; +use crate::model::visibility::Visibility; +use crate::operation::ruby_builder::OperationBuilderResult; +use crate::operation::{ + AliasConstant, AliasGlobalVariable, AliasMethod, AttrKind, DefineAttribute, DefineClassVariable, DefineConstant, + DefineGlobalVariable, DefineInstanceVariable, EnterClass, EnterMethod, EnterModule, EnterSingletonClass, MixinKind, + Operation, ReferenceConstant, ReferenceMethod, SetConstantVisibility, SetMethodVisibility, Target, +}; + +enum ApplierScope { + Namespace { + definition_id: DefinitionId, + is_lexical_scope: bool, + }, + Method { + definition_id: DefinitionId, + }, +} + +struct OperationApplier { + local_graph: LocalGraph, + scope_stack: Vec, + scope_visibility: HashMap, Visibility>, + // Maps the most recently emitted ReferenceConstant per name. The builder emits + // ReferenceConstant immediately before the operation that consumes it (Mixin, + // EnterClass superclass, SetConstantVisibility), so the last entry always wins. + constant_ref_ids: HashMap, +} + +impl OperationApplier { + fn current_owner_id(&self) -> Option { + self.scope_stack.iter().rev().find_map(|scope| match scope { + ApplierScope::Namespace { definition_id, .. } => Some(*definition_id), + ApplierScope::Method { .. } => None, + }) + } + + fn current_lexical_scope_id(&self) -> Option { + self.scope_stack.iter().rev().find_map(|scope| match scope { + ApplierScope::Namespace { + definition_id, + is_lexical_scope: true, + } => Some(*definition_id), + _ => None, + }) + } + + fn current_scope_id(&self) -> Option { + self.scope_stack.last().map(|scope| match scope { + ApplierScope::Namespace { definition_id, .. } | ApplierScope::Method { definition_id } => *definition_id, + }) + } + + fn resolve_receiver(&self, receiver: Option<&Target>) -> Option { + let current_owner_id = self.current_owner_id(); + match receiver { + Some(Target::ExplicitSelf) => current_owner_id.map(Receiver::SelfReceiver), + Some(Target::Constant(name_id)) => Some(Receiver::ConstantReceiver(*name_id)), + Some(Target::Other) | None => None, + } + } + + fn resolve_visibility(&self, has_receiver: bool) -> Visibility { + if has_receiver { + return Visibility::Public; + } + let scope = self.current_owner_id(); + let default = self + .scope_visibility + .get(&scope) + .copied() + .unwrap_or(if scope.is_none() { + Visibility::Private + } else { + Visibility::Public + }); + match default { + Visibility::ModuleFunction => Visibility::Private, + v => v, + } + } + + fn add_member(&mut self, owner_id: Option, member_id: DefinitionId) { + let Some(owner_id) = owner_id else { + return; + }; + + let Some(owner) = self.local_graph.get_definition_mut(owner_id) else { + return; + }; + + match owner { + Definition::Class(class) => class.add_member(member_id), + Definition::Module(module) => module.add_member(member_id), + Definition::SingletonClass(singleton) => singleton.add_member(member_id), + _ => {} + } + } +} + +impl OperationApplier { + fn apply_operation(&mut self, op: Operation) { + match op { + Operation::EnterClass(op) => self.apply_enter_class(op), + Operation::EnterModule(op) => self.apply_enter_module(op), + Operation::EnterSingletonClass(op) => self.apply_enter_singleton_class(op), + Operation::EnterMethod(op) => self.apply_enter_method(op), + Operation::ExitScope => { + debug_assert!(!self.scope_stack.is_empty(), "ExitScope with empty scope stack"); + self.scope_stack.pop(); + } + Operation::AliasMethod(op) => self.apply_alias_method(op), + Operation::SetMethodVisibility(op) => self.apply_set_method_visibility(op), + Operation::SetDefaultVisibility(op) => { + let scope = self.current_owner_id(); + self.scope_visibility.insert(scope, op.visibility); + } + Operation::DefineConstant(op) => self.apply_define_constant(op), + Operation::AliasConstant(op) => self.apply_alias_constant(op), + Operation::SetConstantVisibility(op) => self.apply_set_constant_visibility(op), + Operation::Mixin(ref op) => self.apply_mixin(op), + Operation::DefineAttribute(op) => self.apply_define_attribute(op), + Operation::DefineGlobalVariable(op) => self.apply_define_global_variable(op), + Operation::DefineInstanceVariable(op) => self.apply_define_instance_variable(op), + Operation::DefineClassVariable(op) => self.apply_define_class_variable(op), + Operation::AliasGlobalVariable(op) => self.apply_alias_global_variable(op), + Operation::ReferenceConstant(op) => self.apply_reference_constant(op), + Operation::ReferenceMethod(op) => self.apply_reference_method(op), + } + } + + fn apply_enter_class(&mut self, op: EnterClass) { + let lexical_nesting_id = self.current_lexical_scope_id(); + let superclass_ref = op.superclass_name.and_then(|n| self.constant_ref_ids.get(&n).copied()); + let def = ClassDefinition::new( + op.name_id, + op.uri_id, + op.offset, + op.name_offset, + op.comments, + op.flags, + lexical_nesting_id, + superclass_ref, + ); + let def_id = self.local_graph.add_definition(Definition::Class(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + self.scope_stack.push(ApplierScope::Namespace { + definition_id: def_id, + is_lexical_scope: op.is_lexical_scope, + }); + } + + fn apply_enter_module(&mut self, op: EnterModule) { + let lexical_nesting_id = self.current_lexical_scope_id(); + let def = ModuleDefinition::new( + op.name_id, + op.uri_id, + op.offset, + op.name_offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + let def_id = self.local_graph.add_definition(Definition::Module(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + self.scope_stack.push(ApplierScope::Namespace { + definition_id: def_id, + is_lexical_scope: op.is_lexical_scope, + }); + } + + fn apply_enter_singleton_class(&mut self, op: EnterSingletonClass) { + let lexical_nesting_id = self.current_lexical_scope_id(); + let def = SingletonClassDefinition::new( + op.name_id, + op.uri_id, + op.offset, + op.name_offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + let def_id = self + .local_graph + .add_definition(Definition::SingletonClass(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + self.scope_stack.push(ApplierScope::Namespace { + definition_id: def_id, + is_lexical_scope: true, + }); + } + + fn apply_enter_method(&mut self, op: EnterMethod) { + let lexical_nesting_id = self.current_owner_id(); + let has_receiver = op.receiver.is_some(); + let receiver = self.resolve_receiver(op.receiver.as_ref()); + let visibility = self.resolve_visibility(has_receiver); + let def = MethodDefinition::new( + op.str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + op.signatures, + visibility, + receiver, + ); + let def_id = self.local_graph.add_definition(Definition::Method(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + self.scope_stack.push(ApplierScope::Method { definition_id: def_id }); + } + + fn apply_alias_method(&mut self, op: AliasMethod) { + let lexical_nesting_id = self.current_owner_id(); + let receiver = self.resolve_receiver(op.receiver.as_ref()); + let def = MethodAliasDefinition::new( + op.new_name_str_id, + op.old_name_str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + receiver, + ); + let def_id = self.local_graph.add_definition(Definition::MethodAlias(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + } + + fn apply_set_method_visibility(&mut self, op: SetMethodVisibility) { + let lexical_nesting_id = self.current_owner_id(); + let def = MethodVisibilityDefinition::new( + op.str_id, + op.visibility, + op.uri_id, + op.offset, + Box::default(), + op.flags, + lexical_nesting_id, + ); + let def_id = self + .local_graph + .add_definition(Definition::MethodVisibility(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + } + + fn apply_define_constant(&mut self, op: DefineConstant) { + let lexical_nesting_id = self.current_lexical_scope_id(); + let def = ConstantDefinition::new( + op.name_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + let def_id = self.local_graph.add_definition(Definition::Constant(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + } + + fn apply_alias_constant(&mut self, op: AliasConstant) { + let lexical_nesting_id = self.current_lexical_scope_id(); + let constant = ConstantDefinition::new( + op.name_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + let def = ConstantAliasDefinition::new(op.target_name_id, constant); + let def_id = self + .local_graph + .add_definition(Definition::ConstantAlias(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + } + + fn apply_set_constant_visibility(&mut self, op: SetConstantVisibility) { + let lexical_nesting_id = self.current_owner_id(); + let receiver = match op.receiver { + Some(Target::Constant(name_id)) => Some(name_id), + Some(Target::ExplicitSelf | Target::Other) | None => None, + }; + let def = ConstantVisibilityDefinition::new( + receiver, + op.target, + op.visibility, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + let def_id = self + .local_graph + .add_definition(Definition::ConstantVisibility(Box::new(def))); + self.add_member(lexical_nesting_id, def_id); + } + + fn apply_mixin(&mut self, op: &crate::operation::Mixin) { + let Some(owner_id) = self.current_owner_id() else { + return; + }; + + let constant_reference_id = match op.target { + Target::Constant(name_id) => self.constant_ref_ids.get(&name_id).copied(), + Target::ExplicitSelf | Target::Other => None, + }; + + let Some(constant_reference_id) = constant_reference_id else { + return; + }; + + let mixin = match op.kind { + MixinKind::Include => Mixin::Include(IncludeDefinition::new(constant_reference_id)), + MixinKind::Prepend => Mixin::Prepend(PrependDefinition::new(constant_reference_id)), + MixinKind::Extend => Mixin::Extend(ExtendDefinition::new(constant_reference_id)), + }; + + if let Some(owner) = self.local_graph.get_definition_mut(owner_id) { + match owner { + Definition::Class(class) => class.add_mixin(mixin), + Definition::Module(module) => module.add_mixin(mixin), + Definition::SingletonClass(singleton) => singleton.add_mixin(mixin), + _ => {} + } + } + } + + fn apply_define_attribute(&mut self, op: DefineAttribute) { + let lexical_nesting_id = self.current_scope_id(); + let visibility = self.resolve_visibility(false); + let def_id = match op.kind { + AttrKind::Accessor => { + let def = AttrAccessorDefinition::new( + op.str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + visibility, + ); + self.local_graph.add_definition(Definition::AttrAccessor(Box::new(def))) + } + AttrKind::Reader => { + let def = AttrReaderDefinition::new( + op.str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + visibility, + ); + self.local_graph.add_definition(Definition::AttrReader(Box::new(def))) + } + AttrKind::Writer => { + let def = AttrWriterDefinition::new( + op.str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + visibility, + ); + self.local_graph.add_definition(Definition::AttrWriter(Box::new(def))) + } + }; + self.add_member(lexical_nesting_id, def_id); + } + + fn apply_define_global_variable(&mut self, op: DefineGlobalVariable) { + let lexical_nesting_id = self.current_scope_id(); + let member_owner_id = self.current_owner_id(); + let def = GlobalVariableDefinition::new( + op.str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + let def_id = self + .local_graph + .add_definition(Definition::GlobalVariable(Box::new(def))); + self.add_member(member_owner_id, def_id); + } + + fn apply_define_instance_variable(&mut self, op: DefineInstanceVariable) { + let lexical_nesting_id = self.current_scope_id(); + let member_owner_id = self.current_owner_id(); + let def = InstanceVariableDefinition::new( + op.str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + let def_id = self + .local_graph + .add_definition(Definition::InstanceVariable(Box::new(def))); + self.add_member(member_owner_id, def_id); + } + + fn apply_define_class_variable(&mut self, op: DefineClassVariable) { + let lexical_nesting_id = self.current_lexical_scope_id(); + let member_owner_id = self.current_owner_id(); + let def = ClassVariableDefinition::new( + op.str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + let def_id = self + .local_graph + .add_definition(Definition::ClassVariable(Box::new(def))); + self.add_member(member_owner_id, def_id); + } + + fn apply_alias_global_variable(&mut self, op: AliasGlobalVariable) { + let lexical_nesting_id = self.current_scope_id(); + let def = GlobalVariableAliasDefinition::new( + op.new_name_str_id, + op.old_name_str_id, + op.uri_id, + op.offset, + op.comments, + op.flags, + lexical_nesting_id, + ); + self.local_graph + .add_definition(Definition::GlobalVariableAlias(Box::new(def))); + } + + fn apply_reference_constant(&mut self, op: ReferenceConstant) { + let ref_id = self + .local_graph + .add_constant_reference(ConstantReference::new(op.name_id, op.uri_id, op.offset)); + self.constant_ref_ids.insert(op.name_id, ref_id); + } + + fn apply_reference_method(&mut self, op: ReferenceMethod) { + let receiver = match op.receiver { + Some(Target::Constant(name_id)) => Some(name_id), + Some(Target::ExplicitSelf | Target::Other) | None => None, + }; + self.local_graph + .add_method_reference(MethodRef::new(op.str_id, op.uri_id, op.offset, receiver)); + } +} + +/// Converts an `OperationBuilderResult` into a `LocalGraph`. +/// +/// Walks the operations in order, creating `Definition` objects and registering members/mixins. +/// Scope context is derived from the scope stack maintained by Enter/Exit operations. +#[must_use] +pub fn apply_operations(result: OperationBuilderResult) -> LocalGraph { + let OperationBuilderResult { + uri_id, + document, + operations, + strings, + names, + } = result; + + let mut applier = OperationApplier { + local_graph: LocalGraph::from_parts(uri_id, document, strings, names), + scope_stack: Vec::new(), + scope_visibility: HashMap::new(), + constant_ref_ids: HashMap::new(), + }; + + for op in operations { + applier.apply_operation(op); + } + + applier.local_graph +} + +#[cfg(test)] +fn backend() -> crate::indexing::IndexerBackend { + crate::indexing::IndexerBackend::OperationBuilder +} + +#[cfg(test)] +#[allow(clippy::duplicate_mod)] +#[path = "../indexing/ruby_indexer_tests.rs"] +mod applier_tests; + +#[cfg(test)] +#[allow(clippy::duplicate_mod)] +#[path = "../resolution_tests.rs"] +mod resolution_tests; diff --git a/rust/rubydex/src/operation/mod.rs b/rust/rubydex/src/operation/mod.rs new file mode 100644 index 00000000..797a5fef --- /dev/null +++ b/rust/rubydex/src/operation/mod.rs @@ -0,0 +1,284 @@ +//! Operations represent the ordered actions extracted from Ruby/RBS source code. +//! +//! Unlike definitions (which are unordered and declarative), operations are ordered and imperative. +//! They model what Ruby actually does at execution time: define a class, define a method, change +//! visibility, include a module, etc. +//! +//! Each operation is self-contained: it carries enough context (`name_id`, etc.) to know what it +//! defines, while scope context is provided by surrounding Enter/Exit scope operations. +//! +//! The builder produces a `Vec` for each file. These operations are then applied in order +//! by the applier to produce definitions and declarations in the graph. +//! +//! # Example +//! +//! ```ruby +//! class Foo +//! def bar; end +//! private :bar +//! end +//! ``` +//! +//! Produces the operations: +//! 1. `EnterClass` (name: Foo) +//! 2. `EnterMethod` (name: bar) +//! 3. `ExitScope` # exit method bar +//! 4. `SetMethodVisibility` (name: bar, visibility: private) +//! 5. `ExitScope` # exit class Foo + +pub mod applier; +pub mod printer; +pub mod ruby_builder; + +use crate::model::{ + comment::Comment, + definitions::{DefinitionFlags, Signatures}, + ids::{NameId, StringId, UriId}, + visibility::Visibility, +}; +use crate::offset::Offset; + +/// An ordered instruction extracted from Ruby/RBS source code. +/// +/// Operations are produced by the builder in the order they appear in the source file. +/// Scope context is established by Enter/Exit operations rather than carried on each variant. +#[derive(Debug)] +pub enum Operation { + /// Enter a class scope (`class Foo` or `Class.new`). + EnterClass(EnterClass), + /// Enter a module scope (`module Foo` or `Module.new`). + EnterModule(EnterModule), + /// Enter a singleton class scope (`class << self` or `class << Foo`). + EnterSingletonClass(EnterSingletonClass), + /// Enter a method scope (`def foo` or `def self.foo`). + EnterMethod(EnterMethod), + /// Exit the current scope (class, module, singleton class, or method). + ExitScope, + /// Alias a method (`alias new_name old_name` or `alias_method :new, :old`). + AliasMethod(AliasMethod), + /// Change visibility of a specific method (`private :foo`). + SetMethodVisibility(SetMethodVisibility), + /// Change the default visibility for subsequent method definitions (`private` with no args). + SetDefaultVisibility(SetDefaultVisibility), + /// Define a constant (`FOO = 1`). + DefineConstant(DefineConstant), + /// Define a constant alias (`ALIAS = OtherConstant`). + AliasConstant(AliasConstant), + /// Change visibility of constants (`private_constant :FOO` or `public_constant :BAR`). + SetConstantVisibility(SetConstantVisibility), + /// Include, prepend, or extend a module. + Mixin(Mixin), + /// Define an attribute (`attr_accessor :foo`, `attr_reader :bar`, `attr_writer :baz`). + DefineAttribute(DefineAttribute), + /// Define a global variable (`$foo = 1`). + DefineGlobalVariable(DefineGlobalVariable), + /// Define an instance variable (`@foo = 1`). + DefineInstanceVariable(DefineInstanceVariable), + /// Define a class variable (`@@foo = 1`). + DefineClassVariable(DefineClassVariable), + /// Alias a global variable (`alias $new $old`). + AliasGlobalVariable(AliasGlobalVariable), + /// Record a reference to a constant (for tracking usages). + ReferenceConstant(ReferenceConstant), + /// Record a reference to a method (for tracking usages). + ReferenceMethod(ReferenceMethod), +} + +/// A resolved target as it appears in source code. +/// +/// Used for method receivers (`def self.foo`), mixin targets (`include Foo`), +/// constant visibility (`Foo.private_constant`), and method references (`Foo.bar`). +#[derive(Debug, Clone, Copy)] +pub enum Target { + /// Explicit `self` (e.g. `def self.foo`, `extend self`). + ExplicitSelf, + /// A constant name (e.g. `def Foo.foo`, `include Foo`). + Constant(NameId), + /// An expression we don't resolve (e.g. `def expr.foo`). + Other, +} + +/// The kind of attribute definition. +#[derive(Debug, Clone, Copy)] +pub enum AttrKind { + Accessor, + Reader, + Writer, +} + +/// The kind of mixin operation. +#[derive(Debug, Clone, Copy)] +pub enum MixinKind { + Include, + Prepend, + Extend, +} + +#[derive(Debug)] +pub struct EnterClass { + pub name_id: NameId, + pub uri_id: UriId, + pub offset: Offset, + pub name_offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, + pub superclass_name: Option, + pub is_lexical_scope: bool, +} + +#[derive(Debug)] +pub struct EnterModule { + pub name_id: NameId, + pub uri_id: UriId, + pub offset: Offset, + pub name_offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, + pub is_lexical_scope: bool, +} + +#[derive(Debug)] +pub struct EnterSingletonClass { + pub name_id: NameId, + pub uri_id: UriId, + pub offset: Offset, + pub name_offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct EnterMethod { + pub str_id: StringId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, + pub signatures: Signatures, + pub receiver: Option, +} + +#[derive(Debug)] +pub struct AliasMethod { + pub new_name_str_id: StringId, + pub old_name_str_id: StringId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, + pub receiver: Option, +} + +#[derive(Debug)] +pub struct SetMethodVisibility { + pub str_id: StringId, + pub visibility: Visibility, + pub uri_id: UriId, + pub offset: Offset, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct SetDefaultVisibility { + pub visibility: Visibility, + pub uri_id: UriId, + pub offset: Offset, +} + +#[derive(Debug)] +pub struct DefineConstant { + pub name_id: NameId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct AliasConstant { + pub name_id: NameId, + pub target_name_id: NameId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct SetConstantVisibility { + pub receiver: Option, + pub target: StringId, + pub visibility: Visibility, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct Mixin { + pub kind: MixinKind, + pub target: Target, +} + +#[derive(Debug)] +pub struct DefineAttribute { + pub kind: AttrKind, + pub str_id: StringId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct DefineGlobalVariable { + pub str_id: StringId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct DefineInstanceVariable { + pub str_id: StringId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct DefineClassVariable { + pub str_id: StringId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct AliasGlobalVariable { + pub new_name_str_id: StringId, + pub old_name_str_id: StringId, + pub uri_id: UriId, + pub offset: Offset, + pub comments: Box<[Comment]>, + pub flags: DefinitionFlags, +} + +#[derive(Debug)] +pub struct ReferenceConstant { + pub name_id: NameId, + pub uri_id: UriId, + pub offset: Offset, +} + +#[derive(Debug)] +pub struct ReferenceMethod { + pub str_id: StringId, + pub uri_id: UriId, + pub offset: Offset, + pub receiver: Option, +} diff --git a/rust/rubydex/src/operation/printer.rs b/rust/rubydex/src/operation/printer.rs new file mode 100644 index 00000000..efccefa5 --- /dev/null +++ b/rust/rubydex/src/operation/printer.rs @@ -0,0 +1,260 @@ +use std::fmt::Write; + +use crate::model::{ + identity_maps::IdentityHashMap, + ids::{NameId, StringId}, + name::NameRef, + string_ref::StringRef, + visibility::Visibility, +}; +use crate::operation::{ + AliasConstant, AliasGlobalVariable, AliasMethod, AttrKind, DefineAttribute, DefineClassVariable, DefineConstant, + DefineGlobalVariable, DefineInstanceVariable, EnterClass, EnterMethod, EnterModule, EnterSingletonClass, Mixin, + MixinKind, Operation, ReferenceConstant, ReferenceMethod, SetConstantVisibility, SetDefaultVisibility, + SetMethodVisibility, Target, +}; + +struct OperationPrinter<'a> { + strings: &'a IdentityHashMap, + names: &'a IdentityHashMap, + out: String, + depth: usize, + include_references: bool, +} + +impl OperationPrinter<'_> { + fn name_str(&self, name_id: NameId) -> String { + let mut parts = Vec::new(); + let mut current = Some(name_id); + while let Some(id) = current { + let name = self.names.get(&id).expect("NameId should exist"); + let s = self.strings.get(name.str()).expect("StringId should exist"); + parts.push(s.as_str().to_string()); + current = name.parent_scope().as_ref().copied(); + } + parts.reverse(); + parts.join("::") + } + + fn string_value(&self, str_id: StringId) -> String { + self.strings + .get(&str_id) + .expect("StringId should exist") + .as_str() + .to_string() + } + + fn receiver_prefix(&self, receiver: Option<&Target>) -> String { + match receiver { + Some(Target::ExplicitSelf) => "self.".to_string(), + Some(Target::Constant(name_id)) => format!("{}.", self.name_str(*name_id)), + Some(Target::Other) => ".".to_string(), + None => String::new(), + } + } + + fn vis(visibility: Visibility) -> &'static str { + match visibility { + Visibility::Public => "public", + Visibility::Protected => "protected", + Visibility::Private => "private", + Visibility::ModuleFunction => "module_function", + } + } + + fn indent(&self) -> String { + " ".repeat(self.depth) + } + + fn print_operation(&mut self, op: &Operation) { + match op { + Operation::EnterClass(op) => self.print_enter_class(op), + Operation::EnterModule(op) => self.print_enter_module(op), + Operation::EnterSingletonClass(op) => self.print_enter_singleton_class(op), + Operation::EnterMethod(op) => self.print_enter_method(op), + Operation::ExitScope => { + self.depth = self.depth.saturating_sub(1); + let indent = self.indent(); + writeln!(self.out, "{indent}ExitScope").unwrap(); + } + Operation::AliasMethod(op) => self.print_alias_method(op), + Operation::SetMethodVisibility(op) => self.print_set_method_visibility(op), + Operation::SetDefaultVisibility(op) => self.print_set_default_visibility(op), + Operation::DefineConstant(op) => self.print_define_constant(op), + Operation::AliasConstant(op) => self.print_alias_constant(op), + Operation::SetConstantVisibility(op) => self.print_set_constant_visibility(op), + Operation::Mixin(op) => self.print_mixin(op), + Operation::DefineAttribute(op) => self.print_define_attribute(op), + Operation::DefineGlobalVariable(op) => self.print_define_global_variable(op), + Operation::DefineInstanceVariable(op) => self.print_define_instance_variable(op), + Operation::DefineClassVariable(op) => self.print_define_class_variable(op), + Operation::AliasGlobalVariable(op) => self.print_alias_global_variable(op), + Operation::ReferenceConstant(op) => self.print_reference_constant(op), + Operation::ReferenceMethod(op) => self.print_reference_method(op), + } + } + + fn print_enter_class(&mut self, op: &EnterClass) { + let indent = self.indent(); + let name = self.name_str(op.name_id); + write!(self.out, "{indent}EnterClass({name}").unwrap(); + if let Some(sc_name_id) = op.superclass_name { + let sc = self.name_str(sc_name_id); + write!(self.out, ", superclass: {sc}").unwrap(); + } + writeln!(self.out, ")").unwrap(); + self.depth += 1; + } + + fn print_enter_module(&mut self, op: &EnterModule) { + let indent = self.indent(); + let name = self.name_str(op.name_id); + writeln!(self.out, "{indent}EnterModule({name})").unwrap(); + self.depth += 1; + } + + fn print_enter_singleton_class(&mut self, op: &EnterSingletonClass) { + let indent = self.indent(); + let name = self.name_str(op.name_id); + writeln!(self.out, "{indent}EnterSingletonClass({name})").unwrap(); + self.depth += 1; + } + + fn print_enter_method(&mut self, op: &EnterMethod) { + let indent = self.indent(); + let prefix = self.receiver_prefix(op.receiver.as_ref()); + let name = self.string_value(op.str_id); + writeln!(self.out, "{indent}EnterMethod({prefix}{name})").unwrap(); + self.depth += 1; + } + + fn print_alias_method(&mut self, op: &AliasMethod) { + let indent = self.indent(); + let new_name = self.string_value(op.new_name_str_id); + let old_name = self.string_value(op.old_name_str_id); + writeln!(self.out, "{indent}AliasMethod({new_name} -> {old_name})").unwrap(); + } + + fn print_set_method_visibility(&mut self, op: &SetMethodVisibility) { + let indent = self.indent(); + let name = self.string_value(op.str_id); + let v = Self::vis(op.visibility); + writeln!(self.out, "{indent}SetMethodVisibility({name}, vis: {v})").unwrap(); + } + + fn print_set_default_visibility(&mut self, op: &SetDefaultVisibility) { + let indent = self.indent(); + let v = Self::vis(op.visibility); + writeln!(self.out, "{indent}SetDefaultVisibility({v})").unwrap(); + } + + fn print_define_constant(&mut self, op: &DefineConstant) { + let indent = self.indent(); + let name = self.name_str(op.name_id); + writeln!(self.out, "{indent}DefineConstant({name})").unwrap(); + } + + fn print_alias_constant(&mut self, op: &AliasConstant) { + let indent = self.indent(); + let name = self.name_str(op.name_id); + let target = self.name_str(op.target_name_id); + writeln!(self.out, "{indent}AliasConstant({name} -> {target})").unwrap(); + } + + fn print_set_constant_visibility(&mut self, op: &SetConstantVisibility) { + let indent = self.indent(); + let name = self.string_value(op.target); + let v = Self::vis(op.visibility); + writeln!(self.out, "{indent}SetConstantVisibility({name}, vis: {v})").unwrap(); + } + + fn print_mixin(&mut self, op: &Mixin) { + let indent = self.indent(); + let kind_str = match op.kind { + MixinKind::Include => "include", + MixinKind::Prepend => "prepend", + MixinKind::Extend => "extend", + }; + let target_name = match op.target { + Target::Constant(name_id) => self.name_str(name_id), + Target::ExplicitSelf => "self".to_string(), + Target::Other => "".to_string(), + }; + writeln!(self.out, "{indent}Mixin({kind_str}, {target_name})").unwrap(); + } + + fn print_define_attribute(&mut self, op: &DefineAttribute) { + let indent = self.indent(); + let kind_str = match op.kind { + AttrKind::Accessor => "accessor", + AttrKind::Reader => "reader", + AttrKind::Writer => "writer", + }; + let name = self.string_value(op.str_id); + writeln!(self.out, "{indent}DefineAttribute({kind_str} {name})").unwrap(); + } + + fn print_define_global_variable(&mut self, op: &DefineGlobalVariable) { + let indent = self.indent(); + let name = self.string_value(op.str_id); + writeln!(self.out, "{indent}DefineGlobalVariable({name})").unwrap(); + } + + fn print_define_instance_variable(&mut self, op: &DefineInstanceVariable) { + let indent = self.indent(); + let name = self.string_value(op.str_id); + writeln!(self.out, "{indent}DefineInstanceVariable({name})").unwrap(); + } + + fn print_define_class_variable(&mut self, op: &DefineClassVariable) { + let indent = self.indent(); + let name = self.string_value(op.str_id); + writeln!(self.out, "{indent}DefineClassVariable({name})").unwrap(); + } + + fn print_alias_global_variable(&mut self, op: &AliasGlobalVariable) { + let indent = self.indent(); + let new_name = self.string_value(op.new_name_str_id); + let old_name = self.string_value(op.old_name_str_id); + writeln!(self.out, "{indent}AliasGlobalVariable({new_name} -> {old_name})").unwrap(); + } + + fn print_reference_constant(&mut self, op: &ReferenceConstant) { + if self.include_references { + let indent = self.indent(); + let name = self.name_str(op.name_id); + writeln!(self.out, "{indent}ReferenceConstant({name})").unwrap(); + } + } + + fn print_reference_method(&mut self, op: &ReferenceMethod) { + if self.include_references { + let indent = self.indent(); + let name = self.string_value(op.str_id); + writeln!(self.out, "{indent}ReferenceMethod({name})").unwrap(); + } + } +} + +#[must_use] +#[allow(clippy::implicit_hasher)] +pub fn print_operations( + operations: &[Operation], + strings: &IdentityHashMap, + names: &IdentityHashMap, + include_references: bool, +) -> String { + let mut printer = OperationPrinter { + strings, + names, + out: String::new(), + depth: 0, + include_references, + }; + + for op in operations { + printer.print_operation(op); + } + + printer.out.trim_end().to_string() +} diff --git a/rust/rubydex/src/operation/ruby_builder.rs b/rust/rubydex/src/operation/ruby_builder.rs new file mode 100644 index 00000000..681060f9 --- /dev/null +++ b/rust/rubydex/src/operation/ruby_builder.rs @@ -0,0 +1,2915 @@ +//! Visit the Ruby AST and produce an ordered list of operations. +//! +//! Walks the parsed AST and produces `Vec` that can later be applied +//! by the applier to create definitions and declarations in a `LocalGraph`. + +use std::collections::hash_map::Entry; + +use crate::diagnostic::{Diagnostic, Rule}; +use crate::model::comment::Comment; +use crate::model::definitions::{DefinitionFlags, Parameter, ParameterStruct, Signatures}; +use crate::model::document::Document; +use crate::model::identity_maps::IdentityHashMap; +use crate::model::ids::{NameId, StringId, UriId}; +use crate::model::name::{Name, NameRef, ParentScope}; +use crate::model::string_ref::StringRef; +use crate::model::visibility::Visibility; +use crate::offset::Offset; +use crate::operation::{self as op, AttrKind, MixinKind, Operation, Target}; + +use ruby_prism::{ParseResult, Visit}; + +/// The result of running the operation builder on a Ruby source file. +/// +/// Contains the ordered operations and all interning data (strings, names, references) +/// needed to later apply the operations to a graph. +#[derive(Debug)] +pub struct OperationBuilderResult { + pub uri_id: UriId, + pub document: Document, + pub operations: Vec, + pub strings: IdentityHashMap, + pub names: IdentityHashMap, +} + +#[derive(Clone, Copy)] +enum Nesting { + /// A lexical scope (class/module keyword) that produces a new constant resolution scope. + LexicalScope { name_id: NameId, is_module: bool }, + /// An owner that doesn't produce a lexical scope (Class.new/Module.new). + Owner { name_id: NameId, is_module: bool }, + /// A method entry, used for instance variable ownership. + Method { receiver: Option }, +} + +/// Tracks receiver info for methods on the nesting stack, so `method_receiver` can work +/// without looking up definitions. Distinct from `operation::Target` which represents +/// the source-level receiver without resolved names. +#[derive(Clone, Copy)] +enum NestingReceiver { + SelfReceiver(NameId), + ConstantReceiver(NameId), +} + +struct VisibilityModifier { + visibility: Visibility, + is_inline: bool, + offset: Offset, +} + +impl VisibilityModifier { + #[must_use] + pub fn new(visibility: Visibility, is_inline: bool, offset: Offset) -> Self { + Self { + visibility, + is_inline, + offset, + } + } +} + +/// Visits the Ruby AST and produces an ordered list of operations. +pub struct RubyOperationBuilder<'a> { + uri_id: UriId, + source: &'a str, + // Interning + strings: IdentityHashMap, + names: IdentityHashMap, + document: Document, + // State + comments: Vec, + nesting_stack: Vec, + visibility_stack: Vec, + pending_decorator_offset: Option, + // Output + operations: Vec, +} + +impl<'a> RubyOperationBuilder<'a> { + #[must_use] + pub fn new(uri: String, source: &'a str) -> Self { + let uri_id = UriId::from(&uri); + + Self { + uri_id, + source, + strings: IdentityHashMap::default(), + names: IdentityHashMap::default(), + document: Document::new(uri, source), + comments: Vec::new(), + nesting_stack: Vec::new(), + visibility_stack: vec![VisibilityModifier::new(Visibility::Private, false, Offset::new(0, 0))], + pending_decorator_offset: None, + operations: Vec::new(), + } + } + + #[must_use] + pub fn build(mut self) -> OperationBuilderResult { + let result = ruby_prism::parse(self.source.as_bytes()); + + for error in result.errors() { + self.add_diagnostic( + Rule::ParseError, + Offset::from_prism_location(&error.location()), + error.message().to_string(), + ); + } + + for warning in result.warnings() { + self.add_diagnostic( + Rule::ParseWarning, + Offset::from_prism_location(&warning.location()), + warning.message().to_string(), + ); + } + + self.comments = self.parse_comments_into_groups(&result); + self.visit(&result.node()); + + OperationBuilderResult { + uri_id: self.uri_id, + document: self.document, + operations: self.operations, + strings: self.strings, + names: self.names, + } + } + + // -- Interning -- + + fn intern_string(&mut self, string: String) -> StringId { + let string_id = StringId::from(&string); + + match self.strings.entry(string_id) { + Entry::Occupied(mut entry) => { + debug_assert!(string == **entry.get(), "StringId collision"); + entry.get_mut().increment_ref_count(1); + } + Entry::Vacant(entry) => { + entry.insert(StringRef::new(string)); + } + } + + string_id + } + + fn add_name(&mut self, name: Name) -> NameId { + let name_id = name.id(); + + match self.names.entry(name_id) { + Entry::Occupied(mut entry) => { + debug_assert!(*entry.get() == name, "NameId collision"); + entry.get_mut().increment_ref_count(1); + } + Entry::Vacant(entry) => { + entry.insert(NameRef::Unresolved(Box::new(name))); + } + } + + name_id + } + + fn add_diagnostic(&mut self, rule: Rule, offset: Offset, message: String) { + let diagnostic = Diagnostic::new(rule, self.uri_id, offset, message); + self.document.add_diagnostic(diagnostic); + } + + // -- Nesting helpers -- + + fn current_nesting_is_module(&self) -> bool { + self.nesting_stack + .iter() + .rev() + .find_map(|nesting| match nesting { + Nesting::LexicalScope { is_module, .. } | Nesting::Owner { is_module, .. } => Some(*is_module), + Nesting::Method { .. } => None, + }) + .unwrap_or(false) + } + + fn current_lexical_scope_name_id(&self) -> Option { + self.nesting_stack.iter().rev().find_map(|nesting| match nesting { + Nesting::LexicalScope { name_id, .. } => Some(*name_id), + Nesting::Owner { .. } | Nesting::Method { .. } => None, + }) + } + + fn current_owner_name_id(&self) -> Option { + self.nesting_stack.iter().rev().find_map(|nesting| match nesting { + Nesting::LexicalScope { name_id, .. } | Nesting::Owner { name_id, .. } => Some(*name_id), + Nesting::Method { .. } => None, + }) + } + + fn current_visibility(&self) -> &VisibilityModifier { + self.visibility_stack + .last() + .expect("visibility stack should not be empty") + } + + fn parse_comments_into_groups(&mut self, result: &ParseResult<'_>) -> Vec { + let mut iter = result.comments().peekable(); + let mut groups = Vec::new(); + + while let Some(comment) = iter.next() { + let mut group = CommentGroup::new(); + group.add_comment(&comment); + while let Some(next_comment) = iter.peek() { + if group.accepts(next_comment, self.source) { + let next = iter.next().unwrap(); + group.add_comment(&next); + } else { + break; + } + } + groups.push(group); + } + groups + } + + fn location_to_string(location: &ruby_prism::Location) -> String { + String::from_utf8_lossy(location.as_slice()).to_string() + } + + fn find_comments_for(&self, offset: u32) -> (Box<[Comment]>, DefinitionFlags) { + let offset_usize = offset as usize; + if self.comments.is_empty() { + return (Box::default(), DefinitionFlags::empty()); + } + + let idx = match self.comments.binary_search_by_key(&offset_usize, |g| g.end_offset) { + Ok(_) => { + debug_assert!(false, "Comment ends exactly at definition start"); + return (Box::default(), DefinitionFlags::empty()); + } + Err(i) if i > 0 => i - 1, + Err(_) => return (Box::default(), DefinitionFlags::empty()), + }; + + let group = &self.comments[idx]; + let between = &self.source.as_bytes()[group.end_offset..offset_usize]; + if !between.iter().all(|&b| b.is_ascii_whitespace()) { + return (Box::default(), DefinitionFlags::empty()); + } + + if bytecount::count(between, b'\n') > 2 { + return (Box::default(), DefinitionFlags::empty()); + } + + (group.comments(), group.flags()) + } + + fn take_decorator_offset(&mut self, definition_start: u32) -> Option { + let decorator_offset = self.pending_decorator_offset.take()?; + if decorator_offset.end() > definition_start { + return None; + } + + let between = &self.source.as_bytes()[decorator_offset.end() as usize..definition_start as usize]; + if between.iter().all(|&b| b.is_ascii_whitespace()) && bytecount::count(between, b'\n') <= 1 { + Some(decorator_offset.start()) + } else { + None + } + } + + fn index_constant_reference(&mut self, node: &ruby_prism::Node, push_final_reference: bool) -> Option { + let mut parent_scope_id = ParentScope::None; + + let location = match node { + ruby_prism::Node::ConstantPathNode { .. } => { + let constant = node.as_constant_path_node().unwrap(); + + if let Some(parent) = constant.parent() { + match parent { + ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {} + _ => { + self.add_diagnostic( + Rule::DynamicConstantReference, + Offset::from_prism_location(&parent.location()), + "Dynamic constant reference".to_string(), + ); + return None; + } + } + + parent_scope_id = self + .index_constant_reference(&parent, true) + .map_or(ParentScope::None, ParentScope::Some); + } else { + parent_scope_id = ParentScope::TopLevel; + } + + constant.name_loc() + } + ruby_prism::Node::ConstantPathWriteNode { .. } => { + let constant = node.as_constant_path_write_node().unwrap(); + let target = constant.target(); + + if let Some(parent) = target.parent() { + match parent { + ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {} + _ => { + return None; + } + } + + parent_scope_id = self + .index_constant_reference(&parent, true) + .map_or(ParentScope::None, ParentScope::Some); + } else { + parent_scope_id = ParentScope::TopLevel; + } + + target.name_loc() + } + ruby_prism::Node::ConstantReadNode { .. } => node.location(), + ruby_prism::Node::ConstantAndWriteNode { .. } => node.as_constant_and_write_node().unwrap().name_loc(), + ruby_prism::Node::ConstantOperatorWriteNode { .. } => { + node.as_constant_operator_write_node().unwrap().name_loc() + } + ruby_prism::Node::ConstantOrWriteNode { .. } => node.as_constant_or_write_node().unwrap().name_loc(), + ruby_prism::Node::ConstantTargetNode { .. } => node.as_constant_target_node().unwrap().location(), + ruby_prism::Node::ConstantWriteNode { .. } => node.as_constant_write_node().unwrap().name_loc(), + ruby_prism::Node::ConstantPathTargetNode { .. } => { + let target = node.as_constant_path_target_node().unwrap(); + + if let Some(parent) = target.parent() { + match parent { + ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {} + _ => { + return None; + } + } + + parent_scope_id = self + .index_constant_reference(&parent, true) + .map_or(ParentScope::None, ParentScope::Some); + } else { + parent_scope_id = ParentScope::TopLevel; + } + + target.name_loc() + } + _ => { + return None; + } + }; + + let offset = Offset::from_prism_location(&location); + let name = Self::location_to_string(&location); + let string_id = self.intern_string(name); + let name_id = self.add_name(Name::new( + string_id, + parent_scope_id, + self.current_lexical_scope_name_id(), + )); + + if push_final_reference { + self.operations + .push(Operation::ReferenceConstant(op::ReferenceConstant { + name_id, + uri_id: self.uri_id, + offset, + })); + } + + Some(name_id) + } + + fn index_method_reference(&mut self, name: String, location: &ruby_prism::Location, receiver: Option) { + let offset = Offset::from_prism_location(location); + let str_id = self.intern_string(name); + self.operations.push(Operation::ReferenceMethod(op::ReferenceMethod { + str_id, + uri_id: self.uri_id, + offset, + receiver: receiver.map(Target::Constant), + })); + } + + fn index_method_reference_for_call(&mut self, node: &ruby_prism::CallNode) { + let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location()); + + if method_receiver.is_none() + && let Some(receiver) = node.receiver() + { + self.visit(&receiver); + } + + let message = String::from_utf8_lossy(node.name().as_slice()).to_string(); + self.index_method_reference(message, &node.message_loc().unwrap(), method_receiver); + } + + fn visit_call_node_parts(&mut self, node: &ruby_prism::CallNode) { + if let Some(receiver) = node.receiver() { + self.visit(&receiver); + } + if let Some(arguments) = node.arguments() { + self.visit_arguments_node(&arguments); + } + if let Some(block) = node.block() { + self.visit(&block); + } + } + + // -- Method receiver resolution -- + + fn method_receiver( + &mut self, + receiver: Option<&ruby_prism::Node>, + fallback_location: ruby_prism::Location, + ) -> Option { + let mut is_singleton_name = false; + + let name_id = match receiver { + Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() { + Some(Nesting::LexicalScope { name_id, .. } | Nesting::Owner { name_id, .. }) => { + is_singleton_name = true; + Some(*name_id) + } + Some(Nesting::Method { receiver, .. }) => { + if let Some(recv) = receiver { + is_singleton_name = true; + match recv { + NestingReceiver::SelfReceiver(name_id) | NestingReceiver::ConstantReceiver(name_id) => { + Some(*name_id) + } + } + } else { + self.current_owner_name_id() + } + } + None => { + let str_id = self.intern_string("Object".into()); + Some(self.add_name(Name::new(str_id, ParentScope::None, None))) + } + }, + Some(ruby_prism::Node::CallNode { .. }) => { + let call_node = receiver.unwrap().as_call_node().unwrap(); + if call_node.name().as_slice() == b"singleton_class" { + is_singleton_name = true; + self.method_receiver(call_node.receiver().as_ref(), call_node.location()) + } else { + None + } + } + Some(node) => { + is_singleton_name = true; + self.index_constant_reference(node, true) + } + }?; + + if !is_singleton_name { + return Some(name_id); + } + + let singleton_class_name = { + let name = self.names.get(&name_id).expect("Indexed constant name should exist"); + + let target_str = self + .strings + .get(name.str()) + .expect("Indexed constant string should exist"); + + format!("<{}>", target_str.as_str()) + }; + + let string_id = self.intern_string(singleton_class_name); + let new_name_id = self.add_name(Name::new(string_id, ParentScope::Attached(name_id), None)); + + let location = receiver.map_or(fallback_location, ruby_prism::Node::location); + let offset = Offset::from_prism_location(&location); + self.operations + .push(Operation::ReferenceConstant(op::ReferenceConstant { + name_id: new_name_id, + uri_id: self.uri_id, + offset, + })); + Some(new_name_id) + } + + // -- Parameters -- + + fn collect_parameters(&mut self, node: &ruby_prism::DefNode) -> Vec { + let mut parameters: Vec = Vec::new(); + + let Some(parameters_list) = node.parameters() else { + return parameters; + }; + + for parameter in ¶meters_list.requireds() { + let location = parameter.location(); + let str_id = self.intern_string(Self::location_to_string(&location)); + parameters.push(Parameter::RequiredPositional(ParameterStruct::new( + Offset::from_prism_location(&location), + str_id, + ))); + } + + for parameter in ¶meters_list.optionals() { + let opt_param = parameter.as_optional_parameter_node().unwrap(); + let name_loc = opt_param.name_loc(); + let str_id = self.intern_string(Self::location_to_string(&name_loc)); + parameters.push(Parameter::OptionalPositional(ParameterStruct::new( + Offset::from_prism_location(&name_loc), + str_id, + ))); + self.visit(&opt_param.value()); + } + + if let Some(rest) = parameters_list.rest() { + let rest_param = rest.as_rest_parameter_node().unwrap(); + let location = rest_param.name_loc().unwrap_or_else(|| rest.location()); + let str_id = self.intern_string(Self::location_to_string(&location)); + parameters.push(Parameter::RestPositional(ParameterStruct::new( + Offset::from_prism_location(&location), + str_id, + ))); + } + + for post in ¶meters_list.posts() { + let location = post.location(); + let str_id = self.intern_string(Self::location_to_string(&location)); + parameters.push(Parameter::Post(ParameterStruct::new( + Offset::from_prism_location(&location), + str_id, + ))); + } + + for keyword in ¶meters_list.keywords() { + match keyword { + ruby_prism::Node::RequiredKeywordParameterNode { .. } => { + let required = keyword.as_required_keyword_parameter_node().unwrap(); + let name_loc = required.name_loc(); + let str_id = + self.intern_string(Self::location_to_string(&name_loc).trim_end_matches(':').to_string()); + parameters.push(Parameter::RequiredKeyword(ParameterStruct::new( + Offset::from_prism_location(&name_loc), + str_id, + ))); + } + ruby_prism::Node::OptionalKeywordParameterNode { .. } => { + let optional = keyword.as_optional_keyword_parameter_node().unwrap(); + let name_loc = optional.name_loc(); + let str_id = + self.intern_string(Self::location_to_string(&name_loc).trim_end_matches(':').to_string()); + parameters.push(Parameter::OptionalKeyword(ParameterStruct::new( + Offset::from_prism_location(&name_loc), + str_id, + ))); + self.visit(&optional.value()); + } + _ => {} + } + } + + if let Some(rest) = parameters_list.keyword_rest() { + match rest { + ruby_prism::Node::KeywordRestParameterNode { .. } => { + let location = rest + .as_keyword_rest_parameter_node() + .unwrap() + .name_loc() + .unwrap_or_else(|| rest.location()); + let str_id = self.intern_string(Self::location_to_string(&location)); + parameters.push(Parameter::RestKeyword(ParameterStruct::new( + Offset::from_prism_location(&location), + str_id, + ))); + } + ruby_prism::Node::ForwardingParameterNode { .. } => { + let location = rest.location(); + let str_id = self.intern_string(Self::location_to_string(&location)); + parameters.push(Parameter::Forward(ParameterStruct::new( + Offset::from_prism_location(&location), + str_id, + ))); + } + _ => {} + } + } + + if let Some(block) = parameters_list.block() { + let location = block.name_loc().unwrap_or_else(|| block.location()); + let str_id = self.intern_string(Self::location_to_string(&location)); + parameters.push(Parameter::Block(ParameterStruct::new( + Offset::from_prism_location(&location), + str_id, + ))); + } + + parameters + } + + // -- Helpers -- + + fn each_string_or_symbol_arg(node: &ruby_prism::CallNode, mut f: F) + where + F: FnMut(String, ruby_prism::Location), + { + if let Some(arguments) = node.arguments() { + for argument in &arguments.arguments() { + match argument { + ruby_prism::Node::SymbolNode { .. } => { + let symbol = argument.as_symbol_node().unwrap(); + if let Some(value_loc) = symbol.value_loc() { + let name = Self::location_to_string(&value_loc); + f(name, value_loc); + } + } + ruby_prism::Node::StringNode { .. } => { + let string = argument.as_string_node().unwrap(); + let name = String::from_utf8_lossy(string.unescaped()).to_string(); + f(name, argument.location()); + } + _ => {} + } + } + } + } + + fn is_promotable_value(value: &ruby_prism::Node) -> bool { + value + .as_call_node() + .is_some_and(|call| call.receiver().is_none() || call.call_operator_loc().is_some()) + } + + // -- Definition handlers -- + + fn handle_class_definition( + &mut self, + location: &ruby_prism::Location, + name_node: Option<&ruby_prism::Node>, + body_node: Option, + superclass_node: Option, + is_lexical_scope: bool, + ) { + let offset = Offset::from_prism_location(location); + let (comments, flags) = self.find_comments_for(offset.start()); + let superclass_name = superclass_node.as_ref().and_then(|n| { + if let Some(id) = self.index_constant_reference(n, false) { + self.operations + .push(Operation::ReferenceConstant(op::ReferenceConstant { + name_id: id, + uri_id: self.uri_id, + offset: Offset::from_prism_location(&n.location()), + })); + return Some(id); + } + + if let ruby_prism::Node::CallNode { .. } = n { + let call = n.as_call_node().unwrap(); + if let Some(receiver) = call.receiver() + && let Some(id) = self.index_constant_reference(&receiver, false) + { + self.operations + .push(Operation::ReferenceConstant(op::ReferenceConstant { + name_id: id, + uri_id: self.uri_id, + offset: Offset::from_prism_location(&receiver.location()), + })); + return Some(id); + } + } + + None + }); + + if let Some(superclass_node) = superclass_node + && superclass_name.is_none() + { + self.add_diagnostic( + Rule::DynamicAncestor, + Offset::from_prism_location(&superclass_node.location()), + "Dynamic superclass".to_string(), + ); + } + + let (name_id, name_offset) = if let Some(name_node) = name_node { + let name_loc = match name_node { + ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(), + ruby_prism::Node::ConstantPathWriteNode { .. } => { + name_node.as_constant_path_write_node().unwrap().target().name_loc() + } + _ => name_node.location(), + }; + ( + self.index_constant_reference(name_node, false), + Offset::from_prism_location(&name_loc), + ) + } else { + let string_id = self.intern_string(format!("{}:{}", self.uri_id, offset.start())); + ( + Some(self.add_name(Name::new(string_id, ParentScope::None, None))), + offset.clone(), + ) + }; + + if let Some(name_id) = name_id { + self.operations.push(Operation::EnterClass(op::EnterClass { + name_id, + uri_id: self.uri_id, + offset: offset.clone(), + name_offset, + comments, + flags, + superclass_name, + is_lexical_scope, + })); + + let nesting = if is_lexical_scope { + Nesting::LexicalScope { + name_id, + is_module: false, + } + } else { + Nesting::Owner { + name_id, + is_module: false, + } + }; + self.nesting_stack.push(nesting); + self.visibility_stack + .push(VisibilityModifier::new(Visibility::Public, false, offset)); + if let Some(body) = body_node { + self.visit(&body); + } + self.visibility_stack.pop(); + self.nesting_stack.pop(); + self.operations.push(Operation::ExitScope); + } + } + + fn handle_module_definition( + &mut self, + location: &ruby_prism::Location, + name_node: Option<&ruby_prism::Node>, + body_node: Option, + is_lexical_scope: bool, + ) { + let offset = Offset::from_prism_location(location); + let (comments, flags) = self.find_comments_for(offset.start()); + + let (name_id, name_offset) = if let Some(name_node) = name_node { + let name_loc = match name_node { + ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(), + ruby_prism::Node::ConstantPathWriteNode { .. } => { + name_node.as_constant_path_write_node().unwrap().target().name_loc() + } + _ => name_node.location(), + }; + ( + self.index_constant_reference(name_node, false), + Offset::from_prism_location(&name_loc), + ) + } else { + let string_id = self.intern_string(format!("{}:{}", self.uri_id, offset.start())); + ( + Some(self.add_name(Name::new(string_id, ParentScope::None, None))), + offset.clone(), + ) + }; + + if let Some(name_id) = name_id { + self.operations.push(Operation::EnterModule(op::EnterModule { + name_id, + uri_id: self.uri_id, + offset: offset.clone(), + name_offset, + comments, + flags, + is_lexical_scope, + })); + + let nesting = if is_lexical_scope { + Nesting::LexicalScope { + name_id, + is_module: true, + } + } else { + Nesting::Owner { + name_id, + is_module: true, + } + }; + self.nesting_stack.push(nesting); + self.visibility_stack + .push(VisibilityModifier::new(Visibility::Public, false, offset)); + if let Some(body) = body_node { + self.visit(&body); + } + self.visibility_stack.pop(); + self.nesting_stack.pop(); + self.operations.push(Operation::ExitScope); + } + } + + 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 { + return false; + }; + + if call_node.name().as_slice() != b"new" { + return false; + } + + let Some(receiver) = call_node.receiver() else { + return false; + }; + + let receiver_name = receiver.location().as_slice(); + + if matches!(receiver_name, b"Module" | b"::Module") { + self.handle_module_definition(&node.location(), Some(node), call_node.block(), false); + } else if matches!(receiver_name, b"Class" | b"::Class") { + self.handle_class_definition( + &node.location(), + Some(node), + call_node.block(), + call_node.arguments().and_then(|args| args.arguments().iter().next()), + false, + ); + } else { + return false; + } + + self.index_method_reference_for_call(&call_node); + true + } + + fn handle_mixin(&mut self, node: &ruby_prism::CallNode, kind: MixinKind) { + let Some(arguments) = node.arguments() else { + return; + }; + + let has_owner = self.current_owner_name_id().is_some(); + + let mixin_arguments = arguments + .arguments() + .iter() + .filter_map(|arg| { + if arg.as_self_node().is_some() { + if !has_owner { + self.add_diagnostic( + Rule::TopLevelMixinSelf, + Offset::from_prism_location(&arg.location()), + "Top level mixin self".to_string(), + ); + return None; + } + + Some(( + self.current_lexical_scope_name_id().unwrap(), + Offset::from_prism_location(&arg.location()), + )) + } else if let Some(name_id) = self.index_constant_reference(&arg, false) { + Some((name_id, Offset::from_prism_location(&arg.location()))) + } else { + self.add_diagnostic( + Rule::DynamicAncestor, + Offset::from_prism_location(&arg.location()), + "Dynamic mixin argument".to_string(), + ); + None + } + }) + .collect::>(); + + if mixin_arguments.is_empty() || !has_owner { + return; + } + + // Mixin operations with multiple arguments are inserted in reverse + for (name_id, offset) in mixin_arguments.into_iter().rev() { + self.operations + .push(Operation::ReferenceConstant(op::ReferenceConstant { + name_id, + uri_id: self.uri_id, + offset, + })); + + self.operations.push(Operation::Mixin(op::Mixin { + kind, + target: Target::Constant(name_id), + })); + } + } + + fn handle_constant_visibility(&mut self, node: &ruby_prism::CallNode, visibility: Visibility) { + let receiver = node.receiver(); + + let receiver_name_id = match receiver { + Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }) => { + self.index_constant_reference(&receiver.unwrap(), true) + } + Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() { + Some(Nesting::Method { .. }) => return, + None => { + self.add_diagnostic( + Rule::InvalidPrivateConstant, + Offset::from_prism_location(&node.location()), + "Private constant called at top level".to_string(), + ); + return; + } + _ => None, + }, + _ => { + self.add_diagnostic( + Rule::InvalidPrivateConstant, + Offset::from_prism_location(&node.location()), + "Dynamic receiver for private constant".to_string(), + ); + return; + } + }; + + let Some(arguments) = node.arguments() else { + return; + }; + + for argument in &arguments.arguments() { + let (name, location) = match argument { + ruby_prism::Node::SymbolNode { .. } => { + let symbol = argument.as_symbol_node().unwrap(); + if let Some(value_loc) = symbol.value_loc() { + (Self::location_to_string(&value_loc), value_loc) + } else { + continue; + } + } + ruby_prism::Node::StringNode { .. } => { + let string = argument.as_string_node().unwrap(); + let name = String::from_utf8_lossy(string.unescaped()).to_string(); + (name, argument.location()) + } + _ => { + self.add_diagnostic( + Rule::InvalidPrivateConstant, + Offset::from_prism_location(&argument.location()), + "Private constant called with non-symbol argument".to_string(), + ); + continue; + } + }; + + let str_id = self.intern_string(name); + let offset = Offset::from_prism_location(&location); + + self.operations + .push(Operation::SetConstantVisibility(op::SetConstantVisibility { + receiver: receiver_name_id.map(Target::Constant), + target: str_id, + visibility, + uri_id: self.uri_id, + offset, + comments: Box::default(), + flags: DefinitionFlags::empty(), + })); + } + } + + // -- Constant definition helpers -- + + fn add_constant_definition( + &mut self, + node: &ruby_prism::Node, + also_add_reference: bool, + promotable: bool, + ) -> Option<()> { + let name_id = self.index_constant_reference(node, also_add_reference)?; + + let location = match node { + ruby_prism::Node::ConstantWriteNode { .. } => node.as_constant_write_node().unwrap().name_loc(), + ruby_prism::Node::ConstantOrWriteNode { .. } => node.as_constant_or_write_node().unwrap().name_loc(), + ruby_prism::Node::ConstantPathNode { .. } => node.as_constant_path_node().unwrap().name_loc(), + _ => node.location(), + }; + + let offset = Offset::from_prism_location(&location); + let (comments, mut flags) = self.find_comments_for(offset.start()); + if promotable { + flags |= DefinitionFlags::PROMOTABLE; + } + self.operations.push(Operation::DefineConstant(op::DefineConstant { + name_id, + uri_id: self.uri_id, + offset, + comments, + flags, + })); + + Some(()) + } + + fn index_constant_alias_target(&mut self, value: &ruby_prism::Node) -> Option { + match value { + ruby_prism::Node::ConstantReadNode { .. } | ruby_prism::Node::ConstantPathNode { .. } => { + self.index_constant_reference(value, true) + } + ruby_prism::Node::ConstantWriteNode { .. } => { + let node = value.as_constant_write_node().unwrap(); + let target_name_id = self.index_constant_alias_target(&node.value())?; + self.add_constant_alias_definition(value, target_name_id, false); + Some(target_name_id) + } + ruby_prism::Node::ConstantOrWriteNode { .. } => { + let node = value.as_constant_or_write_node().unwrap(); + let target_name_id = self.index_constant_alias_target(&node.value())?; + self.add_constant_alias_definition(value, target_name_id, false); + Some(target_name_id) + } + ruby_prism::Node::ConstantPathWriteNode { .. } => { + let node = value.as_constant_path_write_node().unwrap(); + let target_name_id = self.index_constant_alias_target(&node.value())?; + self.add_constant_alias_definition(&node.target().as_node(), target_name_id, false); + Some(target_name_id) + } + ruby_prism::Node::ConstantPathOrWriteNode { .. } => { + let node = value.as_constant_path_or_write_node().unwrap(); + let target_name_id = self.index_constant_alias_target(&node.value())?; + self.add_constant_alias_definition(&node.target().as_node(), target_name_id, true); + Some(target_name_id) + } + _ => None, + } + } + + fn add_constant_alias_definition( + &mut self, + name_node: &ruby_prism::Node, + target_name_id: NameId, + also_add_reference: bool, + ) -> Option<()> { + let name_id = self.index_constant_reference(name_node, also_add_reference)?; + + let location = match name_node { + ruby_prism::Node::ConstantWriteNode { .. } => name_node.as_constant_write_node().unwrap().name_loc(), + ruby_prism::Node::ConstantOrWriteNode { .. } => name_node.as_constant_or_write_node().unwrap().name_loc(), + ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(), + _ => name_node.location(), + }; + + let offset = Offset::from_prism_location(&location); + let (comments, flags) = self.find_comments_for(offset.start()); + + self.operations.push(Operation::AliasConstant(op::AliasConstant { + name_id, + target_name_id, + uri_id: self.uri_id, + offset, + comments, + flags, + })); + + Some(()) + } + + fn is_attr_call(arg: &ruby_prism::Node) -> bool { + arg.as_call_node().is_some_and(|call| { + let receiver = call.receiver(); + let bare_or_self = receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()); + bare_or_self + && matches!( + call.name().as_slice(), + b"attr" | b"attr_reader" | b"attr_writer" | b"attr_accessor" + ) + }) + } + + fn handle_visibility_arguments( + &mut self, + arguments: &ruby_prism::ArgumentsNode, + visibility: Visibility, + call_offset: &Offset, + call_name: &str, + ) { + let args = arguments.arguments(); + let arg_count = args.len(); + + for arg in &args { + if matches!(arg, ruby_prism::Node::DefNode { .. }) || (arg_count == 1 && Self::is_attr_call(&arg)) { + let previous_visibility = self.current_visibility().visibility; + + self.operations + .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility { + visibility, + uri_id: self.uri_id, + offset: call_offset.clone(), + })); + + self.visibility_stack + .push(VisibilityModifier::new(visibility, true, call_offset.clone())); + self.visit(&arg); + self.visibility_stack.pop(); + + self.operations + .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility { + visibility: previous_visibility, + uri_id: self.uri_id, + offset: call_offset.clone(), + })); + } else if matches!( + arg, + ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. } + ) { + self.create_method_visibility_operation(&arg, visibility, DefinitionFlags::empty()); + } else { + let arg_offset = Offset::from_prism_location(&arg.location()); + let message = if Self::is_attr_call(&arg) { + format!("`{call_name}` with `attr_*` is only supported as a single argument") + } else { + format!("`{call_name}` called with a non-literal argument") + }; + self.add_diagnostic(Rule::InvalidMethodVisibility, arg_offset, message); + self.visit(&arg); + } + } + } + + fn create_method_visibility_operation( + &mut self, + arg: &ruby_prism::Node, + visibility: Visibility, + flags: DefinitionFlags, + ) { + let (name, location) = match arg { + ruby_prism::Node::SymbolNode { .. } => { + let symbol = arg.as_symbol_node().unwrap(); + if let Some(value_loc) = symbol.value_loc() { + (Self::location_to_string(&value_loc), value_loc) + } else { + return; + } + } + ruby_prism::Node::StringNode { .. } => { + let string = arg.as_string_node().unwrap(); + let name = String::from_utf8_lossy(string.unescaped()).to_string(); + (name, arg.location()) + } + _ => return, + }; + + let str_id = self.intern_string(format!("{name}()")); + let offset = Offset::from_prism_location(&location); + + self.operations + .push(Operation::SetMethodVisibility(op::SetMethodVisibility { + str_id, + visibility, + uri_id: self.uri_id, + offset, + flags, + })); + } + + fn create_method_visibility_operation_from_name( + &mut self, + name: &str, + location: &ruby_prism::Location, + visibility: Visibility, + flags: DefinitionFlags, + ) { + let str_id = self.intern_string(format!("{name}()")); + let offset = Offset::from_prism_location(location); + + self.operations + .push(Operation::SetMethodVisibility(op::SetMethodVisibility { + str_id, + visibility, + uri_id: self.uri_id, + offset, + flags, + })); + } + + #[allow(clippy::too_many_lines)] + fn handle_singleton_method_visibility( + &mut self, + node: &ruby_prism::CallNode, + visibility: Visibility, + call_name: &str, + ) { + 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 => { + self.add_diagnostic( + Rule::InvalidMethodVisibility, + Offset::from_prism_location(&node.location()), + format!("`{call_name}` called at top level"), + ); + self.visit_call_node_parts(node); + return; + } + _ => {} + }, + _ => { + self.visit_call_node_parts(node); + return; + } + } + + let Some(arguments) = node.arguments() else { + return; + }; + + let args = arguments.arguments(); + let arg_count = args.len(); + + for argument in &args { + match argument { + ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. } => { + self.create_method_visibility_operation( + &argument, + visibility, + DefinitionFlags::SINGLETON_METHOD_VISIBILITY, + ); + } + ruby_prism::Node::ArrayNode { .. } if arg_count == 1 => { + let array = argument.as_array_node().unwrap(); + for element in &array.elements() { + match element { + ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. } => { + self.create_method_visibility_operation( + &element, + visibility, + DefinitionFlags::SINGLETON_METHOD_VISIBILITY, + ); + } + ruby_prism::Node::DefNode { .. } => { + let def_node = element.as_def_node().unwrap(); + if def_node.receiver().is_none() { + self.add_diagnostic( + Rule::InvalidMethodVisibility, + Offset::from_prism_location(&element.location()), + format!("`{call_name}` requires a singleton method definition"), + ); + self.visit(&element); + continue; + } + let name_loc = def_node.name_loc(); + let name = Self::location_to_string(&name_loc); + self.create_method_visibility_operation_from_name( + &name, + &name_loc, + visibility, + DefinitionFlags::SINGLETON_METHOD_VISIBILITY, + ); + self.visit(&element); + } + _ => { + self.add_diagnostic( + Rule::InvalidMethodVisibility, + Offset::from_prism_location(&element.location()), + format!( + "`{call_name}` array element must be a Symbol, String, or method definition" + ), + ); + self.visit(&element); + } + } + } + } + ruby_prism::Node::DefNode { .. } => { + let def_node = argument.as_def_node().unwrap(); + if def_node.receiver().is_none() { + self.add_diagnostic( + Rule::InvalidMethodVisibility, + Offset::from_prism_location(&argument.location()), + format!("`{call_name}` requires a singleton method definition"), + ); + self.visit(&argument); + continue; + } + let name_loc = def_node.name_loc(); + let name = Self::location_to_string(&name_loc); + self.create_method_visibility_operation_from_name( + &name, + &name_loc, + visibility, + DefinitionFlags::SINGLETON_METHOD_VISIBILITY, + ); + self.visit(&argument); + } + arg if Self::is_attr_call(&arg) => { + self.add_diagnostic( + Rule::InvalidMethodVisibility, + Offset::from_prism_location(&arg.location()), + format!("`{call_name}` does not accept `attr_*` arguments"), + ); + self.visit(&arg); + } + ruby_prism::Node::ArrayNode { .. } => { + self.add_diagnostic( + Rule::InvalidMethodVisibility, + Offset::from_prism_location(&argument.location()), + format!("`{call_name}` array argument must be the only argument"), + ); + self.visit(&argument); + } + _ => { + self.add_diagnostic( + Rule::InvalidMethodVisibility, + Offset::from_prism_location(&argument.location()), + format!("`{call_name}` called with a non-literal argument"), + ); + self.visit(&argument); + } + } + } + } + + fn add_global_variable_definition(&mut self, location: &ruby_prism::Location) { + let name = Self::location_to_string(location); + let str_id = self.intern_string(name); + let offset = Offset::from_prism_location(location); + let (comments, flags) = self.find_comments_for(offset.start()); + + self.operations + .push(Operation::DefineGlobalVariable(op::DefineGlobalVariable { + str_id, + uri_id: self.uri_id, + offset, + comments, + flags, + })); + } + + fn add_instance_variable_definition(&mut self, location: &ruby_prism::Location) { + let name = Self::location_to_string(location); + let str_id = self.intern_string(name); + let offset = Offset::from_prism_location(location); + let (comments, flags) = self.find_comments_for(offset.start()); + + self.operations + .push(Operation::DefineInstanceVariable(op::DefineInstanceVariable { + str_id, + uri_id: self.uri_id, + offset, + comments, + flags, + })); + } + + fn add_class_variable_definition(&mut self, location: &ruby_prism::Location) { + let name = Self::location_to_string(location); + let str_id = self.intern_string(name); + let offset = Offset::from_prism_location(location); + let (comments, flags) = self.find_comments_for(offset.start()); + + self.operations + .push(Operation::DefineClassVariable(op::DefineClassVariable { + str_id, + uri_id: self.uri_id, + offset, + comments, + flags, + })); + } +} + +struct CommentGroup { + end_offset: usize, + comments: Vec, + deprecated: bool, +} + +impl CommentGroup { + #[must_use] + pub fn new() -> Self { + Self { + end_offset: 0, + comments: Vec::new(), + deprecated: false, + } + } + + fn accepts(&self, next: &ruby_prism::Comment, source: &str) -> bool { + let current_end_offset = self.end_offset; + let next_line_start_offset = next.location().start_offset(); + let between = &source.as_bytes()[current_end_offset..next_line_start_offset]; + if !between.iter().all(|&b| b.is_ascii_whitespace()) { + return false; + } + bytecount::count(between, b'\n') <= 1 + } + + fn add_comment(&mut self, comment: &ruby_prism::Comment) { + self.end_offset = comment.location().end_offset(); + let text = String::from_utf8_lossy(comment.location().as_slice()).to_string(); + if text.lines().any(|line| line.starts_with("# @deprecated")) { + self.deprecated = true; + } + self.comments.push(Comment::new( + Offset::from_prism_location(&comment.location()), + text.trim().to_string(), + )); + } + + fn comments(&self) -> Box<[Comment]> { + self.comments.clone().into_boxed_slice() + } + + fn flags(&self) -> DefinitionFlags { + if self.deprecated { + DefinitionFlags::DEPRECATED + } else { + DefinitionFlags::empty() + } + } +} + +// -- Visit implementation -- + +impl Visit<'_> for RubyOperationBuilder<'_> { + fn visit_class_node(&mut self, node: &ruby_prism::ClassNode<'_>) { + self.handle_class_definition( + &node.location(), + Some(&node.constant_path()), + node.body(), + node.superclass(), + true, + ); + } + + fn visit_module_node(&mut self, node: &ruby_prism::ModuleNode) { + self.handle_module_definition(&node.location(), Some(&node.constant_path()), node.body(), true); + } + + fn visit_singleton_class_node(&mut self, node: &ruby_prism::SingletonClassNode) { + let expression = node.expression(); + + let (attached_target, name_offset) = if expression.as_self_node().is_some() { + ( + self.current_lexical_scope_name_id(), + Offset::from_prism_location(&expression.location()), + ) + } else if matches!( + expression, + ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } + ) { + ( + self.index_constant_reference(&expression, true), + Offset::from_prism_location(&expression.location()), + ) + } else { + self.visit(&expression); + self.add_diagnostic( + Rule::DynamicSingletonDefinition, + Offset::from_prism_location(&node.location()), + "Dynamic singleton class definition".to_string(), + ); + return; + }; + + let Some(attached_target) = attached_target else { + self.add_diagnostic( + Rule::DynamicSingletonDefinition, + Offset::from_prism_location(&node.location()), + "Dynamic singleton class definition".to_string(), + ); + return; + }; + + let offset = Offset::from_prism_location(&node.location()); + let (comments, flags) = self.find_comments_for(offset.start()); + + let singleton_class_name = { + let name = self + .names + .get(&attached_target) + .expect("Attached target name should exist"); + let target_str = self + .strings + .get(name.str()) + .expect("Attached target string should exist"); + format!("<{}>", target_str.as_str()) + }; + + let string_id = self.intern_string(singleton_class_name); + let nesting = self.current_lexical_scope_name_id(); + let name_id = self.add_name(Name::new(string_id, ParentScope::Attached(attached_target), nesting)); + self.operations + .push(Operation::EnterSingletonClass(op::EnterSingletonClass { + name_id, + uri_id: self.uri_id, + offset: offset.clone(), + name_offset, + comments, + flags, + })); + + self.nesting_stack.push(Nesting::LexicalScope { + name_id, + is_module: false, + }); + self.visibility_stack + .push(VisibilityModifier::new(Visibility::Public, false, offset)); + if let Some(body) = node.body() { + self.visit(&body); + } + self.visibility_stack.pop(); + self.nesting_stack.pop(); + self.operations.push(Operation::ExitScope); + } + + #[allow(clippy::too_many_lines)] + fn visit_def_node(&mut self, node: &ruby_prism::DefNode) { + let name = Self::location_to_string(&node.name_loc()); + let str_id = self.intern_string(format!("{name}()")); + let offset = Offset::from_prism_location(&node.location()); + let parameters = self.collect_parameters(node); + let is_singleton = node.receiver().is_some(); + + let current_visibility = self.current_visibility(); + let visibility = if is_singleton { + Visibility::Public + } else { + current_visibility.visibility + }; + let offset_for_comments = if is_singleton { + offset.clone() + } else if current_visibility.is_inline { + current_visibility.offset.clone() + } else { + offset.clone() + }; + + let comment_offset = self + .take_decorator_offset(offset_for_comments.start()) + .unwrap_or_else(|| offset_for_comments.start()); + let (comments, flags) = self.find_comments_for(comment_offset); + + let (receiver, method_nesting_receiver) = if let Some(recv_node) = node.receiver() { + match recv_node { + ruby_prism::Node::SelfNode { .. } => { + let nesting_name = self.current_owner_name_id(); + ( + Some(Target::ExplicitSelf), + nesting_name.map(NestingReceiver::SelfReceiver), + ) + } + ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => { + let name_id = self.index_constant_reference(&recv_node, true); + ( + name_id.map(Target::Constant), + name_id.map(NestingReceiver::ConstantReceiver), + ) + } + _ => { + self.add_diagnostic( + Rule::DynamicSingletonDefinition, + Offset::from_prism_location(&node.location()), + "Dynamic receiver for singleton method definition".to_string(), + ); + self.visit(&recv_node); + return; + } + } + } else { + (None, None) + }; + + if receiver.is_none() && visibility == Visibility::ModuleFunction { + // module_function: emit two EnterMethod/ExitScope pairs (singleton + instance), + // each visiting the body so ivars are associated with both methods. + let singleton_receiver = Some(Target::ExplicitSelf); + let body = node.body(); + + self.operations.push(Operation::EnterMethod(op::EnterMethod { + str_id, + uri_id: self.uri_id, + offset: offset.clone(), + comments: comments.clone(), + flags: flags.clone(), + signatures: Signatures::Simple(parameters.clone().into_boxed_slice()), + receiver: singleton_receiver, + })); + self.nesting_stack.push(Nesting::Method { + receiver: method_nesting_receiver, + }); + if let Some(ref body) = body { + self.visit(body); + } + self.nesting_stack.pop(); + self.operations.push(Operation::ExitScope); + + self.operations.push(Operation::EnterMethod(op::EnterMethod { + str_id, + uri_id: self.uri_id, + offset: offset.clone(), + comments, + flags, + signatures: Signatures::Simple(parameters.into_boxed_slice()), + receiver, + })); + self.nesting_stack.push(Nesting::Method { + receiver: method_nesting_receiver, + }); + if let Some(ref body) = body { + self.visit(body); + } + self.nesting_stack.pop(); + self.operations.push(Operation::ExitScope); + } else { + // Singleton methods at top level have receiver=None (no class to point self to). + // Bracket with SetDefaultVisibility(Public) so the applier assigns the correct visibility. + let needs_singleton_visibility_bracket = is_singleton && receiver.is_none(); + let previous_visibility = if needs_singleton_visibility_bracket { + let prev = self.current_visibility().visibility; + self.operations + .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility { + visibility: Visibility::Public, + uri_id: self.uri_id, + offset: offset.clone(), + })); + Some(prev) + } else { + None + }; + + self.operations.push(Operation::EnterMethod(op::EnterMethod { + str_id, + uri_id: self.uri_id, + offset: offset.clone(), + comments, + flags, + signatures: Signatures::Simple(parameters.into_boxed_slice()), + receiver, + })); + self.nesting_stack.push(Nesting::Method { + receiver: method_nesting_receiver, + }); + if let Some(body) = node.body() { + self.visit(&body); + } + self.nesting_stack.pop(); + self.operations.push(Operation::ExitScope); + + if let Some(prev) = previous_visibility { + self.operations + .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility { + visibility: prev, + uri_id: self.uri_id, + offset: offset.clone(), + })); + } + } + } + + fn visit_constant_and_write_node(&mut self, node: &ruby_prism::ConstantAndWriteNode) { + self.index_constant_reference(&node.as_node(), true); + self.visit(&node.value()); + } + + fn visit_constant_operator_write_node(&mut self, node: &ruby_prism::ConstantOperatorWriteNode) { + self.index_constant_reference(&node.as_node(), true); + self.visit(&node.value()); + } + + fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode) { + if let Some(target_name_id) = self.index_constant_alias_target(&node.value()) { + self.add_constant_alias_definition(&node.as_node(), target_name_id, true); + } else { + self.add_constant_definition(&node.as_node(), true, Self::is_promotable_value(&node.value())); + self.visit(&node.value()); + } + } + + fn visit_constant_write_node(&mut self, node: &ruby_prism::ConstantWriteNode) { + let value = node.value(); + if self.handle_dynamic_class_or_module(&node.as_node(), &value) { + return; + } + + if let Some(target_name_id) = self.index_constant_alias_target(&value) { + self.add_constant_alias_definition(&node.as_node(), target_name_id, false); + } else { + self.add_constant_definition(&node.as_node(), false, Self::is_promotable_value(&value)); + self.visit(&value); + } + } + + fn visit_constant_path_and_write_node(&mut self, node: &ruby_prism::ConstantPathAndWriteNode) { + self.visit_constant_path_node(&node.target()); + self.visit(&node.value()); + } + + fn visit_constant_path_operator_write_node(&mut self, node: &ruby_prism::ConstantPathOperatorWriteNode) { + self.visit_constant_path_node(&node.target()); + self.visit(&node.value()); + } + + fn visit_constant_path_or_write_node(&mut self, node: &ruby_prism::ConstantPathOrWriteNode) { + if let Some(target_name_id) = self.index_constant_alias_target(&node.value()) { + self.add_constant_alias_definition(&node.target().as_node(), target_name_id, true); + } else { + self.add_constant_definition(&node.target().as_node(), true, Self::is_promotable_value(&node.value())); + self.visit(&node.value()); + } + } + + fn visit_constant_path_write_node(&mut self, node: &ruby_prism::ConstantPathWriteNode) { + let value = node.value(); + if self.handle_dynamic_class_or_module(&node.as_node(), &value) { + return; + } + + if let Some(target_name_id) = self.index_constant_alias_target(&value) { + self.add_constant_alias_definition(&node.target().as_node(), target_name_id, false); + } else { + self.add_constant_definition(&node.target().as_node(), false, Self::is_promotable_value(&value)); + self.visit(&value); + } + } + + fn visit_constant_read_node(&mut self, node: &ruby_prism::ConstantReadNode<'_>) { + self.index_constant_reference(&node.as_node(), true); + } + + fn visit_constant_path_node(&mut self, node: &ruby_prism::ConstantPathNode<'_>) { + self.index_constant_reference(&node.as_node(), true); + } + + fn visit_multi_write_node(&mut self, node: &ruby_prism::MultiWriteNode) { + for left in &node.lefts() { + match left { + ruby_prism::Node::ConstantTargetNode { .. } | ruby_prism::Node::ConstantPathTargetNode { .. } => { + self.add_constant_definition(&left, false, true); + } + ruby_prism::Node::GlobalVariableTargetNode { .. } => { + self.add_global_variable_definition(&left.location()); + } + ruby_prism::Node::InstanceVariableTargetNode { .. } => { + self.add_instance_variable_definition(&left.location()); + } + ruby_prism::Node::ClassVariableTargetNode { .. } => { + self.add_class_variable_definition(&left.location()); + } + ruby_prism::Node::CallTargetNode { .. } => { + let call_target_node = left.as_call_target_node().unwrap(); + let method_receiver = self.method_receiver(Some(&call_target_node.receiver()), left.location()); + + if method_receiver.is_none() { + self.visit(&call_target_node.receiver()); + } + + let name = String::from_utf8_lossy(call_target_node.name().as_slice()).to_string(); + self.index_method_reference(name, &call_target_node.location(), method_receiver); + } + _ => {} + } + } + + self.visit(&node.value()); + } + + #[allow(clippy::too_many_lines)] + fn visit_call_node(&mut self, node: &ruby_prism::CallNode) { + let index_attr = |kind: AttrKind, call: &ruby_prism::CallNode, builder: &mut Self| { + let receiver = call.receiver(); + if receiver.is_some() && receiver.unwrap().as_self_node().is_none() { + return; + } + + let call_offset = Offset::from_prism_location(&call.location()); + + let current_visibility = builder.current_visibility(); + let offset_for_comments = if current_visibility.is_inline { + current_visibility.offset.clone() + } else { + call_offset + }; + + let comment_offset = builder + .take_decorator_offset(offset_for_comments.start()) + .unwrap_or_else(|| offset_for_comments.start()); + + Self::each_string_or_symbol_arg(call, |name, location| { + let str_id = builder.intern_string(format!("{name}()")); + let offset = Offset::from_prism_location(&location); + let (comments, flags) = builder.find_comments_for(comment_offset); + + builder.operations.push(Operation::DefineAttribute(op::DefineAttribute { + kind, + str_id, + uri_id: builder.uri_id, + offset, + comments, + flags, + })); + }); + }; + + let message_loc = node.message_loc(); + if message_loc.is_none() { + return; + } + + let message = String::from_utf8_lossy(node.name().as_slice()).to_string(); + + match message.as_str() { + "attr_accessor" => { + index_attr(AttrKind::Accessor, node, self); + } + "attr_reader" => { + index_attr(AttrKind::Reader, node, self); + } + "attr_writer" => { + index_attr(AttrKind::Writer, node, self); + } + "attr" => { + let create_writer = if let Some(arguments) = node.arguments() { + let args_vec: Vec<_> = arguments.arguments().iter().collect(); + matches!(args_vec.as_slice(), [_, ruby_prism::Node::TrueNode { .. }]) + } else { + false + }; + + if create_writer { + index_attr(AttrKind::Accessor, node, self); + } else { + index_attr(AttrKind::Reader, node, self); + } + } + "alias_method" => { + let recv_node = node.receiver(); + let recv_ref = recv_node.as_ref(); + if recv_ref.is_some_and(|recv| { + !matches!( + recv, + ruby_prism::Node::SelfNode { .. } + | ruby_prism::Node::ConstantReadNode { .. } + | ruby_prism::Node::ConstantPathNode { .. } + ) + }) { + self.visit_call_node_parts(node); + return; + } + + let mut names: Vec<(String, Offset)> = Vec::new(); + Self::each_string_or_symbol_arg(node, |name, location| { + names.push((name, Offset::from_prism_location(&location))); + }); + + if names.len() != 2 { + return; + } + + let (new_name, _new_offset) = &names[0]; + let (old_name, old_offset) = &names[1]; + + let new_name_str_id = self.intern_string(format!("{new_name}()")); + let old_name_str_id = self.intern_string(format!("{old_name}()")); + + let (receiver, method_receiver) = match recv_ref { + Some( + recv @ (ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }), + ) => { + let name_id = self.index_constant_reference(recv, true); + (name_id.map(Target::Constant), name_id) + } + _ => (None, self.method_receiver(recv_ref, node.location())), + }; + + let ref_str_id = self.intern_string(format!("{old_name}()")); + self.operations.push(Operation::ReferenceMethod(op::ReferenceMethod { + str_id: ref_str_id, + uri_id: self.uri_id, + offset: old_offset.clone(), + receiver: method_receiver.map(Target::Constant), + })); + + let offset = Offset::from_prism_location(&node.location()); + let (comments, flags) = self.find_comments_for(offset.start()); + + self.operations.push(Operation::AliasMethod(op::AliasMethod { + new_name_str_id, + old_name_str_id, + uri_id: self.uri_id, + offset, + comments, + flags, + receiver, + })); + } + "include" => { + let receiver = node.receiver(); + if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) { + self.handle_mixin(node, MixinKind::Include); + } else { + self.visit_call_node_parts(node); + } + } + "prepend" => { + let receiver = node.receiver(); + if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) { + self.handle_mixin(node, MixinKind::Prepend); + } else { + self.visit_call_node_parts(node); + } + } + "extend" => { + let receiver = node.receiver(); + if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) { + self.handle_mixin(node, MixinKind::Extend); + } else { + self.visit_call_node_parts(node); + } + } + "private" | "protected" | "public" | "module_function" => { + if node.receiver().is_some() { + let offset = Offset::from_prism_location(&node.location()); + self.add_diagnostic( + Rule::InvalidMethodVisibility, + offset, + format!("`{message}` cannot be called with an explicit receiver"), + ); + self.visit_call_node_parts(node); + 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.add_diagnostic( + Rule::InvalidMethodVisibility, + offset, + "`module_function` can only be used in modules".to_string(), + ); + self.visit_arguments_node(&arguments); + } else { + self.handle_visibility_arguments(&arguments, visibility, &offset, &message); + } + } else { + let last_visibility = self.visibility_stack.last_mut().unwrap(); + *last_visibility = VisibilityModifier::new(visibility, false, offset); + self.operations + .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility { + visibility, + uri_id: self.uri_id, + offset: Offset::from_prism_location(&node.location()), + })); + } + } + "new" => { + let receiver_name = node.receiver().map(|r| r.location().as_slice()); + + if matches!(receiver_name, Some(b"Class" | b"::Class")) { + self.handle_class_definition( + &node.location(), + None, + node.block(), + node.arguments().and_then(|args| args.arguments().iter().next()), + false, + ); + } else if matches!(receiver_name, Some(b"Module" | b"::Module")) { + self.handle_module_definition(&node.location(), None, node.block(), false); + } else { + 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); + } + "sig" + if node.receiver().is_none() + || matches!( + node.receiver(), + Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }) + ) => + { + self.pending_decorator_offset = Some(Offset::from_prism_location(&node.location())); + + 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); + } + "private_constant" => { + self.handle_constant_visibility(node, Visibility::Private); + } + "public_constant" => { + self.handle_constant_visibility(node, Visibility::Public); + } + "private_class_method" => { + self.handle_singleton_method_visibility(node, Visibility::Private, "private_class_method"); + } + "public_class_method" => { + self.handle_singleton_method_visibility(node, Visibility::Public, "public_class_method"); + } + _ => { + if let Some(arguments) = node.arguments() { + self.visit_arguments_node(&arguments); + } + if let Some(block) = node.block() { + self.visit(&block); + } + + let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location()); + + if method_receiver.is_none() + && let Some(receiver) = node.receiver() + { + self.visit(&receiver); + } + + self.index_method_reference(message.clone(), &node.message_loc().unwrap(), method_receiver); + + match message.as_str() { + ">" | "<" | ">=" | "<=" => { + self.index_method_reference("<=>".to_string(), &node.message_loc().unwrap(), method_receiver); + } + _ => {} + } + } + } + } + + fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode) { + let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location()); + if method_receiver.is_none() + && let Some(receiver) = node.receiver() + { + self.visit(&receiver); + } + + let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string(); + self.index_method_reference(read_name, &node.operator_loc(), method_receiver); + + let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string(); + self.index_method_reference(write_name, &node.operator_loc(), method_receiver); + + self.visit(&node.value()); + } + + fn visit_call_operator_write_node(&mut self, node: &ruby_prism::CallOperatorWriteNode) { + let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location()); + if method_receiver.is_none() + && let Some(receiver) = node.receiver() + { + self.visit(&receiver); + } + + let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string(); + self.index_method_reference(read_name, &node.call_operator_loc().unwrap(), method_receiver); + + let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string(); + self.index_method_reference(write_name, &node.call_operator_loc().unwrap(), method_receiver); + + self.visit(&node.value()); + } + + fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode) { + let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location()); + if method_receiver.is_none() + && let Some(receiver) = node.receiver() + { + self.visit(&receiver); + } + + let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string(); + self.index_method_reference(read_name, &node.operator_loc(), method_receiver); + + let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string(); + self.index_method_reference(write_name, &node.operator_loc(), method_receiver); + + self.visit(&node.value()); + } + + fn visit_global_variable_write_node(&mut self, node: &ruby_prism::GlobalVariableWriteNode) { + self.add_global_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_global_variable_and_write_node(&mut self, node: &ruby_prism::GlobalVariableAndWriteNode<'_>) { + self.add_global_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_global_variable_or_write_node(&mut self, node: &ruby_prism::GlobalVariableOrWriteNode<'_>) { + self.add_global_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_global_variable_operator_write_node(&mut self, node: &ruby_prism::GlobalVariableOperatorWriteNode<'_>) { + self.add_global_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_instance_variable_and_write_node(&mut self, node: &ruby_prism::InstanceVariableAndWriteNode) { + self.add_instance_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_instance_variable_operator_write_node(&mut self, node: &ruby_prism::InstanceVariableOperatorWriteNode) { + self.add_instance_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_instance_variable_or_write_node(&mut self, node: &ruby_prism::InstanceVariableOrWriteNode) { + self.add_instance_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_instance_variable_write_node(&mut self, node: &ruby_prism::InstanceVariableWriteNode) { + self.add_instance_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_class_variable_and_write_node(&mut self, node: &ruby_prism::ClassVariableAndWriteNode) { + self.add_class_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_class_variable_operator_write_node(&mut self, node: &ruby_prism::ClassVariableOperatorWriteNode) { + self.add_class_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_class_variable_or_write_node(&mut self, node: &ruby_prism::ClassVariableOrWriteNode) { + self.add_class_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_class_variable_write_node(&mut self, node: &ruby_prism::ClassVariableWriteNode) { + self.add_class_variable_definition(&node.name_loc()); + self.visit(&node.value()); + } + + fn visit_block_argument_node(&mut self, node: &ruby_prism::BlockArgumentNode<'_>) { + let expression = node.expression(); + if let Some(expression) = expression { + match expression { + ruby_prism::Node::SymbolNode { .. } => { + let symbol = expression.as_symbol_node().unwrap(); + let name = Self::location_to_string(&symbol.value_loc().unwrap()); + self.index_method_reference(name, &node.location(), None); + } + _ => { + self.visit(&expression); + } + } + } + } + + fn visit_alias_method_node(&mut self, node: &ruby_prism::AliasMethodNode<'_>) { + let mut new_name = if let Some(symbol_node) = node.new_name().as_symbol_node() { + Self::location_to_string(&symbol_node.value_loc().unwrap()) + } else { + Self::location_to_string(&node.new_name().location()) + }; + + let mut old_name = if let Some(symbol_node) = node.old_name().as_symbol_node() { + Self::location_to_string(&symbol_node.value_loc().unwrap()) + } else { + Self::location_to_string(&node.old_name().location()) + }; + + new_name.push_str("()"); + old_name.push_str("()"); + + let offset = Offset::from_prism_location(&node.location()); + let (comments, flags) = self.find_comments_for(offset.start()); + let new_name_str_id = self.intern_string(new_name); + let old_name_str_id = self.intern_string(old_name.clone()); + + self.operations.push(Operation::AliasMethod(op::AliasMethod { + new_name_str_id, + old_name_str_id, + uri_id: self.uri_id, + offset, + comments, + flags, + receiver: None, + })); + + self.index_method_reference(old_name, &node.old_name().location(), None); + } + + fn visit_alias_global_variable_node(&mut self, node: &ruby_prism::AliasGlobalVariableNode<'_>) { + let new_name = Self::location_to_string(&node.new_name().location()); + let old_name = Self::location_to_string(&node.old_name().location()); + let new_name_str_id = self.intern_string(new_name); + let old_name_str_id = self.intern_string(old_name); + let offset = Offset::from_prism_location(&node.location()); + let (comments, flags) = self.find_comments_for(offset.start()); + + self.operations + .push(Operation::AliasGlobalVariable(op::AliasGlobalVariable { + new_name_str_id, + old_name_str_id, + uri_id: self.uri_id, + offset, + comments, + flags, + })); + } + + fn visit_and_node(&mut self, node: &ruby_prism::AndNode) { + let left = node.left(); + let method_receiver = self.method_receiver(Some(&left), left.location()); + + if method_receiver.is_none() { + self.visit(&left); + } + + self.index_method_reference("&&".to_string(), &node.location(), method_receiver); + self.visit(&node.right()); + } + + fn visit_or_node(&mut self, node: &ruby_prism::OrNode) { + let left = node.left(); + let method_receiver = self.method_receiver(Some(&left), left.location()); + + if method_receiver.is_none() { + self.visit(&left); + } + + self.index_method_reference("||".to_string(), &node.location(), method_receiver); + self.visit(&node.right()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::operation::printer; + + fn build_operations(source: &str) -> OperationBuilderResult { + let source = crate::test_utils::normalize_indentation(source); + let builder = RubyOperationBuilder::new("file:///test.rb".to_string(), &source); + builder.build() + } + + fn normalize_expected(expected: &str) -> String { + crate::test_utils::normalize_indentation(expected).trim().to_string() + } + + fn assert_operations(source: &str, expected: &str) { + let result = build_operations(source); + let actual = printer::print_operations(&result.operations, &result.strings, &result.names, false); + let expected = normalize_expected(expected); + assert_eq!(actual, expected, "\n\nActual:\n{actual}\n\nExpected:\n{expected}\n"); + } + + fn assert_operations_with_references(source: &str, expected: &str) { + let result = build_operations(source); + let actual = printer::print_operations(&result.operations, &result.strings, &result.names, true); + let expected = normalize_expected(expected); + assert_eq!(actual, expected, "\n\nActual:\n{actual}\n\nExpected:\n{expected}\n"); + } + + // -- Namespace tests -- + + #[test] + fn build_class_node() { + assert_operations( + " + class Foo + class Bar; end + end + ", + " + EnterClass(Foo) + EnterClass(Bar) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_class_with_qualified_name() { + assert_operations( + " + class Foo::Bar; end + ", + " + EnterClass(Foo::Bar) + ExitScope + ", + ); + } + + #[test] + fn build_class_with_superclass() { + assert_operations( + " + class Foo < Bar; end + ", + " + EnterClass(Foo, superclass: Bar) + ExitScope + ", + ); + } + + #[test] + fn build_module_node() { + assert_operations( + " + module Foo + module Bar; end + end + ", + " + EnterModule(Foo) + EnterModule(Bar) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_singleton_class() { + assert_operations( + " + class Foo + class << self + def bar; end + end + end + ", + " + EnterClass(Foo) + EnterSingletonClass(Foo::) + EnterMethod(bar()) + ExitScope + ExitScope + ExitScope + ", + ); + } + + // -- Method tests -- + + #[test] + fn build_def_node() { + assert_operations( + " + def foo; end + + class Foo + def bar; end + def self.baz; end + end + ", + " + EnterMethod(foo()) + ExitScope + EnterClass(Foo) + EnterMethod(bar()) + ExitScope + EnterMethod(self.baz()) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_def_node_with_constant_receiver() { + assert_operations( + " + class Bar + def Foo.quz; end + end + ", + " + EnterClass(Bar) + EnterMethod(Foo.quz()) + ExitScope + ExitScope + ", + ); + } + + // -- Visibility tests -- + + #[test] + fn build_default_visibility() { + assert_operations( + " + class Foo + private + + def m1; end + + public + + def m2; end + end + ", + " + EnterClass(Foo) + SetDefaultVisibility(private) + EnterMethod(m1()) + ExitScope + SetDefaultVisibility(public) + EnterMethod(m2()) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_inline_visibility() { + assert_operations( + " + protected def m1; end + ", + " + SetDefaultVisibility(protected) + EnterMethod(m1()) + ExitScope + SetDefaultVisibility(private) + ", + ); + } + + // TODO: `private :bar` with symbol args should produce SetMethodVisibility operations. + // This is one of the key motivations for the operation-based approach. + + #[test] + fn build_module_function() { + assert_operations( + " + module Foo + module_function + + def bar; end + end + ", + " + EnterModule(Foo) + SetDefaultVisibility(module_function) + EnterMethod(self.bar()) + ExitScope + EnterMethod(bar()) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_module_function_with_ivar() { + assert_operations( + " + module Foo + module_function + + def bar + @x = 1 + end + end + ", + " + EnterModule(Foo) + SetDefaultVisibility(module_function) + EnterMethod(self.bar()) + DefineInstanceVariable(@x) + ExitScope + EnterMethod(bar()) + DefineInstanceVariable(@x) + ExitScope + ExitScope + ", + ); + } + + // -- Constant tests -- + + #[test] + fn build_constant_write() { + assert_operations( + " + FOO = 1 + + class Bar + BAZ = 2 + end + ", + " + DefineConstant(FOO) + EnterClass(Bar) + DefineConstant(BAZ) + ExitScope + ", + ); + } + + #[test] + fn build_constant_path_write() { + assert_operations( + " + FOO::BAR = 1 + ", + " + DefineConstant(FOO::BAR) + ", + ); + } + + #[test] + fn build_constant_alias() { + assert_operations( + " + ALIAS = OtherConstant + ", + " + AliasConstant(ALIAS -> OtherConstant) + ", + ); + } + + #[test] + fn build_set_constant_visibility() { + assert_operations( + " + module Foo + BAR = 42 + private_constant :BAR + end + ", + " + EnterModule(Foo) + DefineConstant(BAR) + SetConstantVisibility(BAR, vis: private) + ExitScope + ", + ); + } + + #[test] + fn build_public_constant() { + assert_operations( + " + module Foo + BAR = 42 + public_constant :BAR + end + ", + " + EnterModule(Foo) + DefineConstant(BAR) + SetConstantVisibility(BAR, vis: public) + ExitScope + ", + ); + } + + #[test] + fn build_private_constant_multiple() { + assert_operations( + " + module Foo + BAR = 42 + BAZ = 43 + private_constant :BAR, :BAZ + end + ", + " + EnterModule(Foo) + DefineConstant(BAR) + DefineConstant(BAZ) + SetConstantVisibility(BAR, vis: private) + SetConstantVisibility(BAZ, vis: private) + ExitScope + ", + ); + } + + // -- Attribute tests -- + + #[test] + fn build_attr_accessor() { + assert_operations( + " + class Foo + attr_accessor :bar + attr_reader :baz + attr_writer :qux + end + ", + " + EnterClass(Foo) + DefineAttribute(accessor bar()) + DefineAttribute(reader baz()) + DefineAttribute(writer qux()) + ExitScope + ", + ); + } + + #[test] + fn build_multiple_attr_accessors() { + assert_operations( + " + class Foo + attr_accessor :bar, :baz + end + ", + " + EnterClass(Foo) + DefineAttribute(accessor bar()) + DefineAttribute(accessor baz()) + ExitScope + ", + ); + } + + #[test] + fn build_attr_with_visibility() { + assert_operations( + " + class Foo + private + + attr_reader :bar + end + ", + " + EnterClass(Foo) + SetDefaultVisibility(private) + DefineAttribute(reader bar()) + ExitScope + ", + ); + } + + // -- Mixin tests -- + + #[test] + fn build_mixins() { + assert_operations( + " + class Foo + include Bar + prepend Baz + extend Qux + end + ", + " + EnterClass(Foo) + Mixin(include, Bar) + Mixin(prepend, Baz) + Mixin(extend, Qux) + ExitScope + ", + ); + } + + // -- Alias tests -- + + #[test] + fn build_alias_method() { + assert_operations( + " + class Foo + alias foo bar + end + ", + " + EnterClass(Foo) + AliasMethod(foo() -> bar()) + ExitScope + ", + ); + } + + #[test] + fn build_alias_method_call() { + assert_operations( + " + class Foo + alias_method :new_name, :old_name + end + ", + " + EnterClass(Foo) + AliasMethod(new_name() -> old_name()) + ExitScope + ", + ); + } + + #[test] + fn build_alias_global_variable() { + assert_operations( + " + alias $new $old + ", + " + AliasGlobalVariable($new -> $old) + ", + ); + } + + // -- Variable tests -- + + #[test] + fn build_instance_variable() { + assert_operations( + " + class Foo + def initialize + @bar = 1 + end + end + ", + " + EnterClass(Foo) + EnterMethod(initialize()) + DefineInstanceVariable(@bar) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_class_variable() { + assert_operations( + " + class Foo + @@bar = 1 + end + ", + " + EnterClass(Foo) + DefineClassVariable(@@bar) + ExitScope + ", + ); + } + + #[test] + fn build_global_variable() { + assert_operations( + " + $foo = 1 + ", + " + DefineGlobalVariable($foo) + ", + ); + } + + // -- Reference tests -- + + #[test] + fn build_constant_references() { + assert_operations_with_references( + " + Foo + ", + " + ReferenceConstant(Foo) + ", + ); + } + + #[test] + fn build_method_references() { + assert_operations_with_references( + " + foo + ", + " + ReferenceMethod(foo) + ", + ); + } + + // -- Ordering tests -- + + #[test] + fn build_operations_ordering_with_visibility() { + assert_operations( + " + class Foo + def m1; end + private + def m2; end + end + ", + " + EnterClass(Foo) + EnterMethod(m1()) + ExitScope + SetDefaultVisibility(private) + EnterMethod(m2()) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_visibility_resets_in_nested_class() { + assert_operations( + " + class Foo + private + + class Bar + def m1; end + end + + def m2; end + end + ", + " + EnterClass(Foo) + SetDefaultVisibility(private) + EnterClass(Bar) + EnterMethod(m1()) + ExitScope + ExitScope + EnterMethod(m2()) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_visibility_in_singleton_class() { + assert_operations( + " + class Foo + protected + + class << self + def m1; end + + private + + def m2; end + end + + def m3; end + end + ", + " + EnterClass(Foo) + SetDefaultVisibility(protected) + EnterSingletonClass(Foo::) + EnterMethod(m1()) + ExitScope + SetDefaultVisibility(private) + EnterMethod(m2()) + ExitScope + ExitScope + EnterMethod(m3()) + ExitScope + ExitScope + ", + ); + } + + #[test] + fn build_top_level_method_visibility() { + assert_operations( + " + def m1; end + + protected def m2; end + + public + + def m3; end + ", + " + EnterMethod(m1()) + ExitScope + SetDefaultVisibility(protected) + EnterMethod(m2()) + ExitScope + SetDefaultVisibility(private) + SetDefaultVisibility(public) + EnterMethod(m3()) + ExitScope + ", + ); + } +} diff --git a/rust/rubydex/src/resolution.rs b/rust/rubydex/src/resolution.rs index 47015b06..10c7f7cd 100644 --- a/rust/rubydex/src/resolution.rs +++ b/rust/rubydex/src/resolution.rs @@ -1832,7 +1832,7 @@ impl<'a> Resolver<'a> { /// Pre-compute name depths for all names into a `NameId → depth` map. Each name's depth is /// computed once via memoized recursion, then used as an O(1) lookup key during sorting in /// `prepare_units`. - fn compute_name_depths(names: &IdentityHashMap) -> IdentityHashMap { + pub(crate) fn compute_name_depths(names: &IdentityHashMap) -> IdentityHashMap { let mut cache = IdentityHashMap::with_capacity_and_hasher(names.len(), IdentityHashBuilder); for &name_id in names.keys() { @@ -2066,6 +2066,11 @@ impl<'a> Resolver<'a> { } } +#[cfg(test)] +fn backend() -> crate::indexing::IndexerBackend { + crate::indexing::IndexerBackend::RubyIndexer +} + #[cfg(test)] #[path = "resolution_tests.rs"] mod tests; diff --git a/rust/rubydex/src/resolution_tests.rs b/rust/rubydex/src/resolution_tests.rs index 1ddc528d..7f6f854a 100644 --- a/rust/rubydex/src/resolution_tests.rs +++ b/rust/rubydex/src/resolution_tests.rs @@ -1,4 +1,7 @@ -use super::Resolver; +// This file is included via #[path] by both resolution.rs and operation/applier.rs +// to run the same tests against both indexing backends. Each parent module provides +// a `backend()` function that `graph_test()` calls via `super::backend()`. + use crate::{ assert_alias_targets_contain, assert_ancestors_eq, assert_constant_alias_target_eq, assert_constant_reference_to, assert_constant_reference_unresolved, assert_declaration_definitions_count_eq, assert_declaration_does_not_exist, @@ -7,15 +10,20 @@ use crate::{ assert_no_diagnostics, assert_no_members, assert_owner_eq, assert_singleton_class_eq, diagnostic::Rule, model::{declaration::Ancestors, ids::DeclarationId, name::NameRef}, + resolution::Resolver, test_utils::GraphTest, }; +fn graph_test() -> GraphTest { + GraphTest::new_with_backend(super::backend()) +} + mod constant_resolution_tests { use super::*; #[test] fn resolving_top_level_references() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///bar.rb", { r" class Bar; end @@ -42,7 +50,7 @@ mod constant_resolution_tests { #[test] fn resolving_nested_reference() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///bar.rb", { r" module Foo @@ -63,7 +71,7 @@ mod constant_resolution_tests { #[test] fn resolving_nested_reference_that_refer_to_top_level_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///bar.rb", { r" class Baz; end @@ -84,7 +92,7 @@ mod constant_resolution_tests { #[test] fn resolving_constant_path_references_at_top_level() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///bar.rb", { r" module Foo @@ -103,7 +111,7 @@ mod constant_resolution_tests { #[test] fn resolving_reference_for_non_existing_declaration() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo @@ -117,7 +125,7 @@ mod constant_resolution_tests { #[test] fn resolution_for_top_level_references() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -148,7 +156,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_to_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -168,7 +176,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_to_nested_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -190,7 +198,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_inside_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -213,7 +221,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_in_superclass() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -236,7 +244,7 @@ mod constant_alias_tests { #[test] fn resolving_chained_constant_aliases() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -259,7 +267,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_to_non_existent_target() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" ALIAS_1 = NonExistent @@ -276,7 +284,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_to_value_in_constant_path() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" VALUE = 1 @@ -296,7 +304,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_defined_before_target() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" ALIAS = Foo @@ -316,7 +324,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_to_value() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -338,7 +346,7 @@ mod constant_alias_tests { #[test] fn resolving_circular_constant_aliases() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" A = B @@ -357,7 +365,7 @@ mod constant_alias_tests { #[test] fn resolving_circular_constant_aliases_cross_namespace() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A @@ -383,7 +391,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_ping_pong() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Left @@ -422,7 +430,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_self_referential() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module M @@ -461,7 +469,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_with_multiple_definitions() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module A; end @@ -486,7 +494,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_with_multiple_targets() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module A @@ -519,7 +527,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_alias_multi_target_with_circular() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module A @@ -543,7 +551,7 @@ mod constant_alias_tests { #[test] fn multi_target_alias_constant_added_to_primary_owner() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///modules.rb", { r" module Foo; end @@ -575,7 +583,7 @@ mod constant_alias_tests { #[test] fn resolving_class_through_constant_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Outer @@ -608,7 +616,7 @@ mod constant_alias_tests { #[test] fn resolving_class_definition_through_constant_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Outer @@ -645,7 +653,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_reference_through_chained_aliases() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///defs.rb", { r" module Foo @@ -668,7 +676,7 @@ mod constant_alias_tests { #[test] fn resolving_constant_reference_through_top_level_alias_target() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///defs.rb", { r" module Foo @@ -688,7 +696,7 @@ mod constant_alias_tests { // Regression test: defining singleton method on alias triggers get_or_create_singleton_class #[test] fn resolving_singleton_method_on_alias_does_not_panic() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo; end @@ -703,7 +711,7 @@ mod constant_alias_tests { #[test] fn resolving_instance_variable_on_alias_does_not_panic() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo; end @@ -721,7 +729,7 @@ mod constant_alias_tests { fn method_call_on_namespace_alias() { // When a method call occurs in a constant alias to a namespace, the singleton class has to be created for the // target namespace and not for the alias - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -741,7 +749,7 @@ mod constant_alias_tests { #[test] fn method_def_on_namespace_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -763,7 +771,7 @@ mod constant_alias_tests { #[test] fn re_opening_constant_alias_as_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///alias.rb", { r" module Foo @@ -793,7 +801,7 @@ mod constant_alias_tests { #[test] fn constant_alias_reopened_as_class_with_nested_inheritance() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module Foo @@ -815,7 +823,7 @@ mod constant_alias_tests { #[test] fn superclass_through_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" class Base; end @@ -830,7 +838,7 @@ mod constant_alias_tests { #[test] fn mixin_through_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module M; end @@ -847,7 +855,7 @@ mod constant_alias_tests { #[test] fn including_unresolved_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module Foo; end @@ -865,7 +873,7 @@ mod constant_alias_tests { #[test] fn prepending_unresolved_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module Foo; end @@ -883,7 +891,7 @@ mod constant_alias_tests { #[test] fn inheriting_unresolved_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module Foo; end @@ -900,7 +908,7 @@ mod constant_alias_tests { #[test] fn re_opening_unresolved_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module Foo; end @@ -934,7 +942,7 @@ mod constant_alias_tests { #[test] fn re_opening_namespace_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module Foo; end @@ -976,7 +984,7 @@ mod superclass_tests { #[test] fn linearizing_super_classes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo; end @@ -998,7 +1006,7 @@ mod superclass_tests { #[test] fn descendants_are_tracked_for_parent_classes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -1026,7 +1034,7 @@ mod superclass_tests { #[test] fn linearizing_circular_super_classes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo < Bar; end @@ -1043,7 +1051,7 @@ mod superclass_tests { #[test] fn resolving_a_constant_inherited_from_the_super_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -1064,7 +1072,7 @@ mod superclass_tests { #[test] fn does_not_loop_forever_on_non_existing_parents() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Bar < Foo @@ -1085,7 +1093,7 @@ mod superclass_tests { #[test] fn resolving_inherited_constant_dependent_on_complex_parent() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1109,7 +1117,7 @@ mod superclass_tests { #[test] fn linearizing_parent_classes_with_parent_scope() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1129,7 +1137,7 @@ mod superclass_tests { #[test] fn references_with_parent_scope_search_inheritance() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1152,7 +1160,7 @@ mod superclass_tests { #[test] fn ancestors_for_unresolved_parent_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", " @@ -1185,7 +1193,7 @@ mod include_tests { #[test] fn resolving_constant_references_involved_in_includes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -1203,7 +1211,7 @@ mod include_tests { #[test] fn resolving_include_using_inherited_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1228,7 +1236,7 @@ mod include_tests { #[test] fn linearizing_included_modules() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -1256,7 +1264,7 @@ mod include_tests { #[test] fn include_on_dynamic_namespace_definitions() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module B; end @@ -1286,7 +1294,7 @@ mod include_tests { #[test] fn cyclic_include() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1303,7 +1311,7 @@ mod include_tests { #[test] fn duplicate_includes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1324,7 +1332,7 @@ mod include_tests { #[test] fn indirect_duplicate_includes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1355,7 +1363,7 @@ mod include_tests { #[test] fn includes_involving_parent_scopes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A @@ -1389,7 +1397,7 @@ mod include_tests { #[test] fn duplicate_includes_in_parents() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1420,7 +1428,7 @@ mod include_tests { #[test] fn included_modules_involved_in_definitions() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1448,7 +1456,7 @@ mod include_tests { #[test] fn multiple_mixins_in_same_include() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1468,7 +1476,7 @@ mod include_tests { #[test] fn descendants_are_tracked_for_includes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -1494,7 +1502,7 @@ mod prepend_tests { #[test] fn resolving_constant_references_involved_in_prepends() { - let mut context = GraphTest::new(); + let mut context = graph_test(); // To linearize the ancestors of `Bar`, we need to resolve `Foo` first. However, during that resolution, we need // to check `Bar`'s ancestor chain before checking the top level (which is where we'll find `Foo`). In these @@ -1516,7 +1524,7 @@ mod prepend_tests { #[test] fn resolving_prepend_using_inherited_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); // Prepending `Foo` makes `Bar` available, which we can then prepend as well. This requires resolving constants // with partially linearized ancestors context.index_uri("file:///foo.rb", { @@ -1543,7 +1551,7 @@ mod prepend_tests { #[test] fn linearizing_prepended_modules() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -1571,7 +1579,7 @@ mod prepend_tests { #[test] fn prepend_on_dynamic_namespace_definitions() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module B; end @@ -1601,7 +1609,7 @@ mod prepend_tests { #[test] fn prepends_track_descendants() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -1623,7 +1631,7 @@ mod prepend_tests { #[test] fn cyclic_prepend() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1640,7 +1648,7 @@ mod prepend_tests { #[test] fn duplicate_prepends() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1661,7 +1669,7 @@ mod prepend_tests { #[test] fn indirect_duplicate_prepends() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1692,7 +1700,7 @@ mod prepend_tests { #[test] fn multiple_mixins_in_same_prepend() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1712,7 +1720,7 @@ mod prepend_tests { #[test] fn prepends_involving_parent_scopes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A @@ -1746,7 +1754,7 @@ mod prepend_tests { #[test] fn duplicate_prepends_in_parents() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1777,7 +1785,7 @@ mod prepend_tests { #[test] fn prepended_modules_involved_in_definitions() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -1809,7 +1817,7 @@ mod mixin_dedup_tests { #[test] fn duplicate_includes_and_prepends() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1835,7 +1843,7 @@ mod mixin_dedup_tests { #[test] fn duplicate_indirect_includes_and_prepends() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1899,7 +1907,7 @@ mod mixin_dedup_tests { #[test] fn duplicate_includes_and_prepends_through_parents() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -1939,7 +1947,7 @@ mod object_ancestors_tests { #[test] fn ancestors_with_missing_core() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", " @@ -1958,7 +1966,7 @@ mod object_ancestors_tests { #[test] fn ancestor_patches_to_object_are_correctly_processed() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", " @@ -1976,7 +1984,7 @@ mod object_ancestors_tests { #[test] fn basic_object_ancestors() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", " @@ -1991,7 +1999,7 @@ mod object_ancestors_tests { #[test] fn basic_object_ancestors_including_kernel() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", " @@ -2007,7 +2015,7 @@ mod object_ancestors_tests { #[test] fn constant_resolution_inside_basic_object() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class String; end @@ -2025,7 +2033,7 @@ mod object_ancestors_tests { #[test] fn top_level_scope_searches_object_ancestors() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Kernel @@ -2049,7 +2057,7 @@ mod object_ancestors_tests { #[test] fn top_level_script_constant_resolution_searches_object_ancestors() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Kernel @@ -2071,7 +2079,7 @@ mod object_ancestors_tests { #[test] fn module_own_ancestors_take_priority_over_object_fallback() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module MyConstants @@ -2100,7 +2108,7 @@ mod object_ancestors_tests { #[test] fn object_inherited_constant_inside_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Kernel @@ -2132,7 +2140,7 @@ mod singleton_ancestors_tests { #[test] fn singleton_ancestors_for_classes() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -2199,7 +2207,7 @@ mod singleton_ancestors_tests { #[test] fn singleton_ancestors_for_modules() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -2249,7 +2257,7 @@ mod singleton_ancestors_tests { #[test] fn singleton_ancestors_with_inherited_parent_modules() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -2330,7 +2338,7 @@ mod singleton_ancestors_tests { #[test] fn singleton_ancestor_chain_cascades_through_intermediate_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -2366,7 +2374,7 @@ mod singleton_ancestors_tests { #[test] fn extend_creates_singleton_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", " @@ -2399,7 +2407,7 @@ mod singleton_ancestors_tests { #[test] fn extend_creates_singleton_class_with_existing_singleton_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", " @@ -2434,7 +2442,7 @@ mod singleton_ancestors_tests { #[test] fn extend_creates_singleton_class_on_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", " @@ -2457,7 +2465,7 @@ mod singleton_ancestors_tests { #[test] fn singleton_class_created_in_remaining_definitions_has_linearized_ancestors() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -2491,7 +2499,7 @@ mod method_tests { #[test] fn resolution_for_method_with_receiver() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2525,7 +2533,7 @@ mod method_tests { #[test] fn resolution_for_self_method_with_same_name_instance_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -2545,7 +2553,7 @@ mod method_tests { #[test] fn resolution_for_self_method_alias_with_same_name_instance_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri( "file:///foo.rbs", r" @@ -2567,7 +2575,7 @@ mod method_tests { #[test] fn resolving_method_defined_inside_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2587,7 +2595,7 @@ mod method_tests { #[test] fn resolving_attr_accessors_inside_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2615,7 +2623,7 @@ mod method_alias_tests { #[test] fn resolving_method_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2635,7 +2643,7 @@ mod method_alias_tests { #[test] fn resolving_method_alias_with_self_receiver() { // SelfReceiver resolves to instance methods (the class directly), not the singleton - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2654,7 +2662,7 @@ mod method_alias_tests { #[test] fn resolving_alias_method_in_singleton_class_lands_on_singleton() { // `class << self; alias_method ...; end` — alias lands on singleton via lexical nesting - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2676,7 +2684,7 @@ mod method_alias_tests { #[test] fn resolving_self_alias_method_is_equivalent_to_bare_alias_method() { // `self.alias_method` and bare `alias_method` resolve identically (instance methods) - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///with_self.rb", { r" class WithSelf @@ -2704,7 +2712,7 @@ mod method_alias_tests { #[test] fn resolving_method_alias_with_constant_receiver() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Bar @@ -2727,7 +2735,7 @@ mod method_alias_tests { #[test] fn resolving_global_variable_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" $foo = 123 @@ -2747,7 +2755,7 @@ mod method_alias_tests { #[test] fn resolving_global_variable_alias_inside_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2775,7 +2783,7 @@ mod variable_tests { #[test] fn resolution_for_class_variable_in_nested_singleton_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2799,7 +2807,7 @@ mod variable_tests { #[test] fn resolution_for_class_variable_in_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2818,7 +2826,7 @@ mod variable_tests { #[test] fn resolution_for_class_variable_only_follows_lexical_nesting() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo; end @@ -2845,7 +2853,7 @@ mod variable_tests { #[test] fn resolution_for_class_variable_at_top_level() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" @@var = 123 @@ -2861,7 +2869,7 @@ mod variable_tests { #[test] fn resolution_for_instance_and_class_instance_variables() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2899,7 +2907,7 @@ mod variable_tests { #[test] fn resolution_for_instance_variables_with_dynamic_method_owner() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2928,7 +2936,7 @@ mod variable_tests { #[test] fn resolution_for_class_instance_variable_in_compact_namespace() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Bar; end @@ -2950,7 +2958,7 @@ mod variable_tests { #[test] fn resolution_for_instance_variable_in_singleton_class_body() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -2974,7 +2982,7 @@ mod variable_tests { #[test] fn resolution_for_instance_variable_in_constant_receiver_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -2995,7 +3003,7 @@ mod variable_tests { #[test] fn resolution_for_top_level_instance_variable() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" @foo = 0 @@ -3012,7 +3020,7 @@ mod variable_tests { #[test] fn resolution_for_instance_variable_with_unresolved_receiver() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3040,7 +3048,7 @@ mod declaration_creation_tests { #[test] fn resolution_creates_global_declaration() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -3068,7 +3076,7 @@ mod declaration_creation_tests { #[test] fn resolution_for_non_constant_declarations() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3094,7 +3102,7 @@ mod declaration_creation_tests { // // If `bar.rb` is loaded first, then `Bar` resolves to top level `Bar` and `Bar::Baz` is defined, completely // escaping the `Foo` nesting. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -3122,7 +3130,7 @@ mod declaration_creation_tests { #[test] fn expected_name_depth_order() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -3178,7 +3186,7 @@ mod singleton_class_tests { #[test] fn resolution_for_singleton_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3203,7 +3211,7 @@ mod singleton_class_tests { #[test] fn resolution_for_nested_singleton_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3231,7 +3239,7 @@ mod singleton_class_tests { #[test] fn resolution_for_singleton_class_of_external_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo; end @@ -3261,7 +3269,7 @@ mod singleton_class_tests { #[test] fn singleton_class_is_set() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3281,7 +3289,7 @@ mod singleton_class_tests { #[test] fn incomplete_method_calls_automatically_trigger_singleton_creation() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3312,7 +3320,7 @@ mod singleton_class_tests { #[test] fn singleton_class_calls_create_nested_singletons() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3351,7 +3359,7 @@ mod singleton_class_tests { #[test] fn singleton_class_on_a_scoped_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -3388,7 +3396,7 @@ mod singleton_class_tests { #[test] fn singleton_class_on_a_self_call() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3428,7 +3436,7 @@ mod singleton_class_tests { fn resolves_sibling_constant_inside_singleton_class_method_body() { // Constant referenced from inside a method defined in `class << self` must resolve against // the lexical scope that encloses the singleton class block, not stop at the singleton class. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A @@ -3456,7 +3464,7 @@ mod singleton_class_tests { fn resolves_sibling_constant_inside_nested_singleton_class() { // Nested `class << self` inside a nested class: lookup must still walk outward through // every enclosing lexical scope to find a sibling defined far above. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A @@ -3486,7 +3494,7 @@ mod singleton_class_tests { fn resolves_sibling_constant_directly_in_singleton_class_body() { // Constant referenced directly in the `class << self` body (not inside a method) — e.g. // passed as an argument to a class-level DSL call — must also resolve lexically. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A @@ -3512,7 +3520,7 @@ mod singleton_class_tests { fn singleton_class_lexical_scope_still_resolves_sibling_from_other_scopes() { // Sanity / non-regression: a sibling constant must continue to resolve from every other // scope where it already worked (instance method body, class body, top level). - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A @@ -3544,7 +3552,7 @@ mod singleton_class_tests { fn singleton_class_scope_does_not_over_resolve_unknown_constant() { // Sanity: a constant that genuinely does not exist must remain unresolved even with the // fix in place — the fix must not invent resolutions by walking too far. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A @@ -3570,7 +3578,7 @@ mod fqn_and_naming_tests { #[test] fn distinct_declarations_with_conflicting_string_ids() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -3593,7 +3601,7 @@ mod fqn_and_naming_tests { #[test] fn fully_qualified_names_are_unique() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -3663,7 +3671,7 @@ mod fqn_and_naming_tests { #[test] fn test_nested_same_names() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -3697,7 +3705,7 @@ mod todo_tests { #[test] fn resolution_does_not_loop_infinitely_on_non_existing_constants() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo::Bar @@ -3724,7 +3732,7 @@ mod todo_tests { #[test] fn resolve_missing_declaration_to_todo() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo::Bar @@ -3756,7 +3764,7 @@ mod todo_tests { #[test] fn qualified_name_inside_nesting_resolves_when_discovered_incrementally() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///baz.rb", { r" module Foo @@ -3790,7 +3798,7 @@ mod todo_tests { #[test] fn promoted_to_real_namespace() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo::Bar @@ -3820,7 +3828,7 @@ mod todo_tests { #[test] fn promoted_to_real_namespace_incrementally() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///bar.rb", { r" class Foo::Bar @@ -3859,7 +3867,7 @@ mod todo_tests { #[test] fn two_levels_unknown() { // class A::B::C — neither A nor B exist. Both should become Todos, C is a Class. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" class A::B::C @@ -3885,7 +3893,7 @@ mod todo_tests { #[test] fn three_levels_unknown() { // class A::B::C::D — A, B, C are all unknown. Tests recursion beyond depth 2. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" class A::B::C::D @@ -3913,7 +3921,7 @@ mod todo_tests { #[test] fn partially_unresolvable() { // A exists but B doesn't — A resolves to a real Module, B becomes a Todo under A. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module A; end @@ -3936,7 +3944,7 @@ mod todo_tests { fn shared_by_sibling_classes() { // Two classes share the same unknown parent chain. The Todos for A and B should // be created once and reused, with both C and D as members of B. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" class A::B::C @@ -3974,7 +3982,7 @@ mod todo_tests { // clears all declarations and re-resolves from scratch. This test verifies that // the promotion works when both files are present during the second resolution pass, // not that Todos are surgically updated in place. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///c.rb", { r" class A::B::C @@ -4011,7 +4019,7 @@ mod todo_tests { fn with_self_method_and_ivar() { // def self.foo with @x inside a multi-level compact class — the SelfReceiver // on the method must find C's declaration to create the singleton class and ivar. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" class A::B::C @@ -4034,7 +4042,7 @@ mod todo_tests { fn nested_inside_module_with_separate_intermediate() { // Compact namespace nested inside a module, where the intermediate namespace // is defined separately. Bar::Baz should become a Todo since only Bar exists. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" module Foo @@ -4060,7 +4068,7 @@ mod todo_tests { // Baz::Qux inside Foo, where Baz comes from included Bar module. // Baz::Qux should resolve through inheritance to Bar::Baz::Qux, not create // a top-level Baz Todo. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///file1.rb", { r" module Foo @@ -4091,7 +4099,7 @@ mod todo_tests { #[test] fn intermediate_todo_on_constant_alias() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///alias.rb", { r" module Bar; end @@ -4118,7 +4126,7 @@ mod todo_tests { #[test] fn rbs_method_definition() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///foo.rbs", { r" class Foo @@ -4141,7 +4149,7 @@ mod todo_tests { fn resolves_constant_with_ancestors_partial() { // B has Ancestors::Partial because its prepend is defined in another file. // X must wait for B's ancestors to resolve, then resolve to A::X. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///1.rb", { r" module A @@ -4174,7 +4182,7 @@ mod todo_tests { fn resolves_constant_with_ancestor_partial() { // C has an Ancestor::Partial entry because O::A is defined in another file. // X must wait for O::A to resolve, then resolve to O::A::X. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///1.rb", { r" class B @@ -4206,7 +4214,7 @@ mod todo_tests { #[test] fn method_call_on_undefined_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo.bar @@ -4220,7 +4228,7 @@ mod todo_tests { #[test] fn qualified_name_inside_nesting_resolves_to_top_level() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo @@ -4258,7 +4266,7 @@ mod dynamic_namespace_tests { // // We need to ensure that the associated Declaration for Bar is transformed into a class if any of its // definitions represent one, otherwise we have no place to store the includes and ancestors - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Baz; end @@ -4279,7 +4287,7 @@ mod dynamic_namespace_tests { #[test] fn resolving_accessing_meta_programming_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo = Protobuf.some_dynamic_class @@ -4293,7 +4301,7 @@ mod dynamic_namespace_tests { #[test] fn inheriting_from_dynamic_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo = some_dynamic_class @@ -4309,7 +4317,7 @@ mod dynamic_namespace_tests { #[test] fn including_dynamic_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo = some_dynamic_module @@ -4326,7 +4334,7 @@ mod dynamic_namespace_tests { #[test] fn prepending_dynamic_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo = some_dynamic_module @@ -4343,7 +4351,7 @@ mod dynamic_namespace_tests { #[test] fn extending_dynamic_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo = some_dynamic_module @@ -4376,7 +4384,7 @@ mod dynamic_namespace_tests { #[test] fn ancestor_operations_on_meta_programming_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Foo; end @@ -4399,7 +4407,7 @@ mod promotability_tests { #[test] fn non_promotable_constant_not_promoted_to_class_with_members() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" FOO = 42 @@ -4416,7 +4424,7 @@ mod promotability_tests { #[test] fn non_promotable_constant_not_promoted_to_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r#" FOO = "hello" @@ -4432,7 +4440,7 @@ mod promotability_tests { #[test] fn promotable_constant_is_promoted_to_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Baz; end @@ -4454,7 +4462,7 @@ mod promotability_tests { fn mixed_promotable_and_non_promotable_blocks_promotion() { // If the same constant has both a promotable and non-promotable definition, // promotion should be blocked - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", "Foo = some_call"); context.index_uri("file:///b.rb", "Foo = 42"); context.index_uri("file:///c.rb", "class Foo; end"); @@ -4466,7 +4474,7 @@ mod promotability_tests { #[test] fn promotable_constant_promoted_to_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module Baz; end @@ -4486,7 +4494,7 @@ mod promotability_tests { #[test] fn class_first_then_constant_stays_namespace() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo; end @@ -4501,7 +4509,7 @@ mod promotability_tests { #[test] fn promotable_constant_path_write() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" module A; end @@ -4517,7 +4525,7 @@ mod promotability_tests { #[test] fn method_call_on_promotable_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Qux = some_factory_call @@ -4532,7 +4540,7 @@ mod promotability_tests { #[test] fn singleton_method_on_non_promotable_constant_does_not_crash() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" FOO = 42 @@ -4547,7 +4555,7 @@ mod promotability_tests { #[test] fn def_self_on_promotable_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Qux = some_factory_call @@ -4565,7 +4573,7 @@ mod promotability_tests { // When a promotable constant is auto-promoted via singleton class access, we conservatively // promote to a module (not a class) since we don't know what the call returns. // Modules don't inherit from Object. - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo = some_factory_call @@ -4580,7 +4588,7 @@ mod promotability_tests { #[test] fn meta_programming_class_with_members() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" Foo = dynamic_class do @@ -4597,7 +4605,7 @@ mod promotability_tests { #[test] fn self_method_inside_non_promotable_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" CONST = 1 @@ -4615,7 +4623,7 @@ mod promotability_tests { #[test] fn defining_constant_in_promotable_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" Foo = dynamic @@ -4632,7 +4640,7 @@ mod promotability_tests { #[test] fn singleton_class_block_for_promotable_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" Foo = dynamic @@ -4650,7 +4658,7 @@ mod promotability_tests { #[test] fn singleton_class_block_for_non_promotable_constant() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///a.rb", { r" Foo = 1 @@ -4673,7 +4681,7 @@ mod rbs_tests { #[test] fn rbs_module_and_class_declarations() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///test.rbs", { r" module Foo @@ -4693,7 +4701,7 @@ mod rbs_tests { #[test] fn rbs_nested_declarations() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///test.rbs", { r" module Foo @@ -4720,7 +4728,7 @@ mod rbs_tests { #[test] fn rbs_qualified_module_name() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///parents.rbs", { r" module Foo @@ -4744,7 +4752,7 @@ mod rbs_tests { #[test] fn rbs_qualified_name_inside_nested_module() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///foo.rbs", { r" module Outer @@ -4770,7 +4778,7 @@ mod rbs_tests { #[test] fn rbs_superclass_resolution() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///test.rbs", { r" class Foo @@ -4802,7 +4810,7 @@ mod rbs_tests { #[test] fn rbs_constant_declarations() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///test.rbs", { r" FOO: String @@ -4833,7 +4841,7 @@ mod rbs_tests { #[test] fn rbs_global_declaration() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///test.rbs", "$foo: String"); context.resolve(); @@ -4848,7 +4856,7 @@ mod rbs_tests { #[test] fn rbs_mixin_resolution() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_rbs_uri("file:///test.rbs", { r" module Bar @@ -4872,7 +4880,7 @@ mod rbs_tests { #[test] fn rbs_method_alias_resolution() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri("file:///foo.rb", { r" class Foo @@ -4928,7 +4936,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_override_applies_in_source_order() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -4947,7 +4955,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_on_direct_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -4973,7 +4981,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_on_attr_methods() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -4999,7 +5007,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_on_inherited_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5024,7 +5032,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_on_grandparent_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5048,7 +5056,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_on_included_module_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5071,7 +5079,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_on_undefined_method_emits_diagnostic() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5092,7 +5100,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_across_reopened_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///a.rb", r" @@ -5117,7 +5125,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_visibility_resolves_when_ancestor_discovered_incrementally() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///child.rb", r" @@ -5150,7 +5158,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_constant_visibility_on_direct_member() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5183,7 +5191,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_constant_visibility_via_qualified_receiver() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5206,7 +5214,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_constant_visibility_multi_arg_undefined_emits_per_name_diagnostic() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5228,7 +5236,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_constant_visibility_inherited_constant_emits_diagnostic() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5254,7 +5262,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_constant_visibility_clears_when_call_removed() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5283,7 +5291,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_constant_visibility_inside_singleton_class_body() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5303,7 +5311,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_constant_visibility_persists_across_reopened_class() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///a.rb", r" @@ -5328,7 +5336,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_singleton_method_visibility_on_direct_member() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5351,7 +5359,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_singleton_method_visibility_on_inherited_method() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5374,7 +5382,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_singleton_method_visibility_on_undefined_method_emits_diagnostic() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5395,7 +5403,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_singleton_method_visibility_undefined_target_diagnostic_clears_when_file_deleted() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" @@ -5428,7 +5436,7 @@ mod visibility_resolution_tests { #[test] fn retroactive_singleton_method_visibility_undefined_target_diagnostic_clears_when_target_added() { - let mut context = GraphTest::new(); + let mut context = graph_test(); context.index_uri( "file:///foo.rb", r" diff --git a/rust/rubydex/src/test_utils/graph_test.rs b/rust/rubydex/src/test_utils/graph_test.rs index 6a1a673c..2d1d2755 100644 --- a/rust/rubydex/src/test_utils/graph_test.rs +++ b/rust/rubydex/src/test_utils/graph_test.rs @@ -1,20 +1,34 @@ use super::normalize_indentation; #[cfg(test)] use crate::diagnostic::Rule; -use crate::indexing::{self, LanguageId}; +use crate::indexing::{self, IndexerBackend, LanguageId}; use crate::model::graph::{Graph, NameDependent}; use crate::model::ids::{NameId, StringId}; use crate::resolution::Resolver; -#[derive(Default)] pub struct GraphTest { graph: Graph, + backend: IndexerBackend, +} + +impl Default for GraphTest { + fn default() -> Self { + Self::new() + } } impl GraphTest { #[must_use] pub fn new() -> Self { - Self { graph: Graph::new() } + Self::new_with_backend(IndexerBackend::RubyIndexer) + } + + #[must_use] + pub fn new_with_backend(backend: IndexerBackend) -> Self { + Self { + graph: Graph::new(), + backend, + } } #[must_use] @@ -30,7 +44,8 @@ impl GraphTest { /// Indexes a Ruby source pub fn index_uri(&mut self, uri: &str, source: &str) { let source = normalize_indentation(source); - indexing::index_source(&mut self.graph, uri, &source, &LanguageId::Ruby); + let local_graph = indexing::build_local_graph(uri.to_string(), &source, &LanguageId::Ruby, self.backend); + self.graph.consume_document_changes(local_graph); } /// Indexes an RBS source diff --git a/rust/rubydex/src/test_utils/local_graph_test.rs b/rust/rubydex/src/test_utils/local_graph_test.rs index fdd8a939..bf3830c4 100644 --- a/rust/rubydex/src/test_utils/local_graph_test.rs +++ b/rust/rubydex/src/test_utils/local_graph_test.rs @@ -1,7 +1,7 @@ use super::normalize_indentation; use crate::indexing::local_graph::LocalGraph; use crate::indexing::rbs_indexer::RBSIndexer; -use crate::indexing::ruby_indexer::RubyIndexer; +use crate::indexing::{IndexerBackend, LanguageId, build_local_graph}; use crate::model::definitions::Definition; use crate::model::graph::NameDependent; use crate::model::ids::{NameId, StringId, UriId}; @@ -19,13 +19,14 @@ pub struct LocalGraphTest { impl LocalGraphTest { #[must_use] pub fn new(uri: &str, source: &str) -> Self { + Self::new_with_backend(uri, source, IndexerBackend::RubyIndexer) + } + + #[must_use] + pub fn new_with_backend(uri: &str, source: &str, backend: IndexerBackend) -> Self { let uri = uri.to_string(); let source = normalize_indentation(source); - - let mut indexer = RubyIndexer::new(uri.clone(), &source); - indexer.index(); - let graph = indexer.local_graph(); - + let graph = build_local_graph(uri.clone(), &source, &LanguageId::Ruby, backend); Self { uri, source, graph } }