use serde_json::Value;
use solidity_language_server::completion::{
AccessKind, DotSegment, build_completion_cache, extract_identifier_before_dot,
extract_mapping_value_type, extract_node_id_from_type, get_dot_completions,
get_general_completions, handle_completion, parse_dot_chain,
};
use solidity_language_server::types::{FileId, NodeId};
use std::fs;
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, CompletionResponse, CompletionTextEdit, Position,
};
fn load_ast() -> Value {
let raw: Value =
serde_json::from_str(&fs::read_to_string("poolmanager.json").unwrap()).unwrap();
solidity_language_server::solc::normalize_solc_output(raw, None)
}
fn load_cache() -> solidity_language_server::completion::CompletionCache {
let ast_data = load_ast();
let sources = ast_data.get("sources").unwrap();
let contracts = ast_data.get("contracts");
build_completion_cache(sources, contracts, None)
}
fn response_labels(resp: Option<CompletionResponse>) -> Vec<String> {
match resp {
Some(CompletionResponse::List(list)) => list.items.into_iter().map(|i| i.label).collect(),
Some(CompletionResponse::Array(items)) => items.into_iter().map(|i| i.label).collect(),
None => vec![],
}
}
fn signature_of(item: &CompletionItem) -> String {
let params = item
.label_details
.as_ref()
.and_then(|d| d.detail.as_deref())
.unwrap_or("");
format!("{}{}", item.label, params)
}
fn response_items(resp: Option<CompletionResponse>) -> Vec<CompletionItem> {
match resp {
Some(CompletionResponse::List(list)) => list.items,
Some(CompletionResponse::Array(items)) => items,
None => vec![],
}
}
#[test]
fn test_extract_node_id_from_struct_type() {
assert_eq!(
extract_node_id_from_type("t_struct$_PoolKey_$6871_storage_ptr"),
Some(NodeId(6871))
);
}
#[test]
fn test_extract_node_id_from_contract_type() {
assert_eq!(
extract_node_id_from_type("t_contract$_IHooks_$1840"),
Some(NodeId(1840))
);
}
#[test]
fn test_extract_node_id_from_user_defined_value_type() {
assert_eq!(
extract_node_id_from_type("t_userDefinedValueType$_Currency_$6525"),
Some(NodeId(6525))
);
}
#[test]
fn test_extract_node_id_from_primitive_type() {
assert_eq!(extract_node_id_from_type("t_uint256"), None);
assert_eq!(extract_node_id_from_type("t_address"), None);
assert_eq!(extract_node_id_from_type("t_bool"), None);
}
#[test]
fn test_extract_identifier_simple() {
assert_eq!(
extract_identifier_before_dot("key.", 4),
Some("key".to_string())
);
}
#[test]
fn test_extract_identifier_in_expression() {
assert_eq!(
extract_identifier_before_dot(" poolKey.", 16),
Some("poolKey".to_string())
);
}
#[test]
fn test_extract_identifier_at_start() {
assert_eq!(extract_identifier_before_dot(".", 1), None);
}
#[test]
fn test_extract_identifier_msg() {
assert_eq!(
extract_identifier_before_dot("msg.", 4),
Some("msg".to_string())
);
}
#[test]
fn test_parse_chain_partial_member_is_filter_prefix() {
let line = "obj.start";
let chain = parse_dot_chain(line, line.len() as u32);
assert_eq!(
chain,
vec![DotSegment {
name: "obj".to_string(),
kind: AccessKind::Plain,
call_args: None,
}]
);
assert!(solidity_language_server::completion::is_member_completion(
line,
line.len() as u32
));
}
#[test]
fn test_parse_chain_simple() {
let chain = parse_dot_chain("msg.", 4);
assert_eq!(
chain,
vec![DotSegment {
name: "msg".to_string(),
kind: AccessKind::Plain,
call_args: None,
}]
);
}
#[test]
fn test_parse_chain_function_call() {
let chain = parse_dot_chain("foo().", 6);
assert_eq!(
chain,
vec![DotSegment {
name: "foo".to_string(),
kind: AccessKind::Call,
call_args: None,
}]
);
}
#[test]
fn test_parse_chain_function_call_with_args() {
let chain = parse_dot_chain("swap(key, params).", 18);
assert_eq!(
chain,
vec![DotSegment {
name: "swap".to_string(),
kind: AccessKind::Call,
call_args: Some("key, params".to_string()),
}]
);
}
#[test]
fn test_parse_chain_nested_calls_in_args() {
let line = "swap(getKey(), getParams(x, y())).";
let chain = parse_dot_chain(line, line.len() as u32);
assert_eq!(
chain,
vec![DotSegment {
name: "swap".to_string(),
kind: AccessKind::Call,
call_args: Some("getKey(), getParams(x, y())".to_string()),
}]
);
}
#[test]
fn test_parse_chain_index_access() {
let chain = parse_dot_chain("_pools[poolId].", 15);
assert_eq!(
chain,
vec![DotSegment {
name: "_pools".to_string(),
kind: AccessKind::Index,
call_args: None,
}]
);
}
#[test]
fn test_parse_chain_index_with_nested_call() {
let line = "mapping[someFunc(a, b)].";
let chain = parse_dot_chain(line, line.len() as u32);
assert_eq!(
chain,
vec![DotSegment {
name: "mapping".to_string(),
kind: AccessKind::Index,
call_args: None,
}]
);
}
#[test]
fn test_parse_chain_two_segments() {
let line = "poolManager.swap().";
let chain = parse_dot_chain(line, line.len() as u32);
assert_eq!(
chain,
vec![
DotSegment {
name: "poolManager".to_string(),
kind: AccessKind::Plain,
call_args: None,
},
DotSegment {
name: "swap".to_string(),
kind: AccessKind::Call,
call_args: None,
},
]
);
}
#[test]
fn test_parse_chain_three_segments_mixed() {
let line = "_pools[poolId].slot0.";
let chain = parse_dot_chain(line, line.len() as u32);
assert_eq!(
chain,
vec![
DotSegment {
name: "_pools".to_string(),
kind: AccessKind::Index,
call_args: None,
},
DotSegment {
name: "slot0".to_string(),
kind: AccessKind::Plain,
call_args: None,
},
]
);
}
#[test]
fn test_parse_chain_call_then_index() {
let line = "getPool(key).positions[posId].";
let chain = parse_dot_chain(line, line.len() as u32);
assert_eq!(
chain,
vec![
DotSegment {
name: "getPool".to_string(),
kind: AccessKind::Call,
call_args: Some("key".to_string()),
},
DotSegment {
name: "positions".to_string(),
kind: AccessKind::Index,
call_args: None,
},
]
);
}
#[test]
fn test_mapping_value_simple() {
assert_eq!(
extract_mapping_value_type("t_mapping$_t_address_$_t_uint256_$"),
Some("t_uint256".to_string())
);
}
#[test]
fn test_mapping_value_struct() {
assert_eq!(
extract_mapping_value_type(
"t_mapping$_t_userDefinedValueType$_PoolId_$6825_$_t_struct$_State_$3870_storage_$"
),
Some("t_struct$_State_$3870_storage".to_string())
);
}
#[test]
fn test_mapping_value_nested() {
assert_eq!(
extract_mapping_value_type("t_mapping$_t_address_$_t_mapping$_t_uint256_$_t_uint256_$_$"),
Some("t_uint256".to_string())
);
}
#[test]
fn test_mapping_value_triple_nested() {
assert_eq!(
extract_mapping_value_type(
"t_mapping$_t_address_$_t_mapping$_t_address_$_t_mapping$_t_uint256_$_t_uint256_$_$_$"
),
Some("t_uint256".to_string())
);
}
#[test]
fn test_mapping_value_not_a_mapping() {
assert_eq!(
extract_mapping_value_type("t_uint256"),
Some("t_uint256".to_string())
);
}
#[test]
fn test_mapping_value_struct_with_node_id() {
let result =
extract_mapping_value_type("t_mapping$_t_int24_$_t_struct$_TickInfo_$3845_storage_$");
assert_eq!(result, Some("t_struct$_TickInfo_$3845_storage".to_string()));
assert_eq!(
extract_node_id_from_type(&result.unwrap()),
Some(NodeId(3845))
);
}
#[test]
fn test_general_completions_include_ast_names() {
let cache = load_cache();
let completions = get_general_completions(&cache);
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"PoolManager"), "Should have PoolManager");
assert!(names.contains(&"SwapMath"), "Should have SwapMath");
assert!(names.contains(&"PoolKey"), "Should have PoolKey");
assert!(
names.contains(&"getSqrtPriceTarget"),
"Should have getSqrtPriceTarget"
);
}
#[test]
fn test_general_completions_include_keywords() {
let cache = load_cache();
let completions = get_general_completions(&cache);
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(
names.contains(&"contract"),
"Should have 'contract' keyword"
);
assert!(
names.contains(&"function"),
"Should have 'function' keyword"
);
assert!(names.contains(&"if"), "Should have 'if' keyword");
assert!(names.contains(&"mapping"), "Should have 'mapping' keyword");
}
#[test]
fn test_context_completion_spdx_directive() {
let items = response_items(handle_completion(
None,
"// SPDX",
Position {
line: 0,
character: 7,
},
None,
None,
));
let labels: Vec<&str> = items.iter().map(|item| item.label.as_str()).collect();
let mit = items
.iter()
.find(|item| item.label == "// SPDX-License-Identifier: MIT")
.expect("MIT SPDX completion");
assert!(labels.contains(&"// SPDX-License-Identifier: MIT"));
assert!(!labels.contains(&"pragma"));
assert_eq!(mit.kind, Some(CompletionItemKind::CONSTANT));
assert_ne!(mit.kind, Some(CompletionItemKind::TEXT));
assert_eq!(
mit.filter_text.as_deref(),
Some("spdx // SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT")
);
}
#[test]
fn test_context_completion_spdx_directive_from_short_prefix() {
let labels = response_labels(handle_completion(
None,
"// SP",
Position {
line: 0,
character: 5,
},
None,
None,
));
assert!(labels.contains(&"// SPDX-License-Identifier: MIT".to_string()));
assert!(!labels.contains(&"selfdestruct(address payable recipient)".to_string()));
assert!(!labels.contains(&"super".to_string()));
}
#[test]
fn test_context_completion_spdx_directive_replaces_comment_prefix() {
let items = response_items(handle_completion(
None,
"// SP",
Position {
line: 0,
character: 5,
},
None,
None,
));
let mit = items
.iter()
.find(|item| item.label == "// SPDX-License-Identifier: MIT")
.expect("MIT SPDX completion");
let edit = match mit.text_edit.as_ref().expect("SPDX text edit") {
CompletionTextEdit::Edit(edit) => edit,
CompletionTextEdit::InsertAndReplace(_) => panic!("expected line replacement edit"),
};
assert_eq!(edit.range.start.line, 0);
assert_eq!(edit.range.start.character, 0);
assert_eq!(edit.range.end.line, 0);
assert_eq!(edit.range.end.character, 5);
assert_eq!(edit.new_text, "// SPDX-License-Identifier: MIT");
}
#[test]
fn test_context_completion_pragma_keyword() {
let labels = response_labels(handle_completion(
None,
"pragma ",
Position {
line: 0,
character: 7,
},
None,
None,
));
assert_eq!(labels, vec!["solidity".to_string(), "abicoder".to_string()]);
}
#[test]
fn test_context_completion_pragma_solidity_versions() {
let labels = response_labels(handle_completion(
None,
"pragma solidity ",
Position {
line: 0,
character: 16,
},
None,
None,
));
assert!(labels.contains(&"^0.8.35".to_string()));
assert!(labels.contains(&"^0.8.0".to_string()));
assert!(!labels.contains(&"revert()".to_string()));
assert!(!labels.contains(&"msg".to_string()));
}
#[test]
fn test_context_completion_pragma_directive_requires_token_boundary() {
let labels = response_labels(handle_completion(
None,
"pragma solidityx",
Position {
line: 0,
character: "pragma solidityx".len() as u32,
},
None,
None,
));
assert!(!labels.contains(&"^0.8.35".to_string()));
assert!(!labels.contains(&"^0.8.0".to_string()));
let labels = response_labels(handle_completion(
None,
"pragma abicoderx",
Position {
line: 0,
character: "pragma abicoderx".len() as u32,
},
None,
None,
));
assert!(!labels.contains(&"v2".to_string()));
assert!(!labels.contains(&"v1".to_string()));
}
#[test]
fn test_context_completion_pragma_solidity_version_replaces_partial_token() {
let source = "pragma solidity 0.* ^0.8.35;";
let items = response_items(handle_completion(
None,
source,
Position {
line: 0,
character: "pragma solidity 0.*".len() as u32,
},
None,
None,
));
let version = items
.iter()
.find(|item| item.label == "^0.8.35")
.expect("^0.8.35 completion");
let edit = match version.text_edit.as_ref().expect("version text edit") {
CompletionTextEdit::Edit(edit) => edit,
CompletionTextEdit::InsertAndReplace(_) => panic!("expected replacement edit"),
};
assert_eq!(edit.range.start.line, 0);
assert_eq!(edit.range.start.character, "pragma solidity ".len() as u32);
assert_eq!(edit.range.end.line, 0);
assert_eq!(
edit.range.end.character,
"pragma solidity 0.* ^0.8.35".len() as u32
);
assert_eq!(edit.new_text, "^0.8.35");
}
#[test]
fn test_context_completion_finished_pragma_is_empty() {
let source = "pragma solidity ^0.8.0;";
let labels = response_labels(handle_completion(
None,
source,
Position {
line: 0,
character: source.len() as u32,
},
None,
None,
));
assert!(labels.is_empty());
}
#[test]
fn test_general_completions_include_magic_globals() {
let cache = load_cache();
let completions = get_general_completions(&cache);
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"msg"), "Should have 'msg'");
assert!(names.contains(&"block"), "Should have 'block'");
assert!(names.contains(&"tx"), "Should have 'tx'");
assert!(names.contains(&"abi"), "Should have 'abi'");
}
#[test]
fn test_general_completions_include_global_functions() {
let cache = load_cache();
let completions = get_general_completions(&cache);
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(
names.contains(&"keccak256(bytes memory)"),
"Should have keccak256"
);
assert!(names.contains(&"gasleft()"), "Should have gasleft");
assert!(
names.contains(&"require(bool condition)"),
"Should have require"
);
}
#[test]
fn test_dot_completion_msg() {
let cache = load_cache();
let items = get_dot_completions(&cache, "msg", None);
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"sender"), "msg.sender");
assert!(names.contains(&"value"), "msg.value");
assert!(names.contains(&"data"), "msg.data");
assert!(names.contains(&"sig"), "msg.sig");
assert_eq!(names.len(), 4, "msg should have exactly 4 members");
}
#[test]
fn test_dot_completion_block() {
let cache = load_cache();
let items = get_dot_completions(&cache, "block", None);
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"number"), "block.number");
assert!(names.contains(&"timestamp"), "block.timestamp");
assert!(names.contains(&"chainid"), "block.chainid");
assert!(names.contains(&"basefee"), "block.basefee");
}
#[test]
fn test_dot_completion_tx() {
let cache = load_cache();
let items = get_dot_completions(&cache, "tx", None);
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"gasprice"), "tx.gasprice");
assert!(names.contains(&"origin"), "tx.origin");
assert_eq!(names.len(), 2, "tx should have exactly 2 members");
}
#[test]
fn test_dot_completion_abi() {
let cache = load_cache();
let items = get_dot_completions(&cache, "abi", None);
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
let sigs: Vec<String> = items.iter().map(signature_of).collect();
assert!(names.contains(&"encode"), "abi.encode");
assert!(names.contains(&"encodePacked"), "abi.encodePacked");
assert!(names.contains(&"decode"), "abi.decode");
assert!(sigs.contains(&"encode(...)".to_string()));
assert!(sigs.contains(&"decode(bytes memory, (...))".to_string()));
}
#[test]
fn test_dot_completion_on_struct() {
let cache = load_cache();
let pool_key_vars: Vec<&str> = cache
.name_to_type
.iter()
.filter(|(_, tid)| tid.starts_with("t_struct$_PoolKey"))
.map(|(name, _)| name.as_str())
.collect();
assert!(
!pool_key_vars.is_empty(),
"Should have variables with PoolKey type, name_to_type has {} entries",
cache.name_to_type.len()
);
let var_name = pool_key_vars[0];
let items = get_dot_completions(&cache, var_name, None);
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
assert!(
names.contains(&"currency0"),
"PoolKey should have currency0 member, got: {:?}",
names
);
assert!(
names.contains(&"fee"),
"PoolKey should have fee member, got: {:?}",
names
);
assert!(
names.contains(&"hooks"),
"PoolKey should have hooks member, got: {:?}",
names
);
}
#[test]
fn test_name_to_type_populated() {
let cache = load_cache();
assert!(
!cache.name_to_type.is_empty(),
"name_to_type should not be empty"
);
assert!(
cache.name_to_type.values().any(|v| v.contains("PoolKey")),
"Should have at least one PoolKey-typed variable"
);
}
#[test]
fn test_node_members_populated() {
let cache = load_cache();
assert!(
!cache.node_members.is_empty(),
"node_members should not be empty"
);
assert!(
cache.node_members.contains_key(&NodeId(6871)),
"Should have members for PoolKey (6871)"
);
let pool_key_members = &cache.node_members[&NodeId(6871)];
let member_names: Vec<&str> = pool_key_members.iter().map(|c| c.label.as_str()).collect();
assert_eq!(
member_names.len(),
5,
"PoolKey should have 5 members: {:?}",
member_names
);
assert!(member_names.contains(&"currency0"));
assert!(member_names.contains(&"currency1"));
assert!(member_names.contains(&"fee"));
assert!(member_names.contains(&"tickSpacing"));
assert!(member_names.contains(&"hooks"));
}
#[test]
fn test_dot_completion_fullmath_library() {
let cache = load_cache();
let fullmath_type = cache.name_to_type.get("FullMath");
eprintln!("FullMath type: {:?}", fullmath_type);
let items = get_dot_completions(&cache, "FullMath", None);
let labels: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
eprintln!("FullMath dot completions ({}): {:?}", labels.len(), labels);
assert!(
labels.contains(&"mulDiv"),
"Should have mulDiv, got: {:?}",
labels
);
assert!(
labels.contains(&"mulDivRoundingUp"),
"Should have mulDivRoundingUp, got: {:?}",
labels
);
}
#[test]
fn test_method_identifier_swap_has_description_with_param_names() {
let cache = load_cache();
let ipm_methods = &cache.method_identifiers[&NodeId(2123)];
let swap_item = ipm_methods.iter().find(|c| c.label == "swap").unwrap();
let desc = swap_item
.label_details
.as_ref()
.unwrap()
.description
.as_deref()
.expect("swap should have a description with parameter names");
eprintln!("swap description: {}", desc);
assert!(
desc.contains("key"),
"description should contain param name 'key': {}",
desc
);
assert!(
desc.contains("params"),
"description should contain param name 'params': {}",
desc
);
assert!(
desc.contains("hookData"),
"description should contain param name 'hookData': {}",
desc
);
assert!(
desc.contains("returns"),
"description should show return type: {}",
desc
);
assert!(
desc.contains("swapDelta"),
"description should contain return name 'swapDelta': {}",
desc
);
}
#[test]
fn test_method_identifier_settle_has_description_no_params() {
let cache = load_cache();
let pm_methods = &cache.method_identifiers[&NodeId(1216)];
let settle_item = pm_methods.iter().find(|c| c.label == "settle").unwrap();
let desc = settle_item
.label_details
.as_ref()
.unwrap()
.description
.as_deref()
.expect("settle should have a description");
eprintln!("settle description: {}", desc);
assert!(
desc.contains("settle()"),
"settle should show empty params: {}",
desc
);
}
#[test]
fn test_method_identifier_initialize_has_return_type() {
let cache = load_cache();
let ipm_methods = &cache.method_identifiers[&NodeId(2123)];
let init_item = ipm_methods
.iter()
.find(|c| c.label == "initialize")
.unwrap();
let desc = init_item
.label_details
.as_ref()
.unwrap()
.description
.as_deref()
.expect("initialize should have a description");
eprintln!("initialize description: {}", desc);
assert!(
desc.contains("key"),
"should have param name 'key': {}",
desc
);
assert!(
desc.contains("sqrtPriceX96"),
"should have param name 'sqrtPriceX96': {}",
desc
);
assert!(desc.contains("returns"), "should show returns: {}", desc);
assert!(
desc.contains("tick"),
"should contain return name 'tick': {}",
desc
);
}
#[test]
fn test_node_members_function_has_signature_as_detail() {
let cache = load_cache();
let fm_members = &cache.node_members[&NodeId(8913)];
let mul_div = fm_members.iter().find(|c| c.label == "mulDiv").unwrap();
eprintln!("mulDiv detail: {:?}", mul_div.detail);
let detail = mul_div
.detail
.as_deref()
.expect("mulDiv should have detail");
assert!(
detail.contains("mulDiv("),
"detail should be a signature: {}",
detail
);
assert!(
detail.contains("uint256"),
"should contain uint256 params: {}",
detail
);
}
#[test]
fn test_method_identifiers_populated() {
let cache = load_cache();
assert!(
!cache.method_identifiers.is_empty(),
"method_identifiers should not be empty"
);
assert!(
cache.method_identifiers.contains_key(&NodeId(1216)),
"Should have method identifiers for PoolManager (1767), keys: {:?}",
cache.method_identifiers.keys().collect::<Vec<_>>()
);
assert!(
cache.method_identifiers.contains_key(&NodeId(2123)),
"Should have method identifiers for IPoolManager (2531)"
);
}
#[test]
fn test_overloaded_extsload_on_extsload_contract() {
let cache = load_cache();
let ext_methods = &cache.method_identifiers[&NodeId(1332)];
let extsload_items: Vec<_> = ext_methods
.iter()
.filter(|c| c.label == "extsload")
.collect();
assert_eq!(extsload_items.len(), 3);
let descriptions: Vec<Option<&str>> = extsload_items
.iter()
.map(|c| {
c.label_details
.as_ref()
.and_then(|ld| ld.description.as_deref())
})
.collect();
eprintln!("extsload descriptions: {:?}", descriptions);
for (i, desc) in descriptions.iter().enumerate() {
assert!(
desc.is_some(),
"extsload overload {} should have a description",
i
);
}
}
#[test]
fn test_inherited_functions_have_no_description() {
let cache = load_cache();
let pm_methods = &cache.method_identifiers[&NodeId(1216)];
let extsload_items: Vec<_> = pm_methods
.iter()
.filter(|c| c.label == "extsload")
.collect();
for item in &extsload_items {
let desc = item
.label_details
.as_ref()
.and_then(|ld| ld.description.as_deref());
assert!(
desc.is_none(),
"Inherited extsload on PoolManager should have no description (it's on Extsload base), got: {:?}",
desc
);
}
for item in &extsload_items {
assert!(
item.detail.is_some(),
"Should still have ABI signature as detail"
);
}
}
#[test]
fn test_method_identifiers_have_function_names() {
let cache = load_cache();
let pm_methods = &cache.method_identifiers[&NodeId(1216)];
let labels: Vec<&str> = pm_methods.iter().map(|c| c.label.as_str()).collect();
assert!(
labels.contains(&"swap"),
"Should have swap, got: {:?}",
labels
);
assert!(labels.contains(&"initialize"), "Should have initialize");
assert!(labels.contains(&"settle"), "Should have settle");
assert!(labels.contains(&"take"), "Should have take");
assert!(labels.contains(&"donate"), "Should have donate");
assert!(labels.contains(&"unlock"), "Should have unlock");
assert!(labels.contains(&"owner"), "Should have owner");
}
#[test]
fn test_method_identifiers_have_full_signatures_as_detail() {
let cache = load_cache();
let pm_methods = &cache.method_identifiers[&NodeId(1216)];
let swap_item = pm_methods.iter().find(|c| c.label == "swap").unwrap();
assert_eq!(
swap_item.detail.as_deref(),
Some("swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes)"),
"swap detail should be the full signature"
);
let settle_item = pm_methods.iter().find(|c| c.label == "settle").unwrap();
assert_eq!(
settle_item.detail.as_deref(),
Some("settle()"),
"settle detail should be settle()"
);
}
#[test]
fn test_method_identifiers_have_selectors() {
let cache = load_cache();
let pm_methods = &cache.method_identifiers[&NodeId(1216)];
let swap_item = pm_methods.iter().find(|c| c.label == "swap").unwrap();
let label_details = swap_item.label_details.as_ref().unwrap();
assert_eq!(
label_details.detail.as_deref(),
Some("0xf3cd914c"),
"swap selector should be 0xf3cd914c"
);
let settle_item = pm_methods.iter().find(|c| c.label == "settle").unwrap();
let settle_details = settle_item.label_details.as_ref().unwrap();
assert_eq!(
settle_details.detail.as_deref(),
Some("0x11da60b4"),
"settle selector should be 0x11da60b4"
);
}
#[test]
fn test_method_identifiers_are_function_kind() {
let cache = load_cache();
let pm_methods = &cache.method_identifiers[&NodeId(1216)];
for item in pm_methods {
assert_eq!(
item.kind,
Some(CompletionItemKind::FUNCTION),
"{} should be FUNCTION kind",
item.label
);
}
}
#[test]
fn test_method_identifiers_handles_overloads() {
let cache = load_cache();
let pm_methods = &cache.method_identifiers[&NodeId(1216)];
let extsload_items: Vec<_> = pm_methods
.iter()
.filter(|c| c.label == "extsload")
.collect();
assert_eq!(
extsload_items.len(),
3,
"Should have 3 extsload overloads, got {}",
extsload_items.len()
);
let selectors: Vec<&str> = extsload_items
.iter()
.map(|c| c.label_details.as_ref().unwrap().detail.as_deref().unwrap())
.collect();
assert!(selectors.contains(&"0x1e2eaeaf"));
assert!(selectors.contains(&"0x35fd631a"));
assert!(selectors.contains(&"0xdbd035ff"));
}
#[test]
fn test_method_identifiers_ipool_manager_interface() {
let cache = load_cache();
let ipm_methods = &cache.method_identifiers[&NodeId(2123)];
let labels: Vec<&str> = ipm_methods.iter().map(|c| c.label.as_str()).collect();
assert_eq!(ipm_methods.len(), 30, "IPoolManager should have 30 methods");
assert!(labels.contains(&"swap"), "Interface should have swap");
assert!(
labels.contains(&"modifyLiquidity"),
"Interface should have modifyLiquidity"
);
}
#[test]
fn test_dot_completion_supplements_method_identifiers_with_node_members() {
let cache = load_cache();
let has_methods = cache.method_identifiers.contains_key(&NodeId(1216));
let has_members = cache.node_members.contains_key(&NodeId(1216));
assert!(has_methods, "PoolManager should have method_identifiers");
assert!(has_members, "PoolManager should have node_members");
let method_labels: std::collections::HashSet<&str> = cache.method_identifiers[&NodeId(1216)]
.iter()
.map(|c| c.label.as_str())
.collect();
let member_only: Vec<&str> = cache.node_members[&NodeId(1216)]
.iter()
.map(|c| c.label.as_str())
.filter(|l| !method_labels.contains(l))
.collect();
assert!(
!member_only.is_empty(),
"node_members should have entries not in method_identifiers (events, errors, etc.), method labels: {:?}",
method_labels
);
}
#[test]
fn test_cache_without_contracts_has_empty_method_identifiers() {
let ast_data = load_ast();
let sources = ast_data.get("sources").unwrap();
let cache = build_completion_cache(sources, None, None);
assert!(
cache.method_identifiers.is_empty(),
"Without contracts, method_identifiers should be empty"
);
assert!(
!cache.node_members.is_empty(),
"node_members should still be populated from AST"
);
}
#[test]
fn test_function_return_types_pool_manager_swap() {
let cache = load_cache();
let ret = cache
.function_return_types
.get(&(NodeId(1216), "swap".to_string()));
assert_eq!(
ret,
Some(&"t_userDefinedValueType$_BalanceDelta_$6311".to_string()),
"PoolManager.swap should return BalanceDelta"
);
}
#[test]
fn test_function_return_types_pool_manager_internal_swap() {
let cache = load_cache();
let ret = cache
.function_return_types
.get(&(NodeId(1216), "_swap".to_string()));
assert_eq!(
ret,
Some(&"t_userDefinedValueType$_BalanceDelta_$6311".to_string()),
"_swap should also return BalanceDelta"
);
}
#[test]
fn test_function_return_types_ipool_manager_interface() {
let cache = load_cache();
let ret = cache
.function_return_types
.get(&(NodeId(2123), "swap".to_string()));
assert_eq!(
ret,
Some(&"t_userDefinedValueType$_BalanceDelta_$6311".to_string()),
"IPoolManager.swap should return BalanceDelta"
);
}
#[test]
fn test_function_return_types_pool_manager_initialize() {
let cache = load_cache();
let ret = cache
.function_return_types
.get(&(NodeId(2123), "initialize".to_string()));
assert_eq!(
ret,
Some(&"t_int24".to_string()),
"IPoolManager.initialize should return int24"
);
}
#[test]
fn test_function_return_types_get_pool() {
let cache = load_cache();
let ret = cache
.function_return_types
.get(&(NodeId(1216), "_getPool".to_string()));
assert_eq!(
ret,
Some(&"t_struct$_State_$3870_storage_ptr".to_string()),
"_getPool should return Pool.State"
);
}
#[test]
fn test_function_return_types_populated_count() {
let cache = load_cache();
assert!(
cache.function_return_types.len() > 50,
"Should have many function return types, got {}",
cache.function_return_types.len()
);
}
#[test]
fn test_using_for_balance_delta_has_amount_functions() {
let cache = load_cache();
let bd_type = "t_userDefinedValueType$_BalanceDelta_$6311";
let items = cache.using_for.get(bd_type);
assert!(
items.is_some(),
"Should have using-for entries for BalanceDelta"
);
let labels: Vec<&str> = items.unwrap().iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"amount0"),
"BalanceDelta should have amount0"
);
assert!(
labels.contains(&"amount1"),
"BalanceDelta should have amount1"
);
}
#[test]
fn test_using_for_ihooks_has_hook_functions() {
let cache = load_cache();
let hooks_type = "t_contract$_IHooks_$1840";
let items = cache.using_for.get(hooks_type);
assert!(items.is_some(), "Should have using-for entries for IHooks");
let labels: Vec<&str> = items.unwrap().iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"beforeSwap"),
"IHooks should have beforeSwap"
);
assert!(
labels.contains(&"afterSwap"),
"IHooks should have afterSwap"
);
assert!(
labels.contains(&"hasPermission"),
"IHooks should have hasPermission"
);
}
#[test]
fn test_using_for_pool_state_has_pool_functions() {
let cache = load_cache();
let state_type = "t_struct$_State_$3870_storage_ptr";
let items = cache.using_for.get(state_type);
assert!(
items.is_some(),
"Should have using-for entries for Pool.State"
);
let labels: Vec<&str> = items.unwrap().iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"initialize"),
"Pool.State should have initialize"
);
assert!(labels.contains(&"swap"), "Pool.State should have swap");
assert!(labels.contains(&"donate"), "Pool.State should have donate");
assert!(
labels.contains(&"modifyLiquidity"),
"Pool.State should have modifyLiquidity"
);
}
#[test]
fn test_using_for_slot0_has_accessor_functions() {
let cache = load_cache();
let slot0_type = "t_userDefinedValueType$_Slot0_$8632";
let items = cache.using_for.get(slot0_type);
assert!(items.is_some(), "Should have using-for entries for Slot0");
let labels: Vec<&str> = items.unwrap().iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"sqrtPriceX96"),
"Slot0 should have sqrtPriceX96"
);
assert!(labels.contains(&"tick"), "Slot0 should have tick");
assert!(labels.contains(&"lpFee"), "Slot0 should have lpFee");
}
#[test]
fn test_using_for_uint256_has_safecast() {
let cache = load_cache();
let uint256_type = "t_uint256";
let items = cache.using_for.get(uint256_type);
assert!(items.is_some(), "Should have using-for entries for uint256");
let labels: Vec<&str> = items.unwrap().iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"toUint160"),
"uint256 should have SafeCast.toUint160"
);
assert!(
labels.contains(&"toInt256"),
"uint256 should have SafeCast.toInt256"
);
}
#[test]
fn test_using_for_wildcard_populated() {
let cache = load_cache();
assert!(
!cache.using_for_wildcard.is_empty(),
"using_for_wildcard should not be empty"
);
let labels: Vec<&str> = cache
.using_for_wildcard
.iter()
.map(|i| i.label.as_str())
.collect();
assert!(
labels.contains(&"toUint160") || labels.contains(&"toInt128"),
"Wildcards should include SafeCast functions"
);
}
#[test]
fn test_using_for_pool_key_has_to_id() {
let cache = load_cache();
let pk_type = "t_struct$_PoolKey_$6871_storage_ptr";
let items = cache.using_for.get(pk_type);
assert!(items.is_some(), "Should have using-for entries for PoolKey");
let labels: Vec<&str> = items.unwrap().iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"toId"),
"PoolKey should have toId from PoolIdLibrary"
);
}
use solidity_language_server::completion::get_chain_completions;
#[test]
fn test_chain_single_plain_contract_name() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "PoolManager".to_string(),
kind: AccessKind::Plain,
call_args: None,
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"swap"), "PoolManager. should show swap");
assert!(
labels.contains(&"initialize"),
"PoolManager. should show initialize"
);
}
#[test]
fn test_chain_single_index_mapping_access() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "_pools".to_string(),
kind: AccessKind::Index,
call_args: None,
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"slot0"), "_pools[id]. should show slot0");
assert!(
labels.contains(&"liquidity"),
"_pools[id]. should show liquidity"
);
assert!(labels.contains(&"ticks"), "_pools[id]. should show ticks");
assert!(
labels.contains(&"positions"),
"_pools[id]. should show positions"
);
assert!(
labels.contains(&"initialize"),
"_pools[id]. should show Pool.initialize"
);
assert!(
labels.contains(&"swap"),
"_pools[id]. should show Pool.swap"
);
assert!(
labels.contains(&"donate"),
"_pools[id]. should show Pool.donate"
);
assert_eq!(
items.len(),
labels.len(),
"All items should have unique labels (no duplicates)"
);
let unique: std::collections::HashSet<&str> = labels.iter().copied().collect();
assert_eq!(unique.len(), labels.len(), "All labels should be unique");
}
#[test]
fn test_chain_two_segment_contract_function_call() {
let cache = load_cache();
let chain = vec![
DotSegment {
name: "PoolManager".to_string(),
kind: AccessKind::Plain,
call_args: None,
},
DotSegment {
name: "swap".to_string(),
kind: AccessKind::Call,
call_args: None,
},
];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"amount0"),
"PoolManager.swap(). should show amount0"
);
assert!(
labels.contains(&"amount1"),
"PoolManager.swap(). should show amount1"
);
}
#[test]
fn test_chain_two_segment_contract_get_pool_call() {
let cache = load_cache();
let chain = vec![
DotSegment {
name: "PoolManager".to_string(),
kind: AccessKind::Plain,
call_args: None,
},
DotSegment {
name: "_getPool".to_string(),
kind: AccessKind::Call,
call_args: None,
},
];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"slot0"),
"PoolManager._getPool(). should show slot0"
);
assert!(
labels.contains(&"liquidity"),
"PoolManager._getPool(). should show liquidity"
);
assert!(
labels.contains(&"ticks"),
"PoolManager._getPool(). should show ticks"
);
assert!(
labels.contains(&"initialize"),
"PoolManager._getPool(). should show Pool.initialize"
);
}
#[test]
fn test_chain_variable_with_type_then_call() {
let cache = load_cache();
assert!(
cache.name_to_type.contains_key("pool"),
"pool should be in name_to_type"
);
let chain = vec![
DotSegment {
name: "pool".to_string(),
kind: AccessKind::Plain,
call_args: None,
},
DotSegment {
name: "swap".to_string(),
kind: AccessKind::Call,
call_args: None,
},
];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
if !labels.is_empty() {
assert!(
labels.contains(&"amount0"),
"pool.swap(). should show amount0 if resolved"
);
}
}
#[test]
fn test_chain_mapping_value_type_extraction() {
let pools_type =
"t_mapping$_t_userDefinedValueType$_PoolId_$6825_$_t_struct$_State_$3870_storage_$";
let val_type = extract_mapping_value_type(pools_type);
assert_eq!(
val_type,
Some("t_struct$_State_$3870_storage".to_string()),
"Mapping value type should be Pool.State"
);
}
#[test]
fn test_chain_nested_mapping_value_extraction() {
let nested_mapping =
"t_mapping$_t_bytes32_$_t_mapping$_t_bytes32_$_t_struct$_State_$5433_storage_$_$";
let val_type = extract_mapping_value_type(nested_mapping);
assert!(
val_type.is_some(),
"Should extract innermost value type from nested mapping"
);
let val = val_type.unwrap();
assert!(
val.contains("5433"),
"Innermost type should reference Position.State (5433), got: {}",
val
);
}
#[test]
fn test_chain_completions_deduplication() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "_pools".to_string(),
kind: AccessKind::Index,
call_args: None,
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
let unique: std::collections::HashSet<&str> = labels.iter().copied().collect();
assert_eq!(
labels.len(),
unique.len(),
"Completions should not have duplicate labels. Duplicates: {:?}",
labels
.iter()
.filter(|l| labels.iter().filter(|l2| l2 == l).count() > 1)
.collect::<Vec<_>>()
);
}
#[test]
fn test_chain_ipool_manager_initialize_returns_int24() {
let cache = load_cache();
let chain = vec![
DotSegment {
name: "IPoolManager".to_string(),
kind: AccessKind::Plain,
call_args: None,
},
DotSegment {
name: "initialize".to_string(),
kind: AccessKind::Call,
call_args: None,
},
];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
if !labels.is_empty() {
assert!(
labels.contains(&"toInt128") || labels.contains(&"toUint160"),
"int24 return should get SafeCast wildcard functions"
);
}
}
#[test]
fn test_chain_pool_key_to_id() {
let cache = load_cache();
assert!(
cache.name_to_type.contains_key("poolKey"),
"poolKey should be in name_to_type"
);
let chain = vec![DotSegment {
name: "poolKey".to_string(),
kind: AccessKind::Plain,
call_args: None,
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"currency0"),
"poolKey. should show currency0"
);
assert!(
labels.contains(&"currency1"),
"poolKey. should show currency1"
);
assert!(labels.contains(&"fee"), "poolKey. should show fee");
assert!(
labels.contains(&"tickSpacing"),
"poolKey. should show tickSpacing"
);
assert!(labels.contains(&"hooks"), "poolKey. should show hooks");
assert!(
labels.contains(&"toId"),
"poolKey. should show toId from PoolIdLibrary"
);
}
#[test]
fn test_chain_empty_returns_nothing() {
let cache = load_cache();
let items = get_chain_completions(&cache, &[], None);
assert!(items.is_empty(), "Empty chain should return no completions");
}
#[test]
fn test_chain_unknown_name_returns_nothing() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "nonexistentVariable".to_string(),
kind: AccessKind::Plain,
call_args: None,
}];
let items = get_chain_completions(&cache, &chain, None);
assert!(
items.is_empty(),
"Unknown name should return no completions"
);
}
#[test]
fn test_parse_chain_type_cast_expression() {
let line = " IUnlockCallback(msg.sender).unlockCallback(data).";
let col = line.len() as u32;
let chain = parse_dot_chain(line, col);
assert_eq!(chain.len(), 2);
assert_eq!(chain[0].name, "IUnlockCallback");
assert_eq!(chain[0].kind, AccessKind::Call);
assert_eq!(chain[1].name, "unlockCallback");
assert_eq!(chain[1].kind, AccessKind::Call);
}
#[test]
fn test_chain_type_cast_shows_interface_members() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "IUnlockCallback".to_string(),
kind: AccessKind::Call,
call_args: None,
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"unlockCallback"),
"IUnlockCallback(addr). should show unlockCallback, got: {:?}",
labels
);
}
#[test]
fn test_chain_type_cast_then_method_returns_bytes_completions() {
let cache = load_cache();
let chain = vec![
DotSegment {
name: "IUnlockCallback".to_string(),
kind: AccessKind::Call,
call_args: None,
},
DotSegment {
name: "unlockCallback".to_string(),
kind: AccessKind::Call,
call_args: None,
},
];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!items.is_empty(),
"IUnlockCallback(msg.sender).unlockCallback(data). should return completions"
);
assert!(
labels.contains(&"parseSelector"),
"bytes return should show parseSelector from using-for, got: {:?}",
labels
);
}
#[test]
fn test_general_completions_include_blobhash() {
let cache = load_cache();
let completions = get_general_completions(&cache);
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(
names.contains(&"blobhash(uint256 index)"),
"Should have blobhash"
);
}
#[test]
fn test_general_completions_include_ether_units() {
let cache = load_cache();
let completions = get_general_completions(&cache);
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"wei"), "Should have wei");
assert!(names.contains(&"gwei"), "Should have gwei");
assert!(names.contains(&"ether"), "Should have ether");
let wei = completions.iter().find(|c| c.label == "wei").unwrap();
assert_eq!(wei.kind, Some(CompletionItemKind::UNIT));
assert_eq!(wei.detail.as_deref(), Some("1"));
let ether = completions.iter().find(|c| c.label == "ether").unwrap();
assert_eq!(ether.detail.as_deref(), Some("1e18"));
}
#[test]
fn test_general_completions_include_time_units() {
let cache = load_cache();
let completions = get_general_completions(&cache);
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"seconds"), "Should have seconds");
assert!(names.contains(&"minutes"), "Should have minutes");
assert!(names.contains(&"hours"), "Should have hours");
assert!(names.contains(&"days"), "Should have days");
assert!(names.contains(&"weeks"), "Should have weeks");
let days = completions.iter().find(|c| c.label == "days").unwrap();
assert_eq!(days.kind, Some(CompletionItemKind::UNIT));
assert_eq!(days.detail.as_deref(), Some("86400 seconds"));
}
#[test]
fn test_dot_completion_type_members() {
let cache = load_cache();
let items = get_dot_completions(&cache, "type", None);
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"name"), "type(C).name");
assert!(names.contains(&"creationCode"), "type(C).creationCode");
assert!(names.contains(&"runtimeCode"), "type(C).runtimeCode");
assert!(names.contains(&"interfaceId"), "type(I).interfaceId");
assert!(names.contains(&"min"), "type(T).min");
assert!(names.contains(&"max"), "type(T).max");
}
#[test]
fn test_dot_completion_bytes_concat() {
let cache = load_cache();
let items = get_dot_completions(&cache, "bytes", None);
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"concat"), "bytes.concat");
let concat = items.iter().find(|c| c.label == "concat").unwrap();
assert_eq!(signature_of(concat), "concat(...)");
assert_eq!(concat.detail.as_deref(), Some("bytes memory"));
}
#[test]
fn test_dot_completion_string_concat() {
let cache = load_cache();
let items = get_dot_completions(&cache, "string", None);
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"concat"), "string.concat");
let concat = items.iter().find(|c| c.label == "concat").unwrap();
assert_eq!(signature_of(concat), "concat(...)");
assert_eq!(concat.detail.as_deref(), Some("string memory"));
}
#[test]
fn test_dot_completion_abi_full_signatures() {
let cache = load_cache();
let items = get_dot_completions(&cache, "abi", None);
let sigs: Vec<String> = items.iter().map(signature_of).collect();
let names: Vec<&str> = sigs.iter().map(String::as_str).collect();
assert!(
names.contains(&"encodeWithSelector(bytes4, ...)"),
"abi.encodeWithSelector with selector param"
);
assert!(
names.contains(&"encodeWithSignature(string memory, ...)"),
"abi.encodeWithSignature with signature param"
);
assert!(
names.contains(&"encodeCall(function, (...))"),
"abi.encodeCall with function pointer"
);
}
#[test]
fn test_general_completions_include_common_int_types() {
let cache = load_cache();
let completions = get_general_completions(&cache);
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"uint8"), "Should have uint8");
assert!(names.contains(&"uint128"), "Should have uint128");
assert!(names.contains(&"uint160"), "Should have uint160");
assert!(names.contains(&"uint256"), "Should have uint256");
assert!(names.contains(&"int24"), "Should have int24");
assert!(names.contains(&"int128"), "Should have int128");
assert!(names.contains(&"int256"), "Should have int256");
assert!(names.contains(&"bytes1"), "Should have bytes1");
assert!(names.contains(&"bytes4"), "Should have bytes4");
}
use solidity_language_server::completion::{ScopeContext, resolve_name_in_scope};
#[test]
fn test_scope_declarations_populated() {
let cache = load_cache();
assert!(
!cache.scope_declarations.is_empty(),
"scope_declarations should be populated"
);
}
#[test]
fn test_scope_parent_populated() {
let cache = load_cache();
assert!(
!cache.scope_parent.is_empty(),
"scope_parent should be populated"
);
}
#[test]
fn test_scope_ranges_populated() {
let cache = load_cache();
assert!(
!cache.scope_ranges.is_empty(),
"scope_ranges should be populated"
);
for pair in cache.scope_ranges.windows(2) {
let span_a = pair[0].end - pair[0].start;
let span_b = pair[1].end - pair[1].start;
assert!(
span_a <= span_b,
"scope_ranges should be sorted by span size ascending"
);
}
}
#[test]
fn test_path_to_file_id_populated() {
let cache = load_cache();
assert!(
!cache.path_to_file_id.is_empty(),
"path_to_file_id should be populated"
);
}
#[test]
fn test_scope_resolve_self_in_pool_swap_is_pool_state() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "self", 12300, FileId(28));
assert_eq!(
result,
Some("t_struct$_State_$3870_storage_ptr".to_string()),
"self inside Pool.swap should be Pool.State storage"
);
}
#[test]
fn test_scope_resolve_self_in_hooks_is_ihooks() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "self", 3650, FileId(22));
assert_eq!(
result,
Some("t_contract$_IHooks_$1840".to_string()),
"self inside Hooks library should be IHooks"
);
}
#[test]
fn test_scope_resolve_key_in_pool_manager_swap_is_pool_key() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "key", 9500, FileId(5));
assert_eq!(
result,
Some("t_struct$_PoolKey_$6871_memory_ptr".to_string()),
"key inside PoolManager.swap should be PoolKey memory"
);
}
#[test]
fn test_scope_resolve_walks_up_to_contract_state_var() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "owner", 1050, FileId(44));
assert_eq!(
result,
Some("t_address".to_string()),
"owner inside Owned constructor should resolve to state variable (address)"
);
}
#[test]
fn test_scope_resolve_unknown_name_falls_back() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "PoolManager", 9500, FileId(5));
assert!(
result.is_some(),
"Contract names should be resolved via fallback"
);
}
#[test]
fn test_scope_chain_completions_with_context() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "self".to_string(),
kind: AccessKind::Plain,
call_args: None,
}];
let ctx = ScopeContext {
byte_pos: 12300,
file_id: FileId(28),
};
let items = get_chain_completions(&cache, &chain, Some(&ctx));
let names: Vec<&str> = items.iter().map(|c| c.label.as_str()).collect();
assert!(
names.contains(&"slot0") || names.contains(&"liquidity"),
"self. inside Pool.swap should show Pool.State members, got: {:?}",
names
);
}
#[test]
fn test_scope_chain_completions_without_context_uses_flat() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "self".to_string(),
kind: AccessKind::Plain,
call_args: None,
}];
let items_no_scope = get_chain_completions(&cache, &chain, None);
assert!(
!items_no_scope.is_empty() || items_no_scope.is_empty(),
"Should handle None scope context gracefully"
);
}
#[test]
fn test_linearized_base_contracts_populated() {
let cache = load_cache();
assert!(
!cache.linearized_base_contracts.is_empty(),
"linearized_base_contracts should be populated"
);
let pm_bases = cache.linearized_base_contracts.get(&NodeId(1216));
assert!(
pm_bases.is_some(),
"PoolManager should have linearized base contracts"
);
let bases = pm_bases.unwrap();
assert!(
bases.len() > 1,
"PoolManager should inherit from multiple contracts"
);
assert_eq!(
bases[0],
NodeId(1216),
"First base should be the contract itself"
);
assert!(
bases.contains(&NodeId(7455)),
"PoolManager should inherit from Owned (id=59), got: {:?}",
bases
);
}
#[test]
fn test_scope_resolve_inherited_state_var_owner_in_pool_manager() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "owner", 9500, FileId(5));
assert_eq!(
result,
Some("t_address".to_string()),
"owner inside PoolManager.swap should resolve to inherited state var from Owned"
);
}
#[test]
fn test_scope_resolve_inherited_protocol_fee_controller() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "protocolFeeController", 9500, FileId(5));
assert_eq!(
result,
Some("t_address".to_string()),
"protocolFeeController inside PoolManager.swap should resolve to inherited state var from ProtocolFees"
);
}
#[test]
fn test_scope_resolve_own_state_var_still_works() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "_pools", 9500, FileId(5));
assert!(
result.is_some(),
"_pools inside PoolManager.swap should resolve to PoolManager's own state variable"
);
}
fn collect_all_nodes(ast: &Value) -> Vec<&Value> {
let mut result = Vec::new();
let mut stack = vec![ast];
while let Some(node) = stack.pop() {
match node {
Value::Object(map) => {
result.push(node);
for v in map.values() {
stack.push(v);
}
}
Value::Array(arr) => {
for v in arr {
stack.push(v);
}
}
_ => {}
}
}
result
}
fn count_nodes<F: Fn(&Value) -> bool>(nodes: &[&Value], pred: F) -> usize {
nodes.iter().filter(|n| pred(n)).count()
}
#[test]
fn test_ast_variable_declarations_with_scope() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let count = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration")
&& n.get("scope").is_some()
});
assert_eq!(count, 957, "VariableDeclaration nodes with scope field");
}
#[test]
fn test_ast_function_definitions_with_scope() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let count = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("FunctionDefinition")
&& n.get("scope").is_some()
});
assert_eq!(count, 215, "FunctionDefinition nodes with scope field");
}
#[test]
fn test_ast_import_directives_with_scope() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let count = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("ImportDirective")
&& n.get("scope").is_some()
});
assert_eq!(count, 107, "ImportDirective nodes with scope field");
}
#[test]
fn test_ast_contract_definitions_with_scope() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let count = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("ContractDefinition")
&& n.get("scope").is_some()
});
assert_eq!(count, 43, "ContractDefinition nodes with scope field");
}
#[test]
fn test_ast_struct_definitions_with_scope() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let count = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("StructDefinition")
&& n.get("scope").is_some()
});
assert_eq!(count, 12, "StructDefinition nodes with scope field");
}
fn build_id_to_node_type(nodes: &[&Value]) -> std::collections::HashMap<i64, String> {
let mut map = std::collections::HashMap::new();
for n in nodes {
if let (Some(id), Some(nt)) = (
n.get("id").and_then(|v| v.as_i64()),
n.get("nodeType").and_then(|v| v.as_str()),
) {
map.insert(id, nt.to_string());
}
}
map
}
fn count_unique_scope_targets(
nodes: &[&Value],
id_to_type: &std::collections::HashMap<i64, String>,
target_type: &str,
) -> usize {
let mut scope_ids: std::collections::HashSet<i64> = std::collections::HashSet::new();
for n in nodes.iter() {
if let Some(scope_id) = n.get("scope").and_then(|v| v.as_i64())
&& id_to_type.get(&scope_id).map(|s| s.as_str()) == Some(target_type)
{
scope_ids.insert(scope_id);
}
}
scope_ids.len()
}
#[test]
fn test_ast_scope_targets_function_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "FunctionDefinition"),
208,
"unique scope IDs pointing to FunctionDefinition"
);
}
#[test]
fn test_ast_scope_targets_block() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "Block"),
52,
"unique scope IDs pointing to Block"
);
}
#[test]
fn test_ast_scope_targets_source_unit() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "SourceUnit"),
45,
"unique scope IDs pointing to SourceUnit"
);
}
#[test]
fn test_ast_scope_targets_contract_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "ContractDefinition"),
43,
"unique scope IDs pointing to ContractDefinition"
);
}
#[test]
fn test_ast_scope_targets_error_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "ErrorDefinition"),
16,
"unique scope IDs pointing to ErrorDefinition"
);
}
#[test]
fn test_ast_scope_targets_struct_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "StructDefinition"),
12,
"unique scope IDs pointing to StructDefinition"
);
}
#[test]
fn test_ast_scope_targets_event_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "EventDefinition"),
12,
"unique scope IDs pointing to EventDefinition"
);
}
#[test]
fn test_ast_scope_targets_unchecked_block() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "UncheckedBlock"),
11,
"unique scope IDs pointing to UncheckedBlock"
);
}
#[test]
fn test_ast_scope_targets_modifier_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_unique_scope_targets(&nodes, &id_map, "ModifierDefinition"),
1,
"unique scope IDs pointing to ModifierDefinition"
);
}
#[test]
fn test_ast_blocks_have_no_scope_field() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let total_blocks = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("Block")
});
let blocks_with_scope = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("Block") && n.get("scope").is_some()
});
assert_eq!(total_blocks, 277, "total Block nodes in AST");
assert_eq!(
blocks_with_scope, 0,
"Block nodes should have no scope field — this is the bug surface"
);
}
#[test]
fn test_ast_unchecked_blocks_have_no_scope_field() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let total = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("UncheckedBlock")
});
let with_scope = count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some("UncheckedBlock")
&& n.get("scope").is_some()
});
assert_eq!(total, 29, "total UncheckedBlock nodes in AST");
assert_eq!(
with_scope, 0,
"UncheckedBlock nodes should have no scope field"
);
}
#[test]
fn test_ast_scope_node_counts() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let count_type = |t: &str| {
count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some(t)
})
};
assert_eq!(count_type("SourceUnit"), 45);
assert_eq!(count_type("ContractDefinition"), 43);
assert_eq!(count_type("FunctionDefinition"), 215);
assert_eq!(count_type("ModifierDefinition"), 4);
assert_eq!(count_type("Block"), 277);
assert_eq!(count_type("UncheckedBlock"), 29);
}
#[test]
fn test_ast_scope_nodes_without_scope_field() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let missing = |t: &str| {
count_nodes(&nodes, |n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some(t) && n.get("scope").is_none()
})
};
assert_eq!(missing("Block"), 277, "all Blocks lack scope field");
assert_eq!(
missing("UncheckedBlock"),
29,
"all UncheckedBlocks lack scope field"
);
assert_eq!(
missing("ModifierDefinition"),
4,
"all ModifierDefinitions lack scope field"
);
assert_eq!(
missing("SourceUnit"),
45,
"SourceUnits are roots — no parent expected"
);
}
fn count_children_scoped_to(
nodes: &[&Value],
id_to_type: &std::collections::HashMap<i64, String>,
child_type: &str,
parent_type: &str,
) -> usize {
nodes
.iter()
.filter(|n| {
n.get("nodeType").and_then(|v| v.as_str()) == Some(child_type)
&& n.get("scope")
.and_then(|v| v.as_i64())
.and_then(|id| id_to_type.get(&id))
.map(|s| s.as_str())
== Some(parent_type)
})
.count()
}
#[test]
fn test_ast_vars_scoped_to_function_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "VariableDeclaration", "FunctionDefinition"),
637
);
}
#[test]
fn test_ast_vars_scoped_to_block() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "VariableDeclaration", "Block"),
99
);
}
#[test]
fn test_ast_vars_scoped_to_contract_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "VariableDeclaration", "ContractDefinition"),
56
);
}
#[test]
fn test_ast_vars_scoped_to_unchecked_block() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "VariableDeclaration", "UncheckedBlock"),
26
);
}
#[test]
fn test_ast_vars_scoped_to_struct_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "VariableDeclaration", "StructDefinition"),
66
);
}
#[test]
fn test_ast_vars_scoped_to_event_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "VariableDeclaration", "EventDefinition"),
49
);
}
#[test]
fn test_ast_vars_scoped_to_error_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "VariableDeclaration", "ErrorDefinition"),
23
);
}
#[test]
fn test_ast_functions_scoped_to_contract_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "FunctionDefinition", "ContractDefinition"),
205
);
}
#[test]
fn test_ast_functions_scoped_to_source_unit() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "FunctionDefinition", "SourceUnit"),
10
);
}
#[test]
fn test_ast_imports_scoped_to_source_unit() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "ImportDirective", "SourceUnit"),
107
);
}
#[test]
fn test_ast_contracts_scoped_to_source_unit() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "ContractDefinition", "SourceUnit"),
43
);
}
#[test]
fn test_ast_structs_scoped_to_contract_definition() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "StructDefinition", "ContractDefinition"),
9
);
}
#[test]
fn test_ast_structs_scoped_to_source_unit() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
assert_eq!(
count_children_scoped_to(&nodes, &id_map, "StructDefinition", "SourceUnit"),
3
);
}
#[test]
fn test_ast_all_contract_definitions() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let contracts: Vec<(String, i64)> = nodes
.iter()
.filter_map(|n| {
if n.get("nodeType").and_then(|v| v.as_str()) == Some("ContractDefinition") {
let name = n.get("name").and_then(|v| v.as_str())?.to_string();
let id = n.get("id").and_then(|v| v.as_i64())?;
Some((name, id))
} else {
None
}
})
.collect();
let expected: Vec<(&str, i64)> = vec![
("Owned", 7455),
("ERC6909", 7191),
("ERC6909Claims", 1289),
("Extsload", 1332),
("Exttload", 1362),
("NoDelegateCall", 1414),
("PoolManager", 1216),
("ProtocolFees", 1641),
("IExtsload", 7224),
("IExttload", 7246),
("IHooks", 1840),
("IPoolManager", 2123),
("IProtocolFees", 7323),
("IUnlockCallback", 2135),
("IERC20Minimal", 9021),
("IERC6909Claims", 7569),
("BitMath", 8949),
("CurrencyDelta", 2204),
("CurrencyReserves", 2252),
("CustomRevert", 2358),
("FixedPoint128", 7607),
("FixedPoint96", 9031),
("FullMath", 8913),
("Hooks", 3530),
("LPFeeLibrary", 3679),
("LiquidityMath", 7623),
("Lock", 3703),
("NonzeroDeltaCount", 3728),
("ParseBytes", 7600),
("Pool", 5409),
("Position", 5575),
("ProtocolFeeLibrary", 7395),
("SafeCast", 5751),
("SqrtPriceMath", 8114),
("SwapMath", 8366),
("TickBitmap", 8598),
("TickMath", 6305),
("UnsafeMath", 8628),
("BalanceDeltaLibrary", 6469),
("BeforeSwapDeltaLibrary", 6517),
("CurrencyLibrary", 6819),
("PoolIdLibrary", 6839),
("Slot0Library", 8745),
];
assert_eq!(contracts.len(), expected.len(), "contract count mismatch");
for (name, id) in &expected {
assert!(
contracts.iter().any(|(n, i)| n == name && i == id),
"missing contract {} (id={})",
name,
id
);
}
}
#[test]
fn test_cache_scope_declarations_has_block_scoped_vars() {
let cache = load_cache();
let mut block_scoped_count = 0;
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
for (scope_id, decls) in &cache.scope_declarations {
if id_map.get(&scope_id.0).map(|s| s.as_str()) == Some("Block") {
block_scoped_count += decls.len();
}
}
assert!(
block_scoped_count >= 99,
"should capture all 99 block-scoped variables, got {}",
block_scoped_count
);
}
#[test]
fn test_cache_scope_declarations_has_function_params() {
let cache = load_cache();
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
let mut fn_scoped_count = 0;
for (scope_id, decls) in &cache.scope_declarations {
if id_map.get(&scope_id.0).map(|s| s.as_str()) == Some("FunctionDefinition") {
fn_scoped_count += decls.len();
}
}
assert!(
fn_scoped_count >= 557,
"should capture function-scoped variables, got {}",
fn_scoped_count
);
}
#[test]
fn test_cache_scope_declarations_has_state_vars() {
let cache = load_cache();
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
let mut contract_scoped_count = 0;
for (scope_id, decls) in &cache.scope_declarations {
if id_map.get(&scope_id.0).map(|s| s.as_str()) == Some("ContractDefinition") {
contract_scoped_count += decls.len();
}
}
assert!(
contract_scoped_count >= 56,
"should capture at least 56 contract-scoped state variables, got {}",
contract_scoped_count
);
}
#[test]
fn test_cache_scope_parent_count() {
let cache = load_cache();
assert_eq!(
cache.scope_parent.len(),
568,
"scope_parent should have 568 entries (258 from AST + 310 inferred)"
);
}
#[test]
fn test_cache_scope_ranges_count() {
let cache = load_cache();
assert_eq!(
cache.scope_ranges.len(),
613,
"scope_ranges should have 613 entries (all scope-creating nodes)"
);
}
#[test]
fn test_cache_blocks_have_no_parent_link() {
let cache = load_cache();
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let block_ids: Vec<i64> = nodes
.iter()
.filter_map(|n| {
if n.get("nodeType").and_then(|v| v.as_str()) == Some("Block") {
n.get("id").and_then(|v| v.as_i64())
} else {
None
}
})
.collect();
assert_eq!(block_ids.len(), 277);
let blocks_with_parent = block_ids
.iter()
.filter(|id| cache.scope_parent.contains_key(&NodeId(**id)))
.count();
assert_eq!(
blocks_with_parent, 277,
"all Block nodes should have inferred parent links in scope_parent"
);
}
#[test]
fn test_cache_unchecked_blocks_have_no_parent_link() {
let cache = load_cache();
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let ub_ids: Vec<i64> = nodes
.iter()
.filter_map(|n| {
if n.get("nodeType").and_then(|v| v.as_str()) == Some("UncheckedBlock") {
n.get("id").and_then(|v| v.as_i64())
} else {
None
}
})
.collect();
assert_eq!(ub_ids.len(), 29);
let with_parent = ub_ids
.iter()
.filter(|id| cache.scope_parent.contains_key(&NodeId(**id)))
.count();
assert_eq!(
with_parent, 29,
"all UncheckedBlock nodes should have inferred parent links in scope_parent"
);
}
#[test]
fn test_scope_chain_lpfee_in_initialize() {
let cache = load_cache();
let block_880_decls = cache.scope_declarations.get(&NodeId(329));
assert!(
block_880_decls.is_some(),
"Block 880 should have declarations"
);
let names: Vec<&str> = block_880_decls
.unwrap()
.iter()
.map(|d| d.name.as_str())
.collect();
assert!(
names.contains(&"lpFee"),
"lpFee should be in Block 880, got: {:?}",
names
);
assert!(
names.contains(&"id"),
"id should be in Block 880, got: {:?}",
names
);
assert_eq!(
cache.scope_parent.get(&NodeId(330)),
Some(&NodeId(1216)),
"initialize (881) should have parent PoolManager (1767)"
);
assert_eq!(
cache.scope_parent.get(&NodeId(1216)),
Some(&NodeId(1217)),
"PoolManager (1767) should have parent SourceUnit (1768)"
);
assert_eq!(
cache.scope_parent.get(&NodeId(329)),
Some(&NodeId(330)),
"Block 880 should have inferred parent link to FunctionDefinition 881"
);
}
#[test]
fn test_ast_pool_key_struct_fields() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let id_map = build_id_to_node_type(&nodes);
let fields: Vec<String> = nodes
.iter()
.filter_map(|n| {
if n.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration")
&& n.get("scope").and_then(|v| v.as_i64()) == Some(6871)
&& id_map.get(&6871).map(|s| s.as_str()) == Some("StructDefinition")
{
n.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
} else {
None
}
})
.collect();
assert_eq!(fields.len(), 5);
assert!(fields.contains(&"currency0".to_string()));
assert!(fields.contains(&"currency1".to_string()));
assert!(fields.contains(&"fee".to_string()));
assert!(fields.contains(&"tickSpacing".to_string()));
assert!(fields.contains(&"hooks".to_string()));
}
#[test]
fn test_ast_pool_state_struct_fields() {
let ast = load_ast();
let nodes = collect_all_nodes(&ast);
let fields: Vec<String> = nodes
.iter()
.filter_map(|n| {
if n.get("nodeType").and_then(|v| v.as_str()) == Some("VariableDeclaration")
&& n.get("scope").and_then(|v| v.as_i64()) == Some(3870)
{
n.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
} else {
None
}
})
.collect();
assert!(fields.contains(&"slot0".to_string()));
assert!(fields.contains(&"liquidity".to_string()));
assert!(fields.contains(&"feeGrowthGlobal0X128".to_string()));
}
#[test]
fn test_cache_linearized_base_contracts_pool_manager() {
let cache = load_cache();
let bases = cache.linearized_base_contracts.get(&NodeId(1216)).unwrap();
assert_eq!(
bases,
&vec![
NodeId(1216),
NodeId(1362),
NodeId(1332),
NodeId(1289),
NodeId(7191),
NodeId(1414),
NodeId(1641),
NodeId(7455),
NodeId(2123),
NodeId(7246),
NodeId(7224),
NodeId(7569),
NodeId(7323)
]
);
}
#[test]
fn test_cache_linearized_base_contracts_erc6909_claims() {
let cache = load_cache();
let bases = cache.linearized_base_contracts.get(&NodeId(1289)).unwrap();
assert_eq!(bases, &vec![NodeId(1289), NodeId(7191), NodeId(7569)]);
}
#[test]
fn test_cache_linearized_base_contracts_simple_contract() {
let cache = load_cache();
let bases = cache.linearized_base_contracts.get(&NodeId(7455)).unwrap();
assert_eq!(bases, &vec![NodeId(7455)]);
}
use solidity_language_server::completion::find_innermost_scope;
fn walk_scope_chain(
cache: &solidity_language_server::completion::CompletionCache,
start: NodeId,
) -> Vec<NodeId> {
let mut chain = vec![start];
let mut current = start;
while let Some(&parent) = cache.scope_parent.get(¤t) {
chain.push(parent);
current = parent;
}
chain
}
#[test]
fn test_innermost_scope_in_swap_body() {
let cache = load_cache();
let scope = find_innermost_scope(&cache, 9600, FileId(5));
assert_eq!(
scope,
Some(NodeId(615)),
"innermost scope at byte 9600/file 6 should be Block 1166 (swap body)"
);
}
#[test]
fn test_scope_chain_from_swap_body_block() {
let cache = load_cache();
let chain = walk_scope_chain(&cache, NodeId(615));
assert_eq!(
chain,
vec![NodeId(615), NodeId(616), NodeId(1216), NodeId(1217)]
);
}
#[test]
fn test_scope_chain_from_swap_fn() {
let cache = load_cache();
let chain = walk_scope_chain(&cache, NodeId(616));
assert_eq!(
chain,
vec![NodeId(616), NodeId(1216), NodeId(1217)],
"swap fn → PoolManager → SourceUnit"
);
}
#[test]
fn test_swap_body_declarations() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(615)).unwrap();
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"id"), "swap body should declare 'id'");
assert!(names.contains(&"pool"), "swap body should declare 'pool'");
assert!(
names.contains(&"beforeSwapDelta"),
"swap body should declare 'beforeSwapDelta'"
);
assert!(
names.contains(&"hookDelta"),
"swap body should declare 'hookDelta'"
);
}
#[test]
fn test_swap_fn_declarations() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(616)).unwrap();
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"key"), "swap fn should declare param 'key'");
assert!(
names.contains(&"params"),
"swap fn should declare param 'params'"
);
assert!(
names.contains(&"hookData"),
"swap fn should declare param 'hookData'"
);
}
#[test]
fn test_swap_body_local_var_types() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(615)).unwrap();
let type_of = |name: &str| {
decls
.iter()
.find(|d| d.name == name)
.map(|d| d.type_id.as_str())
};
assert_eq!(type_of("id"), Some("t_userDefinedValueType$_PoolId_$6825"));
assert_eq!(type_of("pool"), Some("t_struct$_State_$3870_storage_ptr"));
assert_eq!(
type_of("beforeSwapDelta"),
Some("t_userDefinedValueType$_BeforeSwapDelta_$6473")
);
assert_eq!(
type_of("hookDelta"),
Some("t_userDefinedValueType$_BalanceDelta_$6311")
);
}
#[test]
fn test_swap_fn_param_types() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(616)).unwrap();
let type_of = |name: &str| {
decls
.iter()
.find(|d| d.name == name)
.map(|d| d.type_id.as_str())
};
assert_eq!(type_of("key"), Some("t_struct$_PoolKey_$6871_memory_ptr"));
assert_eq!(
type_of("params"),
Some("t_struct$_SwapParams_$6898_memory_ptr")
);
assert_eq!(type_of("hookData"), Some("t_bytes_calldata_ptr"));
}
#[test]
fn test_innermost_scope_in_nested_block() {
let cache = load_cache();
let scope = find_innermost_scope(&cache, 9900, FileId(5));
assert_eq!(
scope,
Some(NodeId(574)),
"innermost scope at byte 9900/file 6 should be nested Block 1125"
);
}
#[test]
fn test_nested_block_declarations() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(574)).unwrap();
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"amountToSwap"));
assert!(names.contains(&"lpFeeOverride"));
}
#[test]
fn test_nested_block_declaration_types() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(574)).unwrap();
let type_of = |name: &str| {
decls
.iter()
.find(|d| d.name == name)
.map(|d| d.type_id.as_str())
};
assert_eq!(type_of("amountToSwap"), Some("t_int256"));
assert_eq!(type_of("lpFeeOverride"), Some("t_uint24"));
}
#[test]
fn test_scope_chain_from_nested_block() {
let cache = load_cache();
let chain = walk_scope_chain(&cache, NodeId(574));
assert_eq!(
chain,
vec![
NodeId(574),
NodeId(615),
NodeId(616),
NodeId(1216),
NodeId(1217)
]
);
}
#[test]
fn test_innermost_scope_in_initialize_body() {
let cache = load_cache();
let scope = find_innermost_scope(&cache, 6300, FileId(5));
assert_eq!(
scope,
Some(NodeId(329)),
"innermost scope at byte 6300/file 6 should be Block 880 (initialize body)"
);
}
#[test]
fn test_initialize_body_declarations() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(329)).unwrap();
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"lpFee"));
assert!(names.contains(&"id"));
}
#[test]
fn test_initialize_body_declaration_types() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(329)).unwrap();
let type_of = |name: &str| {
decls
.iter()
.find(|d| d.name == name)
.map(|d| d.type_id.as_str())
};
assert_eq!(type_of("lpFee"), Some("t_uint24"));
assert_eq!(type_of("id"), Some("t_userDefinedValueType$_PoolId_$6825"));
}
#[test]
fn test_initialize_fn_params() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(330)).unwrap();
let type_of = |name: &str| {
decls
.iter()
.find(|d| d.name == name)
.map(|d| d.type_id.as_str())
};
assert_eq!(type_of("key"), Some("t_struct$_PoolKey_$6871_memory_ptr"));
assert_eq!(type_of("sqrtPriceX96"), Some("t_uint160"));
}
#[test]
fn test_resolve_local_var_pool_in_swap_body() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "pool", 9600, FileId(5));
assert_eq!(
result,
Some("t_struct$_State_$3870_storage_ptr".to_string())
);
}
#[test]
fn test_resolve_param_key_in_swap_body() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "key", 9600, FileId(5));
assert_eq!(
result,
Some("t_struct$_PoolKey_$6871_memory_ptr".to_string()),
"key in swap body should walk up to FnDef scope and find the parameter"
);
}
#[test]
fn test_resolve_nested_block_sees_own_vars() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "amountToSwap", 9900, FileId(5));
assert_eq!(result, Some("t_int256".to_string()));
}
#[test]
fn test_resolve_nested_block_outer_local_walks_up() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "pool", 9900, FileId(5));
assert_eq!(
result,
Some("t_struct$_State_$3870_storage_ptr".to_string()),
"pool in nested block should walk up to outer block and find the local var"
);
}
#[test]
fn test_resolve_state_var_in_function_body() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "_pools", 9600, FileId(5));
assert!(
result.is_some(),
"_pools should walk up to contract scope and resolve"
);
}
#[test]
fn test_innermost_scope_in_fn_header() {
let cache = load_cache();
let scope = find_innermost_scope(&cache, 9500, FileId(5));
assert_eq!(
scope,
Some(NodeId(616)),
"byte 9500 in fn header should resolve to FnDef 1167, not Block"
);
}
#[test]
fn test_resolve_param_in_fn_header_uses_scope_walk() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "key", 9500, FileId(5));
assert_eq!(
result,
Some("t_struct$_PoolKey_$6871_memory_ptr".to_string()),
"key at fn header position should resolve via scope walk (not fallback)"
);
}
#[test]
fn test_resolve_inherited_var_in_fn_header() {
let cache = load_cache();
let result = resolve_name_in_scope(&cache, "owner", 9500, FileId(5));
assert_eq!(
result,
Some("t_address".to_string()),
"owner at fn header should resolve via inheritance walk"
);
}
#[test]
fn test_pool_manager_state_vars_in_scope_declarations() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(1216)).unwrap();
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"MAX_TICK_SPACING"));
assert!(names.contains(&"MIN_TICK_SPACING"));
assert!(names.contains(&"_pools"));
}
#[test]
fn test_owned_state_var_in_scope_declarations() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(7455)).unwrap();
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"owner"));
}
#[test]
fn test_protocol_fees_state_vars_in_scope_declarations() {
let cache = load_cache();
let decls = cache.scope_declarations.get(&NodeId(1641)).unwrap();
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"protocolFeesAccrued"));
assert!(names.contains(&"protocolFeeController"));
}
#[test]
fn test_parse_chain_type_expression() {
let chain = parse_dot_chain("type(uint256).", 14);
assert_eq!(chain.len(), 1);
assert_eq!(
chain[0],
DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("uint256".to_string()),
}
);
}
#[test]
fn test_parse_chain_type_expression_contract() {
let line = "type(PoolManager).";
let chain = parse_dot_chain(line, line.len() as u32);
assert_eq!(chain.len(), 1);
assert_eq!(chain[0].name, "type");
assert_eq!(chain[0].kind, AccessKind::Call);
assert_eq!(chain[0].call_args, Some("PoolManager".to_string()));
}
#[test]
fn test_parse_chain_type_expression_interface() {
let line = "type(IHooks).";
let chain = parse_dot_chain(line, line.len() as u32);
assert_eq!(chain.len(), 1);
assert_eq!(chain[0].name, "type");
assert_eq!(chain[0].kind, AccessKind::Call);
assert_eq!(chain[0].call_args, Some("IHooks".to_string()));
}
#[test]
fn test_contract_kinds_populated() {
let cache = load_cache();
assert_eq!(
cache.contract_kinds.get(&NodeId(1216)).map(|s| s.as_str()),
Some("contract"),
"PoolManager should be classified as contract"
);
assert_eq!(
cache.contract_kinds.get(&NodeId(1840)).map(|s| s.as_str()),
Some("interface"),
"IHooks should be classified as interface"
);
assert_eq!(
cache.contract_kinds.get(&NodeId(8913)).map(|s| s.as_str()),
Some("library"),
"FullMath should be classified as library"
);
}
#[test]
fn test_type_meta_contract_completions() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("PoolManager".to_string()),
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"name"), "type(Contract). should have name");
assert!(
labels.contains(&"creationCode"),
"type(Contract). should have creationCode"
);
assert!(
labels.contains(&"runtimeCode"),
"type(Contract). should have runtimeCode"
);
assert!(
!labels.contains(&"interfaceId"),
"type(Contract). should NOT have interfaceId"
);
assert!(
!labels.contains(&"min"),
"type(Contract). should NOT have min"
);
assert!(
!labels.contains(&"max"),
"type(Contract). should NOT have max"
);
assert_eq!(
labels.len(),
3,
"type(Contract). should have exactly 3 members"
);
}
#[test]
fn test_type_meta_interface_completions() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("IHooks".to_string()),
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"name"),
"type(Interface). should have name"
);
assert!(
labels.contains(&"interfaceId"),
"type(Interface). should have interfaceId"
);
assert!(
!labels.contains(&"creationCode"),
"type(Interface). should NOT have creationCode"
);
assert!(
!labels.contains(&"runtimeCode"),
"type(Interface). should NOT have runtimeCode"
);
assert!(
!labels.contains(&"min"),
"type(Interface). should NOT have min"
);
assert!(
!labels.contains(&"max"),
"type(Interface). should NOT have max"
);
assert_eq!(
labels.len(),
2,
"type(Interface). should have exactly 2 members"
);
}
#[test]
fn test_type_meta_integer_completions() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("uint256".to_string()),
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"min"), "type(uint256). should have min");
assert!(labels.contains(&"max"), "type(uint256). should have max");
assert!(
!labels.contains(&"name"),
"type(uint256). should NOT have name"
);
assert!(
!labels.contains(&"creationCode"),
"type(uint256). should NOT have creationCode"
);
assert!(
!labels.contains(&"interfaceId"),
"type(uint256). should NOT have interfaceId"
);
assert_eq!(
labels.len(),
2,
"type(uint256). should have exactly 2 members"
);
}
#[test]
fn test_type_meta_int8_completions() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("int8".to_string()),
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"min"), "type(int8). should have min");
assert!(labels.contains(&"max"), "type(int8). should have max");
assert_eq!(labels.len(), 2);
}
#[test]
fn test_type_meta_unknown_fallback() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("SomethingUnknown".to_string()),
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"name"),
"unknown fallback should have name"
);
assert!(
labels.contains(&"creationCode"),
"unknown fallback should have creationCode"
);
assert!(
labels.contains(&"runtimeCode"),
"unknown fallback should have runtimeCode"
);
assert!(
labels.contains(&"interfaceId"),
"unknown fallback should have interfaceId"
);
assert!(labels.contains(&"min"), "unknown fallback should have min");
assert!(labels.contains(&"max"), "unknown fallback should have max");
assert_eq!(
labels.len(),
6,
"unknown fallback should have all 6 members"
);
}
#[test]
fn test_type_meta_no_args_fallback() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: None,
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert_eq!(
labels.len(),
6,
"type(). with no args should have all 6 members"
);
}
#[test]
fn test_type_meta_library_completions() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("FullMath".to_string()),
}];
let items = get_chain_completions(&cache, &chain, None);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"name"), "type(Library). should have name");
assert!(
labels.contains(&"creationCode"),
"type(Library). should have creationCode"
);
assert!(
labels.contains(&"runtimeCode"),
"type(Library). should have runtimeCode"
);
assert!(
!labels.contains(&"interfaceId"),
"type(Library). should NOT have interfaceId"
);
assert_eq!(
labels.len(),
3,
"type(Library). should have exactly 3 members"
);
}
#[test]
fn test_type_meta_items_are_property_kind() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("uint256".to_string()),
}];
let items = get_chain_completions(&cache, &chain, None);
for item in &items {
assert_eq!(
item.kind,
Some(CompletionItemKind::PROPERTY),
"{} should be PROPERTY kind",
item.label
);
}
}
#[test]
fn test_type_meta_items_have_detail() {
let cache = load_cache();
let chain = vec![DotSegment {
name: "type".to_string(),
kind: AccessKind::Call,
call_args: Some("IPoolManager".to_string()),
}];
let items = get_chain_completions(&cache, &chain, None);
let name_item = items.iter().find(|i| i.label == "name").unwrap();
assert_eq!(name_item.detail.as_deref(), Some("string"));
let iid_item = items.iter().find(|i| i.label == "interfaceId").unwrap();
assert_eq!(iid_item.detail.as_deref(), Some("bytes4"));
}
const SCOPED_LOCALS_SRC: &str = include_str!("../example/ScopedLocals.sol");
fn load_scoped_locals_cache() -> solidity_language_server::completion::CompletionCache {
let raw: Value =
serde_json::from_str(&fs::read_to_string("scoped_locals.json").unwrap()).unwrap();
let ast_data = solidity_language_server::solc::normalize_solc_output(raw, None);
build_completion_cache(ast_data.get("sources").unwrap(), None, None)
}
fn position_after(text: &str, needle: &str) -> Position {
let offset = text.find(needle).expect("needle in text") + needle.len();
let before = &text[..offset];
let line = before.matches('\n').count() as u32;
let character = (offset - before.rfind('\n').map_or(0, |i| i + 1)) as u32;
Position { line, character }
}
fn scoped_dot_labels(
cache: &solidity_language_server::completion::CompletionCache,
live: &str,
compiled: Option<&str>,
needle: &str,
) -> Vec<String> {
response_labels(
solidity_language_server::completion::handle_completion_with_tail_candidates(
Some(cache),
live,
position_after(live, needle),
Some("."),
Some(FileId(0)),
compiled,
vec![],
),
)
}
#[test]
fn test_for_init_variable_resolves_in_its_for_scope() {
let cache = load_scoped_locals_cache();
let labels = scoped_dot_labels(&cache, SCOPED_LOCALS_SRC, None, "sum += item.");
assert!(labels.contains(&"innerField".to_string()), "{labels:?}");
assert!(!labels.contains(&"outerField".to_string()), "{labels:?}");
}
#[test]
fn test_try_returns_variable_resolves_in_its_clause_scope() {
let cache = load_scoped_locals_cache();
let labels = scoped_dot_labels(&cache, SCOPED_LOCALS_SRC, None, "out = got.");
assert!(labels.contains(&"innerField".to_string()), "{labels:?}");
assert!(!labels.contains(&"outerField".to_string()), "{labels:?}");
}
#[test]
fn test_scope_lookup_survives_unsaved_lines_above_cursor() {
let cache = load_scoped_locals_cache();
let drift = SCOPED_LOCALS_SRC.find("out = got.").unwrap()
- SCOPED_LOCALS_SRC.find("sum += item.").unwrap();
let padding = format!(" //{}\n", "x".repeat(drift - 7));
let live = SCOPED_LOCALS_SRC.replacen(
" function loop(",
&format!("{padding} function loop("),
1,
);
let unmapped = scoped_dot_labels(&cache, &live, None, "sum += item.");
assert!(
unmapped.contains(&"outerField".to_string()),
"fixture no longer exercises the drift: {unmapped:?}"
);
let mapped = scoped_dot_labels(&cache, &live, Some(SCOPED_LOCALS_SRC), "sum += item.");
assert!(mapped.contains(&"innerField".to_string()), "{mapped:?}");
assert!(!mapped.contains(&"outerField".to_string()), "{mapped:?}");
}
#[test]
fn test_map_live_offset_to_compiled() {
use solidity_language_server::completion::map_live_offset_to_compiled as map;
let compiled = "aaaa{bbbb}cccc";
assert_eq!(map(compiled, compiled, 7), 7);
let live = "aaXYZaa{bbbb}cccc";
assert_eq!(map(live, compiled, 1), 1);
assert_eq!(map(live, compiled, 10), 7);
assert_eq!(map(live, compiled, live.len()), compiled.len());
let live = "aaaa{bbbb.}cccc";
assert_eq!(map(live, compiled, 10), 9);
let live = "aaaa{}cccc";
assert_eq!(map(live, compiled, 6), 10);
}
fn assert_labels(needle: &str, present: &[&str], absent: &[&str]) {
let cache = load_scoped_locals_cache();
let labels = scoped_dot_labels(&cache, SCOPED_LOCALS_SRC, None, needle);
for p in present {
assert!(
labels.iter().any(|l| l == p),
"{needle} missing {p}: {labels:?}"
);
}
for a in absent {
assert!(
!labels.iter().any(|l| l == a),
"{needle} offers {a}: {labels:?}"
);
}
}
#[test]
fn test_storage_array_offers_length_push_pop() {
assert_labels("stored.", &["length", "push", "pop"], &[]);
}
#[test]
fn test_memory_array_and_bytes_offer_length_only() {
assert_labels("stored.push(list.", &["length"], &["push", "pop"]);
assert_labels("Wad.wrap(raw.", &["length"], &["push"]);
assert_labels("word.", &["length"], &[]);
}
#[test]
fn test_array_index_resolves_element_type() {
assert_labels("list[0].", &["innerField"], &["length"]);
}
#[test]
fn test_value_type_name_offers_wrap_unwrap() {
assert_labels("return Wad.", &["wrap", "unwrap"], &[]);
}
#[test]
fn test_function_and_error_members_offer_selector() {
assert_labels("ISource.fetch.", &["selector"], &[]);
assert_labels("Derived.Bad.", &["selector"], &[]);
}
#[test]
fn test_private_base_member_is_not_inherited() {
assert_labels("+ hidden.", &["innerField"], &["outerField"]);
}
#[test]
fn test_builtin_members_insert_bare_names() {
let cache = load_cache();
for item in get_dot_completions(&cache, "abi", None) {
assert!(!item.label.contains('('), "label {:?}", item.label);
}
}
fn plain_items_after(needle: &str, file_id: Option<FileId>) -> Vec<CompletionItem> {
let cache = load_scoped_locals_cache();
let offset = SCOPED_LOCALS_SRC.find(needle).expect("needle in text") + needle.len();
let before = &SCOPED_LOCALS_SRC[..offset];
let position = Position {
line: before.matches('\n').count() as u32,
character: (offset - before.rfind('\n').map_or(0, |i| i + 1)) as u32,
};
response_items(handle_completion(
Some(&cache),
SCOPED_LOCALS_SRC,
position,
None,
file_id,
))
}
#[test]
fn test_struct_field_chain_still_resolves() {
let cache = load_cache();
let chain = vec![
DotSegment {
name: "_pools".to_string(),
kind: AccessKind::Index,
call_args: None,
},
DotSegment {
name: "slot0".to_string(),
kind: AccessKind::Plain,
call_args: None,
},
];
let labels: Vec<String> = get_chain_completions(&cache, &chain, None)
.into_iter()
.map(|i| i.label)
.collect();
assert!(labels.iter().any(|l| l == "sqrtPriceX96"), "{labels:?}");
}
#[test]
fn test_import_aliases_stay_in_general_completions() {
let raw: Value = serde_json::from_str(&fs::read_to_string("alias.json").unwrap()).unwrap();
let ast = solidity_language_server::solc::normalize_solc_output(raw, None);
let cache = build_completion_cache(ast.get("sources").unwrap(), None, None);
assert!(
cache
.general_completions
.iter()
.any(|i| i.label == "MyTest"),
"`import {{Test as MyTest}}` alias missing"
);
}
#[test]
fn test_scoped_locals_respect_declaration_order() {
let before = plain_items_after("stored.push(", Some(FileId(0)));
let labels: Vec<&str> = before.iter().map(|i| i.label.as_str()).collect();
for param in ["list", "raw", "word"] {
assert!(labels.contains(¶m), "missing {param}: {labels:?}");
}
for later in ["sel", "err"] {
assert!(
!labels.contains(&later),
"{later} offered before it is declared"
);
}
assert!(!labels.contains(&"items"));
let after = plain_items_after(" return ", Some(FileId(0)));
let labels: Vec<&str> = after.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"sel") && labels.contains(&"err"),
"{labels:?}"
);
}
#[test]
fn test_scoped_locals_keep_type_detail() {
let items = plain_items_after("stored.push(", Some(FileId(0)));
let word = items
.iter()
.find(|i| i.label == "word")
.expect("word offered");
assert_eq!(word.detail.as_deref(), Some("bytes32"));
}
#[test]
fn test_unscoped_request_still_offers_parameters() {
let items = plain_items_after("stored.push(", None);
assert!(items.iter().any(|i| i.label == "list"));
}
#[test]
fn test_invoked_completion_at_unresolved_member_gets_general_list() {
let cache = load_cache();
let source = "uint256 a = 1.";
let items = response_items(handle_completion(
Some(&cache),
source,
Position {
line: 0,
character: source.len() as u32,
},
None,
None,
));
assert!(
items.iter().any(|i| i.label == "uint256"),
"{} items",
items.len()
);
}
#[test]
fn test_using_for_extensions_appear_on_a_contract_typed_value() {
let cache = load_cache();
let labels: Vec<String> = get_dot_completions(&cache, "hooks", None)
.into_iter()
.map(|i| i.label)
.collect();
assert!(
!labels.is_empty(),
"`hooks` (declared as IHooks) resolved to nothing"
);
for expected in ["hasPermission", "isValidHookAddress"] {
assert!(
labels.iter().any(|l| l == expected),
"`hooks.` is missing the `using Hooks for IHooks` extension {expected:?}; got {labels:?}"
);
}
}
#[test]
fn test_using_for_extensions_absent_on_the_type_name() {
let cache = load_cache();
let labels: Vec<String> = get_dot_completions(&cache, "IHooks", None)
.into_iter()
.map(|i| i.label)
.collect();
for unexpected in ["hasPermission", "isValidHookAddress"] {
assert!(
!labels.iter().any(|l| l == unexpected),
"`IHooks.` should not offer the library extension {unexpected:?}; got {labels:?}"
);
}
}
#[test]
fn test_using_for_extensions_appear_after_a_cast() {
use solidity_language_server::completion::get_chain_completions;
let cache = load_cache();
let chain = vec![DotSegment {
name: "IHooks".to_string(),
kind: AccessKind::Call,
call_args: Some("addr".to_string()),
}];
let labels: Vec<String> = get_chain_completions(&cache, &chain, None)
.into_iter()
.map(|i| i.label)
.collect();
assert!(
labels.iter().any(|l| l == "hasPermission"),
"`IHooks(addr).` should offer library extensions; got {labels:?}"
);
}