rubydex 0.4.0 → 0.4.2

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -14
  3. data/THIRD_PARTY_LICENSES.html +13 -24
  4. data/ext/rubydex/definition.c +34 -0
  5. data/ext/rubydex/definition.h +3 -0
  6. data/ext/rubydex/diagnostic.c +2 -49
  7. data/ext/rubydex/extconf.rb +4 -2
  8. data/ext/rubydex/graph.c +59 -11
  9. data/lib/rubydex/cli/command/dead_code.rb +59 -0
  10. data/lib/rubydex/cli/command/lint/explain.rb +8 -6
  11. data/lib/rubydex/cli/command/lint.rb +10 -4
  12. data/lib/rubydex/linter/rule_loader.rb +1 -1
  13. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +1 -1
  14. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +4 -4
  15. data/lib/rubydex/mcp_server/tools/find_dead_code_candidates_tool.rb +33 -0
  16. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +2 -2
  17. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +4 -4
  18. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +2 -2
  19. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +6 -6
  20. data/lib/rubydex/mcp_server.rb +24 -12
  21. data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
  22. data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
  23. data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
  24. data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
  25. data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
  26. data/lib/rubydex/rules/parse_error.rb +25 -0
  27. data/lib/rubydex/rules/parse_warning.rb +28 -0
  28. data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
  29. data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
  30. data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
  31. data/lib/rubydex/skill.rb +1 -0
  32. data/lib/rubydex/version.rb +1 -1
  33. data/lib/rubydex.rb +2 -0
  34. data/lib/rubydex_linter/rules/rule_structure.rb +19 -4
  35. data/rbi/rubydex.rbi +32 -130
  36. data/rust/Cargo.lock +17 -11
  37. data/rust/Cargo.toml +3 -0
  38. data/rust/rubydex/Cargo.toml +6 -2
  39. data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
  40. data/rust/rubydex/src/compile_assertions.rs +2 -0
  41. data/rust/rubydex/src/config.rs +1 -1
  42. data/rust/rubydex/src/diagnostic.rs +142 -51
  43. data/rust/rubydex/src/indexing/rbs_indexer.rs +4 -4
  44. data/rust/rubydex/src/indexing/ruby_indexer.rs +26 -18
  45. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +42 -2
  46. data/rust/rubydex/src/indexing.rs +6 -6
  47. data/rust/rubydex/src/listing.rs +12 -12
  48. data/rust/rubydex/src/model/built_in.rs +1 -1
  49. data/rust/rubydex/src/model/comment.rs +4 -4
  50. data/rust/rubydex/src/model/declaration.rs +25 -17
  51. data/rust/rubydex/src/model/document.rs +11 -11
  52. data/rust/rubydex/src/model/graph.rs +3 -3
  53. data/rust/rubydex/src/offset.rs +1 -13
  54. data/rust/rubydex/src/operation/ruby_builder.rs +29 -19
  55. data/rust/rubydex/src/query.rs +193 -2
  56. data/rust/rubydex/src/test_utils/graph_test.rs +2 -2
  57. data/rust/rubydex/src/test_utils/local_graph_test.rs +13 -6
  58. data/rust/rubydex-sys/Cargo.toml +2 -2
  59. data/rust/rubydex-sys/src/definition_api.rs +69 -2
  60. data/rust/rubydex-sys/src/diagnostic_api.rs +0 -35
  61. data/rust/rubydex-sys/src/graph_api.rs +193 -45
  62. data/rust/rubydex-sys/src/name_api.rs +219 -48
  63. metadata +191 -180
data/rust/Cargo.lock CHANGED
@@ -99,7 +99,7 @@ dependencies = [
99
99
  "quote",
100
100
  "regex",
101
101
  "rustc-hash",
102
- "shlex",
102
+ "shlex 1.3.0",
103
103
  "syn",
104
104
  ]
105
105
 
@@ -151,7 +151,7 @@ version = "1.2.30"
151
151
  source = "registry+https://github.com/rust-lang/crates.io-index"
152
152
  checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
153
153
  dependencies = [
154
- "shlex",
154
+ "shlex 1.3.0",
155
155
  ]
156
156
 
157
157
  [[package]]
@@ -718,9 +718,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
718
718
 
719
719
  [[package]]
720
720
  name = "ruby-prism"
721
- version = "1.9.0"
721
+ version = "2.0.0-rc.2"
722
722
  source = "registry+https://github.com/rust-lang/crates.io-index"
723
- checksum = "3b302f00359d0b5423a600314935ca5f994484e15da2db5345631d28c6e029d6"
723
+ checksum = "6d34b0f30ca0b7233a454827204ee7dc1a70d8faa3a26eae95f09ada5386757c"
724
724
  dependencies = [
725
725
  "ruby-prism-sys",
726
726
  "serde",
@@ -729,12 +729,12 @@ dependencies = [
729
729
 
730
730
  [[package]]
731
731
  name = "ruby-prism-sys"
732
- version = "1.9.0"
732
+ version = "2.0.0-rc.2"
733
733
  source = "registry+https://github.com/rust-lang/crates.io-index"
734
- checksum = "9f85fd9571455bdca2b3bf0b2f543cd13ac39d5de0026a8eb2deb94ffd264f00"
734
+ checksum = "f0e59b94e20c878bb618a355abd00724ad1eb4514341936c6668bea987437a29"
735
735
  dependencies = [
736
- "bindgen",
737
736
  "cc",
737
+ "shlex 2.0.1",
738
738
  ]
739
739
 
740
740
  [[package]]
@@ -760,7 +760,7 @@ dependencies = [
760
760
 
761
761
  [[package]]
762
762
  name = "rubydex"
763
- version = "0.2.6"
763
+ version = "0.4.2"
764
764
  dependencies = [
765
765
  "assert_cmd",
766
766
  "bitflags",
@@ -789,7 +789,7 @@ dependencies = [
789
789
 
790
790
  [[package]]
791
791
  name = "rubydex-sys"
792
- version = "0.2.6"
792
+ version = "0.4.2"
793
793
  dependencies = [
794
794
  "cbindgen",
795
795
  "libc",
@@ -902,6 +902,12 @@ version = "1.3.0"
902
902
  source = "registry+https://github.com/rust-lang/crates.io-index"
903
903
  checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
904
904
 
905
+ [[package]]
906
+ name = "shlex"
907
+ version = "2.0.1"
908
+ source = "registry+https://github.com/rust-lang/crates.io-index"
909
+ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
910
+
905
911
  [[package]]
906
912
  name = "smallvec"
907
913
  version = "1.15.1"
@@ -1326,9 +1332,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
1326
1332
 
1327
1333
  [[package]]
1328
1334
  name = "xxhash-rust"
1329
- version = "0.8.15"
1335
+ version = "0.8.16"
1330
1336
  source = "registry+https://github.com/rust-lang/crates.io-index"
1331
- checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
1337
+ checksum = "4d93c89cdc2d3a63c3ec48ffe926931bdc069eafa8e4402fe6d8f790c9d1e576"
1332
1338
 
1333
1339
  [[package]]
1334
1340
  name = "yoke"
data/rust/Cargo.toml CHANGED
@@ -6,6 +6,9 @@ members = [
6
6
 
7
7
  resolver = "3"
8
8
 
9
+ [workspace.package]
10
+ version = "0.4.2"
11
+
9
12
  [profile.release]
10
13
  lto = true
11
14
  opt-level = 3
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rubydex"
3
- version = "0.2.6"
3
+ version.workspace = true
4
4
  edition = "2024"
5
5
  rust-version = "1.89.0"
6
6
  license = "MIT"
@@ -15,6 +15,10 @@ categories = ["development-tools"]
15
15
  name = "rubydex_cli"
16
16
  path = "src/main.rs"
17
17
 
18
+ [[bin]]
19
+ name = "generate_ruby_rules"
20
+ path = "src/bin/generate_ruby_rules.rs"
21
+
18
22
  [lib]
19
23
  crate-type = ["rlib"]
20
24
 
@@ -28,7 +32,7 @@ test_utils = ["dep:tempfile"]
28
32
 
29
33
  [dependencies]
30
34
  cypher-parser = "0.8.1"
31
- ruby-prism = "1.9.0"
35
+ ruby-prism = "2.0.0-rc.2"
32
36
  ruby-rbs = "0.3"
33
37
  url = "2.5.4"
34
38
  xxhash-rust = { version = "0.8.15", features = ["xxh3"] }
@@ -0,0 +1,94 @@
1
+ use rubydex::diagnostic::{Rule, Severity};
2
+ use std::collections::HashSet;
3
+ use std::path::Path;
4
+ use std::{env, fs};
5
+
6
+ // Takes the built-in diagnostic rules and generates Ruby classes for them in `lib/rubydex/rules`.
7
+ fn main() {
8
+ let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..");
9
+ let directory = root.join("lib").join("rubydex").join("rules");
10
+ let mut generated_files = HashSet::new();
11
+
12
+ for rule in Rule::all().iter().copied() {
13
+ write_rule(&directory, rule, &mut generated_files);
14
+ }
15
+
16
+ // Remove stale rules
17
+ for entry in fs::read_dir(&directory).unwrap() {
18
+ let entry = entry.unwrap();
19
+ let path = entry.path();
20
+
21
+ if path.is_file() {
22
+ let file_name = path.file_name().unwrap().to_string_lossy();
23
+
24
+ if !generated_files.contains(&file_name.to_string()) {
25
+ fs::remove_file(path).unwrap();
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ fn write_rule(dir: &Path, rule: Rule, generated_files: &mut HashSet<String>) {
32
+ let documentation = rule
33
+ .documentation()
34
+ .iter()
35
+ .map(|line| format!(" #{line}").trim_end().to_owned())
36
+ .collect::<Vec<String>>()
37
+ .join("\n");
38
+
39
+ let contents = format!(
40
+ "# typed: strict
41
+ # frozen_string_literal: true
42
+
43
+ # This file is generated by `generate_ruby_rules.rs`. Do not edit this manually.
44
+
45
+ module Rubydex
46
+ module Rules
47
+ {documentation}
48
+ class {name} < Rule
49
+ class << self
50
+ # @override
51
+ #: -> singleton(Severity::Base)
52
+ def default_severity
53
+ {severity}
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ ",
60
+ name = rule.name(),
61
+ severity = to_ruby_severity(rule.default_severity()),
62
+ );
63
+
64
+ let file_name = format!("{}.rb", snake_case(rule.name()));
65
+ fs::write(dir.join(&file_name), contents).unwrap();
66
+ generated_files.insert(file_name);
67
+ }
68
+
69
+ fn to_ruby_severity(severity: Severity) -> &'static str {
70
+ match severity {
71
+ Severity::Error => "Severity::Error",
72
+ Severity::Warning => "Severity::Warning",
73
+ Severity::Information => "Severity::Information",
74
+ Severity::Hint => "Severity::Hint",
75
+ }
76
+ }
77
+
78
+ fn snake_case(name: &str) -> String {
79
+ let mut result = String::with_capacity(name.len() + 4);
80
+
81
+ for (index, character) in name.char_indices() {
82
+ if character.is_uppercase() {
83
+ if index != 0 {
84
+ result.push('_');
85
+ }
86
+
87
+ result.extend(character.to_lowercase());
88
+ } else {
89
+ result.push(character);
90
+ }
91
+ }
92
+
93
+ result
94
+ }
@@ -3,11 +3,13 @@
3
3
  macro_rules! assert_mem_size {
4
4
  ($struct:ident, $size:expr) => {
5
5
  // Check struct name on this assert! error
6
+ #[cfg(target_pointer_width = "64")]
6
7
  const _: () = assert!(
7
8
  std::mem::size_of::<$struct>() == $size,
8
9
  concat!("Incorrect size for `", stringify!($struct), "`")
9
10
  );
10
11
  // Check actual size on this array error
12
+ #[cfg(target_pointer_width = "64")]
11
13
  const _: [(); $size] = [(); std::mem::size_of::<$struct>()];
12
14
  };
13
15
  }
@@ -433,7 +433,7 @@ mod tests {
433
433
 
434
434
  let other = rules.iter().find(|rule| rule.name() == "Other").unwrap();
435
435
  assert!(!other.enabled());
436
- assert!(other.exclude_patterns().is_empty());
436
+ assert_eq!(other.exclude_patterns(), []);
437
437
  assert_eq!(other.severity(), None);
438
438
  }
439
439
 
@@ -64,10 +64,10 @@ pub enum Severity {
64
64
  }
65
65
 
66
66
  macro_rules! rules {
67
- ($($(#[$attribute:meta])* $rule:ident),+ $(,)?) => {
67
+ ($($(#[doc = $documentation:literal])+ $rule:ident => $severity:ident),+ $(,)?) => {
68
68
  #[derive(Debug, Copy, Clone, PartialEq, Eq)]
69
69
  pub enum Rule {
70
- $($(#[$attribute])* $rule,)+
70
+ $($(#[doc = $documentation])+ $rule,)+
71
71
  }
72
72
 
73
73
  impl Rule {
@@ -75,60 +75,151 @@ macro_rules! rules {
75
75
  pub fn all() -> &'static [Self] {
76
76
  &[$(Self::$rule,)+]
77
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
+ }
98
+ }
78
99
  }
79
100
  };
80
101
  }
81
102
 
82
103
  rules! {
83
- // Parsing
84
- ParseError,
85
- ParseWarning,
86
-
87
- // Indexing
88
- DynamicConstantReference,
89
- DynamicSingletonDefinition,
90
- DynamicAncestor,
91
- TopLevelMixinSelf,
92
- InvalidConstantVisibility,
93
- InvalidMethodVisibility,
94
-
95
- // Resolution
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
- }
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,
132
223
  }
133
224
 
134
225
  impl std::fmt::Display for Rule {
@@ -33,8 +33,8 @@ pub struct RBSIndexer<'a> {
33
33
 
34
34
  impl<'a> RBSIndexer<'a> {
35
35
  #[must_use]
36
- pub fn new(uri: String, source: &'a str) -> Self {
37
- let uri_id = UriId::from(&uri);
36
+ pub fn new(uri: Box<str>, source: &'a str) -> Self {
37
+ let uri_id = UriId::from(&*uri);
38
38
  let local_graph = LocalGraph::new(uri_id, Document::new(uri, source));
39
39
 
40
40
  Self {
@@ -197,7 +197,7 @@ impl<'a> RBSIndexer<'a> {
197
197
  // Skip past indentation to the comment text
198
198
  current_offset += line_indent as u32;
199
199
 
200
- let line_text = line[line_indent..].to_string();
200
+ let line_text: Box<str> = line[line_indent..].into();
201
201
  let line_bytes = line_text.len() as u32;
202
202
  let offset = Offset::new(current_offset, current_offset + line_bytes);
203
203
  comments.push(Comment::new(offset, line_text));
@@ -1454,7 +1454,7 @@ mod tests {
1454
1454
  fn split_multiline_comments_crlf() {
1455
1455
  // Build the indexer directly to bypass normalize_indentation, which strips \r
1456
1456
  let source = "# First line\r\n# Second line\r\nclass Foo\r\nend\r\n";
1457
- let mut indexer = RBSIndexer::new("file:///foo.rbs".to_string(), source);
1457
+ let mut indexer = RBSIndexer::new("file:///foo.rbs".into(), source);
1458
1458
  indexer.index();
1459
1459
  let context = LocalGraphTest::from_local_graph("file:///foo.rbs", source, indexer.local_graph());
1460
1460
 
@@ -93,8 +93,8 @@ pub struct RubyIndexer<'a> {
93
93
 
94
94
  impl<'a> RubyIndexer<'a> {
95
95
  #[must_use]
96
- pub fn new(uri: String, source: &'a str) -> Self {
97
- let uri_id = UriId::from(&uri);
96
+ pub fn new(uri: Box<str>, source: &'a str) -> Self {
97
+ let uri_id = UriId::from(&*uri);
98
98
  let local_graph = LocalGraph::new(uri_id, Document::new(uri, source));
99
99
 
100
100
  Self {
@@ -120,7 +120,7 @@ impl<'a> RubyIndexer<'a> {
120
120
  self.local_graph.add_diagnostic(
121
121
  Rule::ParseError,
122
122
  Offset::from_prism_location(&error.location()),
123
- error.message().to_string(),
123
+ String::from_utf8_lossy(error.message()).to_string(),
124
124
  );
125
125
  }
126
126
 
@@ -128,7 +128,7 @@ impl<'a> RubyIndexer<'a> {
128
128
  self.local_graph.add_diagnostic(
129
129
  Rule::ParseWarning,
130
130
  Offset::from_prism_location(&warning.location()),
131
- warning.message().to_string(),
131
+ String::from_utf8_lossy(warning.message()).to_string(),
132
132
  );
133
133
  }
134
134
 
@@ -313,7 +313,9 @@ impl<'a> RubyIndexer<'a> {
313
313
  }
314
314
  }
315
315
 
316
- if let Some(block) = parameters_list.block() {
316
+ if let Some(block) = parameters_list.block()
317
+ && let Some(block) = block.as_block_parameter_node()
318
+ {
317
319
  let location = block.name_loc().unwrap_or_else(|| block.location());
318
320
  let str_id = self.local_graph.intern_string(Self::location_to_string(&location));
319
321
 
@@ -1389,8 +1391,8 @@ impl<'a> RubyIndexer<'a> {
1389
1391
  match arg {
1390
1392
  ruby_prism::Node::SymbolNode { .. } => {
1391
1393
  let symbol = arg.as_symbol_node().unwrap();
1392
- let value_loc = symbol.value_loc()?;
1393
- Some((Self::location_to_string(&value_loc), value_loc))
1394
+ let content_loc = symbol.content_loc();
1395
+ Some((Self::location_to_string(&content_loc), content_loc))
1394
1396
  }
1395
1397
  ruby_prism::Node::StringNode { .. } => {
1396
1398
  let string = arg.as_string_node().unwrap();
@@ -1522,7 +1524,7 @@ impl CommentGroup {
1522
1524
  // Accepts the next line if it is continuous
1523
1525
  fn accepts(&self, next: &ruby_prism::Comment, source: &str) -> bool {
1524
1526
  let current_end_offset = self.end_offset;
1525
- let next_line_start_offset = next.location().start_offset();
1527
+ let next_line_start_offset = next.location().start() as usize;
1526
1528
 
1527
1529
  let between = &source.as_bytes()[current_end_offset..next_line_start_offset];
1528
1530
  if !between.iter().all(|&b| b.is_ascii_whitespace()) {
@@ -1538,7 +1540,7 @@ impl CommentGroup {
1538
1540
  // 1. still move the group end offset to the end of the magic comment
1539
1541
  // 2. not add the comment to the comments array
1540
1542
  fn add_comment(&mut self, comment: &ruby_prism::Comment) {
1541
- self.end_offset = comment.location().end_offset();
1543
+ self.end_offset = comment.location().end() as usize;
1542
1544
  let text = String::from_utf8_lossy(comment.location().as_slice()).to_string();
1543
1545
 
1544
1546
  if text.lines().any(|line| line.starts_with("# @deprecated")) {
@@ -1547,7 +1549,7 @@ impl CommentGroup {
1547
1549
 
1548
1550
  self.comments.push(Comment::new(
1549
1551
  Offset::from_prism_location(&comment.location()),
1550
- text.trim().to_string(),
1552
+ text.trim().into(),
1551
1553
  ));
1552
1554
  }
1553
1555
 
@@ -1984,12 +1986,18 @@ impl Visit<'_> for RubyIndexer<'_> {
1984
1986
  });
1985
1987
  };
1986
1988
 
1987
- let message_loc = node.message_loc();
1989
+ let message_loc = if node.name().as_slice().is_empty() {
1990
+ // Incomplete calls like `Foo.` have no message. We still index them using the call operator location, so
1991
+ // that the receiver's singleton class gets created, which completion depends on
1992
+ node.call_operator_loc()
1993
+ } else {
1994
+ node.message_loc()
1995
+ };
1988
1996
 
1989
- if message_loc.is_none() {
1997
+ let Some(message_loc) = message_loc else {
1990
1998
  // No message, we can't index this node
1991
1999
  return;
1992
- }
2000
+ };
1993
2001
 
1994
2002
  let message = String::from_utf8_lossy(node.name().as_slice()).to_string();
1995
2003
 
@@ -2217,11 +2225,11 @@ impl Visit<'_> for RubyIndexer<'_> {
2217
2225
  self.visit(&receiver);
2218
2226
  }
2219
2227
 
2220
- self.index_method_reference(message.clone(), &node.message_loc().unwrap(), method_receiver);
2228
+ self.index_method_reference(message.clone(), &message_loc, method_receiver);
2221
2229
 
2222
2230
  match message.as_str() {
2223
2231
  ">" | "<" | ">=" | "<=" => {
2224
- self.index_method_reference("<=>".to_string(), &node.message_loc().unwrap(), method_receiver);
2232
+ self.index_method_reference("<=>".to_string(), &message_loc, method_receiver);
2225
2233
  }
2226
2234
  _ => {}
2227
2235
  }
@@ -2382,7 +2390,7 @@ impl Visit<'_> for RubyIndexer<'_> {
2382
2390
  match expression {
2383
2391
  ruby_prism::Node::SymbolNode { .. } => {
2384
2392
  let symbol = expression.as_symbol_node().unwrap();
2385
- let name = Self::location_to_string(&symbol.value_loc().unwrap());
2393
+ let name = Self::location_to_string(&symbol.content_loc());
2386
2394
  self.index_method_reference(name, &node.location(), None);
2387
2395
  }
2388
2396
  _ => {
@@ -2394,13 +2402,13 @@ impl Visit<'_> for RubyIndexer<'_> {
2394
2402
 
2395
2403
  fn visit_alias_method_node(&mut self, node: &ruby_prism::AliasMethodNode<'_>) {
2396
2404
  let mut new_name = if let Some(symbol_node) = node.new_name().as_symbol_node() {
2397
- Self::location_to_string(&symbol_node.value_loc().unwrap())
2405
+ Self::location_to_string(&symbol_node.content_loc())
2398
2406
  } else {
2399
2407
  Self::location_to_string(&node.new_name().location())
2400
2408
  };
2401
2409
 
2402
2410
  let mut old_name = if let Some(symbol_node) = node.old_name().as_symbol_node() {
2403
- Self::location_to_string(&symbol_node.value_loc().unwrap())
2411
+ Self::location_to_string(&symbol_node.content_loc())
2404
2412
  } else {
2405
2413
  Self::location_to_string(&node.old_name().location())
2406
2414
  };