rubydex 0.1.0.beta1-x86_64-linux → 0.1.0.beta2-x86_64-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.
- checksums.yaml +4 -4
- data/ext/rubydex/declaration.c +146 -0
- data/ext/rubydex/declaration.h +10 -0
- data/ext/rubydex/definition.c +234 -0
- data/ext/rubydex/definition.h +28 -0
- data/ext/rubydex/diagnostic.c +6 -0
- data/ext/rubydex/diagnostic.h +11 -0
- data/ext/rubydex/document.c +98 -0
- data/ext/rubydex/document.h +10 -0
- data/ext/rubydex/extconf.rb +36 -15
- data/ext/rubydex/graph.c +405 -0
- data/ext/rubydex/graph.h +10 -0
- data/ext/rubydex/handle.h +44 -0
- data/ext/rubydex/location.c +22 -0
- data/ext/rubydex/location.h +15 -0
- data/ext/rubydex/reference.c +104 -0
- data/ext/rubydex/reference.h +16 -0
- data/ext/rubydex/rubydex.c +22 -0
- data/ext/rubydex/utils.c +27 -0
- data/ext/rubydex/utils.h +13 -0
- data/lib/rubydex/3.2/rubydex.so +0 -0
- data/lib/rubydex/3.3/rubydex.so +0 -0
- data/lib/rubydex/3.4/rubydex.so +0 -0
- data/lib/rubydex/4.0/rubydex.so +0 -0
- data/lib/rubydex/librubydex_sys.so +0 -0
- data/lib/rubydex/version.rb +1 -1
- data/rust/Cargo.lock +1275 -0
- data/rust/Cargo.toml +23 -0
- data/rust/about.hbs +78 -0
- data/rust/about.toml +9 -0
- data/rust/rubydex/Cargo.toml +41 -0
- data/rust/rubydex/src/diagnostic.rs +108 -0
- data/rust/rubydex/src/errors.rs +28 -0
- data/rust/rubydex/src/indexing/local_graph.rs +172 -0
- data/rust/rubydex/src/indexing/ruby_indexer.rs +5397 -0
- data/rust/rubydex/src/indexing.rs +128 -0
- data/rust/rubydex/src/job_queue.rs +186 -0
- data/rust/rubydex/src/lib.rs +15 -0
- data/rust/rubydex/src/listing.rs +249 -0
- data/rust/rubydex/src/main.rs +116 -0
- data/rust/rubydex/src/model/comment.rs +24 -0
- data/rust/rubydex/src/model/declaration.rs +541 -0
- data/rust/rubydex/src/model/definitions.rs +1475 -0
- data/rust/rubydex/src/model/document.rs +111 -0
- data/rust/rubydex/src/model/encoding.rs +22 -0
- data/rust/rubydex/src/model/graph.rs +1387 -0
- data/rust/rubydex/src/model/id.rs +90 -0
- data/rust/rubydex/src/model/identity_maps.rs +54 -0
- data/rust/rubydex/src/model/ids.rs +32 -0
- data/rust/rubydex/src/model/name.rs +188 -0
- data/rust/rubydex/src/model/references.rs +129 -0
- data/rust/rubydex/src/model/string_ref.rs +44 -0
- data/rust/rubydex/src/model/visibility.rs +41 -0
- data/rust/rubydex/src/model.rs +13 -0
- data/rust/rubydex/src/offset.rs +70 -0
- data/rust/rubydex/src/position.rs +6 -0
- data/rust/rubydex/src/query.rs +103 -0
- data/rust/rubydex/src/resolution.rs +4421 -0
- data/rust/rubydex/src/stats/memory.rs +71 -0
- data/rust/rubydex/src/stats/timer.rs +126 -0
- data/rust/rubydex/src/stats.rs +9 -0
- data/rust/rubydex/src/test_utils/context.rs +226 -0
- data/rust/rubydex/src/test_utils/graph_test.rs +229 -0
- data/rust/rubydex/src/test_utils/local_graph_test.rs +166 -0
- data/rust/rubydex/src/test_utils.rs +52 -0
- data/rust/rubydex/src/visualization/dot.rs +176 -0
- data/rust/rubydex/src/visualization.rs +6 -0
- data/rust/rubydex/tests/cli.rs +167 -0
- data/rust/rubydex-sys/Cargo.toml +20 -0
- data/rust/rubydex-sys/build.rs +14 -0
- data/rust/rubydex-sys/cbindgen.toml +12 -0
- data/rust/rubydex-sys/src/declaration_api.rs +114 -0
- data/rust/rubydex-sys/src/definition_api.rs +350 -0
- data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
- data/rust/rubydex-sys/src/document_api.rs +54 -0
- data/rust/rubydex-sys/src/graph_api.rs +493 -0
- data/rust/rubydex-sys/src/lib.rs +9 -0
- data/rust/rubydex-sys/src/location_api.rs +79 -0
- data/rust/rubydex-sys/src/name_api.rs +81 -0
- data/rust/rubydex-sys/src/reference_api.rs +191 -0
- data/rust/rubydex-sys/src/utils.rs +50 -0
- data/rust/rustfmt.toml +2 -0
- metadata +77 -2
|
@@ -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,126 @@
|
|
|
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
|
+
querying, "Querying";
|
|
126
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
use line_index::LineIndex;
|
|
2
|
+
|
|
3
|
+
use super::normalize_indentation;
|
|
4
|
+
use crate::indexing::local_graph::LocalGraph;
|
|
5
|
+
use crate::indexing::ruby_indexer::RubyIndexer;
|
|
6
|
+
use crate::model::graph::Graph;
|
|
7
|
+
use crate::model::ids::UriId;
|
|
8
|
+
use crate::offset::Offset;
|
|
9
|
+
use crate::position::Position;
|
|
10
|
+
use crate::resolution::Resolver;
|
|
11
|
+
|
|
12
|
+
#[derive(Default)]
|
|
13
|
+
pub struct GraphTest {
|
|
14
|
+
graph: Graph,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl GraphTest {
|
|
18
|
+
#[must_use]
|
|
19
|
+
pub fn new() -> Self {
|
|
20
|
+
Self { graph: Graph::new() }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[must_use]
|
|
24
|
+
pub fn graph(&self) -> &Graph {
|
|
25
|
+
&self.graph
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[must_use]
|
|
29
|
+
fn index_source(uri: &str, source: &str) -> LocalGraph {
|
|
30
|
+
let mut indexer = RubyIndexer::new(uri.to_string(), source);
|
|
31
|
+
indexer.index();
|
|
32
|
+
indexer.local_graph()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn index_uri(&mut self, uri: &str, source: &str) {
|
|
36
|
+
let source = normalize_indentation(source);
|
|
37
|
+
let local_index = Self::index_source(uri, &source);
|
|
38
|
+
self.graph.update(local_index);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub fn delete_uri(&mut self, uri: &str) {
|
|
42
|
+
self.graph.delete_uri(uri);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub fn resolve(&mut self) {
|
|
46
|
+
let mut resolver = Resolver::new(&mut self.graph);
|
|
47
|
+
resolver.resolve_all();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Parses a location string like `<file:///foo.rb:3:0-3:5>` into `(uri, start_offset, end_offset)`
|
|
51
|
+
///
|
|
52
|
+
/// Format: uri:start_line:start_column-end_line:end_column
|
|
53
|
+
/// Line and column numbers are 0-indexed
|
|
54
|
+
///
|
|
55
|
+
/// # Panics
|
|
56
|
+
///
|
|
57
|
+
/// Panics if the location format is invalid, the URI has no source, or the positions are invalid.
|
|
58
|
+
#[must_use]
|
|
59
|
+
pub fn parse_location(&self, location: &str) -> (String, u32, u32) {
|
|
60
|
+
let (uri, start_position, end_position) = Self::parse_location_positions(location);
|
|
61
|
+
let line_index = self.line_index_for(uri.as_str());
|
|
62
|
+
|
|
63
|
+
(
|
|
64
|
+
uri,
|
|
65
|
+
line_index
|
|
66
|
+
.offset(start_position)
|
|
67
|
+
.unwrap_or_else(|| panic!("Invalid start position {}:{}", start_position.line, start_position.col))
|
|
68
|
+
.into(),
|
|
69
|
+
line_index
|
|
70
|
+
.offset(end_position)
|
|
71
|
+
.unwrap_or_else(|| panic!("Invalid end position {}:{}", end_position.line, end_position.col))
|
|
72
|
+
.into(),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Asserts that the given offset matches the expected offset, providing clear error messages
|
|
77
|
+
/// with line:column positions when they don't match
|
|
78
|
+
///
|
|
79
|
+
/// # Panics
|
|
80
|
+
///
|
|
81
|
+
/// Panics if the source is not found for the URI, byte offsets are invalid, or if the actual
|
|
82
|
+
/// offset doesn't match the expected offset.
|
|
83
|
+
pub fn assert_offset_matches(
|
|
84
|
+
&self,
|
|
85
|
+
uri: &str,
|
|
86
|
+
actual_offset: &Offset,
|
|
87
|
+
expected_start: u32,
|
|
88
|
+
expected_end: u32,
|
|
89
|
+
context_message: &str,
|
|
90
|
+
location: &str,
|
|
91
|
+
) {
|
|
92
|
+
let line_index = self.line_index_for(uri);
|
|
93
|
+
|
|
94
|
+
if actual_offset.start() == expected_start && actual_offset.end() == expected_end {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let actual_start_pos = line_index.line_col(actual_offset.start().into());
|
|
99
|
+
let actual_end_pos = line_index.line_col(actual_offset.end().into());
|
|
100
|
+
let expected_start_pos = line_index.line_col(expected_start.into());
|
|
101
|
+
let expected_end_pos = line_index.line_col(expected_end.into());
|
|
102
|
+
|
|
103
|
+
assert!(
|
|
104
|
+
actual_offset.start() == expected_start,
|
|
105
|
+
"Start position mismatch for {} at {}\n actual: {}\n expected: {}",
|
|
106
|
+
context_message,
|
|
107
|
+
location,
|
|
108
|
+
Self::format_position(actual_start_pos),
|
|
109
|
+
Self::format_position(expected_start_pos)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
assert!(
|
|
113
|
+
actual_offset.end() == expected_end,
|
|
114
|
+
"End position mismatch for {} at {}\n actual: {}\n expected: {}",
|
|
115
|
+
context_message,
|
|
116
|
+
location,
|
|
117
|
+
Self::format_position(actual_end_pos),
|
|
118
|
+
Self::format_position(expected_end_pos)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn line_index_for(&self, uri: &str) -> &LineIndex {
|
|
123
|
+
let uri_id = UriId::from(uri);
|
|
124
|
+
let document = self.graph.documents().get(&uri_id).unwrap();
|
|
125
|
+
document.line_index()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn parse_location_positions(location: &str) -> (String, Position, Position) {
|
|
129
|
+
let trimmed = location.trim().trim_start_matches('<').trim_end_matches('>');
|
|
130
|
+
|
|
131
|
+
let (start_part, end_part) = trimmed.rsplit_once('-').unwrap_or_else(|| {
|
|
132
|
+
panic!("Invalid location format: {location} (expected uri:start_line:start_column-end_line:end_column)")
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
let (start_prefix, start_column_str) = start_part
|
|
136
|
+
.rsplit_once(':')
|
|
137
|
+
.unwrap_or_else(|| panic!("Invalid location format: missing start column in {location}"));
|
|
138
|
+
let (uri, start_line_str) = start_prefix
|
|
139
|
+
.rsplit_once(':')
|
|
140
|
+
.unwrap_or_else(|| panic!("Invalid location format: missing start line in {location}"));
|
|
141
|
+
|
|
142
|
+
let (end_line_str, end_column_str) = end_part
|
|
143
|
+
.split_once(':')
|
|
144
|
+
.unwrap_or_else(|| panic!("Invalid location format: missing end line or column in {location}"));
|
|
145
|
+
|
|
146
|
+
let start_line = Self::parse_number(start_line_str, "start line", location);
|
|
147
|
+
let start_column = Self::parse_number(start_column_str, "start column", location);
|
|
148
|
+
let end_line = Self::parse_number(end_line_str, "end line", location);
|
|
149
|
+
let end_column = Self::parse_number(end_column_str, "end column", location);
|
|
150
|
+
|
|
151
|
+
(
|
|
152
|
+
uri.to_string(),
|
|
153
|
+
Position {
|
|
154
|
+
line: start_line,
|
|
155
|
+
col: start_column,
|
|
156
|
+
},
|
|
157
|
+
Position {
|
|
158
|
+
line: end_line,
|
|
159
|
+
col: end_column,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn parse_number(value: &str, field: &str, location: &str) -> u32 {
|
|
165
|
+
value
|
|
166
|
+
.parse()
|
|
167
|
+
.unwrap_or_else(|_| panic!("Invalid {field} '{value}' in location {location}"))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn format_position(position: Position) -> String {
|
|
171
|
+
format!("line {}, column {}", position.line, position.col)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[cfg(test)]
|
|
176
|
+
mod tests {
|
|
177
|
+
use super::*;
|
|
178
|
+
|
|
179
|
+
#[test]
|
|
180
|
+
fn test_index_uri_with_single_line() {
|
|
181
|
+
let mut context = GraphTest::new();
|
|
182
|
+
|
|
183
|
+
context.index_uri("file://method.rb", "class Foo; end");
|
|
184
|
+
context.resolve();
|
|
185
|
+
|
|
186
|
+
let foo_defs = context.graph.get("Foo").unwrap();
|
|
187
|
+
assert_eq!(foo_defs.len(), 1);
|
|
188
|
+
assert_eq!(foo_defs[0].offset().start(), 0);
|
|
189
|
+
assert_eq!(foo_defs[0].offset().end(), 14);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[test]
|
|
193
|
+
fn test_index_uri_with_multiple_lines() {
|
|
194
|
+
let mut context = GraphTest::new();
|
|
195
|
+
|
|
196
|
+
context.index_uri("file://method.rb", {
|
|
197
|
+
"
|
|
198
|
+
class Foo
|
|
199
|
+
class Bar; end
|
|
200
|
+
end
|
|
201
|
+
"
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
context.resolve();
|
|
205
|
+
|
|
206
|
+
let foo_defs = context.graph.get("Foo").unwrap();
|
|
207
|
+
assert_eq!(foo_defs.len(), 1);
|
|
208
|
+
assert_eq!(foo_defs[0].offset().start(), 0);
|
|
209
|
+
assert_eq!(foo_defs[0].offset().end(), 30);
|
|
210
|
+
|
|
211
|
+
let bar_defs = context.graph.get("Foo::Bar").unwrap();
|
|
212
|
+
assert_eq!(bar_defs.len(), 1);
|
|
213
|
+
assert_eq!(bar_defs[0].offset().start(), 12);
|
|
214
|
+
assert_eq!(bar_defs[0].offset().end(), 26);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn test_index_uri_with_new_lines() {
|
|
219
|
+
let mut context = GraphTest::new();
|
|
220
|
+
|
|
221
|
+
context.index_uri("file://method.rb", "\n\nclass Foo; end");
|
|
222
|
+
context.resolve();
|
|
223
|
+
|
|
224
|
+
let foo_defs = context.graph.get("Foo").unwrap();
|
|
225
|
+
assert_eq!(foo_defs.len(), 1);
|
|
226
|
+
assert_eq!(foo_defs[0].offset().start(), 2);
|
|
227
|
+
assert_eq!(foo_defs[0].offset().end(), 16);
|
|
228
|
+
}
|
|
229
|
+
}
|