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 fn infer_dep(type_name: &str) -> Option<String> {
99 let crate_name = type_name.split("::").next()?;
100 if crate_name.chars().next()?.is_ascii_lowercase() {
101 Some(format!(r#"{crate_name} = "*""#))
102 } else {
103 None
104 }
105 }
106 pub fn for_definition_with_deps(
113 type_name: &str,
114 method_name: &str,
115 deps: Option<&str>,
116 ) -> std::io::Result<Self> {
117 Self::create_probe(type_name, Some(method_name), deps)
118 }
119
120 fn create_probe(
127 type_name: &str,
128 method_name: Option<&str>,
129 deps: Option<&str>,
130 ) -> std::io::Result<Self> {
131 let id = PROBE_COUNTER.fetch_add(1, Ordering::Relaxed);
132 let suffix = method_name.map_or("probe", |_| "probe-def");
133 let dir =
134 std::env::temp_dir().join(format!("rust-meth-{suffix}-{}-{id}", std::process::id()));
135
136 let src_dir = dir.join("src");
138 fs::create_dir_all(&src_dir)?;
139
140 let effective_deps = deps
141 .map(str::to_owned)
142 .or_else(|| Self::infer_dep(type_name));
143
144 let cargo_toml = effective_deps.map_or_else(
145 || "[package]\nname = \"probe\"\nversion = \"0.1.0\"\nedition = \"2024\"\n".to_string(),
146 |d| format!(
147 "[package]\nname = \"probe\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n{d}\n"
148 ),
149 );
150
151 fs::write(dir.join("Cargo.toml"), cargo_toml)?;
154
155 let preamble_lines =
163 u32::try_from(PREAMBLE.lines().count()).expect("Preamble is too long to fit in u32");
164
165 let source = method_name.map_or_else(|| format!("{PREAMBLE}fn main() {{\n let _x: {type_name} = todo!();\n _x.\n}}\n"), |method| format!(
167 "{PREAMBLE}fn main() {{\n let _x: {type_name} = todo!();\n _x.{method}();\n}}\n"
168 ));
169
170 let src_path = src_dir.join("main.rs");
171 fs::write(&src_path, &source)?;
172
173 let dot_line = preamble_lines + 2;
175 let dot_col = u32::try_from(" _x.".len()).expect("failed");
176
177 Ok(Self {
178 dir,
179 src_path,
180 dot_line,
181 dot_col,
182 })
183 }
184
185 #[must_use]
189 pub fn src_uri(&self) -> String {
190 path_to_uri(&self.src_path)
191 }
192
193 #[must_use]
195 pub fn root_uri(&self) -> String {
196 path_to_uri(&self.dir)
197 }
198
199 pub fn source(&self) -> std::io::Result<String> {
209 fs::read_to_string(&self.src_path)
210 }
211}
212
213impl Drop for Probe {
214 fn drop(&mut self) {
215 let _ = fs::remove_dir_all(&self.dir);
216 }
217}
218
219fn path_to_uri(path: &Path) -> String {
220 let s = path.to_string_lossy();
221 if s.starts_with('/') {
222 format!("file://{s}")
223 } else {
224 format!("file:///{s}")
225 }
226}
227
228#[cfg(test)]
229#[allow(clippy::unwrap_used)]
230mod tests {
231 use super::*;
232
233 fn preamble_line_count() -> u32 {
236 u32::try_from(PREAMBLE.lines().count()).unwrap()
237 }
238
239 #[test]
242 fn no_deps_omits_dependencies_section() {
243 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
244 let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
245 assert!(
246 !cargo.contains("[dependencies]"),
247 "Cargo.toml should not have a [dependencies] section when deps is None"
248 );
249 assert!(cargo.contains("[package]"));
250 assert!(cargo.contains(r#"name = "probe""#));
251 assert!(cargo.contains(r#"edition = "2024""#));
252 }
253
254 #[test]
255 fn with_deps_injects_dependencies_section() {
256 let p = Probe::new_with_deps("serde_json::Value", Some(r#"serde_json = "1.0""#)).unwrap();
257 let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
258 assert!(cargo.contains("[dependencies]"));
259 assert!(cargo.contains(r#"serde_json = "1.0""#));
260 }
261
262 #[test]
263 fn multiple_deps_all_appear_in_cargo_toml() {
264 let deps = "serde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"";
265 let p = Probe::new_with_deps("serde_json::Value", Some(deps)).unwrap();
266 let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
267 assert!(cargo.contains("[dependencies]"));
268 assert!(cargo.contains("serde ="));
269 assert!(cargo.contains(r#"serde_json = "1.0""#));
270 }
271
272 #[test]
275 fn completion_probe_source_has_dot_trigger() {
276 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
277 let src = p.source().unwrap();
278 assert!(
279 src.contains("let _x: Vec<u8> = todo!();"),
280 "source should declare the type"
281 );
282 assert!(
284 src.contains(" _x.\n"),
285 "completion probe should have bare dot trigger"
286 );
287 }
288
289 #[test]
290 fn definition_probe_source_has_method_call() {
291 let p = Probe::for_definition_with_deps("Vec<u8>", "push", None).unwrap();
292 let src = p.source().unwrap();
293 assert!(src.contains("let _x: Vec<u8> = todo!();"));
294 assert!(
295 src.contains("_x.push();"),
296 "definition probe should contain the method call"
297 );
298 }
299
300 #[test]
301 fn completion_probe_with_deps_type_in_source() {
302 let p = Probe::new_with_deps("serde_json::Value", Some(r#"serde_json = "1.0""#)).unwrap();
303 let src = p.source().unwrap();
304 assert!(src.contains("let _x: serde_json::Value = todo!();"));
305 }
306
307 #[test]
308 fn definition_probe_with_deps_cargo_and_source_correct() {
309 let p = Probe::for_definition_with_deps(
310 "serde_json::Value",
311 "as_str",
312 Some(r#"serde_json = "1.0""#),
313 )
314 .unwrap();
315 let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
316 assert!(cargo.contains("[dependencies]"));
317 let src = p.source().unwrap();
318 assert!(src.contains("serde_json::Value"));
319 assert!(src.contains("_x.as_str();"));
320 }
321
322 #[test]
325 fn dot_col_is_seven() {
326 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
328 assert_eq!(p.dot_col, 7, r#"" _x." should be 7 chars"#);
329 }
330
331 #[test]
332 fn dot_line_is_preamble_plus_two() {
333 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
335 assert_eq!(p.dot_line, preamble_line_count() + 2);
336 }
337
338 #[test]
339 fn dot_line_same_for_definition_probe() {
340 let p = Probe::for_definition_with_deps("Vec<u8>", "len", None).unwrap();
341 assert_eq!(p.dot_line, preamble_line_count() + 2);
342 }
343
344 #[test]
347 fn src_uri_is_file_uri_ending_in_main_rs() {
348 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
349 let uri = p.src_uri();
350 assert!(
351 uri.starts_with("file://"),
352 "src_uri should be a file:// URI"
353 );
354 assert!(
355 uri.ends_with("/src/main.rs"),
356 "src_uri should end in /src/main.rs"
357 );
358 }
359
360 #[test]
361 fn root_uri_is_file_uri_not_ending_in_main_rs() {
362 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
363 let uri = p.root_uri();
364 assert!(uri.starts_with("file://"));
365 assert!(!uri.ends_with("main.rs"));
366 }
367
368 #[test]
371 fn drop_removes_temp_directory() {
372 let dir = {
373 let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
374 assert!(p.dir.exists(), "dir should exist while probe is alive");
375 p.dir.clone()
376 };
377 assert!(!dir.exists(), "temp dir should be removed after drop");
378 }
379
380 #[test]
381 fn definition_probe_drop_removes_temp_directory() {
382 let dir = {
383 let p = Probe::for_definition_with_deps("Vec<u8>", "len", None).unwrap();
384 p.dir.clone()
385 };
386 assert!(!dir.exists());
387 }
388}