rubydex 0.2.0 → 0.2.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.
@@ -287,11 +287,8 @@ impl<'a> Resolver<'a> {
287
287
  unreachable!("SelfReceiver methods should be routed to handle_definition_unit");
288
288
  }
289
289
  Some(Receiver::ConstantReceiver(name_id)) => {
290
- let receiver_decl_id = match self.graph.names().get(name_id).unwrap() {
291
- NameRef::Resolved(resolved) => *resolved.declaration_id(),
292
- NameRef::Unresolved(_) => {
293
- continue;
294
- }
290
+ let Some(receiver_decl_id) = self.resolve_constant_receiver(*name_id, id) else {
291
+ continue;
295
292
  };
296
293
 
297
294
  let Some(singleton_id) = self.get_or_create_singleton_class(receiver_decl_id, true) else {
@@ -301,8 +298,8 @@ impl<'a> Resolver<'a> {
301
298
  singleton_id
302
299
  }
303
300
  None => {
304
- let Some(resolved) = self.resolve_lexical_owner(*method_definition.lexical_nesting_id())
305
- else {
301
+ let lexical = *method_definition.lexical_nesting_id();
302
+ let Some(resolved) = self.resolve_lexical_owner(lexical, id) else {
306
303
  continue;
307
304
  };
308
305
  resolved
@@ -314,29 +311,35 @@ impl<'a> Resolver<'a> {
314
311
  });
315
312
  }
316
313
  Definition::AttrAccessor(attr) => {
317
- let Some(owner_id) = self.resolve_lexical_owner(*attr.lexical_nesting_id()) else {
314
+ let lexical = *attr.lexical_nesting_id();
315
+ let str_id = *attr.str_id();
316
+ let Some(owner_id) = self.resolve_lexical_owner(lexical, id) else {
318
317
  continue;
319
318
  };
320
319
 
321
- self.create_declaration(*attr.str_id(), id, owner_id, |name| {
320
+ self.create_declaration(str_id, id, owner_id, |name| {
322
321
  Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
323
322
  });
324
323
  }
325
324
  Definition::AttrReader(attr) => {
326
- let Some(owner_id) = self.resolve_lexical_owner(*attr.lexical_nesting_id()) else {
325
+ let lexical = *attr.lexical_nesting_id();
326
+ let str_id = *attr.str_id();
327
+ let Some(owner_id) = self.resolve_lexical_owner(lexical, id) else {
327
328
  continue;
328
329
  };
329
330
 
330
- self.create_declaration(*attr.str_id(), id, owner_id, |name| {
331
+ self.create_declaration(str_id, id, owner_id, |name| {
331
332
  Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
332
333
  });
333
334
  }
334
335
  Definition::AttrWriter(attr) => {
335
- let Some(owner_id) = self.resolve_lexical_owner(*attr.lexical_nesting_id()) else {
336
+ let lexical = *attr.lexical_nesting_id();
337
+ let str_id = *attr.str_id();
338
+ let Some(owner_id) = self.resolve_lexical_owner(lexical, id) else {
336
339
  continue;
337
340
  };
338
341
 
339
- self.create_declaration(*attr.str_id(), id, owner_id, |name| {
342
+ self.create_declaration(str_id, id, owner_id, |name| {
340
343
  Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
341
344
  });
342
345
  }
@@ -377,11 +380,11 @@ impl<'a> Resolver<'a> {
377
380
  .definition_id_to_declaration_id(*def_id)
378
381
  .expect("SelfReceiver definition should have a declaration"),
379
382
  Receiver::ConstantReceiver(name_id) => {
380
- let Some(NameRef::Resolved(resolved)) = self.graph.names().get(name_id) else {
383
+ let Some(receiver_decl_id) = self.resolve_constant_receiver(*name_id, id)
384
+ else {
381
385
  continue;
382
386
  };
383
-
384
- *resolved.declaration_id()
387
+ receiver_decl_id
385
388
  }
386
389
  };
387
390
 
@@ -407,7 +410,8 @@ impl<'a> Resolver<'a> {
407
410
  }
408
411
 
409
412
  // If the method has no explicit receiver, we resolve the owner based on the lexical nesting
410
- let Some(method_owner_id) = self.resolve_lexical_owner(*method.lexical_nesting_id()) else {
413
+ let Some(method_owner_id) = self.resolve_lexical_owner(*method.lexical_nesting_id(), id)
414
+ else {
411
415
  continue;
412
416
  };
413
417
 
@@ -464,11 +468,14 @@ impl<'a> Resolver<'a> {
464
468
  // If in a singleton class body directly, the owner is the singleton class's singleton class
465
469
  // Like `class << Foo; @bar = 1; end`, where `@bar` is owned by `Foo::<Foo>::<<Foo>>`
466
470
  Definition::SingletonClass(_) => {
467
- let singleton_class_decl_id = self
468
- .graph
469
- .definition_id_to_declaration_id(nesting_id)
470
- .copied()
471
- .unwrap_or(*OBJECT_ID);
471
+ // The singleton's declaration may be missing (e.g. its receiver was
472
+ // just deleted). Re-queue and let the next resolve place `@bar` on
473
+ // the right owner instead of falling back to Object.
474
+ let Some(&singleton_class_decl_id) = self.graph.definition_id_to_declaration_id(nesting_id)
475
+ else {
476
+ self.graph.push_work(Unit::Definition(id));
477
+ continue;
478
+ };
472
479
  let owner_id = self
473
480
  .get_or_create_singleton_class(singleton_class_decl_id, true)
474
481
  .expect("singleton class nesting should always be a namespace");
@@ -515,14 +522,15 @@ impl<'a> Resolver<'a> {
515
522
  };
516
523
  owner_id
517
524
  }
518
- Some(Receiver::ConstantReceiver(name_id)) => match self.graph.names().get(name_id).unwrap() {
519
- NameRef::Resolved(resolved) => *resolved.declaration_id(),
520
- NameRef::Unresolved(_) => {
525
+ Some(Receiver::ConstantReceiver(name_id)) => {
526
+ let Some(resolved) = self.resolve_constant_receiver(*name_id, id) else {
521
527
  continue;
522
- }
523
- },
528
+ };
529
+ resolved
530
+ }
524
531
  None => {
525
- let Some(resolved) = self.resolve_lexical_owner(*alias.lexical_nesting_id()) else {
532
+ let lexical = *alias.lexical_nesting_id();
533
+ let Some(resolved) = self.resolve_lexical_owner(lexical, id) else {
526
534
  continue;
527
535
  };
528
536
  resolved
@@ -538,8 +546,63 @@ impl<'a> Resolver<'a> {
538
546
  Declaration::GlobalVariable(Box::new(GlobalVariableDeclaration::new(name, *OBJECT_ID)))
539
547
  });
540
548
  }
541
- Definition::ConstantVisibility(_constant_visibility) => {
542
- // TODO
549
+ Definition::ConstantVisibility(constant_visibility) => {
550
+ // Both `private_constant` and `public_constant` can only target direct members.
551
+ // Inheritance or surrounding lexical scopes are not taken into account.
552
+ let receiver = *constant_visibility.receiver();
553
+ let target = *constant_visibility.target();
554
+ let uri_id = *constant_visibility.uri_id();
555
+ let offset = constant_visibility.offset().clone();
556
+ let lexical_nesting_id = *constant_visibility.lexical_nesting_id();
557
+ let constant_name = self.graph.strings().get(&target).unwrap().as_str().to_string();
558
+
559
+ let owner_id = if let Some(receiver_name_id) = receiver {
560
+ let NameRef::Resolved(resolved_receiver) = self.graph.names().get(&receiver_name_id).unwrap()
561
+ else {
562
+ continue;
563
+ };
564
+ let Some(namespace_id) = self.resolve_to_namespace(*resolved_receiver.declaration_id()) else {
565
+ continue;
566
+ };
567
+ namespace_id
568
+ } else {
569
+ let Some(decl_id) = self.resolve_lexical_owner(lexical_nesting_id, id) else {
570
+ continue;
571
+ };
572
+ decl_id
573
+ };
574
+
575
+ let Some(Declaration::Namespace(namespace)) = self.graph.declarations().get(&owner_id) else {
576
+ continue;
577
+ };
578
+
579
+ if let Some(member) = namespace
580
+ .member(&target)
581
+ .and_then(|member_id| self.graph.declarations().get(member_id))
582
+ && matches!(
583
+ member,
584
+ Declaration::Constant(_)
585
+ | Declaration::ConstantAlias(_)
586
+ | Declaration::Namespace(Namespace::Class(_) | Namespace::Module(_))
587
+ )
588
+ {
589
+ // `add_declaration` deduplicates by fully qualified name, so this appends
590
+ // the visibility definition to the existing constant declaration.
591
+ self.graph.add_declaration(id, member.name().to_string(), |name| {
592
+ Declaration::Constant(Box::new(ConstantDeclaration::new(name, owner_id)))
593
+ });
594
+ } else {
595
+ let diagnostic = Diagnostic::new(
596
+ Rule::UndefinedConstantVisibilityTarget,
597
+ uri_id,
598
+ offset,
599
+ format!(
600
+ "undefined constant `{constant_name}` for visibility change in `{}`",
601
+ namespace.name()
602
+ ),
603
+ );
604
+ self.graph.add_document_diagnostic(uri_id, diagnostic);
605
+ }
543
606
  }
544
607
  Definition::MethodVisibility(_) => {
545
608
  method_visibility_ids.push(id);
@@ -574,7 +637,7 @@ impl<'a> Resolver<'a> {
574
637
  let offset = method_visibility.offset().clone();
575
638
  let lexical_nesting_id = *method_visibility.lexical_nesting_id();
576
639
 
577
- let Some(owner_id) = self.resolve_lexical_owner(lexical_nesting_id) else {
640
+ let Some(owner_id) = self.resolve_lexical_owner(lexical_nesting_id, id) else {
578
641
  continue;
579
642
  };
580
643
 
@@ -640,6 +703,19 @@ impl<'a> Resolver<'a> {
640
703
  self.graph.extend_work(pending_work);
641
704
  }
642
705
 
706
+ /// Resolves a constant receiver for `handle_remaining_definitions`.
707
+ /// If the receiver name is unresolved, preserve the definition for a later
708
+ /// resolve cycle instead of dropping work during an incremental delete/re-add gap.
709
+ fn resolve_constant_receiver(&mut self, name_id: NameId, id: DefinitionId) -> Option<DeclarationId> {
710
+ match self.graph.names().get(&name_id).unwrap() {
711
+ NameRef::Resolved(resolved) => Some(*resolved.declaration_id()),
712
+ NameRef::Unresolved(_) => {
713
+ self.graph.push_work(Unit::Definition(id));
714
+ None
715
+ }
716
+ }
717
+ }
718
+
643
719
  fn create_declaration<F>(
644
720
  &mut self,
645
721
  str_id: StringId,
@@ -689,37 +765,62 @@ impl<'a> Resolver<'a> {
689
765
  }
690
766
 
691
767
  /// Resolves owner from lexical nesting.
692
- fn resolve_lexical_owner(&self, lexical_nesting_id: Option<DefinitionId>) -> Option<DeclarationId> {
693
- let Some(id) = lexical_nesting_id else {
694
- return Some(*OBJECT_ID);
695
- };
768
+ ///
769
+ /// If the owner cannot be resolved yet, re-queues the current definition so
770
+ /// a later resolve cycle can retry instead of permanently dropping it.
771
+ fn resolve_lexical_owner(
772
+ &mut self,
773
+ lexical_nesting_id: Option<DefinitionId>,
774
+ definition_id: DefinitionId,
775
+ ) -> Option<DeclarationId> {
776
+ let mut current_nesting = lexical_nesting_id;
696
777
 
697
- // If no declaration exists yet for this definition, walk up the lexical chain.
698
- // This handles the case where attr_* definitions inside methods are processed
699
- // before the method definition itself.
700
- let Some(declaration_id) = self.graph.definition_id_to_declaration_id(id) else {
701
- let definition = self.graph.definitions().get(&id).unwrap();
702
- return self.resolve_lexical_owner(*definition.lexical_nesting_id());
703
- };
778
+ let resolved = loop {
779
+ let Some(id) = current_nesting else {
780
+ break Some(*OBJECT_ID);
781
+ };
782
+
783
+ // If no declaration exists yet for this definition, walk up the lexical chain.
784
+ // This handles the case where attr_* definitions inside methods are processed
785
+ // before the method definition itself. A SingletonClass with no declaration
786
+ // is an exception: returning the surrounding scope would attach its members to
787
+ // the wrong owner (e.g. `Object`) and never recover, so retry later instead.
788
+ let Some(declaration_id) = self.graph.definition_id_to_declaration_id(id) else {
789
+ let definition = self.graph.definitions().get(&id).unwrap();
790
+ if matches!(definition, Definition::SingletonClass(_)) {
791
+ break None;
792
+ }
793
+ current_nesting = *definition.lexical_nesting_id();
794
+ continue;
795
+ };
704
796
 
705
- let decl = self.graph.declarations().get(declaration_id).unwrap();
797
+ let decl = self.graph.declarations().get(declaration_id).unwrap();
798
+
799
+ // If the associated declaration is a namespace that can own things, we found the right owner. Otherwise, we might
800
+ // have found something nested inside something else (like a method), in which case we have to walk up until we find
801
+ // the appropriate owner.
802
+ if matches!(
803
+ decl,
804
+ Declaration::Namespace(Namespace::Class(_) | Namespace::Module(_) | Namespace::SingletonClass(_))
805
+ ) {
806
+ break Some(*declaration_id);
807
+ }
808
+
809
+ if matches!(decl, Declaration::ConstantAlias(_)) {
810
+ // Follow the alias chain to find the target namespace. If the alias is unresolved,
811
+ // the definition cannot be properly owned yet and should be retried later.
812
+ break self.resolve_to_namespace(*declaration_id);
813
+ }
706
814
 
707
- // If the associated declaration is a namespace that can own things, we found the right owner. Otherwise, we might
708
- // have found something nested inside something else (like a method), in which case we have to recurse until we find
709
- // the appropriate owner
710
- if matches!(
711
- decl,
712
- Declaration::Namespace(Namespace::Class(_) | Namespace::Module(_) | Namespace::SingletonClass(_))
713
- ) {
714
- Some(*declaration_id)
715
- } else if matches!(decl, Declaration::ConstantAlias(_)) {
716
- // Follow the alias chain to find the target namespace. If the alias is unresolved,
717
- // the definition cannot be properly owned and should be skipped by the caller.
718
- self.resolve_to_namespace(*declaration_id)
719
- } else {
720
815
  let definition = self.graph.definitions().get(&id).unwrap();
721
- self.resolve_lexical_owner(*definition.lexical_nesting_id())
816
+ current_nesting = *definition.lexical_nesting_id();
817
+ };
818
+
819
+ if resolved.is_none() {
820
+ self.graph.push_work(Unit::Definition(definition_id));
722
821
  }
822
+
823
+ resolved
723
824
  }
724
825
 
725
826
  /// Gets or creates a singleton class declaration for a given class/module declaration. For class `Foo`, this
@@ -1124,11 +1225,17 @@ impl<'a> Resolver<'a> {
1124
1225
  let name_ref = self.graph.names().get(&name_id).unwrap();
1125
1226
  let str_id = *name_ref.str();
1126
1227
 
1127
- let outcome = match self.name_owner_id(name_id) {
1228
+ let outcome = match self.name_owner_id(name_id, singleton) {
1128
1229
  // name_owner_id returns Unresolved(None) only when the parent scope is genuinely unknown
1129
1230
  // (e.g., `class A::B::C` where `A` doesn't exist). This definition needs an owner, so
1130
1231
  // create Todo placeholders for the missing parent chain. Todos get promoted when real
1131
1232
  // definitions appear later.
1233
+ //
1234
+ // Singleton classes are the exception: `class << UndefinedReceiver` attaches via
1235
+ // `set_singleton_class_id`, not `add_member`, so a TODO receiver would never gain a
1236
+ // member. Emit Retry so the unit is preserved for a later resolve where the receiver
1237
+ // may exist.
1238
+ Outcome::Unresolved(None) if singleton => Outcome::Retry(None),
1132
1239
  Outcome::Unresolved(None) => Outcome::Resolved(self.create_todo_for_parent(name_id), None),
1133
1240
  other => other,
1134
1241
  };
@@ -1200,7 +1307,11 @@ impl<'a> Resolver<'a> {
1200
1307
  // Returns the owner declaration ID for a given name. If the name is simple and has no parent scope, then the owner is
1201
1308
  // either the nesting or Object. If the name has a parent scope, we attempt to resolve the reference and that should be
1202
1309
  // the name's owner. For aliases, resolves through to get the actual namespace.
1203
- fn name_owner_id(&mut self, name_id: NameId) -> Outcome {
1310
+ //
1311
+ // When `preserve_retry` is true, Retry from resolve_constant_internal is NOT folded into
1312
+ // Unresolved(None). This is used by the singleton path so the unit can retry when the
1313
+ // receiver might resolve later rather than being dropped.
1314
+ fn name_owner_id(&mut self, name_id: NameId, preserve_retry: bool) -> Outcome {
1204
1315
  let name_ref = self.graph.names().get(&name_id).unwrap();
1205
1316
 
1206
1317
  if let Some(&parent_scope) = name_ref.parent_scope().as_ref() {
@@ -1210,7 +1321,8 @@ impl<'a> Resolver<'a> {
1210
1321
  Outcome::Resolved(id, linearization) => self.resolve_to_primary_namespace(id, linearization),
1211
1322
  // The parent scope is genuinely unknown — not a circular alias or pending
1212
1323
  // linearization, but a name that doesn't exist anywhere in the graph.
1213
- Outcome::Retry(None) | Outcome::Unresolved(None) => Outcome::Unresolved(None),
1324
+ Outcome::Unresolved(None) => Outcome::Unresolved(None),
1325
+ Outcome::Retry(None) if !preserve_retry => Outcome::Unresolved(None),
1214
1326
  other => other,
1215
1327
  }
1216
1328
  } else if let Some(nesting_id) = name_ref.nesting()
@@ -1251,7 +1363,7 @@ impl<'a> Resolver<'a> {
1251
1363
  // Object so it becomes top-level `A`. This way `module A; end` appearing later
1252
1364
  // promotes it correctly. Using nesting would incorrectly create `SomeModule::A`.
1253
1365
  let parent_owner_id = if parent_has_parent_scope {
1254
- match self.name_owner_id(parent_scope) {
1366
+ match self.name_owner_id(parent_scope, false) {
1255
1367
  Outcome::Resolved(id, _) => id,
1256
1368
  _ => self.create_todo_for_parent(parent_scope),
1257
1369
  }
@@ -5128,4 +5128,182 @@ mod visibility_resolution_tests {
5128
5128
  assert_owner_eq!(context, "Child#foo()", "Child");
5129
5129
  assert_visibility_eq!(context, "Child#foo()", Visibility::Private);
5130
5130
  }
5131
+
5132
+ #[test]
5133
+ fn retroactive_constant_visibility_on_direct_member() {
5134
+ let mut context = GraphTest::new();
5135
+ context.index_uri(
5136
+ "file:///foo.rb",
5137
+ r"
5138
+ class Foo
5139
+ BAR = 1
5140
+ private_constant :BAR
5141
+
5142
+ BAZ = 2
5143
+ public_constant :BAZ
5144
+
5145
+ QUX = 3
5146
+
5147
+ class Inner; end
5148
+ private_constant :Inner
5149
+
5150
+ module InnerMod; end
5151
+ private_constant :InnerMod
5152
+ end
5153
+ ",
5154
+ );
5155
+ context.resolve();
5156
+
5157
+ assert_no_diagnostics!(&context);
5158
+ assert_visibility_eq!(context, "Foo::BAR", Visibility::Private);
5159
+ assert_visibility_eq!(context, "Foo::BAZ", Visibility::Public);
5160
+ assert_visibility_eq!(context, "Foo::QUX", Visibility::Public);
5161
+ assert_visibility_eq!(context, "Foo::Inner", Visibility::Private);
5162
+ assert_visibility_eq!(context, "Foo::InnerMod", Visibility::Private);
5163
+ }
5164
+
5165
+ #[test]
5166
+ fn retroactive_constant_visibility_via_qualified_receiver() {
5167
+ let mut context = GraphTest::new();
5168
+ context.index_uri(
5169
+ "file:///foo.rb",
5170
+ r"
5171
+ class Foo
5172
+ BAR = 1
5173
+ BAZ = 2
5174
+ end
5175
+
5176
+ ALIAS = Foo
5177
+ Foo.private_constant :BAR
5178
+ ALIAS.private_constant :BAZ
5179
+ ",
5180
+ );
5181
+ context.resolve();
5182
+
5183
+ assert_no_diagnostics!(&context);
5184
+ assert_visibility_eq!(context, "Foo::BAR", Visibility::Private);
5185
+ assert_visibility_eq!(context, "Foo::BAZ", Visibility::Private);
5186
+ }
5187
+
5188
+ #[test]
5189
+ fn retroactive_constant_visibility_multi_arg_undefined_emits_per_name_diagnostic() {
5190
+ let mut context = GraphTest::new();
5191
+ context.index_uri(
5192
+ "file:///foo.rb",
5193
+ r"
5194
+ class Foo
5195
+ private_constant :NOPE_ONE, :NOPE_TWO
5196
+ end
5197
+ ",
5198
+ );
5199
+ context.resolve();
5200
+
5201
+ assert_diagnostics_eq!(
5202
+ context,
5203
+ &[
5204
+ "undefined-constant-visibility-target: undefined constant `NOPE_ONE` for visibility change in `Foo` (2:21-2:29)",
5205
+ "undefined-constant-visibility-target: undefined constant `NOPE_TWO` for visibility change in `Foo` (2:32-2:40)",
5206
+ ]
5207
+ );
5208
+ }
5209
+
5210
+ #[test]
5211
+ fn retroactive_constant_visibility_inherited_constant_emits_diagnostic() {
5212
+ let mut context = GraphTest::new();
5213
+ context.index_uri(
5214
+ "file:///foo.rb",
5215
+ r"
5216
+ class Parent
5217
+ CONST = 1
5218
+ end
5219
+
5220
+ class Child < Parent
5221
+ private_constant :CONST
5222
+ end
5223
+ ",
5224
+ );
5225
+ context.resolve();
5226
+
5227
+ assert_diagnostics_eq!(
5228
+ context,
5229
+ &[
5230
+ "undefined-constant-visibility-target: undefined constant `CONST` for visibility change in `Child` (6:21-6:26)"
5231
+ ]
5232
+ );
5233
+ assert_visibility_eq!(context, "Parent::CONST", Visibility::Public);
5234
+ }
5235
+
5236
+ #[test]
5237
+ fn retroactive_constant_visibility_clears_when_call_removed() {
5238
+ let mut context = GraphTest::new();
5239
+ context.index_uri(
5240
+ "file:///foo.rb",
5241
+ r"
5242
+ class Foo
5243
+ BAR = 1
5244
+ end
5245
+ ",
5246
+ );
5247
+ context.index_uri(
5248
+ "file:///vis.rb",
5249
+ r"
5250
+ Foo.private_constant :BAR
5251
+ ",
5252
+ );
5253
+ context.resolve();
5254
+
5255
+ assert_no_diagnostics!(&context);
5256
+ assert_visibility_eq!(context, "Foo::BAR", Visibility::Private);
5257
+
5258
+ context.delete_uri("file:///vis.rb");
5259
+ context.resolve();
5260
+
5261
+ assert_no_diagnostics!(&context);
5262
+ assert_visibility_eq!(context, "Foo::BAR", Visibility::Public);
5263
+ }
5264
+
5265
+ #[test]
5266
+ fn retroactive_constant_visibility_inside_singleton_class_body() {
5267
+ let mut context = GraphTest::new();
5268
+ context.index_uri(
5269
+ "file:///foo.rb",
5270
+ r"
5271
+ class Foo
5272
+ class << self
5273
+ BAR = 1
5274
+ private_constant :BAR
5275
+ end
5276
+ end
5277
+ ",
5278
+ );
5279
+ context.resolve();
5280
+
5281
+ assert_no_diagnostics!(&context);
5282
+ assert_visibility_eq!(context, "Foo::<Foo>::BAR", Visibility::Private);
5283
+ }
5284
+
5285
+ #[test]
5286
+ fn retroactive_constant_visibility_persists_across_reopened_class() {
5287
+ let mut context = GraphTest::new();
5288
+ context.index_uri(
5289
+ "file:///a.rb",
5290
+ r"
5291
+ class Foo
5292
+ BAR = 1
5293
+ private_constant :BAR
5294
+ end
5295
+ ",
5296
+ );
5297
+ context.index_uri(
5298
+ "file:///b.rb",
5299
+ r"
5300
+ class Foo
5301
+ BAR = 2
5302
+ end
5303
+ ",
5304
+ );
5305
+ context.resolve();
5306
+
5307
+ assert_visibility_eq!(context, "Foo::BAR", Visibility::Private);
5308
+ }
5131
5309
  }
@@ -64,6 +64,25 @@ impl CDeclaration {
64
64
  }
65
65
  }
66
66
 
67
+ /// Convert a nullable C string to `Option<DeclarationId>`.
68
+ /// Null, empty, or non-UTF-8 input yields `None`.
69
+ ///
70
+ /// # Safety
71
+ ///
72
+ /// If non-null, `ptr` must point to a valid, NUL-terminated C string that remains valid for the
73
+ /// duration of the call. The contents do not need to be UTF-8 — non-UTF-8 input is handled by returning
74
+ /// `None`.
75
+ pub(crate) unsafe fn decl_id_from_char_ptr(ptr: *const c_char) -> Option<DeclarationId> {
76
+ if ptr.is_null() {
77
+ return None;
78
+ }
79
+ let s = unsafe { utils::convert_char_ptr_to_string(ptr) }.ok()?;
80
+ if s.is_empty() {
81
+ return None;
82
+ }
83
+ Some(DeclarationId::from(s.as_str()))
84
+ }
85
+
67
86
  /// An iterator over declaration IDs
68
87
  ///
69
88
  /// We snapshot the IDs at iterator creation so if the graph is modified, the iterator will not see the changes