rubydex 0.3.0 → 0.4.1

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 +29 -7
  3. data/THIRD_PARTY_LICENSES.html +238 -2
  4. data/exe/rdx +2 -149
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/definition.c +34 -0
  8. data/ext/rubydex/definition.h +3 -0
  9. data/ext/rubydex/diagnostic.c +28 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/extconf.rb +4 -2
  12. data/ext/rubydex/graph.c +71 -55
  13. data/ext/rubydex/graph.h +6 -0
  14. data/ext/rubydex/query.c +398 -16
  15. data/ext/rubydex/rubydex.c +2 -0
  16. data/ext/rubydex/utils.c +11 -4
  17. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  18. data/lib/rubydex/cli/command/console.rb +55 -0
  19. data/lib/rubydex/cli/command/lint/explain.rb +76 -0
  20. data/lib/rubydex/cli/command/lint.rb +208 -0
  21. data/lib/rubydex/cli/command/mcp.rb +30 -0
  22. data/lib/rubydex/cli/command/query.rb +70 -0
  23. data/lib/rubydex/cli/command/skill.rb +69 -0
  24. data/lib/rubydex/cli/command.rb +168 -0
  25. data/lib/rubydex/cli.rb +93 -0
  26. data/lib/rubydex/config.rb +59 -0
  27. data/lib/rubydex/diagnostic.rb +12 -3
  28. data/lib/rubydex/errors.rb +42 -1
  29. data/lib/rubydex/graph.rb +10 -3
  30. data/lib/rubydex/linter/custom_rule.rb +97 -0
  31. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  32. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  33. data/lib/rubydex/linter/rule_loader.rb +36 -0
  34. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  35. data/lib/rubydex/linter/runner.rb +56 -0
  36. data/lib/rubydex/linter.rb +19 -0
  37. data/lib/rubydex/location.rb +3 -0
  38. data/lib/rubydex/mcp_server.rb +1 -2
  39. data/lib/rubydex/related_information.rb +17 -0
  40. data/lib/rubydex/rule.rb +33 -0
  41. data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
  42. data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
  43. data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
  44. data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
  45. data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
  46. data/lib/rubydex/rules/parse_error.rb +25 -0
  47. data/lib/rubydex/rules/parse_warning.rb +28 -0
  48. data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
  49. data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
  50. data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
  51. data/lib/rubydex/severity.rb +70 -0
  52. data/lib/rubydex/skill.rb +89 -0
  53. data/lib/rubydex/skill_registry.rb +62 -0
  54. data/lib/rubydex/version.rb +1 -1
  55. data/lib/rubydex.rb +8 -0
  56. data/lib/rubydex_linter/rules/rule_structure.rb +140 -0
  57. data/rbi/rubydex.rbi +464 -21
  58. data/rust/Cargo.lock +2 -2
  59. data/rust/rubydex/Cargo.toml +5 -1
  60. data/rust/rubydex/benches/graph_memory.rs +3 -5
  61. data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
  62. data/rust/rubydex/src/config.rs +538 -157
  63. data/rust/rubydex/src/diagnostic.rs +162 -45
  64. data/rust/rubydex/src/errors.rs +0 -1
  65. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  66. data/rust/rubydex/src/indexing/rbs_indexer.rs +270 -6
  67. data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -12
  68. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +134 -81
  69. data/rust/rubydex/src/lib.rs +1 -0
  70. data/rust/rubydex/src/listing.rs +26 -1
  71. data/rust/rubydex/src/main.rs +7 -4
  72. data/rust/rubydex/src/model/declaration.rs +302 -219
  73. data/rust/rubydex/src/model/graph.rs +27 -40
  74. data/rust/rubydex/src/model/name.rs +58 -17
  75. data/rust/rubydex/src/operation/ruby_builder.rs +30 -45
  76. data/rust/rubydex/src/path_helpers.rs +77 -0
  77. data/rust/rubydex/src/query/cypher/schema.rs +63 -0
  78. data/rust/rubydex/src/query/cypher/tests.rs +26 -1
  79. data/rust/rubydex/src/query/cypher.rs +8 -11
  80. data/rust/rubydex/src/query.rs +314 -44
  81. data/rust/rubydex/src/resolution.rs +139 -187
  82. data/rust/rubydex/src/resolution_tests.rs +247 -19
  83. data/rust/rubydex/src/test_utils/context.rs +2 -1
  84. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  86. data/rust/rubydex/tests/cli.rs +4 -4
  87. data/rust/rubydex-sys/src/config_api.rs +205 -0
  88. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  89. data/rust/rubydex-sys/src/definition_api.rs +68 -1
  90. data/rust/rubydex-sys/src/diagnostic_api.rs +42 -8
  91. data/rust/rubydex-sys/src/graph_api.rs +125 -205
  92. data/rust/rubydex-sys/src/lib.rs +2 -0
  93. data/rust/rubydex-sys/src/name_api.rs +49 -17
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +42 -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,176 @@ 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
+ ($($(#[doc = $documentation:literal])+ $rule:ident => $severity:ident),+ $(,)?) => {
77
68
  #[derive(Debug, Copy, Clone, PartialEq, Eq)]
78
69
  pub enum Rule {
79
- $(
80
- $variant,
81
- )*
70
+ $($(#[doc = $documentation])+ $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,)+]
77
+ }
78
+
79
+ #[must_use]
80
+ pub fn name(&self) -> &'static str {
81
+ match self {
82
+ $(Self::$rule => stringify!($rule),)+
83
+ }
84
+ }
85
+
86
+ #[must_use]
87
+ pub fn default_severity(&self) -> Severity {
88
+ match self {
89
+ $(Self::$rule => Severity::$severity,)+
90
+ }
91
+ }
92
+
93
+ #[must_use]
94
+ pub fn documentation(&self) -> &'static [&'static str] {
95
+ match self {
96
+ $(Self::$rule => &[$($documentation,)+],)+
97
+ }
91
98
  }
92
99
  }
93
- }
100
+ };
94
101
  }
95
102
 
96
103
  rules! {
97
- // Parsing
98
- ParseError;
99
- ParseWarning;
100
-
101
- // Indexing
102
- DynamicConstantReference;
103
- DynamicSingletonDefinition;
104
- DynamicAncestor;
105
- TopLevelMixinSelf;
106
- InvalidConstantVisibility;
107
- InvalidMethodVisibility;
108
-
109
- // Resolution
110
- UndefinedMethodVisibilityTarget;
111
- UndefinedConstantVisibilityTarget;
104
+ // ******** Parsing ******** //
105
+
106
+ /// A parse error represents invalid Ruby syntax and a program that will fail to execute. For example, a missing
107
+ /// `end`, an unterminated string, a missing parenthesis.
108
+ ///
109
+ /// ```ruby
110
+ /// class Foo
111
+ /// ^^^^^ Syntax error. Missing end token
112
+ /// ```
113
+ ParseError => Error,
114
+
115
+ /// Parse warnings represent code that has valid syntax, but may not do what the developer expects. For example,
116
+ /// local variables that are completely unused or usage in a void context (creating a line of code that does
117
+ /// nothing).
118
+ ///
119
+ /// ```ruby
120
+ /// CONST = 1
121
+ /// CONST
122
+ /// ^^^^^ Constant used in void context (the expression does nothing).
123
+ /// puts CONST + 2
124
+ /// ```
125
+ ParseWarning => Warning,
126
+
127
+ // ******** Indexing ******** //
128
+
129
+ /// Dynamic constant references cannot be reasoned about statically because they depend on runtime values. The
130
+ /// program may still be valid, but the quality of the analysis degrades.
131
+ ///
132
+ /// ```ruby
133
+ /// var::Foo
134
+ /// ^^^^^^^^ Dynamic constant reference. This might be correct, but it cannot be understood by the analysis.
135
+ /// ```
136
+ DynamicConstantReference => Information,
137
+
138
+ /// Dynamic singleton targets cannot be reasoned about statically because they depend on runtime values. The program
139
+ /// may still be valid, but the quality of the analysis degrades.
140
+ ///
141
+ /// ```ruby
142
+ /// class << var
143
+ /// ^^^ Dynamic singleton target. This might be correct, but it cannot be understood by the analysis.
144
+ /// end
145
+ ///
146
+ /// def var.bar
147
+ /// ^^^ Dynamic singleton target.
148
+ /// end
149
+ /// ```
150
+ DynamicSingletonDefinition => Information,
151
+
152
+ /// Dynamic ancestor references cannot be reasoned about statically because they depend on runtime values. The
153
+ /// program may still be valid, but the quality of the analysis degrades. In the case of ancestors, since they
154
+ /// influence constant resolution, the degradation may be more impactful than other dynamic references.
155
+ ///
156
+ /// ```ruby
157
+ /// class Foo < var
158
+ /// ^^^ Dynamic ancestor reference. This might be correct, but it cannot be understood by the analysis.
159
+ /// include SomeClass.method_call
160
+ /// ^^^^^^^^^^^^^^^^^^^^^ Dynamic ancestor reference.
161
+ /// end
162
+ /// ```
163
+ DynamicAncestor => Information,
164
+
165
+ /// The top level of a Ruby program is the special <main> object. It is not possible to use `include self` or
166
+ /// `extend self` on that object.
167
+ ///
168
+ /// ```ruby
169
+ /// include self
170
+ /// ^^^^ Cannot include self at the top level
171
+ /// extend self
172
+ /// ^^^^ Cannot extend self at the top level
173
+ /// ```
174
+ TopLevelMixinSelf => Information,
175
+
176
+ /// Constant visibility operations that depend on dynamic values cannot be reasoned about statically because they
177
+ /// depend on runtime values. The program may still be valid, but the quality of the analysis degrades.
178
+ ///
179
+ /// ```ruby
180
+ /// var.private_constant :Foo
181
+ /// ^^^ Invalid constant visibility. This might be correct, but it cannot be understood by the analysis.
182
+ ///
183
+ /// private_constant(var)
184
+ /// ^^^ Invalid constant visibility.
185
+ /// ```
186
+ InvalidConstantVisibility => Warning,
187
+
188
+ /// Method visibility operations that depend on dynamic values cannot be reasoned about statically because they
189
+ /// depend on runtime values. The program may still be valid, but the quality of the analysis degrades.
190
+ ///
191
+ /// ```ruby
192
+ /// var.private :foo
193
+ /// ^^^ Invalid method visibility. This might be correct, but it cannot be understood by the analysis.
194
+ ///
195
+ /// private(var)
196
+ /// ^^^ Invalid method visibility.
197
+ /// ```
198
+ InvalidMethodVisibility => Warning,
199
+
200
+ // ******** Resolution ******** //
201
+
202
+ /// Undefined method visibility target means the analysis couldn't find the definition for the method the code is
203
+ /// attempting to change visibility of. It could be defined through meta-programming or it indeed does not exist.
204
+ ///
205
+ /// ```ruby
206
+ /// class Foo
207
+ /// private :bar
208
+ /// ^^^ Undefined method visibility target. The method `bar` is not defined.
209
+ /// end
210
+ /// ```
211
+ UndefinedMethodVisibilityTarget => Warning,
212
+
213
+ /// Undefined constant visibility target means the analysis couldn't find the definition for the constant the code is
214
+ /// attempting to change visibility of. It could be defined through meta-programming or it indeed does not exist.
215
+ ///
216
+ /// ```ruby
217
+ /// class Foo
218
+ /// private_constant :Bar
219
+ /// ^^^ Undefined constant visibility target. The constant `Bar` is not defined.
220
+ /// end
221
+ /// ```
222
+ UndefinedConstantVisibilityTarget => Warning,
223
+ }
224
+
225
+ impl std::fmt::Display for Rule {
226
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227
+ write!(f, "{}", self.name())
228
+ }
112
229
  }
@@ -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;
@@ -102,8 +103,7 @@ impl<'a> RBSIndexer<'a> {
102
103
  nesting_name_id: Option<NameId>,
103
104
  ) -> NameId {
104
105
  let string_id = self.local_graph.intern_string(symbol.as_str().to_owned());
105
- self.local_graph
106
- .add_name(Name::new(string_id, parent_scope, nesting_name_id))
106
+ self.local_graph.add_name(string_id, parent_scope, nesting_name_id)
107
107
  }
108
108
 
109
109
  fn parent_lexical_scope_id(&self) -> Option<DefinitionId> {
@@ -223,6 +223,133 @@ impl<'a> RBSIndexer<'a> {
223
223
  definition_id
224
224
  }
225
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
+
226
353
  #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
227
354
  fn source_at(&self, location: &node::RBSLocationRange) -> String {
228
355
  let start = location.start() as usize;
@@ -574,6 +701,51 @@ impl Visit for RBSIndexer<'_> {
574
701
  self.register_definition(definition, lexical_nesting_id);
575
702
  }
576
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
+
577
749
  fn visit_method_definition_node(&mut self, def_node: &node::MethodDefinitionNode) {
578
750
  let str_id = self.local_graph.intern_string(format!("{}()", def_node.name()));
579
751
  let offset = Offset::from_rbs_location(&def_node.location());
@@ -651,6 +823,7 @@ impl Visit for RBSIndexer<'_> {
651
823
  mod tests {
652
824
  use ruby_rbs::node::{self, Node, NodeList};
653
825
 
826
+ use crate::diagnostic::Severity;
654
827
  use crate::indexing::rbs_indexer::RBSIndexer;
655
828
  use crate::model::definitions::{Definition, DefinitionFlags, Parameter, Signatures};
656
829
  use crate::model::visibility::Visibility;
@@ -682,7 +855,11 @@ mod tests {
682
855
  fn index_source_with_errors() {
683
856
  let context = index_source("module");
684
857
 
685
- 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
+ );
686
863
 
687
864
  assert!(context.graph().definitions().is_empty());
688
865
  }
@@ -1051,6 +1228,93 @@ mod tests {
1051
1228
  });
1052
1229
  }
1053
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
+
1054
1318
  #[test]
1055
1319
  fn index_alias_node() {
1056
1320
  let context = index_source({
@@ -12,7 +12,7 @@ use crate::model::definitions::{
12
12
  };
13
13
  use crate::model::document::Document;
14
14
  use crate::model::ids::{DefinitionId, NameId, StringId, UriId};
15
- use crate::model::name::{Name, ParentScope};
15
+ use crate::model::name::ParentScope;
16
16
  use crate::model::references::{ConstantReference, MethodRef};
17
17
  use crate::model::visibility::Visibility;
18
18
  use crate::offset::Offset;
@@ -482,11 +482,9 @@ impl<'a> RubyIndexer<'a> {
482
482
  let offset = Offset::from_prism_location(&location);
483
483
  let name = Self::location_to_string(&location);
484
484
  let string_id = self.local_graph.intern_string(name);
485
- let name_id = self.local_graph.add_name(Name::new(
486
- string_id,
487
- parent_scope_id,
488
- self.current_lexical_scope_name_id(),
489
- ));
485
+ let name_id = self
486
+ .local_graph
487
+ .add_name(string_id, parent_scope_id, self.current_lexical_scope_name_id());
490
488
 
491
489
  if push_final_reference {
492
490
  self.local_graph
@@ -687,7 +685,7 @@ impl<'a> RubyIndexer<'a> {
687
685
  .intern_string(format!("{}:{}<anonymous>", self.uri_id, offset.start()));
688
686
 
689
687
  (
690
- Some(self.local_graph.add_name(Name::new(string_id, ParentScope::None, None))),
688
+ Some(self.local_graph.add_name(string_id, ParentScope::None, None)),
691
689
  offset.clone(),
692
690
  )
693
691
  };
@@ -748,7 +746,7 @@ impl<'a> RubyIndexer<'a> {
748
746
  .intern_string(format!("{}:{}<anonymous>", self.uri_id, offset.start()));
749
747
 
750
748
  (
751
- Some(self.local_graph.add_name(Name::new(string_id, ParentScope::None, None))),
749
+ Some(self.local_graph.add_name(string_id, ParentScope::None, None)),
752
750
  offset.clone(),
753
751
  )
754
752
  };
@@ -969,7 +967,7 @@ impl<'a> RubyIndexer<'a> {
969
967
  }
970
968
 
971
969
  Some((
972
- self.current_lexical_scope_name_id().unwrap(),
970
+ self.current_owner_name_id().unwrap(),
973
971
  Offset::from_prism_location(&arg.location()),
974
972
  ))
975
973
  } else if let Some(name_id) = self.index_constant_reference(&arg, false) {
@@ -1125,7 +1123,7 @@ impl<'a> RubyIndexer<'a> {
1125
1123
  }
1126
1124
  None => {
1127
1125
  let str_id = self.local_graph.intern_string("Object".into());
1128
- Some(self.local_graph.add_name(Name::new(str_id, ParentScope::None, None)))
1126
+ Some(self.local_graph.add_name(str_id, ParentScope::None, None))
1129
1127
  }
1130
1128
  }
1131
1129
  }
@@ -1169,7 +1167,7 @@ impl<'a> RubyIndexer<'a> {
1169
1167
  let string_id = self.local_graph.intern_string(singleton_class_name);
1170
1168
  let new_name_id = self
1171
1169
  .local_graph
1172
- .add_name(Name::new(string_id, ParentScope::Attached(name_id), None));
1170
+ .add_name(string_id, ParentScope::Attached(name_id), None);
1173
1171
 
1174
1172
  let location = receiver.map_or(fallback_location, ruby_prism::Node::location);
1175
1173
  let offset = Offset::from_prism_location(&location);
@@ -1650,7 +1648,7 @@ impl Visit<'_> for RubyIndexer<'_> {
1650
1648
  let nesting = self.current_lexical_scope_name_id();
1651
1649
  let name_id = self
1652
1650
  .local_graph
1653
- .add_name(Name::new(string_id, ParentScope::Attached(attached_target), nesting));
1651
+ .add_name(string_id, ParentScope::Attached(attached_target), nesting);
1654
1652
 
1655
1653
  let definition = Definition::SingletonClass(Box::new(SingletonClassDefinition::new(
1656
1654
  name_id,