rubydex 0.2.7 → 0.2.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fe9d5528801028a40419f3e7b35f5b74406ae2c24f6ce6252270fe15c2b9b77
4
- data.tar.gz: 92595d6014a20544bdace0135d4aeea060ee969ab3a10544d02de6796ff9f84e
3
+ metadata.gz: 6b27fe66d59bdafd3b040278baaca0607a87861d132d70cc7ad65f9edb75c049
4
+ data.tar.gz: 1036e909678edd8183e1359ba9b45323d1e7a26ad99847d3382583a3f97d1f61
5
5
  SHA512:
6
- metadata.gz: d43a2e6d81744f688d12ab483b8b4539a7a56144f0d7c7cf20540506bb376903c5d309c255b743ba77277fc82e03f86b70e0416149fdd490743844e913c24be6
7
- data.tar.gz: 18f6195b18761efdafbe0b9ae7fb9c1ab64bbb607a080fb58792b9f574d17cb6ed25560ff957c5b925186019abbbe509396c2f88dd903c2e625d0265fbb10efa
6
+ metadata.gz: 884a896d38505cdb97ddc2a83b96364dc918f7f449349bf37e0d54675d630750300856df10dc6ab13e80b20d03979cbef5122ca2cc7ed20a160e64cf8ec421bd
7
+ data.tar.gz: cfd66ff47db6ac89e6f5514d307e48dcb5f252c5132eeb17328cf067365c1d764b6f9f87609483135815bf5760502992a9de107322e9445b2a99deacc9dcf7a5
data/ext/rubydex/graph.c CHANGED
@@ -196,40 +196,54 @@ static VALUE rdxr_graph_yield_search_results(VALUE self, void *iter) {
196
196
 
197
197
  /*
198
198
  * call-seq:
199
- * search(query) -> Enumerator[Rubydex::Declaration]
199
+ * search(*queries) -> Enumerator[Rubydex::Declaration]
200
200
  *
201
- * Returns an enumerator that yields declarations matching the query exactly by substring.
201
+ * Returns an enumerator that yields declarations whose name matches any of the queries exactly by substring.
202
202
  */
203
- static VALUE rdxr_graph_search(VALUE self, VALUE query) {
204
- Check_Type(query, T_STRING);
203
+ static VALUE rdxr_graph_search(int argc, VALUE *argv, VALUE self) {
204
+ rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
205
+ VALUE queries = rb_ary_new_from_values(argc, argv);
206
+ rdxi_check_array_of_strings(queries);
205
207
 
206
208
  if (!rb_block_given_p()) {
207
- return rb_enumeratorize(self, rb_str_new2("search"), 1, &query);
209
+ return rb_enumeratorize(self, rb_str_new2("search"), argc, argv);
208
210
  }
209
211
 
210
212
  void *graph;
211
213
  TypedData_Get_Struct(self, void *, &graph_type, graph);
212
214
 
213
- return rdxr_graph_yield_search_results(self, rdx_graph_declarations_search(graph, StringValueCStr(query)));
215
+ size_t length = (size_t)argc;
216
+ char **converted = rdxi_str_array_to_char(queries, length);
217
+ void *iter = rdx_graph_declarations_search(graph, (const char *const *)converted, length);
218
+ rdxi_free_str_array(converted, length);
219
+
220
+ return rdxr_graph_yield_search_results(self, iter);
214
221
  }
215
222
 
216
223
  /*
217
224
  * call-seq:
218
- * fuzzy_search(query) -> Enumerator[Rubydex::Declaration]
225
+ * fuzzy_search(*queries) -> Enumerator[Rubydex::Declaration]
219
226
  *
220
- * Returns an enumerator that yields declarations matching the query fuzzily.
227
+ * Returns an enumerator that yields declarations whose name matches any of the queries fuzzily.
221
228
  */
222
- static VALUE rdxr_graph_fuzzy_search(VALUE self, VALUE query) {
223
- Check_Type(query, T_STRING);
229
+ static VALUE rdxr_graph_fuzzy_search(int argc, VALUE *argv, VALUE self) {
230
+ rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
231
+ VALUE queries = rb_ary_new_from_values(argc, argv);
232
+ rdxi_check_array_of_strings(queries);
224
233
 
225
234
  if (!rb_block_given_p()) {
226
- return rb_enumeratorize(self, rb_str_new2("fuzzy_search"), 1, &query);
235
+ return rb_enumeratorize(self, rb_str_new2("fuzzy_search"), argc, argv);
227
236
  }
228
237
 
229
238
  void *graph;
230
239
  TypedData_Get_Struct(self, void *, &graph_type, graph);
231
240
 
232
- return rdxr_graph_yield_search_results(self, rdx_graph_declarations_fuzzy_search(graph, StringValueCStr(query)));
241
+ size_t length = (size_t)argc;
242
+ char **converted = rdxi_str_array_to_char(queries, length);
243
+ void *iter = rdx_graph_declarations_fuzzy_search(graph, (const char *const *)converted, length);
244
+ rdxi_free_str_array(converted, length);
245
+
246
+ return rdxr_graph_yield_search_results(self, iter);
233
247
  }
234
248
 
235
249
  // Body function for rb_ensure in Graph#documents
@@ -942,8 +956,8 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
942
956
  rb_define_method(cGraph, "diagnostics", rdxr_graph_diagnostics, 0);
943
957
  rb_define_method(cGraph, "check_integrity", rdxr_graph_check_integrity, 0);
944
958
  rb_define_method(cGraph, "[]", rdxr_graph_aref, 1);
945
- rb_define_method(cGraph, "search", rdxr_graph_search, 1);
946
- rb_define_method(cGraph, "fuzzy_search", rdxr_graph_fuzzy_search, 1);
959
+ rb_define_method(cGraph, "search", rdxr_graph_search, -1);
960
+ rb_define_method(cGraph, "fuzzy_search", rdxr_graph_fuzzy_search, -1);
947
961
  rb_define_method(cGraph, "encoding=", rdxr_graph_set_encoding, 1);
948
962
  rb_define_method(cGraph, "resolve_require_path", rdxr_graph_resolve_require_path, 2);
949
963
  rb_define_method(cGraph, "require_paths", rdxr_graph_require_paths, 1);
Binary file
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubydex
4
- VERSION = "0.2.7"
4
+ VERSION = "0.2.8"
5
5
  end
data/rbi/rubydex.rbi CHANGED
@@ -335,11 +335,11 @@ class Rubydex::Graph
335
335
  sig { params(require_path: String, load_paths: T::Array[String]).returns(T.nilable(Rubydex::Document)) }
336
336
  def resolve_require_path(require_path, load_paths); end
337
337
 
338
- sig { params(query: String).returns(T::Enumerable[Rubydex::Declaration]) }
339
- def search(query); end
338
+ sig { params(queries: String).returns(T::Enumerable[Rubydex::Declaration]) }
339
+ def search(*queries); end
340
340
 
341
- sig { params(query: String).returns(T::Enumerable[Rubydex::Declaration]) }
342
- def fuzzy_search(query); end
341
+ sig { params(queries: String).returns(T::Enumerable[Rubydex::Declaration]) }
342
+ def fuzzy_search(*queries); end
343
343
 
344
344
  sig { params(encoding: String).void }
345
345
  def encoding=(encoding); end
@@ -1,9 +1,13 @@
1
1
  use clap::{Parser, ValueEnum};
2
- use std::{collections::HashSet, mem};
2
+ use std::{
3
+ fs, mem,
4
+ path::{Path, PathBuf},
5
+ time::{Duration, Instant},
6
+ };
3
7
 
4
8
  use rubydex::{
5
9
  dot,
6
- indexing::{self, IndexerBackend},
10
+ indexing::{self, IndexerBackend, LanguageId, build_local_graph},
7
11
  integrity, listing,
8
12
  model::graph::Graph,
9
13
  resolution::Resolver,
@@ -12,12 +16,17 @@ use rubydex::{
12
16
  timer::{Timer, time_it},
13
17
  },
14
18
  };
19
+ use url::Url;
15
20
 
16
21
  #[derive(Parser, Debug)]
17
22
  #[command(name = "rubydex_cli", about = "A Static Analysis Toolkit for Ruby", version)]
18
23
  #[allow(clippy::struct_excessive_bools)]
19
24
  struct Args {
20
- #[arg(value_name = "PATHS", default_value = ".")]
25
+ #[arg(
26
+ value_name = "PATHS",
27
+ default_value = ".",
28
+ help = "Path(s) to index. If the first path is a directory, it is used as the workspace root for rubydex.toml"
29
+ )]
21
30
  paths: Vec<String>,
22
31
 
23
32
  #[arg(long = "stop-after", help = "Stop after the given stage")]
@@ -52,6 +61,21 @@ struct Args {
52
61
  help = "Write orphan definitions report to specified file"
53
62
  )]
54
63
  report_orphans: Option<String>,
64
+
65
+ #[arg(
66
+ long = "incremental_cycle",
67
+ value_name = "N",
68
+ help = "After the initial build, run N incremental resolution cycles and report their timings"
69
+ )]
70
+ incremental_cycle: Option<usize>,
71
+
72
+ #[arg(
73
+ long = "incremental_files",
74
+ value_name = "N",
75
+ default_value_t = 1,
76
+ help = "Number of files to re-index per incremental cycle"
77
+ )]
78
+ incremental_files: usize,
55
79
  }
56
80
 
57
81
  #[derive(Debug, Clone, ValueEnum)]
@@ -85,6 +109,11 @@ fn exit(print_stats: bool) {
85
109
  std::process::exit(0);
86
110
  }
87
111
 
112
+ fn workspace_path_for(paths: &[String]) -> Option<PathBuf> {
113
+ let first_path = paths.first()?;
114
+ fs::canonicalize(first_path).ok().filter(|path| path.is_dir())
115
+ }
116
+
88
117
  fn main() {
89
118
  let args = Args::parse();
90
119
 
@@ -92,9 +121,21 @@ fn main() {
92
121
  Timer::set_global_timer(Timer::new());
93
122
  }
94
123
 
124
+ let mut graph = Graph::new();
125
+
126
+ if let Some(workspace_path) = workspace_path_for(&args.paths) {
127
+ graph.set_workspace_path(workspace_path);
128
+ if let Err(error) = graph.load_config(None) {
129
+ eprintln!("{error}");
130
+ std::process::exit(1);
131
+ }
132
+ }
133
+
95
134
  // Listing
96
135
 
97
- let (file_paths, errors) = time_it!(listing, { listing::collect_file_paths(args.paths, &HashSet::new()) });
136
+ let (file_paths, errors) = time_it!(listing, {
137
+ listing::collect_file_paths(args.paths, &graph.excluded_paths())
138
+ });
98
139
 
99
140
  for error in errors {
100
141
  eprintln!("{error}");
@@ -106,8 +147,16 @@ fn main() {
106
147
 
107
148
  // Indexing
108
149
 
109
- let mut graph = Graph::new();
110
150
  let backend = IndexerBackend::from(&args.indexer);
151
+
152
+ // The incremental benchmark re-indexes files after the initial build, so keep a copy of the
153
+ // paths before `index_files` consumes them.
154
+ let incremental_paths = if args.incremental_cycle.is_some() {
155
+ file_paths.clone()
156
+ } else {
157
+ Vec::new()
158
+ };
159
+
111
160
  let errors = time_it!(indexing, { indexing::index_files(&mut graph, file_paths, backend) });
112
161
 
113
162
  for error in errors {
@@ -125,6 +174,12 @@ fn main() {
125
174
  resolver.resolve();
126
175
  });
127
176
 
177
+ // Incremental resolution benchmark. Runs before the stop-after check so it can be combined with
178
+ // `--stop-after=resolution`.
179
+ if let Some(cycles) = args.incremental_cycle {
180
+ run_incremental_resolution(&mut graph, &incremental_paths, cycles, args.incremental_files, backend);
181
+ }
182
+
128
183
  if let Some(StopAfter::Resolution) = args.stop_after {
129
184
  return exit(args.stats);
130
185
  }
@@ -186,3 +241,91 @@ fn main() {
186
241
  // Forget the graph so we don't have to wait for deallocation and let the system reclaim the memory at exit
187
242
  mem::forget(graph);
188
243
  }
244
+
245
+ /// Simulates incremental editing to measure incremental resolution cost. For each cycle it
246
+ /// re-indexes a rotating window of `files_per_cycle` files (parsing plus the same invalidation the
247
+ /// LSP performs on save) and then re-runs resolution over the resulting pending work, reporting
248
+ /// per-cycle and aggregate `resolve()` timings.
249
+ ///
250
+ /// With `--stats`, the `compute_descendants` breakdown printed in the timing summary reflects the
251
+ /// last incremental cycle, since it is recorded on every `resolve()`.
252
+ fn run_incremental_resolution(
253
+ graph: &mut Graph,
254
+ paths: &[PathBuf],
255
+ cycles: usize,
256
+ files_per_cycle: usize,
257
+ backend: IndexerBackend,
258
+ ) {
259
+ if paths.is_empty() || cycles == 0 || files_per_cycle == 0 {
260
+ eprintln!("Skipping incremental resolution: nothing to re-index");
261
+ return;
262
+ }
263
+
264
+ let files_per_cycle = files_per_cycle.min(paths.len());
265
+
266
+ let mut reindex_total = Duration::ZERO;
267
+ let mut resolve_total = Duration::ZERO;
268
+ let mut resolve_min = Duration::MAX;
269
+ let mut resolve_max = Duration::ZERO;
270
+
271
+ println!();
272
+ println!("Incremental resolution ({cycles} cycle(s), {files_per_cycle} file(s)/cycle)");
273
+ println!(" Scenario: no-op reindex; files are indexed again without changing their contents.");
274
+ println!(" This should be the fastest incremental resolution path.");
275
+ println!(" Add scenario-based benchmarks before optimizing incremental resolution.");
276
+
277
+ for cycle in 0..cycles {
278
+ // Re-index the window of files for this cycle. Rotating the window across cycles samples
279
+ // different parts of the codebase rather than measuring the same delta repeatedly.
280
+ let reindex_start = Instant::now();
281
+ for i in 0..files_per_cycle {
282
+ let path = &paths[(cycle * files_per_cycle + i) % paths.len()];
283
+ reindex_file(graph, path, backend);
284
+ }
285
+ reindex_total += reindex_start.elapsed();
286
+
287
+ let resolve_start = Instant::now();
288
+ Resolver::new(graph).resolve();
289
+ let elapsed = resolve_start.elapsed();
290
+
291
+ resolve_total += elapsed;
292
+ resolve_min = resolve_min.min(elapsed);
293
+ resolve_max = resolve_max.max(elapsed);
294
+
295
+ println!(
296
+ " cycle {:>3}: resolve {:9.3}ms",
297
+ cycle + 1,
298
+ elapsed.as_secs_f64() * 1000.0
299
+ );
300
+ }
301
+
302
+ let avg = resolve_total / u32::try_from(cycles).expect("cycle count fits in u32");
303
+
304
+ println!(
305
+ " resolve total {:.3}ms avg {:.3}ms min {:.3}ms max {:.3}ms",
306
+ resolve_total.as_secs_f64() * 1000.0,
307
+ avg.as_secs_f64() * 1000.0,
308
+ resolve_min.as_secs_f64() * 1000.0,
309
+ resolve_max.as_secs_f64() * 1000.0,
310
+ );
311
+ println!(
312
+ " reindex+invalidate total {:.3}ms (parse + document merge, excluded from resolve above)",
313
+ reindex_total.as_secs_f64() * 1000.0,
314
+ );
315
+ }
316
+
317
+ /// Re-indexes a single file into the graph, running the same invalidation the LSP performs on save.
318
+ fn reindex_file(graph: &mut Graph, path: &Path, backend: IndexerBackend) {
319
+ let Ok(source) = fs::read_to_string(path) else {
320
+ eprintln!("Failed to read file `{}`", path.display());
321
+ return;
322
+ };
323
+ let Ok(url) = Url::from_file_path(path) else {
324
+ eprintln!("Couldn't build URI from path `{}`", path.display());
325
+ return;
326
+ };
327
+
328
+ let language = path.extension().map_or(LanguageId::Ruby, LanguageId::from);
329
+ let local_graph = build_local_graph(url.to_string(), &source, &language, backend);
330
+ graph.consume_document_changes(local_graph);
331
+ }
@@ -91,7 +91,7 @@ macro_rules! namespace_declaration {
91
91
  #[derive(Debug)]
92
92
  pub struct $name {
93
93
  /// The fully qualified name of this declaration
94
- name: String,
94
+ name: Box<str>,
95
95
  /// The list of definition IDs that compose this declaration
96
96
  definition_ids: Vec<DefinitionId>,
97
97
  /// The set of references that are made to this declaration
@@ -116,7 +116,7 @@ macro_rules! namespace_declaration {
116
116
  #[must_use]
117
117
  pub fn new(name: String, owner_id: DeclarationId) -> Self {
118
118
  Self {
119
- name,
119
+ name: name.into_boxed_str(),
120
120
  definition_ids: Vec::new(),
121
121
  members: IdentityHashMap::default(),
122
122
  references: IdentityHashSet::default(),
@@ -231,7 +231,7 @@ macro_rules! simple_declaration {
231
231
  #[derive(Debug)]
232
232
  pub struct $name {
233
233
  /// The fully qualified name of this declaration
234
- name: String,
234
+ name: Box<str>,
235
235
  /// The list of definition IDs that compose this declaration
236
236
  definition_ids: Vec<DefinitionId>,
237
237
  /// The set of references that are made to this declaration
@@ -244,7 +244,7 @@ macro_rules! simple_declaration {
244
244
  #[must_use]
245
245
  pub fn new(name: String, owner_id: DeclarationId) -> Self {
246
246
  Self {
247
- name,
247
+ name: name.into_boxed_str(),
248
248
  definition_ids: Vec::new(),
249
249
  references: IdentityHashSet::default(),
250
250
  owner_id,
@@ -608,25 +608,25 @@ impl Namespace {
608
608
  }
609
609
 
610
610
  namespace_declaration!(Class, ClassDeclaration);
611
- assert_mem_size!(ClassDeclaration, 192);
611
+ assert_mem_size!(ClassDeclaration, 184);
612
612
  namespace_declaration!(Module, ModuleDeclaration);
613
- assert_mem_size!(ModuleDeclaration, 192);
613
+ assert_mem_size!(ModuleDeclaration, 184);
614
614
  namespace_declaration!(SingletonClass, SingletonClassDeclaration);
615
- assert_mem_size!(SingletonClassDeclaration, 192);
615
+ assert_mem_size!(SingletonClassDeclaration, 184);
616
616
  namespace_declaration!(Todo, TodoDeclaration);
617
- assert_mem_size!(TodoDeclaration, 192);
617
+ assert_mem_size!(TodoDeclaration, 184);
618
618
  simple_declaration!(ConstantDeclaration, ConstantReferenceId);
619
- assert_mem_size!(ConstantDeclaration, 88);
619
+ assert_mem_size!(ConstantDeclaration, 80);
620
620
  simple_declaration!(MethodDeclaration, MethodReferenceId);
621
- assert_mem_size!(MethodDeclaration, 88);
621
+ assert_mem_size!(MethodDeclaration, 80);
622
622
  simple_declaration!(GlobalVariableDeclaration, GlobalVariableReferenceId);
623
- assert_mem_size!(GlobalVariableDeclaration, 88);
623
+ assert_mem_size!(GlobalVariableDeclaration, 80);
624
624
  simple_declaration!(InstanceVariableDeclaration, InstanceVariableReferenceId);
625
- assert_mem_size!(InstanceVariableDeclaration, 88);
625
+ assert_mem_size!(InstanceVariableDeclaration, 80);
626
626
  simple_declaration!(ClassVariableDeclaration, ClassVariableReferenceId);
627
- assert_mem_size!(ClassVariableDeclaration, 88);
627
+ assert_mem_size!(ClassVariableDeclaration, 80);
628
628
  simple_declaration!(ConstantAliasDeclaration, ConstantReferenceId);
629
- assert_mem_size!(ConstantAliasDeclaration, 88);
629
+ assert_mem_size!(ConstantAliasDeclaration, 80);
630
630
 
631
631
  #[cfg(test)]
632
632
  mod tests {
@@ -13,7 +13,10 @@ use crate::model::definitions::{Definition, MethodVisibilityDefinition, Receiver
13
13
  use crate::model::document::Document;
14
14
  use crate::model::encoding::Encoding;
15
15
  use crate::model::identity_maps::{IdentityHashMap, IdentityHashSet};
16
- use crate::model::ids::{ConstantReferenceId, DeclarationId, DefinitionId, MethodReferenceId, NameId, StringId, UriId};
16
+ use crate::model::ids::{
17
+ ConstantReferenceId, DeclarationId, DefinitionId, MethodReferenceId, NameId, StringId, UriId,
18
+ declaration_id_from_lookup_name,
19
+ };
17
20
  use crate::model::name::{Name, NameRef, ParentScope, ResolvedName};
18
21
  use crate::model::references::{ConstantReference, MethodRef};
19
22
  use crate::model::string_ref::StringRef;
@@ -578,7 +581,7 @@ impl Graph {
578
581
 
579
582
  #[must_use]
580
583
  pub fn get(&self, name: &str) -> Option<Vec<&Definition>> {
581
- let declaration_id = DeclarationId::from(name);
584
+ let declaration_id = declaration_id_from_lookup_name(name);
582
585
  let declaration = self.declarations.get(&declaration_id)?;
583
586
 
584
587
  Some(
@@ -1270,6 +1273,10 @@ impl Graph {
1270
1273
  let unqualified_str_id = StringId::from(&decl.unqualified_name());
1271
1274
  let owner_id = *decl.owner_id();
1272
1275
  let is_singleton_class = matches!(decl, Declaration::Namespace(Namespace::SingletonClass(_)));
1276
+ let ancestors_to_detach: Vec<Ancestor> = decl
1277
+ .as_namespace()
1278
+ .map(|ns| ns.ancestors().iter().copied().collect())
1279
+ .unwrap_or_default();
1273
1280
 
1274
1281
  for def_id in def_ids {
1275
1282
  self.push_work(Unit::Definition(def_id));
@@ -1284,6 +1291,16 @@ impl Graph {
1284
1291
  ns.remove_member(&unqualified_str_id);
1285
1292
  }
1286
1293
  }
1294
+
1295
+ // Detach from each complete ancestor's descendant set so we don't leave a stale id in descendants
1296
+ for ancestor in ancestors_to_detach {
1297
+ if let Ancestor::Complete(ancestor_id) = ancestor
1298
+ && let Some(anc_decl) = self.declarations.get_mut(&ancestor_id)
1299
+ && let Some(ns) = anc_decl.as_namespace_mut()
1300
+ {
1301
+ ns.remove_descendant(&decl_id);
1302
+ }
1303
+ }
1287
1304
  }
1288
1305
 
1289
1306
  self.declarations.remove(&decl_id);
@@ -1570,6 +1587,10 @@ mod tests {
1570
1587
  assert_members_eq, assert_no_diagnostics, assert_no_members,
1571
1588
  };
1572
1589
 
1590
+ fn definition_ids(definitions: Vec<&Definition>) -> Vec<DefinitionId> {
1591
+ definitions.into_iter().map(Definition::id).collect()
1592
+ }
1593
+
1573
1594
  #[test]
1574
1595
  fn deleting_a_uri() {
1575
1596
  let mut context = GraphTest::new();
@@ -1992,6 +2013,34 @@ mod tests {
1992
2013
  assert_eq!(definitions[0].offset().start(), 6);
1993
2014
  }
1994
2015
 
2016
+ #[test]
2017
+ fn get_accepts_leading_double_colon() {
2018
+ let mut context = GraphTest::new();
2019
+
2020
+ context.index_uri("file:///foo.rb", "module Foo; module Bar; end; end");
2021
+ context.resolve();
2022
+
2023
+ // Built-in declarations (root-scoped):
2024
+ let unqualified = context.graph().get("Object").expect("unqualified `Object` lookup");
2025
+ let qualified = context.graph().get("::Object").expect("qualified `::Object` lookup");
2026
+ assert_eq!(definition_ids(unqualified), definition_ids(qualified));
2027
+
2028
+ // Indexed declarations, top-level and nested:
2029
+ let unqualified = context.graph().get("Foo").expect("unqualified `Foo` lookup");
2030
+ let qualified = context.graph().get("::Foo").expect("qualified `::Foo` lookup");
2031
+ assert_eq!(definition_ids(unqualified), definition_ids(qualified));
2032
+
2033
+ let unqualified = context.graph().get("Foo::Bar").expect("unqualified `Foo::Bar` lookup");
2034
+ let qualified = context
2035
+ .graph()
2036
+ .get("::Foo::Bar")
2037
+ .expect("qualified `::Foo::Bar` lookup");
2038
+ assert_eq!(definition_ids(unqualified), definition_ids(qualified));
2039
+
2040
+ // Unknown names still return None when prefixed:
2041
+ assert!(context.graph().get("::DoesNotExist").is_none());
2042
+ }
2043
+
1995
2044
  #[test]
1996
2045
  fn adding_another_definition_from_a_different_uri() {
1997
2046
  let mut context = GraphTest::new();
@@ -2499,6 +2548,7 @@ mod tests {
2499
2548
 
2500
2549
  #[cfg(test)]
2501
2550
  mod incremental_resolution_tests {
2551
+ use crate::model::ids::DeclarationId;
2502
2552
  use crate::model::name::NameRef;
2503
2553
  use crate::test_utils::GraphTest;
2504
2554
  use crate::{
@@ -4126,4 +4176,37 @@ mod incremental_resolution_tests {
4126
4176
  assert_declaration_exists!(context, "Foo::<Foo>");
4127
4177
  assert_declaration_exists!(context, "Bar::<Bar>");
4128
4178
  }
4179
+
4180
+ #[test]
4181
+ fn constant_references_through_object_inheritance_are_invalidated() {
4182
+ let mut context = GraphTest::new();
4183
+ context.index_uri("file:///a.rb", "class Foo; end");
4184
+ context.index_uri(
4185
+ "file:///b.rb",
4186
+ r"
4187
+ module Wrap
4188
+ class Foo; end
4189
+ ::Foo
4190
+ Foo
4191
+ end
4192
+ ",
4193
+ );
4194
+ context.resolve();
4195
+
4196
+ context.delete_uri("file:///a.rb");
4197
+ context.resolve();
4198
+
4199
+ let kernel = context
4200
+ .graph()
4201
+ .declarations()
4202
+ .get(&DeclarationId::from("Kernel"))
4203
+ .unwrap();
4204
+
4205
+ for id in kernel.as_namespace().unwrap().descendants() {
4206
+ assert!(
4207
+ context.graph().declarations().contains_key(id),
4208
+ "Kernel has stale descendant id {id:?} with no backing declaration"
4209
+ );
4210
+ }
4211
+ }
4129
4212
  } // mod incremental_resolution_tests
@@ -9,6 +9,15 @@ pub struct DeclarationMarker;
9
9
  /// `DeclarationId` represents the ID of a fully qualified name. For example, `Foo::Bar` or `Foo#my_method`
10
10
  pub type DeclarationId = Id<DeclarationMarker>;
11
11
 
12
+ /// Creates a `DeclarationId` from a user-facing lookup name.
13
+ ///
14
+ /// Declaration names are stored without the root-scope marker, so `"::Object"`
15
+ /// and `"Object"` must resolve to the same declaration ID.
16
+ #[must_use]
17
+ pub fn declaration_id_from_lookup_name(name: &str) -> DeclarationId {
18
+ DeclarationId::from(name.strip_prefix("::").unwrap_or(name))
19
+ }
20
+
12
21
  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
13
22
  pub struct DefinitionMarker;
14
23
 
@@ -84,3 +93,24 @@ assert_mem_size!(ClassVariableReferenceId, 8);
84
93
  pub struct InstanceVariableMarker;
85
94
  pub type InstanceVariableReferenceId = Id<InstanceVariableMarker>;
86
95
  assert_mem_size!(InstanceVariableReferenceId, 8);
96
+
97
+ #[cfg(test)]
98
+ mod tests {
99
+ use super::*;
100
+
101
+ #[test]
102
+ fn declaration_id_from_lookup_name_accepts_root_scope_marker() {
103
+ assert_eq!(
104
+ DeclarationId::from("Object"),
105
+ declaration_id_from_lookup_name("::Object")
106
+ );
107
+ assert_eq!(
108
+ DeclarationId::from("Foo::Bar"),
109
+ declaration_id_from_lookup_name("::Foo::Bar")
110
+ );
111
+ assert_ne!(
112
+ DeclarationId::from("Object"),
113
+ declaration_id_from_lookup_name("::::Object")
114
+ );
115
+ }
116
+ }
@@ -9,15 +9,23 @@ use std::ops::Deref;
9
9
  /// graph when its count reaches zero.
10
10
  #[derive(Debug)]
11
11
  pub struct StringRef {
12
- value: String,
12
+ value: Box<str>,
13
13
  ref_count: u32,
14
14
  }
15
- assert_mem_size!(StringRef, 32);
15
+ assert_mem_size!(StringRef, 24);
16
16
 
17
17
  impl StringRef {
18
18
  #[must_use]
19
19
  pub fn new(value: String) -> Self {
20
- Self { value, ref_count: 1 }
20
+ Self {
21
+ value: value.into_boxed_str(),
22
+ ref_count: 1,
23
+ }
24
+ }
25
+
26
+ #[must_use]
27
+ pub fn as_str(&self) -> &str {
28
+ &self.value
21
29
  }
22
30
 
23
31
  #[must_use]
@@ -44,7 +52,7 @@ impl StringRef {
44
52
  }
45
53
 
46
54
  impl Deref for StringRef {
47
- type Target = String;
55
+ type Target = str;
48
56
 
49
57
  fn deref(&self) -> &Self::Target {
50
58
  &self.value