use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
static PROBE_COUNTER: AtomicU64 = AtomicU64::new(0);
const PREAMBLE: &str = "\
#![allow(unused_imports)]
use std::collections::*;
use std::sync::*;
use std::cell::*;
use std::rc::Rc;
use std::io::{self, Read, Write, BufRead};
use std::fmt;
use std::ops::*;
use std::path::{Path, PathBuf};
";
pub struct Probe {
pub dir: PathBuf,
pub src_path: PathBuf,
pub dot_line: u32,
pub dot_col: u32,
}
impl Probe {
#[allow(dead_code)]
pub fn new(type_name: &str) -> std::io::Result<Self> {
Self::create_probe(type_name, None, None)
}
pub fn new_with_deps(type_name: &str, deps: Option<&str>) -> std::io::Result<Self> {
Self::create_probe(type_name, None, deps)
}
#[allow(dead_code)]
pub fn for_definition(type_name: &str, method_name: &str) -> std::io::Result<Self> {
Self::create_probe(type_name, Some(method_name), None)
}
pub fn for_definition_with_deps(
type_name: &str,
method_name: &str,
deps: Option<&str>,
) -> std::io::Result<Self> {
Self::create_probe(type_name, Some(method_name), deps)
}
fn create_probe(
type_name: &str,
method_name: Option<&str>,
deps: Option<&str>,
) -> std::io::Result<Self> {
let id = PROBE_COUNTER.fetch_add(1, Ordering::Relaxed);
let suffix = method_name.map_or("probe", |_| "probe-def");
let dir =
std::env::temp_dir().join(format!("rust-meth-{suffix}-{}-{id}", std::process::id()));
let src_dir = dir.join("src");
fs::create_dir_all(&src_dir)?;
let cargo_toml = deps.map_or_else(|| "[package]\nname = \"probe\"\nversion = \"0.1.0\"\nedition = \"2024\"\n".to_string(), |d| format!(
"[package]\nname = \"probe\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n{d}\n"
));
fs::write(dir.join("Cargo.toml"), cargo_toml)?;
let preamble_lines =
u32::try_from(PREAMBLE.lines().count()).expect("Preamble is too long to fit in u32");
let source = method_name.map_or_else(|| format!("{PREAMBLE}fn main() {{\n let _x: {type_name} = todo!();\n _x.\n}}\n"), |method| format!(
"{PREAMBLE}fn main() {{\n let _x: {type_name} = todo!();\n _x.{method}();\n}}\n"
));
let src_path = src_dir.join("main.rs");
fs::write(&src_path, &source)?;
let dot_line = preamble_lines + 2;
let dot_col = u32::try_from(" _x.".len()).expect("failed");
Ok(Self {
dir,
src_path,
dot_line,
dot_col,
})
}
#[must_use]
pub fn src_uri(&self) -> String {
path_to_uri(&self.src_path)
}
#[must_use]
pub fn root_uri(&self) -> String {
path_to_uri(&self.dir)
}
pub fn source(&self) -> std::io::Result<String> {
fs::read_to_string(&self.src_path)
}
}
impl Drop for Probe {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.dir);
}
}
fn path_to_uri(path: &Path) -> String {
let s = path.to_string_lossy();
if s.starts_with('/') {
format!("file://{s}")
} else {
format!("file:///{s}")
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn preamble_line_count() -> u32 {
u32::try_from(PREAMBLE.lines().count()).unwrap()
}
#[test]
fn no_deps_omits_dependencies_section() {
let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
assert!(
!cargo.contains("[dependencies]"),
"Cargo.toml should not have a [dependencies] section when deps is None"
);
assert!(cargo.contains("[package]"));
assert!(cargo.contains(r#"name = "probe""#));
assert!(cargo.contains(r#"edition = "2024""#));
}
#[test]
fn with_deps_injects_dependencies_section() {
let p = Probe::new_with_deps("serde_json::Value", Some(r#"serde_json = "1.0""#)).unwrap();
let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
assert!(cargo.contains("[dependencies]"));
assert!(cargo.contains(r#"serde_json = "1.0""#));
}
#[test]
fn multiple_deps_all_appear_in_cargo_toml() {
let deps = "serde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"";
let p = Probe::new_with_deps("serde_json::Value", Some(deps)).unwrap();
let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
assert!(cargo.contains("[dependencies]"));
assert!(cargo.contains("serde ="));
assert!(cargo.contains(r#"serde_json = "1.0""#));
}
#[test]
fn completion_probe_source_has_dot_trigger() {
let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
let src = p.source().unwrap();
assert!(
src.contains("let _x: Vec<u8> = todo!();"),
"source should declare the type"
);
assert!(
src.contains(" _x.\n"),
"completion probe should have bare dot trigger"
);
}
#[test]
fn definition_probe_source_has_method_call() {
let p = Probe::for_definition_with_deps("Vec<u8>", "push", None).unwrap();
let src = p.source().unwrap();
assert!(src.contains("let _x: Vec<u8> = todo!();"));
assert!(
src.contains("_x.push();"),
"definition probe should contain the method call"
);
}
#[test]
fn completion_probe_with_deps_type_in_source() {
let p = Probe::new_with_deps("serde_json::Value", Some(r#"serde_json = "1.0""#)).unwrap();
let src = p.source().unwrap();
assert!(src.contains("let _x: serde_json::Value = todo!();"));
}
#[test]
fn definition_probe_with_deps_cargo_and_source_correct() {
let p = Probe::for_definition_with_deps(
"serde_json::Value",
"as_str",
Some(r#"serde_json = "1.0""#),
)
.unwrap();
let cargo = fs::read_to_string(p.dir.join("Cargo.toml")).unwrap();
assert!(cargo.contains("[dependencies]"));
let src = p.source().unwrap();
assert!(src.contains("serde_json::Value"));
assert!(src.contains("_x.as_str();"));
}
#[test]
fn dot_col_is_seven() {
let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
assert_eq!(p.dot_col, 7, r#"" _x." should be 7 chars"#);
}
#[test]
fn dot_line_is_preamble_plus_two() {
let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
assert_eq!(p.dot_line, preamble_line_count() + 2);
}
#[test]
fn dot_line_same_for_definition_probe() {
let p = Probe::for_definition_with_deps("Vec<u8>", "len", None).unwrap();
assert_eq!(p.dot_line, preamble_line_count() + 2);
}
#[test]
fn src_uri_is_file_uri_ending_in_main_rs() {
let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
let uri = p.src_uri();
assert!(
uri.starts_with("file://"),
"src_uri should be a file:// URI"
);
assert!(
uri.ends_with("/src/main.rs"),
"src_uri should end in /src/main.rs"
);
}
#[test]
fn root_uri_is_file_uri_not_ending_in_main_rs() {
let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
let uri = p.root_uri();
assert!(uri.starts_with("file://"));
assert!(!uri.ends_with("main.rs"));
}
#[test]
fn drop_removes_temp_directory() {
let dir = {
let p = Probe::new_with_deps("Vec<u8>", None).unwrap();
assert!(p.dir.exists(), "dir should exist while probe is alive");
p.dir.clone()
};
assert!(!dir.exists(), "temp dir should be removed after drop");
}
#[test]
fn definition_probe_drop_removes_temp_directory() {
let dir = {
let p = Probe::for_definition_with_deps("Vec<u8>", "len", None).unwrap();
p.dir.clone()
};
assert!(!dir.exists());
}
}