Skip to content

Commit d73e9db

Browse files
committed
extract
1 parent 843d726 commit d73e9db

14 files changed

Lines changed: 3255 additions & 1392 deletions

File tree

docs/ruby-behaviors.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,14 @@ class Foo; private :foo; end # works — retroactive across reopen
968968
Foo.private_instance_methods(false) # => [:foo]
969969
```
970970
971+
The method must already exist when the visibility call runs. Within one file, `private :foo` before `def foo` matches
972+
Ruby's `NameError` behavior. Across files, Rubydex cannot know runtime load order, so it treats cross-file definitions
973+
as potentially available. A definitely-prior same-file definition is preferred for the visibility snapshot; URI lexical
974+
order is used only as a deterministic tie-breaker when multiple cross-file definitions could apply.
975+
976+
Rubydex treats visibility calls inside method bodies or ordinary blocks as runtime calls. It does not apply those
977+
visibility effects statically because Ruby only executes them if the containing method or block is called.
978+
971979
**`private :inherited_method` creates an implicit copy:**
972980
973981
```ruby
@@ -1039,8 +1047,8 @@ Foo.singleton_methods(false) # => [:foo]
10391047
Ruby can invoke `module_function` indirectly with `send` on a module object. Rubydex indexes the direct call forms above;
10401048
dynamic `send` calls are treated as ordinary method calls rather than visibility operations.
10411049
1042-
Rubydex also treats `module_function` calls inside method bodies or ordinary blocks as runtime calls. It does not apply
1043-
those visibility effects statically because Ruby only executes them if the containing method or block is called.
1050+
Like other visibility operations, Rubydex treats `module_function` calls inside method bodies or ordinary blocks as
1051+
runtime calls rather than static visibility operations.
10441052
10451053
**Creates a copy, not a reference:**
10461054
@@ -1077,8 +1085,9 @@ invalidate and rebuild generated copies when the source file changes, because a
10771085
copy the current source method at the `module_function :foo` call.
10781086

10791087
When the source method and `module_function :foo` call are in different files, static indexing does not know runtime
1080-
load order. Rubydex uses URI lexical order as a deterministic proxy: methods in earlier URI-sorted files are eligible
1081-
for later URI-sorted visibility calls, while methods in later files are not. Within one file, the source method must
1088+
load order. Rubydex treats cross-file source methods as potentially available and uses URI lexical order only as a
1089+
deterministic tie-breaker when multiple cross-file definitions could be copied. A definitely-prior same-file source
1090+
method is preferred over cross-file candidates. Within one file, the source method must
10821091
appear before the `module_function :foo` call, matching Ruby's `NameError` behavior for calls that appear before the
10831092
method definition.
10841093
@@ -1135,6 +1144,9 @@ Foo.constants # => [] (private constants hidden from .constants)
11351144
- `Foo.const_defined?(:PRIV)` returns **true** even for private constants — visibility doesn't affect `const_defined?`
11361145
- `Foo.const_get(:PRIV)` **bypasses** private constant visibility and returns the value — only the `::` operator enforces `private_constant`
11371146

1147+
Rubydex treats `private_constant`/`public_constant` calls inside method bodies or ordinary blocks as runtime calls rather
1148+
than static visibility operations.
1149+
11381150
#### `public_constant`
11391151

11401152
Reverses `private_constant`:

rust/rubydex-sys/src/graph_api.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,11 +1001,16 @@ pub unsafe extern "C" fn rdx_graph_visibility(pointer: GraphPointer, declaration
10011001
return ptr::null();
10021002
};
10031003

1004+
debug_assert_ne!(
1005+
visibility,
1006+
Visibility::ModuleFunction,
1007+
"module_function visibility must be resolved before C API use"
1008+
);
1009+
10041010
let c_visibility = match visibility {
10051011
Visibility::Public => CVisibility::Public,
10061012
Visibility::Protected => CVisibility::Protected,
1007-
Visibility::Private => CVisibility::Private,
1008-
Visibility::ModuleFunction => unreachable!("module_function visibility must be resolved before C API use"),
1013+
Visibility::Private | Visibility::ModuleFunction => CVisibility::Private,
10091014
};
10101015

10111016
Box::into_raw(Box::new(c_visibility)).cast_const()

rust/rubydex/src/indexing/ruby_indexer.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,17 +1230,18 @@ impl<'a> RubyIndexer<'a> {
12301230
}
12311231

12321232
fn handle_constant_visibility(&mut self, node: &ruby_prism::CallNode, visibility: Visibility) {
1233+
if self.current_nesting_is_runtime_body() {
1234+
self.visit_runtime_call_node(node);
1235+
return;
1236+
}
1237+
12331238
let receiver = node.receiver();
12341239

12351240
let receiver_name_id = match receiver {
12361241
Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }) => {
12371242
self.index_constant_reference(&receiver.unwrap(), true)
12381243
}
12391244
Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
1240-
Some(Nesting::Method(_)) => {
1241-
self.visit_call_node_parts(node);
1242-
return;
1243-
}
12441245
None => {
12451246
self.local_graph.add_diagnostic(
12461247
Rule::InvalidPrivateConstant,
@@ -1319,13 +1320,14 @@ impl<'a> RubyIndexer<'a> {
13191320
visibility: Visibility,
13201321
call_name: &str,
13211322
) {
1323+
if self.current_nesting_is_runtime_body() {
1324+
self.visit_runtime_call_node(node);
1325+
return;
1326+
}
1327+
13221328
match node.receiver() {
1323-
Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
1324-
Some(Nesting::Method(_)) => {
1325-
self.visit_call_node_parts(node);
1326-
return;
1327-
}
1328-
None => {
1329+
Some(ruby_prism::Node::SelfNode { .. }) | None => {
1330+
if self.nesting_stack.last().is_none() {
13291331
self.local_graph.add_diagnostic(
13301332
Rule::InvalidMethodVisibility,
13311333
Offset::from_prism_location(&node.location()),
@@ -1334,8 +1336,7 @@ impl<'a> RubyIndexer<'a> {
13341336
self.visit_call_node_parts(node);
13351337
return;
13361338
}
1337-
_ => {}
1338-
},
1339+
}
13391340
_ => {
13401341
self.visit_call_node_parts(node);
13411342
return;
@@ -2003,8 +2004,6 @@ impl Visit<'_> for RubyIndexer<'_> {
20032004
.unwrap_or_else(|| offset_for_comments.start());
20042005

20052006
Self::each_string_or_symbol_arg(call, |name, location| {
2006-
let reader_str_id = self.local_graph.intern_string(format!("{name}()"));
2007-
let writer_str_id = self.local_graph.intern_string(format!("{name}=()"));
20082007
let parent_nesting_id = self.parent_nesting_id();
20092008
let offset = Offset::from_prism_location(&location);
20102009

@@ -2018,6 +2017,9 @@ impl Visit<'_> for RubyIndexer<'_> {
20182017

20192018
match kind {
20202019
AttrKind::Accessor => {
2020+
let reader_str_id = self.local_graph.intern_string(format!("{name}()"));
2021+
let writer_str_id = self.local_graph.intern_string(format!("{name}=()"));
2022+
20212023
let reader = Definition::AttrAccessor(Box::new(AttrAccessorDefinition::new(
20222024
reader_str_id,
20232025
self.uri_id,
@@ -2043,6 +2045,8 @@ impl Visit<'_> for RubyIndexer<'_> {
20432045
self.add_member_to_current_owner(writer_id);
20442046
}
20452047
AttrKind::Reader => {
2048+
let reader_str_id = self.local_graph.intern_string(format!("{name}()"));
2049+
20462050
let definition = Definition::AttrReader(Box::new(AttrReaderDefinition::new(
20472051
reader_str_id,
20482052
self.uri_id,
@@ -2056,6 +2060,8 @@ impl Visit<'_> for RubyIndexer<'_> {
20562060
self.add_member_to_current_owner(definition_id);
20572061
}
20582062
AttrKind::Writer => {
2063+
let writer_str_id = self.local_graph.intern_string(format!("{name}=()"));
2064+
20592065
let definition = Definition::AttrWriter(Box::new(AttrWriterDefinition::new(
20602066
writer_str_id,
20612067
self.uri_id,
@@ -2199,7 +2205,7 @@ impl Visit<'_> for RubyIndexer<'_> {
21992205
"private" | "protected" | "public" | "module_function" => {
22002206
let visibility = Visibility::from_string(message.as_str());
22012207

2202-
if visibility == Visibility::ModuleFunction && self.current_nesting_is_runtime_body() {
2208+
if self.current_nesting_is_runtime_body() {
22032209
self.visit_runtime_call_node(node);
22042210
return;
22052211
}

rust/rubydex/src/model/declaration.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,6 @@ impl Declaration {
425425
if let Some(pos) = it.definition_ids.iter().position(|id| id == definition_id) {
426426
// Definition order is semantic for retroactive visibility; keep it stable.
427427
it.definition_ids.remove(pos);
428-
it.definition_ids.shrink_to_fit();
429428
true
430429
} else {
431430
false

rust/rubydex/src/model/definitions.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,41 @@ impl Definition {
195195
pub fn is_deprecated(&self) -> bool {
196196
all_definitions!(self, it => it.flags().is_deprecated())
197197
}
198+
199+
#[must_use]
200+
pub fn method_effective_visibility(&self) -> Option<Visibility> {
201+
match self {
202+
Definition::MethodVisibility(vis) => Some(match vis.visibility() {
203+
Visibility::ModuleFunction => Visibility::Private,
204+
visibility => *visibility,
205+
}),
206+
Definition::Method(method) => Some(*method.visibility()),
207+
Definition::AttrAccessor(attr) => Some(*attr.visibility()),
208+
Definition::AttrReader(attr) => Some(*attr.visibility()),
209+
Definition::AttrWriter(attr) => Some(*attr.visibility()),
210+
_ => None,
211+
}
212+
}
213+
214+
#[must_use]
215+
pub(crate) fn establishes_method_member(&self) -> bool {
216+
matches!(
217+
self,
218+
Definition::Method(_)
219+
| Definition::MethodAlias(_)
220+
| Definition::AttrAccessor(_)
221+
| Definition::AttrReader(_)
222+
| Definition::AttrWriter(_)
223+
)
224+
}
225+
226+
#[must_use]
227+
pub(crate) fn is_copyable_method_body(&self) -> bool {
228+
matches!(
229+
self,
230+
Definition::Method(_) | Definition::AttrAccessor(_) | Definition::AttrReader(_) | Definition::AttrWriter(_)
231+
)
232+
}
198233
}
199234

200235
/// Represents a mixin: include, prepend, or extend.
@@ -1121,7 +1156,12 @@ impl ParameterStruct {
11211156
}
11221157
}
11231158

1124-
/// An attr accessor definition
1159+
/// The reader-side method definition created by `attr_accessor`.
1160+
///
1161+
/// The writer side is indexed separately as an `AttrWriterDefinition` for the
1162+
/// generated `foo=()` method. Keeping the reader and writer as separate method
1163+
/// declarations lets visibility changes and `module_function :foo=` target the
1164+
/// same methods Ruby creates.
11251165
///
11261166
/// # Example
11271167
/// ```ruby

rust/rubydex/src/model/document.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ impl Document {
8282
self.diagnostics.push(diagnostic);
8383
}
8484

85+
pub fn retain_diagnostics<F>(&mut self, mut f: F)
86+
where
87+
F: FnMut(&Diagnostic) -> bool,
88+
{
89+
self.diagnostics.retain(|diagnostic| f(diagnostic));
90+
}
91+
8592
/// Computes the require path for this document given load paths.
8693
///
8794
/// Returns `None` if:

0 commit comments

Comments
 (0)