use serde_json::Value;
use solidity_language_server::goto::CachedBuild;
use solidity_language_server::links;
use std::fs;
use tower_lsp::lsp_types::Url;
fn load_fixture() -> (Value, CachedBuild) {
let raw: Value =
serde_json::from_str(&fs::read_to_string("poolmanager.json").unwrap()).unwrap();
let ast = solidity_language_server::solc::normalize_solc_output(raw, None);
let build = CachedBuild::new(ast.clone(), 0, None);
(ast, build)
}
fn pm_key(ast: &Value) -> String {
ast["sources"]
.as_object()
.unwrap()
.keys()
.find(|k| k.ends_with("src/PoolManager.sol"))
.expect("fixture should contain PoolManager.sol")
.clone()
}
fn extract_imports<'a>(ast: &'a Value, key: &str) -> Vec<(&'a str, &'a str, &'a str)> {
let nodes = ast["sources"][key]["ast"]["nodes"].as_array().unwrap();
nodes
.iter()
.filter(|n| n["nodeType"].as_str() == Some("ImportDirective"))
.map(|n| {
(
n["src"].as_str().unwrap(),
n["file"].as_str().unwrap(),
n["absolutePath"].as_str().unwrap(),
)
})
.collect()
}
fn build_source_bytes(imports: &[(&str, &str, &str)]) -> Vec<u8> {
let last = imports.last().unwrap();
let parts: Vec<usize> = last.0.split(':').map(|p| p.parse().unwrap()).collect();
let total_len = parts[0] + parts[1] + 1;
let mut buf = vec![b' '; total_len];
let first_start: usize = imports[0].0.split(':').next().unwrap().parse().unwrap();
if first_start >= 3 {
let step = first_start / 3;
buf[step - 1] = b'\n';
buf[2 * step - 1] = b'\n';
buf[first_start - 1] = b'\n';
}
for (src, file, _abs) in imports {
let src_parts: Vec<usize> = src.split(':').map(|p| p.parse().unwrap()).collect();
let start = src_parts[0];
let length = src_parts[1];
let end = start + length;
let file_bytes = file.as_bytes();
let semicolon_pos = end - 1;
let close_quote_pos: usize = end - 2;
let file_start_pos = close_quote_pos - file_bytes.len();
let open_quote_pos = file_start_pos - 1;
if semicolon_pos < total_len {
buf[semicolon_pos] = b';';
}
if close_quote_pos < total_len {
buf[close_quote_pos] = b'"';
}
if open_quote_pos >= start && open_quote_pos < total_len {
buf[open_quote_pos] = b'"';
}
for (j, &b) in file_bytes.iter().enumerate() {
let pos = file_start_pos + j;
if pos < total_len {
buf[pos] = b;
}
}
if end < total_len {
buf[end] = b'\n';
}
}
buf
}
fn import_links(
links: &[tower_lsp::lsp_types::DocumentLink],
) -> Vec<&tower_lsp::lsp_types::DocumentLink> {
links.iter().filter(|l| l.tooltip.is_some()).collect()
}
#[test]
fn test_returns_link_for_every_import() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
let ilinks = import_links(&all_links);
assert_eq!(
ilinks.len(),
imports.len(),
"should return one import link per ImportDirective (expected {}, got {})",
imports.len(),
ilinks.len(),
);
}
#[test]
fn test_tooltips_match_absolute_paths() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
let ilinks = import_links(&all_links);
for (link, (_src, _file, abs_path)) in ilinks.iter().zip(imports.iter()) {
assert_eq!(
link.tooltip.as_deref(),
Some(*abs_path),
"tooltip should be the absolutePath"
);
}
}
#[test]
fn test_targets_are_file_uris() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
for link in &all_links {
let target = link.target.as_ref().expect("link should have a target");
assert_eq!(target.scheme(), "file", "target should be a file:// URI");
}
}
#[test]
fn test_import_range_length_matches_file_field() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
let ilinks = import_links(&all_links);
for (link, (_src, file, _abs)) in ilinks.iter().zip(imports.iter()) {
let char_span = link.range.end.character - link.range.start.character;
assert_eq!(
char_span as usize,
file.len(),
"range character span should equal file field length for '{}'",
file
);
}
}
#[test]
fn test_import_links_each_on_own_line() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
let ilinks = import_links(&all_links);
let mut seen_lines = std::collections::HashSet::new();
for link in &ilinks {
assert_eq!(
link.range.start.line, link.range.end.line,
"import link range should be single-line"
);
assert!(
seen_lines.insert(link.range.start.line),
"line {} appears in multiple import links",
link.range.start.line
);
}
}
#[test]
fn test_links_are_in_ascending_line_order() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
for pair in all_links.windows(2) {
assert!(
(pair[0].range.start.line, pair[0].range.start.character)
<= (pair[1].range.start.line, pair[1].range.start.character),
"links should be in ascending position order"
);
}
}
#[test]
fn test_first_import_hooks() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
let ilinks = import_links(&all_links);
let first = ilinks[0];
let tooltip = first.tooltip.as_deref().unwrap();
assert!(
tooltip.ends_with("src/libraries/Hooks.sol"),
"first import tooltip should end with src/libraries/Hooks.sol, got: {}",
tooltip
);
assert_eq!(first.range.start.line, 3);
assert_eq!(first.range.end.line, 3);
assert_eq!(first.range.end.character - first.range.start.character, 21);
}
#[test]
fn test_last_import_custom_revert() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
let ilinks = import_links(&all_links);
let last = ilinks.last().unwrap();
let tooltip = last.tooltip.as_deref().unwrap();
assert!(
tooltip.ends_with("src/libraries/CustomRevert.sol"),
"last import tooltip should end with src/libraries/CustomRevert.sol, got: {}",
tooltip
);
assert_eq!(last.range.start.line, 27);
assert_eq!(last.range.end.character - last.range.start.character, 28);
}
#[test]
fn test_reference_links_have_no_tooltip() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path(&key).unwrap();
let all_links = links::document_links(&build, &uri, &source_bytes);
let ref_links: Vec<_> = all_links.iter().filter(|l| l.tooltip.is_none()).collect();
for link in &ref_links {
assert!(link.tooltip.is_none());
let target = link.target.as_ref().unwrap();
assert_eq!(target.scheme(), "file");
}
}
#[test]
fn test_empty_sources() {
let build = CachedBuild::new(serde_json::json!({}), 0, None);
let source_bytes = b"";
let uri = Url::from_file_path("/tmp/Empty.sol").unwrap();
let result = links::document_links(&build, &uri, source_bytes);
assert!(result.is_empty());
}
#[test]
fn test_file_uri_mismatch_returns_empty() {
let (ast, build) = load_fixture();
let key = pm_key(&ast);
let imports = extract_imports(&ast, &key);
let source_bytes = build_source_bytes(&imports);
let uri = Url::from_file_path("/tmp/nonexistent.sol").unwrap();
let result = links::document_links(&build, &uri, &source_bytes);
assert!(result.is_empty());
}