rubydex 0.4.0-aarch64-linux → 0.4.2-aarch64-linux

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 (68) 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/3.2/rubydex.so +0 -0
  10. data/lib/rubydex/3.3/rubydex.so +0 -0
  11. data/lib/rubydex/3.4/rubydex.so +0 -0
  12. data/lib/rubydex/4.0/rubydex.so +0 -0
  13. data/lib/rubydex/cli/command/dead_code.rb +59 -0
  14. data/lib/rubydex/cli/command/lint/explain.rb +8 -6
  15. data/lib/rubydex/cli/command/lint.rb +10 -4
  16. data/lib/rubydex/librubydex_sys.so +0 -0
  17. data/lib/rubydex/linter/rule_loader.rb +1 -1
  18. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +1 -1
  19. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +4 -4
  20. data/lib/rubydex/mcp_server/tools/find_dead_code_candidates_tool.rb +33 -0
  21. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +2 -2
  22. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +4 -4
  23. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +2 -2
  24. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +6 -6
  25. data/lib/rubydex/mcp_server.rb +24 -12
  26. data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
  27. data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
  28. data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
  29. data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
  30. data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
  31. data/lib/rubydex/rules/parse_error.rb +25 -0
  32. data/lib/rubydex/rules/parse_warning.rb +28 -0
  33. data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
  34. data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
  35. data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
  36. data/lib/rubydex/skill.rb +1 -0
  37. data/lib/rubydex/version.rb +1 -1
  38. data/lib/rubydex.rb +2 -0
  39. data/lib/rubydex_linter/rules/rule_structure.rb +19 -4
  40. data/rbi/rubydex.rbi +32 -130
  41. data/rust/Cargo.lock +17 -11
  42. data/rust/Cargo.toml +3 -0
  43. data/rust/rubydex/Cargo.toml +6 -2
  44. data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
  45. data/rust/rubydex/src/compile_assertions.rs +2 -0
  46. data/rust/rubydex/src/config.rs +1 -1
  47. data/rust/rubydex/src/diagnostic.rs +142 -51
  48. data/rust/rubydex/src/indexing/rbs_indexer.rs +4 -4
  49. data/rust/rubydex/src/indexing/ruby_indexer.rs +26 -18
  50. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +42 -2
  51. data/rust/rubydex/src/indexing.rs +6 -6
  52. data/rust/rubydex/src/listing.rs +12 -12
  53. data/rust/rubydex/src/model/built_in.rs +1 -1
  54. data/rust/rubydex/src/model/comment.rs +4 -4
  55. data/rust/rubydex/src/model/declaration.rs +25 -17
  56. data/rust/rubydex/src/model/document.rs +11 -11
  57. data/rust/rubydex/src/model/graph.rs +3 -3
  58. data/rust/rubydex/src/offset.rs +1 -13
  59. data/rust/rubydex/src/operation/ruby_builder.rs +29 -19
  60. data/rust/rubydex/src/query.rs +193 -2
  61. data/rust/rubydex/src/test_utils/graph_test.rs +2 -2
  62. data/rust/rubydex/src/test_utils/local_graph_test.rs +13 -6
  63. data/rust/rubydex-sys/Cargo.toml +2 -2
  64. data/rust/rubydex-sys/src/definition_api.rs +69 -2
  65. data/rust/rubydex-sys/src/diagnostic_api.rs +0 -35
  66. data/rust/rubydex-sys/src/graph_api.rs +193 -45
  67. data/rust/rubydex-sys/src/name_api.rs +219 -48
  68. metadata +199 -187
@@ -87,8 +87,8 @@ pub struct RubyOperationBuilder<'a> {
87
87
 
88
88
  impl<'a> RubyOperationBuilder<'a> {
89
89
  #[must_use]
90
- pub fn new(uri: String, source: &'a str) -> Self {
91
- let uri_id = UriId::from(&uri);
90
+ pub fn new(uri: Box<str>, source: &'a str) -> Self {
91
+ let uri_id = UriId::from(&*uri);
92
92
 
93
93
  Self {
94
94
  uri_id,
@@ -112,7 +112,7 @@ impl<'a> RubyOperationBuilder<'a> {
112
112
  self.add_diagnostic(
113
113
  Rule::ParseError,
114
114
  Offset::from_prism_location(&error.location()),
115
- error.message().to_string(),
115
+ String::from_utf8_lossy(error.message()).to_string(),
116
116
  );
117
117
  }
118
118
 
@@ -120,7 +120,7 @@ impl<'a> RubyOperationBuilder<'a> {
120
120
  self.add_diagnostic(
121
121
  Rule::ParseWarning,
122
122
  Offset::from_prism_location(&warning.location()),
123
- warning.message().to_string(),
123
+ String::from_utf8_lossy(warning.message()).to_string(),
124
124
  );
125
125
  }
126
126
 
@@ -489,6 +489,7 @@ impl<'a> RubyOperationBuilder<'a> {
489
489
 
490
490
  // -- Parameters --
491
491
 
492
+ #[allow(clippy::too_many_lines)]
492
493
  fn collect_parameters(&mut self, node: &ruby_prism::DefNode) -> Vec<Parameter> {
493
494
  let mut parameters: Vec<Parameter> = Vec::new();
494
495
 
@@ -588,7 +589,9 @@ impl<'a> RubyOperationBuilder<'a> {
588
589
  }
589
590
  }
590
591
 
591
- if let Some(block) = parameters_list.block() {
592
+ if let Some(block) = parameters_list.block()
593
+ && let Some(block) = block.as_block_parameter_node()
594
+ {
592
595
  let location = block.name_loc().unwrap_or_else(|| block.location());
593
596
  let str_id = self.intern_string(Self::location_to_string(&location));
594
597
  parameters.push(Parameter::Block(ParameterStruct::new(
@@ -1036,8 +1039,8 @@ impl<'a> RubyOperationBuilder<'a> {
1036
1039
  match arg {
1037
1040
  ruby_prism::Node::SymbolNode { .. } => {
1038
1041
  let symbol = arg.as_symbol_node().unwrap();
1039
- let value_loc = symbol.value_loc()?;
1040
- Some((Self::location_to_string(&value_loc), value_loc))
1042
+ let content_loc = symbol.content_loc();
1043
+ Some((Self::location_to_string(&content_loc), content_loc))
1041
1044
  }
1042
1045
  ruby_prism::Node::StringNode { .. } => {
1043
1046
  let string = arg.as_string_node().unwrap();
@@ -1380,7 +1383,7 @@ impl CommentGroup {
1380
1383
 
1381
1384
  fn accepts(&self, next: &ruby_prism::Comment, source: &str) -> bool {
1382
1385
  let current_end_offset = self.end_offset;
1383
- let next_line_start_offset = next.location().start_offset();
1386
+ let next_line_start_offset = next.location().start() as usize;
1384
1387
  let between = &source.as_bytes()[current_end_offset..next_line_start_offset];
1385
1388
  if !between.iter().all(|&b| b.is_ascii_whitespace()) {
1386
1389
  return false;
@@ -1389,14 +1392,14 @@ impl CommentGroup {
1389
1392
  }
1390
1393
 
1391
1394
  fn add_comment(&mut self, comment: &ruby_prism::Comment) {
1392
- self.end_offset = comment.location().end_offset();
1395
+ self.end_offset = comment.location().end() as usize;
1393
1396
  let text = String::from_utf8_lossy(comment.location().as_slice()).to_string();
1394
1397
  if text.lines().any(|line| line.starts_with("# @deprecated")) {
1395
1398
  self.deprecated = true;
1396
1399
  }
1397
1400
  self.comments.push(Comment::new(
1398
1401
  Offset::from_prism_location(&comment.location()),
1399
- text.trim().to_string(),
1402
+ text.trim().into(),
1400
1403
  ));
1401
1404
  }
1402
1405
 
@@ -1783,10 +1786,17 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1783
1786
  });
1784
1787
  };
1785
1788
 
1786
- let message_loc = node.message_loc();
1787
- if message_loc.is_none() {
1789
+ let message_loc = if node.name().as_slice().is_empty() {
1790
+ // Incomplete calls like `Foo.` have no message. We still index them using the call operator location, so
1791
+ // that the receiver's singleton class gets created, which completion depends on
1792
+ node.call_operator_loc()
1793
+ } else {
1794
+ node.message_loc()
1795
+ };
1796
+
1797
+ let Some(message_loc) = message_loc else {
1788
1798
  return;
1789
- }
1799
+ };
1790
1800
 
1791
1801
  let message = String::from_utf8_lossy(node.name().as_slice()).to_string();
1792
1802
 
@@ -2005,11 +2015,11 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
2005
2015
  self.visit(&receiver);
2006
2016
  }
2007
2017
 
2008
- self.index_method_reference(message.clone(), &node.message_loc().unwrap(), method_receiver);
2018
+ self.index_method_reference(message.clone(), &message_loc, method_receiver);
2009
2019
 
2010
2020
  match message.as_str() {
2011
2021
  ">" | "<" | ">=" | "<=" => {
2012
- self.index_method_reference("<=>".to_string(), &node.message_loc().unwrap(), method_receiver);
2022
+ self.index_method_reference("<=>".to_string(), &message_loc, method_receiver);
2013
2023
  }
2014
2024
  _ => {}
2015
2025
  }
@@ -2134,7 +2144,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
2134
2144
  match expression {
2135
2145
  ruby_prism::Node::SymbolNode { .. } => {
2136
2146
  let symbol = expression.as_symbol_node().unwrap();
2137
- let name = Self::location_to_string(&symbol.value_loc().unwrap());
2147
+ let name = Self::location_to_string(&symbol.content_loc());
2138
2148
  self.index_method_reference(name, &node.location(), None);
2139
2149
  }
2140
2150
  _ => {
@@ -2146,13 +2156,13 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
2146
2156
 
2147
2157
  fn visit_alias_method_node(&mut self, node: &ruby_prism::AliasMethodNode<'_>) {
2148
2158
  let mut new_name = if let Some(symbol_node) = node.new_name().as_symbol_node() {
2149
- Self::location_to_string(&symbol_node.value_loc().unwrap())
2159
+ Self::location_to_string(&symbol_node.content_loc())
2150
2160
  } else {
2151
2161
  Self::location_to_string(&node.new_name().location())
2152
2162
  };
2153
2163
 
2154
2164
  let mut old_name = if let Some(symbol_node) = node.old_name().as_symbol_node() {
2155
- Self::location_to_string(&symbol_node.value_loc().unwrap())
2165
+ Self::location_to_string(&symbol_node.content_loc())
2156
2166
  } else {
2157
2167
  Self::location_to_string(&node.old_name().location())
2158
2168
  };
@@ -2229,7 +2239,7 @@ mod tests {
2229
2239
 
2230
2240
  fn build_operations(source: &str) -> OperationBuilderResult {
2231
2241
  let source = crate::test_utils::normalize_indentation(source);
2232
- let builder = RubyOperationBuilder::new("file:///test.rb".to_string(), &source);
2242
+ let builder = RubyOperationBuilder::new("file:///test.rb".into(), &source);
2233
2243
  builder.build()
2234
2244
  }
2235
2245
 
@@ -5,12 +5,12 @@ use std::thread;
5
5
 
6
6
  use url::Url;
7
7
 
8
- use crate::model::built_in::OBJECT_ID;
8
+ use crate::model::built_in::{BUILT_IN_URI_ID, OBJECT_ID};
9
9
  use crate::model::declaration::{Ancestor, Declaration, Namespace};
10
10
  use crate::model::definitions::{Definition, Parameter};
11
11
  use crate::model::graph::Graph;
12
12
  use crate::model::identity_maps::IdentityHashSet;
13
- use crate::model::ids::{DeclarationId, DefinitionId, NameId, StringId, UriId};
13
+ use crate::model::ids::{ConstantReferenceId, DeclarationId, DefinitionId, NameId, StringId, UriId};
14
14
  use crate::model::keywords::{self, Keyword};
15
15
  use crate::model::name::NameRef;
16
16
  use crate::model::visibility::Visibility;
@@ -833,6 +833,64 @@ pub fn follow_method_alias(graph: &Graph, alias_id: DefinitionId) -> Result<Decl
833
833
  }
834
834
  }
835
835
 
836
+ /// Returns `true` when any definition of `declaration` was seeded by rubydex rather than read from indexed source.
837
+ ///
838
+ /// Dead code candidates are reported at declaration granularity, so a seeded definition disqualifies the declaration
839
+ /// even when user code also reopens it.
840
+ fn is_built_in(graph: &Graph, declaration: &Declaration) -> bool {
841
+ declaration.definitions().iter().any(|definition_id| {
842
+ graph
843
+ .definitions()
844
+ .get(definition_id)
845
+ .is_some_and(|definition| definition.uri_id() == &*BUILT_IN_URI_ID)
846
+ })
847
+ }
848
+
849
+ /// Returns a list of declarations that may be unused in the codebase, which means
850
+ /// that the analysis could not find any references to them. This misses
851
+ /// meta-programming or untyped code, which is why the term candidates is used.
852
+ ///
853
+ /// Currently, only supports constants.
854
+ ///
855
+ /// # Panics
856
+ ///
857
+ /// Will panic if any of the threads panic
858
+ pub fn dead_code_candidates(graph: &Graph) -> Vec<DeclarationId> {
859
+ let num_threads = thread::available_parallelism().map_or(4, std::num::NonZero::get);
860
+ let declarations = graph.declarations();
861
+
862
+ let ids: Vec<DeclarationId> = declarations.keys().copied().collect();
863
+ let chunk_size = ids.len().div_ceil(num_threads);
864
+
865
+ if chunk_size == 0 {
866
+ return Vec::new();
867
+ }
868
+
869
+ thread::scope(|s| {
870
+ let handles: Vec<_> = ids
871
+ .chunks(chunk_size)
872
+ .map(|chunk| {
873
+ s.spawn(|| {
874
+ chunk
875
+ .iter()
876
+ .filter(|id| {
877
+ let decl = declarations.get(id).unwrap();
878
+ decl.constant_references()
879
+ .is_some_and(<[ConstantReferenceId]>::is_empty)
880
+ && !matches!(decl, Declaration::Namespace(Namespace::SingletonClass(_)))
881
+ && !decl.has_no_definitions()
882
+ && !is_built_in(graph, decl)
883
+ })
884
+ .copied()
885
+ .collect::<Vec<_>>()
886
+ })
887
+ })
888
+ .collect();
889
+
890
+ handles.into_iter().flat_map(|h| h.join().unwrap()).collect()
891
+ })
892
+ }
893
+
836
894
  #[cfg(test)]
837
895
  mod tests {
838
896
  use std::str::FromStr;
@@ -920,6 +978,21 @@ mod tests {
920
978
  };
921
979
  }
922
980
 
981
+ macro_rules! assert_dead_code_candidates {
982
+ ($context:expr, [$($expected:expr),* $(,)?]) => {
983
+ let mut actual: Vec<String> = dead_code_candidates($context.graph())
984
+ .iter()
985
+ .map(|id| $context.graph().declarations().get(id).unwrap().name().to_string())
986
+ .collect();
987
+ actual.sort();
988
+
989
+ let mut expected: Vec<String> = vec![$(String::from($expected)),*];
990
+ expected.sort();
991
+
992
+ assert_eq!(expected, actual);
993
+ };
994
+ }
995
+
923
996
  #[test]
924
997
  fn fuzzy_search_returns_partial_matches() {
925
998
  let mut context = GraphTest::new();
@@ -3982,4 +4055,122 @@ mod tests {
3982
4055
  Err(FindMemberError::DeclarationNotFound),
3983
4056
  );
3984
4057
  }
4058
+
4059
+ #[test]
4060
+ fn dead_code_candidates_returns_unreferenced_constants() {
4061
+ let mut context = GraphTest::new();
4062
+ context.index_uri(
4063
+ "file:///foo.rb",
4064
+ r"
4065
+ class UnusedClass; end
4066
+ module UnusedModule; end
4067
+ UNUSED_CONSTANT = 1
4068
+ ",
4069
+ );
4070
+ context.resolve();
4071
+
4072
+ assert_dead_code_candidates!(context, ["UnusedClass", "UnusedModule", "UNUSED_CONSTANT"]);
4073
+ }
4074
+
4075
+ #[test]
4076
+ fn dead_code_candidates_excludes_referenced_constants() {
4077
+ let mut context = GraphTest::new();
4078
+ context.index_uri(
4079
+ "file:///foo.rb",
4080
+ r"
4081
+ class UsedClass; end
4082
+ USED_CONSTANT = 1
4083
+ ",
4084
+ );
4085
+ context.index_uri(
4086
+ "file:///bar.rb",
4087
+ r"
4088
+ UsedClass
4089
+ USED_CONSTANT
4090
+ ",
4091
+ );
4092
+ context.resolve();
4093
+
4094
+ assert_dead_code_candidates!(context, []);
4095
+ }
4096
+
4097
+ #[test]
4098
+ fn dead_code_candidates_excludes_methods_and_variables() {
4099
+ let mut context = GraphTest::new();
4100
+ context.index_uri(
4101
+ "file:///foo.rb",
4102
+ r"
4103
+ $global_var = 1
4104
+
4105
+ class Holder
4106
+ @@class_var = 1
4107
+
4108
+ def initialize
4109
+ @instance_var = 1
4110
+ end
4111
+
4112
+ def never_called; end
4113
+ end
4114
+ ",
4115
+ );
4116
+ context.resolve();
4117
+
4118
+ assert_dead_code_candidates!(context, ["Holder"]);
4119
+ }
4120
+
4121
+ #[test]
4122
+ fn dead_code_candidates_counts_references_rather_than_reachability() {
4123
+ let mut context = GraphTest::new();
4124
+ context.index_uri(
4125
+ "file:///foo.rb",
4126
+ r"
4127
+ class Target; end
4128
+ AliasName = Target
4129
+ ",
4130
+ );
4131
+ context.resolve();
4132
+
4133
+ // `AliasName` is itself dead, but it still references `Target`, giving `Target` a non-zero count.
4134
+ // A single pass only peels the outermost layer of a dead subgraph.
4135
+ assert_dead_code_candidates!(context, ["AliasName"]);
4136
+ }
4137
+
4138
+ #[test]
4139
+ fn dead_code_candidates_excludes_built_ins_and_definitionless_declarations() {
4140
+ let mut context = GraphTest::new();
4141
+ context.index_uri(
4142
+ "file:///foo.rb",
4143
+ r"
4144
+ class Unused
4145
+ def self.class_method; end
4146
+ end
4147
+
4148
+ class Class
4149
+ end
4150
+ ",
4151
+ );
4152
+ context.resolve();
4153
+
4154
+ // A reopened built-in remains excluded regardless of definition order.
4155
+ assert_dead_code_candidates!(context, ["Unused"]);
4156
+ }
4157
+
4158
+ #[test]
4159
+ fn dead_code_candidates_excludes_singleton_classes() {
4160
+ let mut context = GraphTest::new();
4161
+ context.index_uri(
4162
+ "file:///foo.rb",
4163
+ r"
4164
+ class Unused; end
4165
+ class Attached; end
4166
+
4167
+ class << Attached
4168
+ def never_called; end
4169
+ end
4170
+ ",
4171
+ );
4172
+ context.resolve();
4173
+
4174
+ assert_dead_code_candidates!(context, ["Unused"]);
4175
+ }
3985
4176
  }
@@ -44,14 +44,14 @@ impl GraphTest {
44
44
  /// Indexes a Ruby source
45
45
  pub fn index_uri(&mut self, uri: &str, source: &str) {
46
46
  let source = normalize_indentation(source);
47
- let local_graph = indexing::build_local_graph(uri.to_string(), &source, &LanguageId::Ruby, self.backend);
47
+ let local_graph = indexing::build_local_graph(uri.into(), &source, &LanguageId::Ruby, self.backend);
48
48
  self.graph.consume_document_changes(local_graph);
49
49
  }
50
50
 
51
51
  /// Indexes an RBS source
52
52
  pub fn index_rbs_uri(&mut self, uri: &str, source: &str) {
53
53
  let source = normalize_indentation(source);
54
- indexing::index_source(&mut self.graph, uri, &source, &LanguageId::Rbs);
54
+ indexing::index_source(&mut self.graph, uri.into(), &source, &LanguageId::Rbs);
55
55
  }
56
56
 
57
57
  pub fn delete_uri(&mut self, uri: &str) {
@@ -24,22 +24,29 @@ impl LocalGraphTest {
24
24
 
25
25
  #[must_use]
26
26
  pub fn new_with_backend(uri: &str, source: &str, backend: IndexerBackend) -> Self {
27
- let uri = uri.to_string();
28
27
  let source = normalize_indentation(source);
29
- let graph = build_local_graph(uri.clone(), &source, &LanguageId::Ruby, backend);
30
- Self { uri, source, graph }
28
+ let graph = build_local_graph(uri.into(), &source, &LanguageId::Ruby, backend);
29
+
30
+ Self {
31
+ uri: uri.to_string(),
32
+ source,
33
+ graph,
34
+ }
31
35
  }
32
36
 
33
37
  #[must_use]
34
38
  pub fn new_rbs(uri: &str, source: &str) -> Self {
35
- let uri = uri.to_string();
36
39
  let source = normalize_indentation(source);
37
40
 
38
- let mut indexer = RBSIndexer::new(uri.clone(), &source);
41
+ let mut indexer = RBSIndexer::new(uri.into(), &source);
39
42
  indexer.index();
40
43
  let graph = indexer.local_graph();
41
44
 
42
- Self { uri, source, graph }
45
+ Self {
46
+ uri: uri.to_string(),
47
+ source,
48
+ graph,
49
+ }
43
50
  }
44
51
 
45
52
  #[must_use]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rubydex-sys"
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,7 +15,7 @@ categories = ["development-tools", "external-ffi-bindings"]
15
15
  crate-type = ["cdylib", "staticlib"]
16
16
 
17
17
  [dependencies]
18
- rubydex = { version = "0.2.6", path = "../rubydex" }
18
+ rubydex = { version = "=0.4.2", path = "../rubydex" }
19
19
  libc = "0.2.174"
20
20
  url = "2.5.4"
21
21
  line-index = "0.1.2"
@@ -6,7 +6,9 @@ use crate::location_api::{Location, create_location_for_uri_and_offset};
6
6
  use crate::reference_api::CConstantReference;
7
7
  use libc::c_char;
8
8
  use rubydex::model::definitions::{Definition, Mixin};
9
- use rubydex::model::ids::DefinitionId;
9
+ use rubydex::model::graph::Graph;
10
+ use rubydex::model::ids::{DefinitionId, NameId};
11
+ use rubydex::model::name::ParentScope;
10
12
  use rubydex::query::AliasResolutionError;
11
13
  use std::ffi::CString;
12
14
  use std::ptr;
@@ -110,6 +112,71 @@ pub unsafe extern "C" fn rdx_definition_name(pointer: GraphPointer, definition_i
110
112
  })
111
113
  }
112
114
 
115
+ fn raw_name_for_name(graph: &Graph, name_id: NameId) -> String {
116
+ let mut raw_name = String::new();
117
+ append_raw_name(graph, name_id, &mut raw_name);
118
+ raw_name
119
+ }
120
+
121
+ fn append_raw_name(graph: &Graph, name_id: NameId, raw_name: &mut String) {
122
+ let name = graph
123
+ .names()
124
+ .get(&name_id)
125
+ .expect("name should exist while building raw_name");
126
+ let simple_name = graph
127
+ .strings()
128
+ .get(name.str())
129
+ .expect("string should exist while building raw_name")
130
+ .as_str();
131
+
132
+ match name.parent_scope() {
133
+ ParentScope::None => {}
134
+ ParentScope::TopLevel => raw_name.push_str("::"),
135
+ ParentScope::Some(parent_id) | ParentScope::Attached(parent_id) => {
136
+ append_raw_name(graph, *parent_id, raw_name);
137
+ raw_name.push_str("::");
138
+ }
139
+ }
140
+
141
+ raw_name.push_str(simple_name);
142
+ }
143
+
144
+ fn raw_name_id(definition: &Definition) -> Option<&NameId> {
145
+ match definition {
146
+ Definition::Class(_) | Definition::Module(_) | Definition::Constant(_) | Definition::ConstantAlias(_) => {
147
+ definition.name_id()
148
+ }
149
+ _ => None,
150
+ }
151
+ }
152
+
153
+ /// Returns the UTF-8 raw name string for a class, module, constant, or constant-alias definition id.
154
+ ///
155
+ /// The raw name is reconstructed from the definition's `Name`, preserving explicit parent scopes like `Foo::Bar` and
156
+ /// rooted paths like `::Bar`. Returns null if the definition cannot be found or does not support raw names. Caller must
157
+ /// free a non-null result with `free_c_string`.
158
+ ///
159
+ /// # Panics
160
+ ///
161
+ /// Panics if the graph contains an invalid name or string reference, or if the name contains an embedded null byte.
162
+ ///
163
+ /// # Safety
164
+ ///
165
+ /// Assumes pointer is valid.
166
+ ///
167
+ #[unsafe(no_mangle)]
168
+ pub unsafe extern "C" fn rdx_definition_raw_name(pointer: GraphPointer, definition_id: u64) -> *const c_char {
169
+ with_graph(pointer, |graph| {
170
+ let def_id = DefinitionId::new(definition_id);
171
+ let Some(name_id) = graph.definitions().get(&def_id).and_then(raw_name_id) else {
172
+ return ptr::null();
173
+ };
174
+
175
+ let name = raw_name_for_name(graph, *name_id);
176
+ CString::new(name).unwrap().into_raw().cast_const()
177
+ })
178
+ }
179
+
113
180
  /// Shared iterator over definition (id, kind) pairs
114
181
  #[derive(Debug)]
115
182
  pub struct DefinitionsIter {
@@ -181,7 +248,7 @@ pub unsafe extern "C" fn rdx_definition_comments(pointer: GraphPointer, definiti
181
248
  .comments()
182
249
  .iter()
183
250
  .map(|c| CommentEntry {
184
- string: CString::new(c.string().as_str()).unwrap().into_raw().cast_const(),
251
+ string: CString::new(c.string()).unwrap().into_raw().cast_const(),
185
252
  location: create_location_for_uri_and_offset(graph, document, c.offset()),
186
253
  })
187
254
  .collect::<Vec<CommentEntry>>()
@@ -32,7 +32,6 @@ impl From<Severity> for DiagnosticSeverity {
32
32
  pub struct CRule {
33
33
  pub name: *const c_char,
34
34
  pub name_length: usize,
35
- pub default_severity: DiagnosticSeverity,
36
35
  }
37
36
 
38
37
  impl From<Rule> for CRule {
@@ -42,44 +41,10 @@ impl From<Rule> for CRule {
42
41
  Self {
43
42
  name: name.as_ptr().cast::<c_char>(),
44
43
  name_length: name.len(),
45
- default_severity: DiagnosticSeverity::from(rule.default_severity()),
46
44
  }
47
45
  }
48
46
  }
49
47
 
50
- #[repr(C)]
51
- pub struct CRuleArray {
52
- pub items: *mut CRule,
53
- pub len: usize,
54
- }
55
-
56
- /// Returns every rule the graph can report. Caller must free it with `rdx_rules_free`.
57
- #[unsafe(no_mangle)]
58
- pub extern "C" fn rdx_rules() -> CRuleArray {
59
- let items = Rule::all().iter().copied().map(CRule::from).collect::<Box<[CRule]>>();
60
-
61
- CRuleArray {
62
- len: items.len(),
63
- items: Box::into_raw(items).cast::<CRule>(),
64
- }
65
- }
66
-
67
- /// Frees an array previously returned by `rdx_rules`.
68
- ///
69
- /// # Safety
70
- ///
71
- /// - `rules` must have been returned by `rdx_rules` and must not be used afterwards.
72
- #[unsafe(no_mangle)]
73
- pub unsafe extern "C" fn rdx_rules_free(rules: CRuleArray) {
74
- if rules.items.is_null() {
75
- return;
76
- }
77
-
78
- unsafe {
79
- let _ = Box::from_raw(ptr::slice_from_raw_parts_mut(rules.items, rules.len));
80
- }
81
- }
82
-
83
48
  /// C-compatible struct representing a diagnostic entry.
84
49
  #[repr(C)]
85
50
  pub struct DiagnosticEntry {