1use std::fs;
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicU64, Ordering};
14
15static PROBE_COUNTER: AtomicU64 = AtomicU64::new(0);
18
19const PREAMBLE: &str = "\
22#![allow(unused_imports)]
23use std::collections::*;
24use std::sync::*;
25use std::cell::*;
26use std::rc::Rc;
27use std::io::{self, Read, Write, BufRead};
28use std::fmt;
29use std::ops::*;
30use std::path::{Path, PathBuf};
31";
32
33pub struct Probe {
37 pub dir: PathBuf,
39 pub src_path: PathBuf,
41 pub dot_line: u32,
43 pub dot_col: u32,
45}
46
47impl Probe {
48 #[allow(dead_code)]
55 pub fn new(type_name: &str) -> std::io::Result<Self> {
56 Self::create_probe(type_name, None, None)
57 }
58
59 pub fn new_with_deps(type_name: &str, deps: Option<&str>) -> std::io::Result<Self> {
70 Self::create_probe(type_name, None, deps)
71 }
72
73 #[allow(dead_code)]
81 pub fn for_definition(type_name: &str, method_name: &str) -> std::io::Result<Self> {
82 Self::create_probe(type_name, Some(method_name), None)
83 }
84
85 pub fn for_definition_with_deps(
92 type_name: &str,
93 method_name: &str,
94 deps: Option<&str>,
95 ) -> std::io::Result<Self> {
96 Self::create_probe(type_name, Some(method_name), deps)
97 }
98
99 fn create_probe(
106 type_name: &str,
107 method_name: Option<&str>,
108 deps: Option<&str>,
109 ) -> std::io::Result<Self> {
110 let id = PROBE_COUNTER.fetch_add(1, Ordering::Relaxed);
111 let suffix = method_name.map_or("probe", |_| "probe-def");
112 let dir =
113 std::env::temp_dir().join(format!("rust-meth-{suffix}-{}-{id}", std::process::id()));
114
115 let src_dir = dir.join("src");
117 fs::create_dir_all(&src_dir)?;
118
119 let cargo_toml = deps.map_or_else(|| "[package]\nname = \"probe\"\nversion = \"0.1.0\"\nedition = \"2024\"\n".to_string(), |d| format!(
120 "[package]\nname = \"probe\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n{d}\n"
121 ));
122
123 fs::write(dir.join("Cargo.toml"), cargo_toml)?;
126
127 let preamble_lines =
135 u32::try_from(PREAMBLE.lines().count()).expect("Preamble is too long to fit in u32");
136
137 let source = method_name.map_or_else(|| format!("{PREAMBLE}fn main() {{\n let _x: {type_name} = todo!();\n _x.\n}}\n"), |method| format!(
139 "{PREAMBLE}fn main() {{\n let _x: {type_name} = todo!();\n _x.{method}();\n}}\n"
140 ));
141
142 let src_path = src_dir.join("main.rs");
143 fs::write(&src_path, &source)?;
144
145 let dot_line = preamble_lines + 2;
147 let dot_col = u32::try_from(" _x.".len()).expect("failed");
148
149 Ok(Self {
150 dir,
151 src_path,
152 dot_line,
153 dot_col,
154 })
155 }
156
157 #[must_use]
161 pub fn src_uri(&self) -> String {
162 path_to_uri(&self.src_path)
163 }
164
165 #[must_use]
167 pub fn root_uri(&self) -> String {
168 path_to_uri(&self.dir)
169 }
170
171 pub fn source(&self) -> std::io::Result<String> {
181 fs::read_to_string(&self.src_path)
182 }
183}
184
185impl Drop for Probe {
186 fn drop(&mut self) {
187 let _ = fs::remove_dir_all(&self.dir);
188 }
189}
190
191fn path_to_uri(path: &Path) -> String {
192 let s = path.to_string_lossy();
193 if s.starts_with('/') {
194 format!("file://{s}")
195 } else {
196 format!("file:///{s}")
197 }
198}
199
200#[cfg(test)]
201#[allow(clippy::unwrap_used)]
202mod tests {
203 use super::*;
204
205 fn preamble_line_count() -> u32 {
208 u32::try_from(PREAMBLE.lines().count()).unwrap()
209 }
210
211 #[test]
214 fn no_deps_omits_dependencies_section() {
215 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
216 let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
217 assert!(
218 !cargo.contains("[dependencies]"),
219 "Cargo.toml should not have a [dependencies] section when deps is None"
220 );
221 assert!(cargo.contains("[package]"));
222 assert!(cargo.contains(r#"name = "probe""#));
223 assert!(cargo.contains(r#"edition = "2024""#));
224 }
225
226 #[test]
227 fn with_deps_injects_dependencies_section() {
228 let p = Probe::new_with_deps("serde_json::Value", Some(r#"serde_json = "1.0""#)).unwrap();
229 let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
230 assert!(cargo.contains("[dependencies]"));
231 assert!(cargo.contains(r#"serde_json = "1.0""#));
232 }
233
234 #[test]
235 fn multiple_deps_all_appear_in_cargo_toml() {
236 let deps = "serde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"";
237 let p = Probe::new_with_deps("serde_json::Value", Some(deps)).unwrap();
238 let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
239 assert!(cargo.contains("[dependencies]"));
240 assert!(cargo.contains("serde ="));
241 assert!(cargo.contains(r#"serde_json = "1.0""#));
242 }
243
244 #[test]
247 fn completion_probe_source_has_dot_trigger() {
248 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
249 let src = p.source().unwrap();
250 assert!(
251 src.contains("let _x: Vec<u8> = todo!();"),
252 "source should declare the type"
253 );
254 assert!(
256 src.contains(" _x.\n"),
257 "completion probe should have bare dot trigger"
258 );
259 }
260
261 #[test]
262 fn definition_probe_source_has_method_call() {
263 let p = Probe::for_definition_with_deps("Vec<u8>", "push", None).unwrap();
264 let src = p.source().unwrap();
265 assert!(src.contains("let _x: Vec<u8> = todo!();"));
266 assert!(
267 src.contains("_x.push();"),
268 "definition probe should contain the method call"
269 );
270 }
271
272 #[test]
273 fn completion_probe_with_deps_type_in_source() {
274 let p = Probe::new_with_deps("serde_json::Value", Some(r#"serde_json = "1.0""#)).unwrap();
275 let src = p.source().unwrap();
276 assert!(src.contains("let _x: serde_json::Value = todo!();"));
277 }
278
279 #[test]
280 fn definition_probe_with_deps_cargo_and_source_correct() {
281 let p = Probe::for_definition_with_deps(
282 "serde_json::Value",
283 "as_str",
284 Some(r#"serde_json = "1.0""#),
285 )
286 .unwrap();
287 let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
288 assert!(cargo.contains("[dependencies]"));
289 let src = p.source().unwrap();
290 assert!(src.contains("serde_json::Value"));
291 assert!(src.contains("_x.as_str();"));
292 }
293
294 #[test]
297 fn dot_col_is_seven() {
298 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
300 assert_eq!(p.dot_col, 7, r#"" _x." should be 7 chars"#);
301 }
302
303 #[test]
304 fn dot_line_is_preamble_plus_two() {
305 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
307 assert_eq!(p.dot_line, preamble_line_count() + 2);
308 }
309
310 #[test]
311 fn dot_line_same_for_definition_probe() {
312 let p = Probe::for_definition_with_deps("Vec<u8>", "len", None).unwrap();
313 assert_eq!(p.dot_line, preamble_line_count() + 2);
314 }
315
316 #[test]
319 fn src_uri_is_file_uri_ending_in_main_rs() {
320 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
321 let uri = p.src_uri();
322 assert!(
323 uri.starts_with("file://"),
324 "src_uri should be a file:// URI"
325 );
326 assert!(
327 uri.ends_with("/src/main.rs"),
328 "src_uri should end in /src/main.rs"
329 );
330 }
331
332 #[test]
333 fn root_uri_is_file_uri_not_ending_in_main_rs() {
334 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
335 let uri = p.root_uri();
336 assert!(uri.starts_with("file://"));
337 assert!(!uri.ends_with("main.rs"));
338 }
339
340 #[test]
343 fn drop_removes_temp_directory() {
344 let dir = {
345 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
346 assert!(p.dir.exists(), "dir should exist while probe is alive");
347 p.dir.clone()
348 };
349 assert!(!dir.exists(), "temp dir should be removed after drop");
350 }
351
352 #[test]
353 fn definition_probe_drop_removes_temp_directory() {
354 let dir = {
355 let p = Probe::for_definition_with_deps("Vec<u8>", "len", None).unwrap();
356 p.dir.clone()
357 };
358 assert!(!dir.exists());
359 }
360}