rubydex 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -6
  3. data/THIRD_PARTY_LICENSES.html +271 -2
  4. data/exe/rdx +2 -73
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/declaration.c +1 -1
  8. data/ext/rubydex/definition.c +32 -4
  9. data/ext/rubydex/diagnostic.c +75 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/graph.c +27 -48
  12. data/ext/rubydex/graph.h +6 -0
  13. data/ext/rubydex/query.c +487 -0
  14. data/ext/rubydex/query.h +8 -0
  15. data/ext/rubydex/reference.c +60 -0
  16. data/ext/rubydex/rubydex.c +4 -0
  17. data/ext/rubydex/utils.c +23 -4
  18. data/ext/rubydex/utils.h +5 -0
  19. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  20. data/lib/rubydex/cli/command/console.rb +55 -0
  21. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  22. data/lib/rubydex/cli/command/lint.rb +202 -0
  23. data/lib/rubydex/cli/command/mcp.rb +30 -0
  24. data/lib/rubydex/cli/command/query.rb +70 -0
  25. data/lib/rubydex/cli/command/skill.rb +69 -0
  26. data/lib/rubydex/cli/command.rb +168 -0
  27. data/lib/rubydex/cli.rb +93 -0
  28. data/lib/rubydex/config.rb +59 -0
  29. data/lib/rubydex/diagnostic.rb +12 -3
  30. data/lib/rubydex/errors.rb +42 -1
  31. data/lib/rubydex/graph.rb +10 -3
  32. data/lib/rubydex/linter/custom_rule.rb +97 -0
  33. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  34. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  35. data/lib/rubydex/linter/rule_loader.rb +36 -0
  36. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  37. data/lib/rubydex/linter/runner.rb +56 -0
  38. data/lib/rubydex/linter.rb +19 -0
  39. data/lib/rubydex/location.rb +3 -0
  40. data/lib/rubydex/mcp_server.rb +1 -2
  41. data/lib/rubydex/related_information.rb +17 -0
  42. data/lib/rubydex/rule.rb +33 -0
  43. data/lib/rubydex/severity.rb +70 -0
  44. data/lib/rubydex/skill.rb +88 -0
  45. data/lib/rubydex/skill_registry.rb +62 -0
  46. data/lib/rubydex/version.rb +1 -1
  47. data/lib/rubydex.rb +6 -0
  48. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  49. data/rbi/rubydex.rbi +578 -15
  50. data/rust/Cargo.lock +7 -0
  51. data/rust/rubydex/Cargo.toml +1 -0
  52. data/rust/rubydex/benches/graph_memory.rs +20 -4
  53. data/rust/rubydex/src/compile_assertions.rs +15 -0
  54. data/rust/rubydex/src/config.rs +538 -157
  55. data/rust/rubydex/src/diagnostic.rs +66 -40
  56. data/rust/rubydex/src/errors.rs +0 -1
  57. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  58. data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
  59. data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
  60. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
  61. data/rust/rubydex/src/lib.rs +1 -0
  62. data/rust/rubydex/src/listing.rs +26 -1
  63. data/rust/rubydex/src/main.rs +9 -128
  64. data/rust/rubydex/src/model/declaration.rs +301 -229
  65. data/rust/rubydex/src/model/definitions.rs +27 -26
  66. data/rust/rubydex/src/model/document.rs +43 -7
  67. data/rust/rubydex/src/model/graph.rs +67 -68
  68. data/rust/rubydex/src/model/id.rs +55 -0
  69. data/rust/rubydex/src/model/ids.rs +21 -9
  70. data/rust/rubydex/src/model/name.rs +88 -19
  71. data/rust/rubydex/src/model/references.rs +16 -13
  72. data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
  73. data/rust/rubydex/src/path_helpers.rs +77 -0
  74. data/rust/rubydex/src/query/cypher/schema.rs +853 -0
  75. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  76. data/rust/rubydex/src/query/cypher/tests.rs +253 -0
  77. data/rust/rubydex/src/query/cypher.rs +54 -0
  78. data/rust/rubydex/src/query.rs +125 -43
  79. data/rust/rubydex/src/resolution.rs +368 -395
  80. data/rust/rubydex/src/resolution_tests.rs +504 -78
  81. data/rust/rubydex/src/test_utils/context.rs +2 -1
  82. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  84. data/rust/rubydex/tests/cli.rs +4 -4
  85. data/rust/rubydex-sys/src/config_api.rs +205 -0
  86. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  87. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  88. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  89. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  90. data/rust/rubydex-sys/src/graph_api.rs +31 -68
  91. data/rust/rubydex-sys/src/lib.rs +2 -0
  92. data/rust/rubydex-sys/src/name_api.rs +2 -6
  93. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +37 -2
@@ -1,6 +1,6 @@
1
1
  #[cfg(any(test, feature = "test_utils"))]
2
2
  use crate::model::document::Document;
3
- use crate::{model::ids::UriId, offset::Offset};
3
+ use crate::{assert_mem_size, model::ids::UriId, offset::Offset};
4
4
 
5
5
  #[derive(Debug)]
6
6
  pub struct Diagnostic {
@@ -9,6 +9,7 @@ pub struct Diagnostic {
9
9
  offset: Offset,
10
10
  message: String,
11
11
  }
12
+ assert_mem_size!(Diagnostic, 48);
12
13
 
13
14
  impl Diagnostic {
14
15
  #[must_use]
@@ -53,60 +54,85 @@ impl Diagnostic {
53
54
  }
54
55
  }
55
56
 
56
- fn camel_to_snake(s: &str) -> String {
57
- let mut snake = String::new();
58
- for (i, ch) in s.chars().enumerate() {
59
- if ch.is_uppercase() {
60
- if i != 0 {
61
- snake.push('-');
62
- }
63
- for lc in ch.to_lowercase() {
64
- snake.push(lc);
65
- }
66
- } else {
67
- snake.push(ch);
68
- }
69
- }
70
- snake
57
+ #[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize)]
58
+ #[serde(rename_all = "lowercase")]
59
+ pub enum Severity {
60
+ Error,
61
+ Warning,
62
+ Information,
63
+ Hint,
71
64
  }
72
65
 
73
66
  macro_rules! rules {
74
- (
75
- $( $variant:ident );* $(;)?
76
- ) => {
67
+ ($($(#[$attribute:meta])* $rule:ident),+ $(,)?) => {
77
68
  #[derive(Debug, Copy, Clone, PartialEq, Eq)]
78
69
  pub enum Rule {
79
- $(
80
- $variant,
81
- )*
70
+ $($(#[$attribute])* $rule,)+
82
71
  }
83
72
 
84
- impl std::fmt::Display for Rule {
85
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86
- write!(f, "{}", match self {
87
- $(
88
- Rule::$variant => camel_to_snake(stringify!($variant)),
89
- )*
90
- })
73
+ impl Rule {
74
+ #[must_use]
75
+ pub fn all() -> &'static [Self] {
76
+ &[$(Self::$rule,)+]
91
77
  }
92
78
  }
93
- }
79
+ };
94
80
  }
95
81
 
96
82
  rules! {
97
83
  // Parsing
98
- ParseError;
99
- ParseWarning;
84
+ ParseError,
85
+ ParseWarning,
100
86
 
101
87
  // Indexing
102
- DynamicConstantReference;
103
- DynamicSingletonDefinition;
104
- DynamicAncestor;
105
- TopLevelMixinSelf;
106
- InvalidPrivateConstant;
107
- InvalidMethodVisibility;
88
+ DynamicConstantReference,
89
+ DynamicSingletonDefinition,
90
+ DynamicAncestor,
91
+ TopLevelMixinSelf,
92
+ InvalidConstantVisibility,
93
+ InvalidMethodVisibility,
108
94
 
109
95
  // Resolution
110
- UndefinedMethodVisibilityTarget;
111
- UndefinedConstantVisibilityTarget;
96
+ UndefinedMethodVisibilityTarget,
97
+ UndefinedConstantVisibilityTarget,
98
+ }
99
+
100
+ impl Rule {
101
+ #[must_use]
102
+ pub fn name(&self) -> &'static str {
103
+ match self {
104
+ Self::ParseError => "ParseError",
105
+ Self::ParseWarning => "ParseWarning",
106
+ Self::DynamicConstantReference => "DynamicConstantReference",
107
+ Self::DynamicSingletonDefinition => "DynamicSingletonDefinition",
108
+ Self::DynamicAncestor => "DynamicAncestor",
109
+ Self::TopLevelMixinSelf => "TopLevelMixinSelf",
110
+ Self::InvalidConstantVisibility => "InvalidConstantVisibility",
111
+ Self::InvalidMethodVisibility => "InvalidMethodVisibility",
112
+ Self::UndefinedMethodVisibilityTarget => "UndefinedMethodVisibilityTarget",
113
+ Self::UndefinedConstantVisibilityTarget => "UndefinedConstantVisibilityTarget",
114
+ }
115
+ }
116
+
117
+ #[must_use]
118
+ pub fn default_severity(&self) -> Severity {
119
+ match self {
120
+ Self::ParseError => Severity::Error,
121
+ Self::ParseWarning
122
+ | Self::InvalidConstantVisibility
123
+ | Self::InvalidMethodVisibility
124
+ | Self::UndefinedMethodVisibilityTarget
125
+ | Self::UndefinedConstantVisibilityTarget => Severity::Warning,
126
+ Self::DynamicConstantReference
127
+ | Self::DynamicSingletonDefinition
128
+ | Self::DynamicAncestor
129
+ | Self::TopLevelMixinSelf => Severity::Information,
130
+ }
131
+ }
132
+ }
133
+
134
+ impl std::fmt::Display for Rule {
135
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136
+ write!(f, "{}", self.name())
137
+ }
112
138
  }
@@ -26,5 +26,4 @@ macro_rules! errors {
26
26
  errors!(
27
27
  FileError;
28
28
  ConfigError;
29
- ConfigNotFound;
30
29
  );
@@ -6,7 +6,7 @@ use crate::model::document::Document;
6
6
  use crate::model::graph::NameDependent;
7
7
  use crate::model::identity_maps::IdentityHashMap;
8
8
  use crate::model::ids::{ConstantReferenceId, DefinitionId, MethodReferenceId, NameId, StringId, UriId};
9
- use crate::model::name::{Name, NameRef};
9
+ use crate::model::name::{Name, NameRef, ParentScope};
10
10
  use crate::model::references::{ConstantReference, MethodRef};
11
11
  use crate::model::string_ref::StringRef;
12
12
  use crate::offset::Offset;
@@ -119,7 +119,8 @@ impl LocalGraph {
119
119
  &self.names
120
120
  }
121
121
 
122
- pub fn add_name(&mut self, name: Name) -> NameId {
122
+ pub fn add_name(&mut self, str: StringId, parent_scope: ParentScope, nesting: Option<NameId>) -> NameId {
123
+ let name = Name::new(&self.names, str, parent_scope, nesting);
123
124
  let name_id = name.id();
124
125
 
125
126
  match self.names.entry(name_id) {
@@ -128,13 +129,13 @@ impl LocalGraph {
128
129
  entry.get_mut().increment_ref_count(1);
129
130
  }
130
131
  Entry::Vacant(entry) => {
131
- if let Some(&parent_scope) = name.parent_scope().as_ref() {
132
+ if let Some(&parent_scope_id) = parent_scope.as_ref() {
132
133
  self.name_dependents
133
- .entry(parent_scope)
134
+ .entry(parent_scope_id)
134
135
  .or_default()
135
136
  .push(NameDependent::ChildName(name_id));
136
137
  }
137
- if let Some(&nesting_id) = name.nesting().as_ref() {
138
+ if let Some(nesting_id) = nesting {
138
139
  self.name_dependents
139
140
  .entry(nesting_id)
140
141
  .or_default()
@@ -3,8 +3,9 @@
3
3
  use core::panic;
4
4
 
5
5
  use ruby_rbs::node::{
6
- self, AliasKind, ClassNode, CommentNode, ConstantNode, ExtendNode, FunctionTypeNode, GlobalNode, IncludeNode,
7
- ModuleNode, Node, NodeList, PrependNode, TypeNameNode, Visit,
6
+ self, AliasKind, AttrAccessorNode, AttrReaderNode, AttrWriterNode, AttributeKind, AttributeVisibility, ClassNode,
7
+ CommentNode, ConstantNode, ExtendNode, FunctionTypeNode, GlobalNode, IncludeNode, ModuleNode, Node, NodeList,
8
+ PrependNode, TypeNameNode, Visit,
8
9
  };
9
10
 
10
11
  use crate::diagnostic::Rule;
@@ -17,7 +18,7 @@ use crate::model::definitions::{
17
18
  };
18
19
  use crate::model::document::Document;
19
20
  use crate::model::ids::{ConstantReferenceId, DefinitionId, NameId, StringId, UriId};
20
- use crate::model::name::{Name, ParentScope};
21
+ use crate::model::name::ParentScope;
21
22
  use crate::model::references::ConstantReference;
22
23
  use crate::model::visibility::Visibility;
23
24
  use crate::offset::Offset;
@@ -80,7 +81,16 @@ impl<'a> RBSIndexer<'a> {
80
81
  let Node::Symbol(symbol) = path_node else {
81
82
  continue;
82
83
  };
83
- parent_scope = ParentScope::Some(self.intern_name(&symbol, parent_scope, nesting_name_id));
84
+ let name_id = self.intern_name(&symbol, parent_scope, nesting_name_id);
85
+
86
+ // Emit a constant reference for each parent-scope segment so it gets its own resolution
87
+ // unit, mirroring the Ruby indexer (see `index_constant_reference`). Otherwise the
88
+ // parent scope is an orphan name that never resolves, leaving qualified references stuck.
89
+ let offset = Offset::from_rbs_location(&symbol.location());
90
+ self.local_graph
91
+ .add_constant_reference(ConstantReference::new(name_id, self.uri_id, offset));
92
+
93
+ parent_scope = ParentScope::Some(name_id);
84
94
  }
85
95
 
86
96
  self.intern_name(&type_name.name(), parent_scope, nesting_name_id)
@@ -93,8 +103,7 @@ impl<'a> RBSIndexer<'a> {
93
103
  nesting_name_id: Option<NameId>,
94
104
  ) -> NameId {
95
105
  let string_id = self.local_graph.intern_string(symbol.as_str().to_owned());
96
- self.local_graph
97
- .add_name(Name::new(string_id, parent_scope, nesting_name_id))
106
+ self.local_graph.add_name(string_id, parent_scope, nesting_name_id)
98
107
  }
99
108
 
100
109
  fn parent_lexical_scope_id(&self) -> Option<DefinitionId> {
@@ -214,6 +223,133 @@ impl<'a> RBSIndexer<'a> {
214
223
  definition_id
215
224
  }
216
225
 
226
+ #[allow(clippy::too_many_arguments)]
227
+ fn register_attribute_methods(
228
+ &mut self,
229
+ name: &str,
230
+ offset: Offset,
231
+ name_offset: Offset,
232
+ comments: Box<[Comment]>,
233
+ flags: DefinitionFlags,
234
+ lexical_nesting_id: Option<DefinitionId>,
235
+ kind: AttributeKind,
236
+ attribute_visibility: AttributeVisibility,
237
+ reader: bool,
238
+ writer: bool,
239
+ ) {
240
+ let (visibility, receiver) = match kind {
241
+ AttributeKind::Instance => {
242
+ let visibility = match attribute_visibility {
243
+ AttributeVisibility::Public => Visibility::Public,
244
+ AttributeVisibility::Private => Visibility::Private,
245
+ AttributeVisibility::Unspecified => self.current_visibility,
246
+ };
247
+ (visibility, None)
248
+ }
249
+ AttributeKind::Singleton => {
250
+ let visibility = match attribute_visibility {
251
+ AttributeVisibility::Private => Visibility::Private,
252
+ AttributeVisibility::Public | AttributeVisibility::Unspecified => Visibility::Public,
253
+ };
254
+ (
255
+ visibility,
256
+ Some(Receiver::SelfReceiver(
257
+ lexical_nesting_id.expect("Singleton attribute must have a lexical enclosing scope"),
258
+ )),
259
+ )
260
+ }
261
+ };
262
+
263
+ match (reader, writer) {
264
+ (true, true) => {
265
+ self.register_attribute_method(
266
+ name,
267
+ false,
268
+ offset.clone(),
269
+ name_offset.clone(),
270
+ comments.clone(),
271
+ flags.clone(),
272
+ lexical_nesting_id,
273
+ visibility,
274
+ receiver.clone(),
275
+ );
276
+ self.register_attribute_method(
277
+ name,
278
+ true,
279
+ offset,
280
+ name_offset,
281
+ comments,
282
+ flags,
283
+ lexical_nesting_id,
284
+ visibility,
285
+ receiver,
286
+ );
287
+ }
288
+ (true, false) => self.register_attribute_method(
289
+ name,
290
+ false,
291
+ offset,
292
+ name_offset,
293
+ comments,
294
+ flags,
295
+ lexical_nesting_id,
296
+ visibility,
297
+ receiver,
298
+ ),
299
+ (false, true) => self.register_attribute_method(
300
+ name,
301
+ true,
302
+ offset,
303
+ name_offset,
304
+ comments,
305
+ flags,
306
+ lexical_nesting_id,
307
+ visibility,
308
+ receiver,
309
+ ),
310
+ (false, false) => unreachable!("attribute must have a reader or writer"),
311
+ }
312
+ }
313
+
314
+ #[allow(clippy::too_many_arguments)]
315
+ fn register_attribute_method(
316
+ &mut self,
317
+ name: &str,
318
+ writer: bool,
319
+ offset: Offset,
320
+ name_offset: Offset,
321
+ comments: Box<[Comment]>,
322
+ flags: DefinitionFlags,
323
+ lexical_nesting_id: Option<DefinitionId>,
324
+ visibility: Visibility,
325
+ receiver: Option<Receiver>,
326
+ ) {
327
+ let str_id = self
328
+ .local_graph
329
+ .intern_string(format!("{name}{}()", if writer { "=" } else { "" }));
330
+ let signatures = if writer {
331
+ let parameter_name = self.local_graph.intern_string(name.to_owned());
332
+ let parameter = Parameter::RequiredPositional(ParameterStruct::new(name_offset.clone(), parameter_name));
333
+ Signatures::Simple(vec![parameter].into_boxed_slice())
334
+ } else {
335
+ Signatures::Simple(Box::new([]))
336
+ };
337
+
338
+ let definition = Definition::Method(Box::new(MethodDefinition::new(
339
+ str_id,
340
+ self.uri_id,
341
+ offset,
342
+ name_offset,
343
+ comments,
344
+ flags,
345
+ lexical_nesting_id,
346
+ signatures,
347
+ visibility,
348
+ receiver,
349
+ )));
350
+ self.register_definition(definition, lexical_nesting_id);
351
+ }
352
+
217
353
  #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
218
354
  fn source_at(&self, location: &node::RBSLocationRange) -> String {
219
355
  let start = location.start() as usize;
@@ -486,7 +622,10 @@ impl Visit for RBSIndexer<'_> {
486
622
  self.uri_id,
487
623
  offset,
488
624
  comments,
489
- Self::flags(&constant_node.annotations()),
625
+ // RBS establishes that the constant exists, but its value type is intentionally not
626
+ // represented in the graph. Treat it like a dynamic Ruby assignment so resolution
627
+ // may promote it when a namespace or singleton receiver is required.
628
+ Self::flags(&constant_node.annotations()) | DefinitionFlags::PROMOTABLE,
490
629
  lexical_nesting_id,
491
630
  )));
492
631
 
@@ -562,6 +701,51 @@ impl Visit for RBSIndexer<'_> {
562
701
  self.register_definition(definition, lexical_nesting_id);
563
702
  }
564
703
 
704
+ fn visit_attr_reader_node(&mut self, attribute_node: &AttrReaderNode) {
705
+ self.register_attribute_methods(
706
+ attribute_node.name().as_str(),
707
+ Offset::from_rbs_location(&attribute_node.location()),
708
+ Offset::from_rbs_location(&attribute_node.name_location()),
709
+ self.collect_comments(attribute_node.comment()),
710
+ Self::flags(&attribute_node.annotations()),
711
+ self.parent_lexical_scope_id(),
712
+ attribute_node.kind(),
713
+ attribute_node.visibility(),
714
+ true,
715
+ false,
716
+ );
717
+ }
718
+
719
+ fn visit_attr_writer_node(&mut self, attribute_node: &AttrWriterNode) {
720
+ self.register_attribute_methods(
721
+ attribute_node.name().as_str(),
722
+ Offset::from_rbs_location(&attribute_node.location()),
723
+ Offset::from_rbs_location(&attribute_node.name_location()),
724
+ self.collect_comments(attribute_node.comment()),
725
+ Self::flags(&attribute_node.annotations()),
726
+ self.parent_lexical_scope_id(),
727
+ attribute_node.kind(),
728
+ attribute_node.visibility(),
729
+ false,
730
+ true,
731
+ );
732
+ }
733
+
734
+ fn visit_attr_accessor_node(&mut self, attribute_node: &AttrAccessorNode) {
735
+ self.register_attribute_methods(
736
+ attribute_node.name().as_str(),
737
+ Offset::from_rbs_location(&attribute_node.location()),
738
+ Offset::from_rbs_location(&attribute_node.name_location()),
739
+ self.collect_comments(attribute_node.comment()),
740
+ Self::flags(&attribute_node.annotations()),
741
+ self.parent_lexical_scope_id(),
742
+ attribute_node.kind(),
743
+ attribute_node.visibility(),
744
+ true,
745
+ true,
746
+ );
747
+ }
748
+
565
749
  fn visit_method_definition_node(&mut self, def_node: &node::MethodDefinitionNode) {
566
750
  let str_id = self.local_graph.intern_string(format!("{}()", def_node.name()));
567
751
  let offset = Offset::from_rbs_location(&def_node.location());
@@ -639,6 +823,7 @@ impl Visit for RBSIndexer<'_> {
639
823
  mod tests {
640
824
  use ruby_rbs::node::{self, Node, NodeList};
641
825
 
826
+ use crate::diagnostic::Severity;
642
827
  use crate::indexing::rbs_indexer::RBSIndexer;
643
828
  use crate::model::definitions::{Definition, DefinitionFlags, Parameter, Signatures};
644
829
  use crate::model::visibility::Visibility;
@@ -670,7 +855,11 @@ mod tests {
670
855
  fn index_source_with_errors() {
671
856
  let context = index_source("module");
672
857
 
673
- assert_local_diagnostics_eq!(&context, ["parse-error: Failed to parse RBS document (1:1-1:1)"]);
858
+ assert_local_diagnostics_eq!(
859
+ &context,
860
+ ["ParseError: Failed to parse RBS document (1:1-1:1)"],
861
+ severity: Severity::Error
862
+ );
674
863
 
675
864
  assert!(context.graph().definitions().is_empty());
676
865
  }
@@ -1039,6 +1228,93 @@ mod tests {
1039
1228
  });
1040
1229
  }
1041
1230
 
1231
+ #[test]
1232
+ fn indexes_attribute_members_as_methods_without_retaining_types_or_instance_variables() {
1233
+ let context = index_source({
1234
+ "
1235
+ class Foo
1236
+ # Reader documentation
1237
+ %a{deprecated}
1238
+ attr_reader inferred: Integer
1239
+ attr_reader absent(): Symbol
1240
+ attr_writer explicit (@writer): String
1241
+ attr_accessor accessor: bool
1242
+ private
1243
+ attr_reader inherited_visibility: Float
1244
+ public
1245
+ private attr_accessor self.class_value (@class_value): bool
1246
+ end
1247
+ "
1248
+ });
1249
+
1250
+ assert_no_local_diagnostics!(&context);
1251
+ assert_eq!(context.graph().definitions().len(), 9);
1252
+
1253
+ let method = |name: &str| {
1254
+ context
1255
+ .graph()
1256
+ .definitions()
1257
+ .values()
1258
+ .find_map(|definition| match definition {
1259
+ Definition::Method(method)
1260
+ if context
1261
+ .graph()
1262
+ .strings()
1263
+ .get(method.str_id())
1264
+ .is_some_and(|string| string.as_str() == name) =>
1265
+ {
1266
+ Some(method)
1267
+ }
1268
+ _ => None,
1269
+ })
1270
+ .unwrap_or_else(|| panic!("expected `{name}` method definition"))
1271
+ };
1272
+
1273
+ for name in [
1274
+ "inferred()",
1275
+ "absent()",
1276
+ "explicit=()",
1277
+ "accessor()",
1278
+ "accessor=()",
1279
+ "inherited_visibility()",
1280
+ "class_value()",
1281
+ "class_value=()",
1282
+ ] {
1283
+ assert_eq!(method(name).signatures().as_slice().len(), 1);
1284
+ }
1285
+
1286
+ for name in [
1287
+ "inferred()",
1288
+ "absent()",
1289
+ "accessor()",
1290
+ "inherited_visibility()",
1291
+ "class_value()",
1292
+ ] {
1293
+ assert!(method(name).signatures().as_slice()[0].is_empty());
1294
+ }
1295
+
1296
+ for (name, parameter_name) in [
1297
+ ("explicit=()", "explicit"),
1298
+ ("accessor=()", "accessor"),
1299
+ ("class_value=()", "class_value"),
1300
+ ] {
1301
+ let signature = &method(name).signatures().as_slice()[0];
1302
+ let [Parameter::RequiredPositional(parameter)] = signature.as_ref() else {
1303
+ panic!("expected `{name}` to have one required positional parameter");
1304
+ };
1305
+ assert_string_eq!(&context, parameter.str(), parameter_name);
1306
+ assert_offset_string!(&context, parameter.offset(), parameter_name);
1307
+ }
1308
+
1309
+ assert_eq!(method("class_value()").visibility(), &Visibility::Private);
1310
+ assert_eq!(method("class_value=()").visibility(), &Visibility::Private);
1311
+ assert_eq!(method("inherited_visibility()").visibility(), &Visibility::Private);
1312
+ assert_method_has_receiver!(&context, method("class_value()"), "Foo");
1313
+ assert_method_has_receiver!(&context, method("class_value=()"), "Foo");
1314
+ assert_def_comments_eq!(&context, method("inferred()"), ["# Reader documentation"]);
1315
+ assert!(method("inferred()").flags().contains(DefinitionFlags::DEPRECATED));
1316
+ }
1317
+
1042
1318
  #[test]
1043
1319
  fn index_alias_node() {
1044
1320
  let context = index_source({