rubydex 0.1.0.beta12-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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +125 -0
  4. data/THIRD_PARTY_LICENSES.html +4562 -0
  5. data/exe/rdx +47 -0
  6. data/ext/rubydex/declaration.c +453 -0
  7. data/ext/rubydex/declaration.h +23 -0
  8. data/ext/rubydex/definition.c +284 -0
  9. data/ext/rubydex/definition.h +28 -0
  10. data/ext/rubydex/diagnostic.c +6 -0
  11. data/ext/rubydex/diagnostic.h +11 -0
  12. data/ext/rubydex/document.c +97 -0
  13. data/ext/rubydex/document.h +10 -0
  14. data/ext/rubydex/extconf.rb +138 -0
  15. data/ext/rubydex/graph.c +681 -0
  16. data/ext/rubydex/graph.h +10 -0
  17. data/ext/rubydex/handle.h +44 -0
  18. data/ext/rubydex/location.c +22 -0
  19. data/ext/rubydex/location.h +15 -0
  20. data/ext/rubydex/reference.c +123 -0
  21. data/ext/rubydex/reference.h +15 -0
  22. data/ext/rubydex/rubydex.c +22 -0
  23. data/ext/rubydex/utils.c +108 -0
  24. data/ext/rubydex/utils.h +34 -0
  25. data/lib/rubydex/3.2/rubydex.so +0 -0
  26. data/lib/rubydex/3.3/rubydex.so +0 -0
  27. data/lib/rubydex/3.4/rubydex.so +0 -0
  28. data/lib/rubydex/4.0/rubydex.so +0 -0
  29. data/lib/rubydex/comment.rb +17 -0
  30. data/lib/rubydex/diagnostic.rb +21 -0
  31. data/lib/rubydex/failures.rb +15 -0
  32. data/lib/rubydex/graph.rb +98 -0
  33. data/lib/rubydex/keyword.rb +17 -0
  34. data/lib/rubydex/keyword_parameter.rb +13 -0
  35. data/lib/rubydex/librubydex_sys.so +0 -0
  36. data/lib/rubydex/location.rb +90 -0
  37. data/lib/rubydex/mixin.rb +22 -0
  38. data/lib/rubydex/version.rb +5 -0
  39. data/lib/rubydex.rb +23 -0
  40. data/rbi/rubydex.rbi +422 -0
  41. data/rust/Cargo.lock +1851 -0
  42. data/rust/Cargo.toml +29 -0
  43. data/rust/about.hbs +78 -0
  44. data/rust/about.toml +10 -0
  45. data/rust/rubydex/Cargo.toml +42 -0
  46. data/rust/rubydex/src/compile_assertions.rs +13 -0
  47. data/rust/rubydex/src/diagnostic.rs +110 -0
  48. data/rust/rubydex/src/errors.rs +28 -0
  49. data/rust/rubydex/src/indexing/local_graph.rs +224 -0
  50. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
  51. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
  52. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  53. data/rust/rubydex/src/indexing.rs +210 -0
  54. data/rust/rubydex/src/integrity.rs +279 -0
  55. data/rust/rubydex/src/job_queue.rs +205 -0
  56. data/rust/rubydex/src/lib.rs +17 -0
  57. data/rust/rubydex/src/listing.rs +371 -0
  58. data/rust/rubydex/src/main.rs +160 -0
  59. data/rust/rubydex/src/model/built_in.rs +83 -0
  60. data/rust/rubydex/src/model/comment.rs +24 -0
  61. data/rust/rubydex/src/model/declaration.rs +671 -0
  62. data/rust/rubydex/src/model/definitions.rs +1682 -0
  63. data/rust/rubydex/src/model/document.rs +222 -0
  64. data/rust/rubydex/src/model/encoding.rs +22 -0
  65. data/rust/rubydex/src/model/graph.rs +3754 -0
  66. data/rust/rubydex/src/model/id.rs +110 -0
  67. data/rust/rubydex/src/model/identity_maps.rs +58 -0
  68. data/rust/rubydex/src/model/ids.rs +60 -0
  69. data/rust/rubydex/src/model/keywords.rs +256 -0
  70. data/rust/rubydex/src/model/name.rs +298 -0
  71. data/rust/rubydex/src/model/references.rs +111 -0
  72. data/rust/rubydex/src/model/string_ref.rs +50 -0
  73. data/rust/rubydex/src/model/visibility.rs +41 -0
  74. data/rust/rubydex/src/model.rs +15 -0
  75. data/rust/rubydex/src/offset.rs +147 -0
  76. data/rust/rubydex/src/position.rs +6 -0
  77. data/rust/rubydex/src/query.rs +1841 -0
  78. data/rust/rubydex/src/resolution.rs +6517 -0
  79. data/rust/rubydex/src/stats/memory.rs +71 -0
  80. data/rust/rubydex/src/stats/orphan_report.rs +264 -0
  81. data/rust/rubydex/src/stats/timer.rs +127 -0
  82. data/rust/rubydex/src/stats.rs +11 -0
  83. data/rust/rubydex/src/test_utils/context.rs +226 -0
  84. data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
  86. data/rust/rubydex/src/test_utils.rs +52 -0
  87. data/rust/rubydex/src/visualization/dot.rs +192 -0
  88. data/rust/rubydex/src/visualization.rs +6 -0
  89. data/rust/rubydex/tests/cli.rs +185 -0
  90. data/rust/rubydex-mcp/Cargo.toml +28 -0
  91. data/rust/rubydex-mcp/src/main.rs +48 -0
  92. data/rust/rubydex-mcp/src/server.rs +1145 -0
  93. data/rust/rubydex-mcp/src/tools.rs +49 -0
  94. data/rust/rubydex-mcp/tests/mcp.rs +302 -0
  95. data/rust/rubydex-sys/Cargo.toml +20 -0
  96. data/rust/rubydex-sys/build.rs +14 -0
  97. data/rust/rubydex-sys/cbindgen.toml +12 -0
  98. data/rust/rubydex-sys/src/declaration_api.rs +485 -0
  99. data/rust/rubydex-sys/src/definition_api.rs +443 -0
  100. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  101. data/rust/rubydex-sys/src/document_api.rs +85 -0
  102. data/rust/rubydex-sys/src/graph_api.rs +948 -0
  103. data/rust/rubydex-sys/src/lib.rs +79 -0
  104. data/rust/rubydex-sys/src/location_api.rs +79 -0
  105. data/rust/rubydex-sys/src/name_api.rs +135 -0
  106. data/rust/rubydex-sys/src/reference_api.rs +267 -0
  107. data/rust/rubydex-sys/src/utils.rs +70 -0
  108. data/rust/rustfmt.toml +2 -0
  109. metadata +159 -0
@@ -0,0 +1,71 @@
1
+ use std::io;
2
+
3
+ #[cfg(unix)]
4
+ use libc::{RUSAGE_SELF, getrusage, rusage};
5
+
6
+ /// Memory statistics for the current process
7
+ #[derive(Debug, Clone, Copy)]
8
+ pub struct MemoryStats {
9
+ /// Maximum resident set size in bytes
10
+ pub max_rss_bytes: i64,
11
+ }
12
+
13
+ impl MemoryStats {
14
+ /// Get current memory statistics using getrusage
15
+ ///
16
+ /// # Errors
17
+ ///
18
+ /// Returns an error if the `getrusage` system call fails.
19
+ #[cfg(unix)]
20
+ pub fn current() -> Result<Self, io::Error> {
21
+ unsafe {
22
+ let mut usage = std::mem::MaybeUninit::<rusage>::uninit();
23
+ let result = getrusage(RUSAGE_SELF, usage.as_mut_ptr());
24
+
25
+ if result == 0 {
26
+ let usage = usage.assume_init();
27
+ // On macOS and BSD, ru_maxrss is in bytes
28
+ // On Linux, ru_maxrss is in kilobytes
29
+ let max_rss_bytes = if cfg!(target_os = "linux") {
30
+ usage.ru_maxrss * 1024
31
+ } else {
32
+ usage.ru_maxrss
33
+ };
34
+
35
+ Ok(Self { max_rss_bytes })
36
+ } else {
37
+ Err(io::Error::last_os_error())
38
+ }
39
+ }
40
+ }
41
+
42
+ /// Get current memory statistics (not supported on non-Unix platforms)
43
+ ///
44
+ /// # Errors
45
+ ///
46
+ /// Returns an error on non-Unix platforms where memory statistics are not supported.
47
+ #[cfg(not(unix))]
48
+ pub fn current() -> Result<Self, io::Error> {
49
+ Err(io::Error::new(
50
+ io::ErrorKind::Unsupported,
51
+ "Memory statistics not supported on this platform",
52
+ ))
53
+ }
54
+
55
+ /// Print memory statistics in a human-readable format
56
+ #[allow(clippy::cast_precision_loss)]
57
+ pub fn print(&self) {
58
+ let mrss_mb = self.max_rss_bytes as f64 / 1024.0 / 1024.0;
59
+
60
+ println!("Maximum RSS: {} bytes ({:.2} MB)", self.max_rss_bytes, mrss_mb);
61
+ println!();
62
+ }
63
+
64
+ /// Retrieve and print memory usage statistics
65
+ pub fn print_memory_usage() {
66
+ match Self::current() {
67
+ Ok(stats) => stats.print(),
68
+ Err(e) => eprintln!("Warning: Could not retrieve memory statistics: {e}"),
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,264 @@
1
+ use std::collections::HashSet;
2
+ use std::io::Write;
3
+
4
+ use crate::model::declaration::Declaration;
5
+ use crate::model::definitions::Definition;
6
+ use crate::model::graph::Graph;
7
+ use crate::model::ids::{DefinitionId, NameId, StringId};
8
+ use crate::model::name::{NameRef, ParentScope};
9
+
10
+ impl Graph {
11
+ /// Writes a report of orphan definitions (definitions not linked to any declaration).
12
+ ///
13
+ /// Format: `type\tconcatenated_name\tlocation` (TSV)
14
+ ///
15
+ /// # Errors
16
+ ///
17
+ /// Returns an error if writing fails.
18
+ pub fn write_orphan_report(&self, writer: &mut impl Write) -> std::io::Result<()> {
19
+ // Collect all definition IDs that are linked to declarations
20
+ let linked_definition_ids: HashSet<&DefinitionId> = self
21
+ .declarations()
22
+ .values()
23
+ .flat_map(Declaration::definitions)
24
+ .collect();
25
+
26
+ // Find orphan definitions
27
+ let mut orphans: Vec<_> = self
28
+ .definitions()
29
+ .iter()
30
+ .filter(|(id, _)| !linked_definition_ids.contains(id))
31
+ .collect();
32
+
33
+ // Sort by type, then by location for consistent output
34
+ orphans.sort_by(|(_, a), (_, b)| {
35
+ a.kind()
36
+ .cmp(b.kind())
37
+ .then_with(|| a.uri_id().cmp(b.uri_id()))
38
+ .then_with(|| a.offset().cmp(b.offset()))
39
+ });
40
+
41
+ for (_, definition) in orphans {
42
+ let kind = definition.kind();
43
+ let name = match definition.name_id().copied() {
44
+ Some(id) => self.build_concatenated_name_from_name(id),
45
+ None => self.build_concatenated_name_from_lexical_nesting(definition),
46
+ };
47
+ let location = self.definition_location(definition);
48
+
49
+ writeln!(writer, "{kind}\t{name}\t{location}")?;
50
+ }
51
+
52
+ Ok(())
53
+ }
54
+
55
+ /// Walks the Name system's `parent_scope` chain to reconstruct the constant path.
56
+ /// Falls back to `nesting` for enclosing scope context when there is no explicit parent scope.
57
+ ///
58
+ /// Note: this produces a concatenated name by piecing together name parts, not a properly
59
+ /// resolved qualified name.
60
+ pub(crate) fn build_concatenated_name_from_name(&self, name_id: NameId) -> String {
61
+ let Some(name_ref) = self.names().get(&name_id) else {
62
+ return "<unknown>".to_string();
63
+ };
64
+ let simple_name = self.string_id_to_string(*name_ref.str());
65
+
66
+ match name_ref.parent_scope() {
67
+ ParentScope::Some(parent_id) | ParentScope::Attached(parent_id) => {
68
+ let parent_name = self.build_concatenated_name_from_name(*parent_id);
69
+ format!("{parent_name}::{simple_name}")
70
+ }
71
+ ParentScope::TopLevel => format!("::{simple_name}"),
72
+ ParentScope::None => {
73
+ let prefix = name_ref
74
+ .nesting()
75
+ .as_ref()
76
+ .map(|nesting_id| self.build_nesting_prefix(*nesting_id))
77
+ .unwrap_or_default();
78
+
79
+ if prefix.is_empty() {
80
+ simple_name
81
+ } else {
82
+ format!("{prefix}::{simple_name}")
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ /// Resolves the enclosing nesting `NameId` to a string prefix.
89
+ /// For resolved names, uses the declaration's fully qualified name.
90
+ /// For unresolved names, recursively walks the name chain.
91
+ fn build_nesting_prefix(&self, nesting_id: NameId) -> String {
92
+ let Some(name_ref) = self.names().get(&nesting_id) else {
93
+ return String::new();
94
+ };
95
+ match name_ref {
96
+ NameRef::Resolved(resolved) => self
97
+ .declarations()
98
+ .get(resolved.declaration_id())
99
+ .map_or_else(String::new, |decl| decl.name().to_string()),
100
+ NameRef::Unresolved(_) => self.build_concatenated_name_from_name(nesting_id),
101
+ }
102
+ }
103
+
104
+ /// Builds a concatenated name for non-constant definitions by walking the `lexical_nesting_id` chain.
105
+ ///
106
+ /// Note: this pieces together name parts from the lexical nesting, not a properly resolved
107
+ /// qualified name.
108
+ pub(crate) fn build_concatenated_name_from_lexical_nesting(&self, definition: &Definition) -> String {
109
+ let simple_name = self.string_id_to_string(self.definition_string_id(definition));
110
+
111
+ // Collect enclosing nesting names from inner to outer
112
+ let mut nesting_parts = Vec::new();
113
+ let mut current_nesting = *definition.lexical_nesting_id();
114
+
115
+ while let Some(nesting_id) = current_nesting {
116
+ let Some(nesting_def) = self.definitions().get(&nesting_id) else {
117
+ break;
118
+ };
119
+ nesting_parts.push(self.string_id_to_string(self.definition_string_id(nesting_def)));
120
+ current_nesting = *nesting_def.lexical_nesting_id();
121
+ }
122
+
123
+ if nesting_parts.is_empty() {
124
+ return simple_name;
125
+ }
126
+
127
+ // Reverse to get outer-to-inner order for the prefix
128
+ nesting_parts.reverse();
129
+ let prefix = nesting_parts.join("::");
130
+
131
+ let separator = match definition {
132
+ Definition::Method(_)
133
+ | Definition::AttrAccessor(_)
134
+ | Definition::AttrReader(_)
135
+ | Definition::AttrWriter(_)
136
+ | Definition::MethodAlias(_)
137
+ | Definition::MethodVisibility(_)
138
+ | Definition::InstanceVariable(_) => "#",
139
+ Definition::Class(_)
140
+ | Definition::SingletonClass(_)
141
+ | Definition::Module(_)
142
+ | Definition::Constant(_)
143
+ | Definition::ConstantAlias(_)
144
+ | Definition::ConstantVisibility(_)
145
+ | Definition::GlobalVariable(_)
146
+ | Definition::ClassVariable(_)
147
+ | Definition::GlobalVariableAlias(_) => "::",
148
+ };
149
+
150
+ format!("{prefix}{separator}{simple_name}")
151
+ }
152
+
153
+ /// Converts a `StringId` to its string value.
154
+ fn string_id_to_string(&self, string_id: StringId) -> String {
155
+ self.strings().get(&string_id).unwrap().to_string()
156
+ }
157
+
158
+ /// Get location in the format of `uri#L<line>` for a definition.
159
+ /// The format is clickable in VS Code.
160
+ pub(crate) fn definition_location(&self, definition: &Definition) -> String {
161
+ let uri_id = definition.uri_id();
162
+
163
+ let Some(document) = self.documents().get(uri_id) else {
164
+ return format!("{uri_id}:<unknown>");
165
+ };
166
+
167
+ let uri = document.uri();
168
+ let line_index = document.line_index();
169
+ let start = line_index.line_col(definition.offset().start().into());
170
+ format!("{uri}#L{}", start.line + 1)
171
+ }
172
+ }
173
+
174
+ #[cfg(test)]
175
+ mod tests {
176
+ use crate::test_utils::GraphTest;
177
+
178
+ #[test]
179
+ fn build_concatenated_name_from_name_for_constants() {
180
+ let cases = vec![
181
+ ("class Foo; end", "Foo"),
182
+ ("module Foo; class Bar; end; end", "Foo::Bar"),
183
+ ("module Foo; module Bar; class Baz; end; end; end", "Foo::Bar::Baz"),
184
+ ];
185
+
186
+ for (source, expected_name) in cases {
187
+ let mut context = GraphTest::new();
188
+ context.index_uri("file:///test.rb", source);
189
+ context.resolve();
190
+
191
+ let definitions = context.graph().get(expected_name).unwrap();
192
+ let definition = definitions.first().unwrap();
193
+ let name_id = *definition.name_id().unwrap();
194
+ let actual = context.graph().build_concatenated_name_from_name(name_id);
195
+
196
+ assert_eq!(actual, expected_name, "For source: {source}");
197
+ }
198
+ }
199
+
200
+ #[test]
201
+ fn build_concatenated_name_from_lexical_nesting_for_methods() {
202
+ let cases = vec![
203
+ ("class Foo; def bar; end; end", "Foo#bar()"),
204
+ ("module Foo; class Bar; def baz; end; end; end", "Foo::Bar#baz()"),
205
+ ("def bar; end", "bar()"),
206
+ ];
207
+
208
+ for (source, expected_name) in cases {
209
+ let mut context = GraphTest::new();
210
+ // Index without resolution so methods remain orphans
211
+ context.index_uri("file:///test.rb", source);
212
+
213
+ let definition = context
214
+ .graph()
215
+ .definitions()
216
+ .values()
217
+ .find(|d| d.kind() == "Method" && d.name_id().is_none())
218
+ .unwrap_or_else(|| panic!("No Method definition without name_id found for source: {source}"));
219
+
220
+ let actual = context.graph().build_concatenated_name_from_lexical_nesting(definition);
221
+ assert_eq!(actual, expected_name, "For source: {source}");
222
+ }
223
+ }
224
+
225
+ #[test]
226
+ fn build_concatenated_name_from_lexical_nesting_for_instance_variables() {
227
+ let mut context = GraphTest::new();
228
+ context.index_uri("file:///test.rb", "class Foo; def initialize; @ivar = 1; end; end");
229
+
230
+ let definition = context
231
+ .graph()
232
+ .definitions()
233
+ .values()
234
+ .find(|d| d.kind() == "InstanceVariable")
235
+ .unwrap();
236
+
237
+ let actual = context.graph().build_concatenated_name_from_lexical_nesting(definition);
238
+ assert_eq!(actual, "Foo::initialize()#@ivar");
239
+ }
240
+
241
+ #[test]
242
+ fn definition_location_uses_clickable_uri_fragment() {
243
+ let mut context = GraphTest::new();
244
+ context.index_uri(
245
+ "file:///foo.rb",
246
+ "
247
+ class Foo
248
+ def bar
249
+ end
250
+ end
251
+ ",
252
+ );
253
+
254
+ let definition = context
255
+ .graph()
256
+ .definitions()
257
+ .values()
258
+ .find(|d| d.kind() == "Method")
259
+ .unwrap();
260
+
261
+ let actual = context.graph().definition_location(definition);
262
+ assert_eq!(actual, "file:///foo.rb#L2");
263
+ }
264
+ }
@@ -0,0 +1,127 @@
1
+ use std::sync::Mutex;
2
+ use std::time::{Duration, Instant};
3
+
4
+ /// Generates a global timer for measuring performance across different phases of execution in the main thread.
5
+ ///
6
+ /// This macro creates:
7
+ /// - A `Timer` struct with fields for each defined phase
8
+ /// - A global `TIMER` static for tracking measurements
9
+ /// - A `time_it!` macro to measure and record individual phase durations
10
+ ///
11
+ /// The timer is only created when running with the `--stats` flag.
12
+ ///
13
+ /// Usage:
14
+ /// 1. To add a new phase, add an entry to the `make_timer!` invocation at the bottom of this file
15
+ /// 2. Wrap code blocks with `time_it!(phase_name, { ... })` to measure them
16
+ macro_rules! make_timer {
17
+ (
18
+ $(
19
+ $phase:ident, $label:literal;
20
+ )*
21
+ ) => {
22
+ #[derive(Clone)]
23
+ pub struct Timer {
24
+ start_time: Instant,
25
+ $(
26
+ pub $phase: Duration,
27
+ )*
28
+ }
29
+
30
+ pub static TIMER: Mutex<Option<Timer>> = Mutex::new(None);
31
+
32
+ impl Default for Timer {
33
+ fn default() -> Self {
34
+ Self::new()
35
+ }
36
+ }
37
+
38
+ impl Timer {
39
+ #[must_use]
40
+ pub fn new() -> Self {
41
+ Self {
42
+ start_time: Instant::now(),
43
+ $(
44
+ $phase: Duration::ZERO,
45
+ )*
46
+ }
47
+ }
48
+
49
+ pub fn set_global_timer(timer: Timer) {
50
+ *TIMER.lock().unwrap() = Some(timer);
51
+ }
52
+
53
+ pub fn print_breakdown() {
54
+ if let Some(ref timer) = *TIMER.lock().unwrap() {
55
+ macro_rules! format_breakdown {
56
+ ($name:expr, $duration:expr, $total:expr) => {
57
+ format!(
58
+ "{:<16} {:8.3}s ({:5.1}%)",
59
+ $name,
60
+ $duration.as_secs_f64(),
61
+ $duration.as_secs_f64() * 100.0 / $total.as_secs_f64()
62
+ )
63
+ };
64
+ }
65
+
66
+ let total_duration = timer.start_time.elapsed();
67
+ let mut accounted_time = Duration::ZERO;
68
+ $(
69
+ accounted_time += timer.$phase;
70
+ )*
71
+ let cleanup = total_duration - accounted_time;
72
+
73
+ println!();
74
+ println!("Timing breakdown");
75
+
76
+ $(
77
+ if timer.$phase != Duration::ZERO {
78
+ println!(" {}", format_breakdown!($label, timer.$phase, total_duration));
79
+ }
80
+ )*
81
+
82
+ println!(" {}", format_breakdown!("Cleanup", cleanup, total_duration));
83
+ println!(" Total: {:8.3}s", total_duration.as_secs_f64());
84
+ println!();
85
+ }
86
+ }
87
+ }
88
+
89
+ #[macro_export]
90
+ macro_rules! time_it {
91
+ $(
92
+ ($phase, $body:block) => {
93
+ {
94
+ let timer_enabled = {
95
+ let guard = $crate::stats::timer::TIMER.lock().unwrap();
96
+ guard.is_some()
97
+ };
98
+
99
+ if timer_enabled {
100
+ let start = std::time::Instant::now();
101
+ let result = $body;
102
+ let elapsed = start.elapsed();
103
+
104
+ if let Some(ref mut timer) = *$crate::stats::timer::TIMER.lock().unwrap() {
105
+ timer.$phase = elapsed;
106
+ }
107
+ result
108
+ } else {
109
+ $body
110
+ }
111
+ }
112
+ };
113
+ )*
114
+ }
115
+
116
+ pub use time_it;
117
+ };
118
+ }
119
+
120
+ make_timer! {
121
+ setup, "Initialization";
122
+ listing, "Listing";
123
+ indexing, "Indexing";
124
+ resolution, "Resolution";
125
+ integrity_check, "Integrity check";
126
+ querying, "Querying";
127
+ }
@@ -0,0 +1,11 @@
1
+ pub mod memory;
2
+ // TODO: When the rubydex is stable enough, turn this into a debug-only feature or revisit if we still need it.
3
+ pub mod orphan_report;
4
+ pub mod timer;
5
+
6
+ /// Helper function to compute percentage
7
+ #[allow(clippy::cast_precision_loss)]
8
+ #[must_use]
9
+ pub fn percentage(numerator: usize, denominator: usize) -> f64 {
10
+ (numerator as f64 / denominator as f64) * 100.0
11
+ }
@@ -0,0 +1,226 @@
1
+ use super::normalize_indentation;
2
+ use std::fs;
3
+ use std::path::{Path, PathBuf};
4
+ use tempfile::TempDir;
5
+
6
+ #[derive(Debug)]
7
+ pub struct Context {
8
+ _root: TempDir,
9
+ absolute_path: PathBuf,
10
+ }
11
+
12
+ /// Executes a closure with a new temporary context, ensuring cleanup afterwards.
13
+ ///
14
+ /// # Examples
15
+ ///
16
+ /// ```
17
+ /// use rubydex::test_utils::with_context;
18
+ ///
19
+ /// with_context(|context| {
20
+ /// context.touch("foo.rb");
21
+ /// });
22
+ /// ```
23
+ pub fn with_context<F, R>(f: F) -> R
24
+ where
25
+ F: FnOnce(&Context) -> R,
26
+ {
27
+ let context = Context::new();
28
+ f(&context)
29
+ }
30
+
31
+ impl Context {
32
+ /// Creates a new test context in a temporary directory
33
+ ///
34
+ /// # Panics
35
+ ///
36
+ /// Panics if the temp directory cannot be created.
37
+ #[must_use]
38
+ pub fn new() -> Self {
39
+ let root = tempfile::tempdir().expect("failed to create temp dir");
40
+ let absolute_path = fs::canonicalize(root.path()).unwrap();
41
+ Self {
42
+ _root: root,
43
+ absolute_path,
44
+ }
45
+ }
46
+
47
+ /// Returns the absolute path to the temp directory as a `PathBuf`
48
+ ///
49
+ /// # Panics
50
+ ///
51
+ /// Panics if the path cannot be canonicalized.
52
+ #[must_use]
53
+ pub fn absolute_path(&self) -> PathBuf {
54
+ self.absolute_path.clone()
55
+ }
56
+
57
+ /// Returns the absolute path to the relative path
58
+ ///
59
+ /// # Panics
60
+ ///
61
+ /// Panics if the path cannot be canonicalized.
62
+ #[must_use]
63
+ pub fn absolute_path_to(&self, relative: &str) -> PathBuf {
64
+ self.absolute_path.join(relative)
65
+ }
66
+
67
+ /// Returns the path of `absolute` relative to the context root.
68
+ ///
69
+ /// # Panics
70
+ ///
71
+ /// Panics if the provided path cannot be canonicalized or is not under the root.
72
+ #[must_use]
73
+ pub fn relative_path_to<P: AsRef<Path>>(&self, absolute: P) -> PathBuf {
74
+ absolute
75
+ .as_ref()
76
+ .strip_prefix(self.absolute_path())
77
+ .unwrap()
78
+ .to_path_buf()
79
+ }
80
+
81
+ /// Create a directory (and parents) relative to the root
82
+ ///
83
+ /// # Panics
84
+ ///
85
+ /// Panics if the directory cannot be created.
86
+ pub fn mkdir<P: AsRef<Path>>(&self, relative: P) {
87
+ let dir = self.absolute_path().join(relative);
88
+ fs::create_dir_all(dir).unwrap();
89
+ }
90
+
91
+ /// Touch a file relative to the root, creating parent directories as needed
92
+ ///
93
+ /// # Panics
94
+ ///
95
+ /// Panics if the file cannot be created.
96
+ pub fn touch<P: AsRef<Path>>(&self, relative: P) {
97
+ self.write(relative, "");
98
+ }
99
+
100
+ /// Read a file relative to the root
101
+ ///
102
+ /// # Panics
103
+ ///
104
+ /// Panics if the file cannot be read.
105
+ #[must_use]
106
+ pub fn read<P: AsRef<Path>>(&self, relative: P) -> String {
107
+ fs::read_to_string(self.absolute_path().join(relative)).unwrap()
108
+ }
109
+
110
+ /// Write a file relative to the root, creating parent directories as needed
111
+ ///
112
+ /// # Panics
113
+ ///
114
+ /// Panics if the file cannot be created.
115
+ pub fn write<P: AsRef<Path>>(&self, relative: P, content: &str) {
116
+ let path = self.absolute_path().join(relative);
117
+ if let Some(parent) = path.parent() {
118
+ fs::create_dir_all(parent).unwrap();
119
+ }
120
+ let content = normalize_indentation(content);
121
+ fs::write(path, content).unwrap();
122
+ }
123
+ }
124
+
125
+ impl Default for Context {
126
+ fn default() -> Self {
127
+ Self::new()
128
+ }
129
+ }
130
+
131
+ // no local normalize_indentation; shared via super::normalize_indentation
132
+
133
+ #[cfg(test)]
134
+ mod tests {
135
+ use super::*;
136
+
137
+ #[test]
138
+ fn creates_and_cleans_up_temp_dir() {
139
+ let context = Context::new();
140
+ let root = context.absolute_path();
141
+
142
+ assert!(root.exists());
143
+
144
+ drop(context);
145
+
146
+ // After drop, the directory should not exist anymore
147
+ assert!(!root.exists());
148
+ }
149
+
150
+ #[test]
151
+ fn mkdir_creates_directories() {
152
+ let context = Context::new();
153
+
154
+ assert!(!context.absolute_path_to("foo").exists());
155
+ context.mkdir("foo");
156
+ assert!(context.absolute_path_to("foo").exists());
157
+
158
+ assert!(!context.absolute_path_to("bar/baz").exists());
159
+ context.mkdir("bar/baz");
160
+ assert!(context.absolute_path_to("bar/baz").exists());
161
+ }
162
+
163
+ #[test]
164
+ fn touch_creates_files() {
165
+ let context = Context::new();
166
+
167
+ assert!(!context.absolute_path_to("foo/bar.rb").exists());
168
+ context.touch("foo/bar.rb");
169
+ assert!(context.absolute_path_to("foo/bar.rb").exists());
170
+
171
+ assert!(!context.absolute_path_to("baz/qux.rb").exists());
172
+ context.touch("baz/qux.rb");
173
+ assert!(context.absolute_path_to("baz/qux.rb").exists());
174
+ }
175
+
176
+ #[test]
177
+ fn write_creates_files_with_content() {
178
+ let context = Context::new();
179
+
180
+ context.write("foo/bar.rb", "class Foo; end\n");
181
+ assert_eq!(context.read("foo/bar.rb"), "class Foo; end\n");
182
+
183
+ context.write("baz/qux.rb", "class Baz; end\n");
184
+ assert_eq!(context.read("baz/qux.rb"), "class Baz; end\n");
185
+ }
186
+
187
+ #[test]
188
+ fn write_creates_files_with_content_and_normalizes_indentation() {
189
+ let context = Context::new();
190
+
191
+ context.write("foo/bar.rb", {
192
+ "
193
+ class Foo
194
+ def bar
195
+ puts 'baz'
196
+ end
197
+ end
198
+ "
199
+ });
200
+
201
+ assert_eq!(
202
+ context.read("foo/bar.rb"),
203
+ normalize_indentation({
204
+ "
205
+ class Foo
206
+ def bar
207
+ puts 'baz'
208
+ end
209
+ end
210
+ "
211
+ }),
212
+ );
213
+ }
214
+
215
+ #[test]
216
+ fn with_context_creates_and_cleans_up_temp_dir() {
217
+ let root = with_context(|context| {
218
+ let root = context.absolute_path();
219
+ assert!(root.exists());
220
+ context.touch("foo.rb");
221
+ root
222
+ });
223
+
224
+ assert!(!root.exists());
225
+ }
226
+ }