rubydex 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -6
  3. data/THIRD_PARTY_LICENSES.html +271 -2
  4. data/exe/rdx +2 -73
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/declaration.c +1 -1
  8. data/ext/rubydex/definition.c +32 -4
  9. data/ext/rubydex/diagnostic.c +75 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/graph.c +27 -48
  12. data/ext/rubydex/graph.h +6 -0
  13. data/ext/rubydex/query.c +487 -0
  14. data/ext/rubydex/query.h +8 -0
  15. data/ext/rubydex/reference.c +60 -0
  16. data/ext/rubydex/rubydex.c +4 -0
  17. data/ext/rubydex/utils.c +23 -4
  18. data/ext/rubydex/utils.h +5 -0
  19. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  20. data/lib/rubydex/cli/command/console.rb +55 -0
  21. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  22. data/lib/rubydex/cli/command/lint.rb +202 -0
  23. data/lib/rubydex/cli/command/mcp.rb +30 -0
  24. data/lib/rubydex/cli/command/query.rb +70 -0
  25. data/lib/rubydex/cli/command/skill.rb +69 -0
  26. data/lib/rubydex/cli/command.rb +168 -0
  27. data/lib/rubydex/cli.rb +93 -0
  28. data/lib/rubydex/config.rb +59 -0
  29. data/lib/rubydex/diagnostic.rb +12 -3
  30. data/lib/rubydex/errors.rb +42 -1
  31. data/lib/rubydex/graph.rb +10 -3
  32. data/lib/rubydex/linter/custom_rule.rb +97 -0
  33. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  34. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  35. data/lib/rubydex/linter/rule_loader.rb +36 -0
  36. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  37. data/lib/rubydex/linter/runner.rb +56 -0
  38. data/lib/rubydex/linter.rb +19 -0
  39. data/lib/rubydex/location.rb +3 -0
  40. data/lib/rubydex/mcp_server.rb +1 -2
  41. data/lib/rubydex/related_information.rb +17 -0
  42. data/lib/rubydex/rule.rb +33 -0
  43. data/lib/rubydex/severity.rb +70 -0
  44. data/lib/rubydex/skill.rb +88 -0
  45. data/lib/rubydex/skill_registry.rb +62 -0
  46. data/lib/rubydex/version.rb +1 -1
  47. data/lib/rubydex.rb +6 -0
  48. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  49. data/rbi/rubydex.rbi +578 -15
  50. data/rust/Cargo.lock +7 -0
  51. data/rust/rubydex/Cargo.toml +1 -0
  52. data/rust/rubydex/benches/graph_memory.rs +20 -4
  53. data/rust/rubydex/src/compile_assertions.rs +15 -0
  54. data/rust/rubydex/src/config.rs +538 -157
  55. data/rust/rubydex/src/diagnostic.rs +66 -40
  56. data/rust/rubydex/src/errors.rs +0 -1
  57. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  58. data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
  59. data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
  60. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
  61. data/rust/rubydex/src/lib.rs +1 -0
  62. data/rust/rubydex/src/listing.rs +26 -1
  63. data/rust/rubydex/src/main.rs +9 -128
  64. data/rust/rubydex/src/model/declaration.rs +301 -229
  65. data/rust/rubydex/src/model/definitions.rs +27 -26
  66. data/rust/rubydex/src/model/document.rs +43 -7
  67. data/rust/rubydex/src/model/graph.rs +67 -68
  68. data/rust/rubydex/src/model/id.rs +55 -0
  69. data/rust/rubydex/src/model/ids.rs +21 -9
  70. data/rust/rubydex/src/model/name.rs +88 -19
  71. data/rust/rubydex/src/model/references.rs +16 -13
  72. data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
  73. data/rust/rubydex/src/path_helpers.rs +77 -0
  74. data/rust/rubydex/src/query/cypher/schema.rs +853 -0
  75. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  76. data/rust/rubydex/src/query/cypher/tests.rs +253 -0
  77. data/rust/rubydex/src/query/cypher.rs +54 -0
  78. data/rust/rubydex/src/query.rs +125 -43
  79. data/rust/rubydex/src/resolution.rs +368 -395
  80. data/rust/rubydex/src/resolution_tests.rs +504 -78
  81. data/rust/rubydex/src/test_utils/context.rs +2 -1
  82. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  84. data/rust/rubydex/tests/cli.rs +4 -4
  85. data/rust/rubydex-sys/src/config_api.rs +205 -0
  86. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  87. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  88. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  89. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  90. data/rust/rubydex-sys/src/graph_api.rs +31 -68
  91. data/rust/rubydex-sys/src/lib.rs +2 -0
  92. data/rust/rubydex-sys/src/name_api.rs +2 -6
  93. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +37 -2
@@ -1,4 +1,5 @@
1
1
  use super::normalize_indentation;
2
+ use crate::path_helpers;
2
3
  use std::fs;
3
4
  use std::path::{Path, PathBuf};
4
5
  use tempfile::TempDir;
@@ -37,7 +38,7 @@ impl Context {
37
38
  #[must_use]
38
39
  pub fn new() -> Self {
39
40
  let root = tempfile::tempdir().expect("failed to create temp dir");
40
- let absolute_path = fs::canonicalize(root.path()).unwrap();
41
+ let absolute_path = path_helpers::resolved(root.path()).unwrap();
41
42
  Self {
42
43
  _root: root,
43
44
  absolute_path,
@@ -540,18 +540,13 @@ macro_rules! assert_descendants {
540
540
  .declarations()
541
541
  .get(&$crate::model::ids::DeclarationId::from($parent))
542
542
  .unwrap();
543
- let actual = match parent {
544
- $crate::model::declaration::Declaration::Namespace($crate::model::declaration::Namespace::Class(class)) => {
545
- class.descendants().iter().cloned().collect::<Vec<_>>()
546
- }
547
- $crate::model::declaration::Declaration::Namespace($crate::model::declaration::Namespace::Module(
548
- module,
549
- )) => module.descendants().iter().cloned().collect::<Vec<_>>(),
550
- $crate::model::declaration::Declaration::Namespace(
551
- $crate::model::declaration::Namespace::SingletonClass(singleton),
552
- ) => singleton.descendants().iter().cloned().collect::<Vec<_>>(),
553
- _ => panic!("Tried to get descendants for a declaration that isn't a namespace"),
554
- };
543
+ let actual = parent
544
+ .as_namespace()
545
+ .expect("Tried to get descendants for a declaration that isn't a namespace")
546
+ .descendants()
547
+ .iter()
548
+ .cloned()
549
+ .collect::<Vec<_>>();
555
550
 
556
551
  for descendant in &$descendants {
557
552
  let descendant_id = $crate::model::ids::DeclarationId::from(*descendant);
@@ -667,6 +662,25 @@ macro_rules! assert_instance_variables_eq {
667
662
  #[cfg(test)]
668
663
  #[macro_export]
669
664
  macro_rules! assert_diagnostics_eq {
665
+ ($context:expr, $expected_diagnostics:expr, severity: $expected_severity:expr) => {{
666
+ let context = &$context;
667
+ $crate::assert_diagnostics_eq!(context, $expected_diagnostics);
668
+
669
+ let expected_severity = $expected_severity;
670
+ let actual_severities = context
671
+ .graph()
672
+ .all_diagnostics()
673
+ .iter()
674
+ .map(|diagnostic| diagnostic.rule().default_severity())
675
+ .collect::<Vec<_>>();
676
+ assert_eq!(
677
+ vec![expected_severity; actual_severities.len()],
678
+ actual_severities,
679
+ "diagnostic severities mismatch: expected all `{:?}`, got `{:?}`",
680
+ expected_severity,
681
+ actual_severities
682
+ );
683
+ }};
670
684
  ($context:expr, $expected_diagnostics:expr) => {{
671
685
  assert_eq!($expected_diagnostics, $context.format_diagnostics(&[]).as_slice());
672
686
  }};
@@ -557,6 +557,25 @@ macro_rules! assert_method_has_receiver {
557
557
  #[cfg(test)]
558
558
  #[macro_export]
559
559
  macro_rules! assert_local_diagnostics_eq {
560
+ ($context:expr, $expected_diagnostics:expr, severity: $expected_severity:expr) => {{
561
+ let context = &$context;
562
+ $crate::assert_local_diagnostics_eq!(context, $expected_diagnostics);
563
+
564
+ let expected_severity = $expected_severity;
565
+ let actual_severities = context
566
+ .graph()
567
+ .diagnostics()
568
+ .iter()
569
+ .map(|diagnostic| diagnostic.rule().default_severity())
570
+ .collect::<Vec<_>>();
571
+ assert_eq!(
572
+ vec![expected_severity; actual_severities.len()],
573
+ actual_severities,
574
+ "diagnostic severities mismatch: expected all `{:?}`, got `{:?}`",
575
+ expected_severity,
576
+ actual_severities
577
+ );
578
+ }};
560
579
  ($context:expr, $expected_diagnostics:expr) => {{
561
580
  let mut diagnostics = $context.graph().diagnostics().iter().collect::<Vec<_>>();
562
581
  diagnostics.sort_by_key(|d| d.offset());
@@ -60,7 +60,7 @@ fn single_directory_argument_is_workspace_root_for_config() {
60
60
  with_context(|context| {
61
61
  context.write("included.rb", "class Included\nend\n");
62
62
  context.write("excluded/skipped.rb", "class Skipped\nend\n");
63
- context.write("rubydex.toml", "exclude = [\"excluded\"]\n");
63
+ context.write("rubydex.toml", "[graph]\nexclude = [\"excluded\"]\n");
64
64
 
65
65
  rdx(&[context.absolute_path().to_str().unwrap()])
66
66
  .success()
@@ -74,7 +74,7 @@ fn first_directory_argument_is_workspace_root_for_config() {
74
74
  with_context(|context| {
75
75
  context.write("included/kept.rb", "class Included\nend\n");
76
76
  context.write("excluded/skipped.rb", "class Skipped\nend\n");
77
- context.write("rubydex.toml", "exclude = [\"excluded\"]\n");
77
+ context.write("rubydex.toml", "[graph]\nexclude = [\"excluded\"]\n");
78
78
 
79
79
  rdx(&[
80
80
  context.absolute_path().to_str().unwrap(),
@@ -92,7 +92,7 @@ fn single_file_argument_is_not_used_as_workspace_root() {
92
92
  with_context(|context| {
93
93
  context.write("included.rb", "class Included\nend\n");
94
94
  context.write("excluded/skipped.rb", "class Skipped\nend\n");
95
- context.write("rubydex.toml", "exclude = [\"excluded\"]\n");
95
+ context.write("rubydex.toml", "[graph]\nexclude = [\"excluded\"]\n");
96
96
 
97
97
  rdx(&[context.absolute_path_to("excluded/skipped.rb").to_str().unwrap()])
98
98
  .success()
@@ -106,7 +106,7 @@ fn first_file_argument_is_not_used_as_workspace_root() {
106
106
  with_context(|context| {
107
107
  context.write("included.rb", "class Included\nend\n");
108
108
  context.write("excluded/skipped.rb", "class Skipped\nend\n");
109
- context.write("rubydex.toml", "exclude = [\"excluded\"]\n");
109
+ context.write("rubydex.toml", "[graph]\nexclude = [\"excluded\"]\n");
110
110
 
111
111
  rdx(&[
112
112
  context.absolute_path_to("included.rb").to_str().unwrap(),
@@ -0,0 +1,205 @@
1
+ use crate::diagnostic_api::DiagnosticSeverity;
2
+ use crate::utils;
3
+ use libc::{c_char, c_void};
4
+ use rubydex::config::{Config, Rule};
5
+ use rubydex::errors::Errors;
6
+ use std::ffi::CString;
7
+ use std::path::Path;
8
+ use std::ptr;
9
+
10
+ /// An opaque pointer to a loaded configuration
11
+ pub type ConfigPointer = *mut c_void;
12
+
13
+ /// The result of loading a configuration file, carrying either a parsed configuration or an error message.
14
+ #[repr(C)]
15
+ pub struct CConfigResult {
16
+ /// Non-null on success: a heap-allocated parsed configuration. Free with `rdx_config_free`.
17
+ pub config: ConfigPointer,
18
+ /// Non-null on error; null on success. Caller must free with `free_c_string`.
19
+ pub error: *const c_char,
20
+ }
21
+
22
+ impl CConfigResult {
23
+ fn success(config: Config) -> Self {
24
+ Self {
25
+ config: Box::into_raw(Box::new(config)).cast::<c_void>(),
26
+ error: ptr::null(),
27
+ }
28
+ }
29
+
30
+ fn error(message: &str) -> Self {
31
+ // Parse errors quote raw file content, which may contain NUL bytes that a C string cannot carry.
32
+ let message = message.replace('\0', "\u{FFFD}");
33
+
34
+ Self {
35
+ config: ptr::null_mut(),
36
+ error: utils::cstring_raw(&message),
37
+ }
38
+ }
39
+ }
40
+
41
+ /// Loads the configuration of the workspace rooted at `workspace_path`, which is where its `rubydex.toml` is expected
42
+ /// to be. A workspace without a configuration file produces an empty configuration.
43
+ ///
44
+ /// # Safety
45
+ ///
46
+ /// - `workspace_path` must be a valid, null-terminated string.
47
+ #[unsafe(no_mangle)]
48
+ pub unsafe extern "C" fn rdx_config_load(workspace_path: *const c_char) -> CConfigResult {
49
+ if workspace_path.is_null() {
50
+ return CConfigResult::error("workspace path is required");
51
+ }
52
+
53
+ let Ok(workspace_path) = (unsafe { utils::convert_char_ptr_to_string(workspace_path) }) else {
54
+ return CConfigResult::error("workspace path is not valid UTF-8");
55
+ };
56
+
57
+ match Config::load(Path::new(&workspace_path)) {
58
+ Ok(config) => CConfigResult::success(config),
59
+ Err(Errors::ConfigError(message) | Errors::FileError(message)) => CConfigResult::error(&message),
60
+ }
61
+ }
62
+
63
+ /// Returns the root directory of the workspace the configuration was loaded for, as a C string. Caller must free with
64
+ /// `free_c_string`.
65
+ ///
66
+ /// # Safety
67
+ ///
68
+ /// - `config` must be a valid `ConfigPointer` previously returned by `rdx_config_load`.
69
+ #[unsafe(no_mangle)]
70
+ pub unsafe extern "C" fn rdx_config_workspace_path(config: ConfigPointer) -> *const c_char {
71
+ let config = unsafe { &*config.cast::<Config>() };
72
+
73
+ CString::new(utils::interop_path(config.workspace_path()))
74
+ .map_or(ptr::null(), |c_string| c_string.into_raw().cast_const())
75
+ }
76
+
77
+ /// Frees a configuration through its pointer. Does nothing when given NULL.
78
+ ///
79
+ /// # Safety
80
+ ///
81
+ /// - `config` must either be NULL or a valid `ConfigPointer` previously returned by `rdx_config_load` and must not
82
+ /// be used afterwards. Any string pointer borrowed from it becomes invalid.
83
+ #[unsafe(no_mangle)]
84
+ pub unsafe extern "C" fn rdx_config_free(config: ConfigPointer) {
85
+ if config.is_null() {
86
+ return;
87
+ }
88
+
89
+ unsafe {
90
+ let _ = Box::from_raw(config.cast::<Config>());
91
+ }
92
+ }
93
+
94
+ /// Borrowed string bytes exposed while building Ruby configuration values.
95
+ #[repr(C)]
96
+ #[derive(Debug)]
97
+ pub struct CConfigString {
98
+ pub data: *const c_char,
99
+ pub length: usize,
100
+ }
101
+
102
+ /// C-compatible struct representing a single configured linter rule.
103
+ #[repr(C)]
104
+ #[derive(Debug)]
105
+ pub struct CLinterRule {
106
+ pub name: *const c_char,
107
+ pub name_length: usize,
108
+ pub enabled: bool,
109
+ pub exclude_patterns: *const CConfigString,
110
+ pub exclude_patterns_length: usize,
111
+ pub severity: *const DiagnosticSeverity,
112
+ }
113
+
114
+ impl From<&Rule> for CLinterRule {
115
+ fn from(rule: &Rule) -> Self {
116
+ let exclude_patterns = rule
117
+ .exclude_patterns()
118
+ .iter()
119
+ .map(|pattern| CConfigString {
120
+ data: pattern.as_ptr().cast::<c_char>(),
121
+ length: pattern.len(),
122
+ })
123
+ .collect::<Box<[CConfigString]>>();
124
+ let exclude_patterns_length = exclude_patterns.len();
125
+ let exclude_patterns = if exclude_patterns.is_empty() {
126
+ ptr::null()
127
+ } else {
128
+ Box::into_raw(exclude_patterns).cast::<CConfigString>().cast_const()
129
+ };
130
+ let severity = rule.severity().map_or(ptr::null(), |severity| {
131
+ Box::into_raw(Box::new(DiagnosticSeverity::from(*severity))).cast_const()
132
+ });
133
+
134
+ Self {
135
+ name: rule.name().as_ptr().cast::<c_char>(),
136
+ name_length: rule.name().len(),
137
+ enabled: rule.enabled(),
138
+ exclude_patterns,
139
+ exclude_patterns_length,
140
+ severity,
141
+ }
142
+ }
143
+ }
144
+
145
+ /// C-compatible array of configured linter rules.
146
+ #[repr(C)]
147
+ pub struct CLinterRuleArray {
148
+ pub items: *mut CLinterRule,
149
+ pub len: usize,
150
+ }
151
+
152
+ /// Returns the configured linter rules as an array. Caller must free it with `rdx_config_linter_rules_free`, while the
153
+ /// configuration is still alive, since the rule names are borrowed from it.
154
+ ///
155
+ /// # Safety
156
+ ///
157
+ /// - `config` must be a valid `ConfigPointer` previously returned by `rdx_config_load`.
158
+ #[unsafe(no_mangle)]
159
+ pub unsafe extern "C" fn rdx_config_linter_rules(config: ConfigPointer) -> CLinterRuleArray {
160
+ let config = unsafe { &*config.cast::<Config>() };
161
+ let rules = config.linter().rules();
162
+
163
+ if rules.is_empty() {
164
+ return CLinterRuleArray {
165
+ items: ptr::null_mut(),
166
+ len: 0,
167
+ };
168
+ }
169
+
170
+ let items = rules.iter().map(CLinterRule::from).collect::<Box<[CLinterRule]>>();
171
+
172
+ CLinterRuleArray {
173
+ len: items.len(),
174
+ items: Box::into_raw(items).cast::<CLinterRule>(),
175
+ }
176
+ }
177
+
178
+ /// Frees an array previously returned by `rdx_config_linter_rules`. The rule names it borrowed are left alone, since
179
+ /// they belong to the configuration.
180
+ ///
181
+ /// # Safety
182
+ ///
183
+ /// - `rules` must have been returned by `rdx_config_linter_rules` and must not be used afterwards.
184
+ #[unsafe(no_mangle)]
185
+ pub unsafe extern "C" fn rdx_config_linter_rules_free(rules: CLinterRuleArray) {
186
+ if rules.items.is_null() {
187
+ return;
188
+ }
189
+
190
+ unsafe {
191
+ let rules = Box::from_raw(ptr::slice_from_raw_parts_mut(rules.items, rules.len));
192
+
193
+ for rule in &*rules {
194
+ if !rule.exclude_patterns.is_null() {
195
+ let exclude_patterns =
196
+ ptr::slice_from_raw_parts_mut(rule.exclude_patterns.cast_mut(), rule.exclude_patterns_length);
197
+ let _ = Box::from_raw(exclude_patterns);
198
+ }
199
+
200
+ if !rule.severity.is_null() {
201
+ let _ = Box::from_raw(rule.severity.cast_mut());
202
+ }
203
+ }
204
+ }
205
+ }