use serde_json::json;
#[test]
fn inner_handle_uses_provided_abs_path_not_raw_args() {
let args = json!({"action": "references", "path": "../escape.rs", "line": 1, "column": 0});
let out = super::handle(&args, "/proj", "/proj/jailed.rs");
assert!(out.contains("/proj/jailed.rs"), "abs_path not used: {out}");
assert!(
!out.contains("../escape.rs"),
"raw path leaked to fs layer: {out}"
);
}
#[test]
fn unknown_action_help_lists_declaration() {
struct StubBackend;
impl crate::lsp::backend::LspBackend for StubBackend {
fn open_file(
&mut self,
_uri: &lsp_types::Uri,
_language_id: &str,
_text: &str,
) -> Result<(), String> {
Ok(())
}
fn references(
&mut self,
_uri: &lsp_types::Uri,
_position: lsp_types::Position,
_scope: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn definition(
&mut self,
_uri: &lsp_types::Uri,
_position: lsp_types::Position,
) -> Result<lsp_types::GotoDefinitionResponse, String> {
Ok(lsp_types::GotoDefinitionResponse::Array(vec![]))
}
fn implementations(
&mut self,
_uri: &lsp_types::Uri,
_position: lsp_types::Position,
_scope: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn rename(
&mut self,
_uri: &lsp_types::Uri,
_position: lsp_types::Position,
_new_name: &str,
) -> Result<Option<lsp_types::WorkspaceEdit>, String> {
Ok(None)
}
}
let dir = std::env::temp_dir().join(format!("leanctx_r1_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("x.rs");
std::fs::write(&file, "fn x() {}\n").unwrap();
let root = dir.to_string_lossy().to_string();
let abs = file.to_string_lossy().to_string();
let _stub = crate::lsp::router::stub_test_lock();
crate::lsp::router::seed_stub_backend("rust", Box::new(StubBackend));
let args = json!({"action": "definitely_bogus", "path": "x.rs", "line": 1});
let out = super::handle(&args, &root, &abs);
assert!(
out.contains("declaration"),
"help text missing declaration: {out}"
);
assert!(
out.contains("inspections"),
"help text missing inspections: {out}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn type_hierarchy_formats_indented_tree() {
use crate::lsp::backend::{
HierarchyDirection, LspBackend, SymbolOverviewItem, TypeHierarchyNode,
};
struct HierBackend;
impl LspBackend for HierBackend {
fn open_file(&mut self, _u: &lsp_types::Uri, _l: &str, _t: &str) -> Result<(), String> {
Ok(())
}
fn references(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_s: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn definition(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
) -> Result<lsp_types::GotoDefinitionResponse, String> {
Ok(lsp_types::GotoDefinitionResponse::Array(vec![]))
}
fn implementations(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_s: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn rename(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_n: &str,
) -> Result<Option<lsp_types::WorkspaceEdit>, String> {
Ok(None)
}
fn type_hierarchy(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
dir: HierarchyDirection,
) -> Result<TypeHierarchyNode, String> {
assert_eq!(dir, HierarchyDirection::Subtypes);
Ok(TypeHierarchyNode {
name: "Animal".into(),
path: "A.kt".into(),
line: 1,
children: vec![TypeHierarchyNode {
name: "Dog".into(),
path: "A.kt".into(),
line: 2,
children: vec![],
}],
})
}
fn symbols_overview(
&mut self,
_u: &lsp_types::Uri,
) -> Result<Vec<SymbolOverviewItem>, String> {
Ok(vec![SymbolOverviewItem {
name: "Animal".into(),
kind: "interface".into(),
line: 1,
}])
}
}
let tree = HierBackend
.type_hierarchy(
&crate::lsp::client::file_path_to_uri("/p/A.kt").unwrap(),
lsp_types::Position::new(0, 0),
HierarchyDirection::Subtypes,
)
.unwrap();
let out = super::format_type_hierarchy(&tree);
assert!(out.contains("Animal (A.kt:1)"), "{out}");
assert!(out.contains(" Dog (A.kt:2)"), "{out}");
let items = HierBackend
.symbols_overview(&crate::lsp::client::file_path_to_uri("/p/A.kt").unwrap())
.unwrap();
let out2 = super::format_symbols_overview(&items);
assert!(out2.contains("interface Animal (line 1)"), "{out2}");
}
#[test]
fn parse_direction_defaults_to_supertypes() {
use crate::lsp::backend::HierarchyDirection;
assert_eq!(
super::parse_direction(&json!({})),
HierarchyDirection::Supertypes
);
assert_eq!(
super::parse_direction(&json!({"direction": "subtypes"})),
HierarchyDirection::Subtypes
);
assert_eq!(
super::parse_direction(&json!({"direction": "supertypes"})),
HierarchyDirection::Supertypes
);
}
#[test]
fn resolve_name_path_unique_class() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
let data = tmp.path().join("data");
std::fs::create_dir_all(&data).unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", data.to_string_lossy().to_string());
let proj = tmp.path().join("proj");
std::fs::create_dir_all(proj.join("src")).unwrap();
std::fs::write(
proj.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.0.0\"\n",
)
.unwrap();
std::fs::write(
proj.join("src/lib.rs"),
"pub struct UniqueZqWidget { pub a: u8 }\n",
)
.unwrap();
let root = proj.to_string_lossy().to_string();
let r = super::resolve_name_path("UniqueZqWidget", &root).expect("unique resolution");
assert!(r.rel_path.ends_with("lib.rs"), "got: {}", r.rel_path);
assert!(r.end_line >= r.start_line && r.start_line > 0);
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
#[test]
fn resolve_name_path_unknown_is_no_symbol() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
let data = tmp.path().join("data");
std::fs::create_dir_all(&data).unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", data.to_string_lossy().to_string());
let proj = tmp.path().join("proj");
std::fs::create_dir_all(proj.join("src")).unwrap();
std::fs::write(
proj.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.0.0\"\n",
)
.unwrap();
std::fs::write(
proj.join("src/lib.rs"),
"pub struct UniqueZqWidget { pub a: u8 }\n",
)
.unwrap();
let root = proj.to_string_lossy().to_string();
let err = super::resolve_name_path("ZzzNoSuchSymbol123", &root).unwrap_err();
assert!(err.starts_with("NO_SYMBOL"), "got: {err}");
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
#[test]
fn resolve_name_path_trait_impl_method() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
let data = tmp.path().join("data");
std::fs::create_dir_all(&data).unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", data.to_string_lossy().to_string());
let proj = tmp.path().join("proj");
std::fs::create_dir_all(proj.join("src")).unwrap();
std::fs::write(
proj.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.0.0\"\n",
)
.unwrap();
std::fs::write(
proj.join("src/lib.rs"),
"pub struct RenderBridge;\n\
pub trait Exec { fn execute(&self); }\n\
impl Exec for RenderBridge {\n\
\x20 fn execute(&self) { let _ = 1; }\n\
}\n",
)
.unwrap();
let root = proj.to_string_lossy().to_string();
let r = super::resolve_name_path("RenderBridge/execute", &root)
.expect("trait-impl method should resolve");
assert!(r.rel_path.ends_with("lib.rs"), "got: {}", r.rel_path);
assert!(
r.start_line >= 3,
"should point at impl method, got L{}",
r.start_line
);
assert!(r.end_line >= r.start_line && r.start_line > 0);
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
#[test]
fn container_matches_ancestor_cases() {
use super::container_matches_ancestor as m;
assert!(m("RenderBridge", "RenderBridge"));
assert!(m("Exec for RenderBridge", "RenderBridge"));
assert!(m("Exec for RenderBridge<Wasm>", "RenderBridge"));
assert!(!m("OtherType", "RenderBridge"));
assert!(!m("Exec for Other", "RenderBridge"));
}
#[test]
fn resolve_name_path_inherent_impl_method() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
let data = tmp.path().join("data");
std::fs::create_dir_all(&data).unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", data.to_string_lossy().to_string());
let proj = tmp.path().join("proj");
std::fs::create_dir_all(proj.join("src")).unwrap();
std::fs::write(
proj.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.0.0\"\n",
)
.unwrap();
std::fs::write(
proj.join("src/lib.rs"),
"pub struct RenderBridge;\n\
impl RenderBridge {\n\
\x20 pub fn run(&self) { let _ = 1; }\n\
}\n",
)
.unwrap();
let root = proj.to_string_lossy().to_string();
let r = super::resolve_name_path("RenderBridge/run", &root)
.expect("inherent-impl method should still resolve");
assert!(r.rel_path.ends_with("lib.rs"), "got: {}", r.rel_path);
assert!(r.start_line >= 2 && r.end_line >= r.start_line);
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
#[test]
fn resolve_name_path_ambiguous_trait_impls() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
let data = tmp.path().join("data");
std::fs::create_dir_all(&data).unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", data.to_string_lossy().to_string());
let proj = tmp.path().join("proj");
std::fs::create_dir_all(proj.join("src")).unwrap();
std::fs::write(
proj.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.0.0\"\n",
)
.unwrap();
std::fs::write(
proj.join("src/lib.rs"),
"pub struct RenderBridge;\n\
pub trait A { fn execute(&self); }\n\
pub trait B { fn execute(&self); }\n\
pub mod a;\n\
pub mod b;\n",
)
.unwrap();
std::fs::write(
proj.join("src/a.rs"),
"impl A for RenderBridge {\n\
\x20 fn execute(&self) { let _ = 1; }\n\
}\n",
)
.unwrap();
std::fs::write(
proj.join("src/b.rs"),
"impl B for RenderBridge {\n\
\x20 fn execute(&self) { let _ = 1; }\n\
}\n",
)
.unwrap();
let root = proj.to_string_lossy().to_string();
let err = super::resolve_name_path("RenderBridge/execute", &root)
.expect_err("two trait impls (cross-file) with same method must be ambiguous");
assert!(err.starts_with("AMBIGUOUS_SYMBOL"), "got: {err}");
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
#[test]
fn anchor_indent_reads_leading_whitespace() {
let content = "class A {\n fun b() {}\n}\n";
assert_eq!(super::anchor_indent(content, 2), " "); assert_eq!(super::anchor_indent(content, 1), ""); }
#[test]
fn reindent_prefixes_first_line_only() {
assert_eq!(
super::reindent_first_line("fun x() {}", " "),
" fun x() {}"
);
assert_eq!(
super::reindent_first_line(" fun x()", " "),
" fun x()"
);
}
#[test]
fn apply_symbol_edit_headless_replaces_range() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("Foo.txt"), "aaa\nBODY\nccc\n").unwrap();
let abs = dir.path().join("Foo.txt").to_string_lossy().to_string();
let edit = crate::lsp::backend::RangeEdit {
abs_path: abs.clone(),
rel_path: "Foo.txt".into(),
range: crate::lsp::backend::TextRange0Based {
start_line: 1,
start_char: 0,
end_line: 1,
end_char: 4,
},
text: "NEW".into(),
expected_hash: None,
};
let res = super::apply_symbol_edit("replace_symbol_body", dir.path().to_str().unwrap(), &edit)
.unwrap();
assert!(res.applied);
assert_eq!(std::fs::read_to_string(&abs).unwrap(), "aaa\nNEW\nccc\n");
}
#[test]
fn handle_replace_symbol_body_via_position_fallback() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.rs"), "fn old() {\n 1\n}\n").unwrap();
let args = serde_json::json!({
"action": "replace_symbol_body",
"path": "a.rs",
"line": 1,
"end_line": 3,
"new_body": "fn new() {\n 2\n}"
});
let out = super::handle(&args, dir.path().to_str().unwrap(), "");
assert!(out.contains("replace_symbol_body applied"), "got: {out}");
let after = std::fs::read_to_string(dir.path().join("a.rs")).unwrap();
assert!(after.contains("fn new()"), "file: {after}");
}
#[test]
fn handle_replace_symbol_body_conflict_on_stale_hash() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.rs"), "fn old() {\n 1\n}\n").unwrap();
let stale = serde_json::json!({
"action": "replace_symbol_body",
"path": "a.rs", "line": 1, "end_line": 3,
"new_body": "fn new() {\n 2\n}",
"expected_hash": "deadbeefnotahash"
});
let out = super::handle(&stale, dir.path().to_str().unwrap(), "");
assert!(out.contains("CONFLICT"), "got: {out}");
assert!(
std::fs::read_to_string(dir.path().join("a.rs"))
.unwrap()
.contains("fn old()")
);
}
#[test]
fn references_output_surfaces_truncation_note() {
use lsp_types::Position;
struct TruncBackend;
impl crate::lsp::backend::LspBackend for TruncBackend {
fn open_file(&mut self, _u: &lsp_types::Uri, _l: &str, _t: &str) -> Result<(), String> {
Ok(())
}
fn references(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_s: &str,
) -> Result<Vec<lsp_types::Location>, String> {
let uri = crate::lsp::client::file_path_to_uri("/proj/a.rs").unwrap();
Ok(vec![lsp_types::Location {
uri,
range: lsp_types::Range::default(),
}])
}
fn definition(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
) -> Result<lsp_types::GotoDefinitionResponse, String> {
Ok(lsp_types::GotoDefinitionResponse::Array(vec![]))
}
fn implementations(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_s: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn rename(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_n: &str,
) -> Result<Option<lsp_types::WorkspaceEdit>, String> {
Ok(None)
}
fn last_truncation(&self) -> Option<crate::lsp::backend::Truncation> {
Some(crate::lsp::backend::Truncation {
truncated: true,
total: 742,
})
}
}
let _stub = crate::lsp::router::stub_test_lock();
crate::lsp::router::seed_stub_backend("rust", Box::new(TruncBackend));
let uri = crate::lsp::client::file_path_to_uri("/proj/a.rs").unwrap();
let out = super::handle_references(
"/proj/a.rs",
"/proj",
&uri,
Position {
line: 0,
character: 0,
},
"project",
);
assert!(
out.contains("truncated"),
"expected truncation note, got: {out}"
);
assert!(out.contains("742"), "expected total in note, got: {out}");
}
#[test]
fn inspections_run_and_list_dispatch_and_truncation() {
struct InspBackend;
impl crate::lsp::backend::LspBackend for InspBackend {
fn open_file(&mut self, _u: &lsp_types::Uri, _l: &str, _t: &str) -> Result<(), String> {
Ok(())
}
fn references(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_s: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn definition(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
) -> Result<lsp_types::GotoDefinitionResponse, String> {
Ok(lsp_types::GotoDefinitionResponse::Array(vec![]))
}
fn implementations(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_s: &str,
) -> Result<Vec<lsp_types::Location>, String> {
Ok(vec![])
}
fn rename(
&mut self,
_u: &lsp_types::Uri,
_p: lsp_types::Position,
_n: &str,
) -> Result<Option<lsp_types::WorkspaceEdit>, String> {
Ok(None)
}
fn inspections(
&mut self,
_u: &lsp_types::Uri,
) -> Result<Vec<crate::lsp::backend::InspectionDiag>, String> {
Ok(vec![crate::lsp::backend::InspectionDiag {
path: "A.kt".into(),
line: 7,
severity: "WARNING".into(),
message: "unused".into(),
}])
}
fn list_inspections(&mut self) -> Result<Vec<crate::lsp::backend::InspectionInfo>, String> {
Ok(vec![crate::lsp::backend::InspectionInfo {
id: "UnusedSymbol".into(),
name: "Unused declaration".into(),
severity: "WARNING".into(),
}])
}
fn last_truncation(&self) -> Option<crate::lsp::backend::Truncation> {
Some(crate::lsp::backend::Truncation {
truncated: true,
total: 99,
})
}
}
let _stub = crate::lsp::router::stub_test_lock();
crate::lsp::router::seed_stub_backend("rust", Box::new(InspBackend));
let uri = crate::lsp::client::file_path_to_uri("/proj/a.rs").unwrap();
let run_out = super::handle_inspections(
&json!({"action": "inspections"}),
"/proj/a.rs",
"/proj",
&uri,
);
assert!(run_out.contains("A.kt:7"), "run diag missing: {run_out}");
assert!(
run_out.contains("WARNING"),
"run severity missing: {run_out}"
);
assert!(run_out.contains("unused"), "run message missing: {run_out}");
assert!(
run_out.contains("truncated"),
"run truncation missing: {run_out}"
);
assert!(run_out.contains("99"), "run total missing: {run_out}");
let list_out = super::handle_inspections(
&json!({"action": "inspections", "mode": "list"}),
"/proj/a.rs",
"/proj",
&uri,
);
assert!(
list_out.contains("UnusedSymbol"),
"list id missing: {list_out}"
);
assert!(
list_out.contains("Unused declaration"),
"list name missing: {list_out}"
);
let bad_out = super::handle_inspections(
&json!({"action": "inspections", "mode": "bogus"}),
"/proj/a.rs",
"/proj",
&uri,
);
assert!(
bad_out.contains("ERROR"),
"unknown mode not rejected: {bad_out}"
);
}
#[test]
fn usage_range_text_reads_jailed_slice() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.rs"), "let foo = 1;\nfoo + foo;\n").unwrap();
let root = dir.path().to_str().unwrap();
let u = crate::lsp::backend::UsageSite {
path: "a.rs".into(),
range: crate::lsp::backend::TextRange0Based {
start_line: 0,
start_char: 4,
end_line: 0,
end_char: 7,
},
context: None,
};
assert_eq!(super::usage_range_text(root, &u).unwrap(), "foo");
}
#[cfg(not(feature = "no-jail"))]
#[test]
fn usage_range_text_rejects_jail_escape() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_str().unwrap();
let u = crate::lsp::backend::UsageSite {
path: "../../etc/passwd".into(),
range: crate::lsp::backend::TextRange0Based {
start_line: 0,
start_char: 0,
end_line: 0,
end_char: 1,
},
context: None,
};
assert!(super::usage_range_text(root, &u).is_err());
}
#[test]
fn plan_hash_is_deterministic_and_order_independent() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.rs"), "let foo = 1;\nfoo + foo;\n").unwrap();
let root = dir.path().to_str().unwrap();
let u1 = crate::lsp::backend::UsageSite {
path: "a.rs".into(),
range: crate::lsp::backend::TextRange0Based {
start_line: 0,
start_char: 4,
end_line: 0,
end_char: 7,
},
context: Some("ignored-in-hash".into()),
};
let u2 = crate::lsp::backend::UsageSite {
path: "a.rs".into(),
range: crate::lsp::backend::TextRange0Based {
start_line: 1,
start_char: 0,
end_line: 1,
end_char: 3,
},
context: None,
};
let h1 = super::plan_hash(root, &[u1.clone(), u2.clone()]).unwrap();
let h2 = super::plan_hash(root, std::slice::from_ref(&u2)).unwrap(); let h3 = super::plan_hash(root, &[u2, u1]).unwrap(); assert_eq!(h1.len(), 64);
assert_eq!(h1, h3, "hash must be order-independent");
assert_ne!(h1, h2, "different usage set must differ");
}