use crate::goto::CachedBuild;
use crate::types::{AbsPath, NodeId, SourceLoc};
use serde_json::Value;
use std::collections::HashMap;
use tower_lsp::lsp_types::*;
use tree_sitter::{Node, Parser};
#[derive(Debug, Clone)]
struct ParamInfo {
names: Vec<String>,
skip: usize,
}
#[derive(Debug, Clone)]
struct CallSite {
info: ParamInfo,
name: String,
decl_id: NodeId,
}
#[derive(Debug, Clone)]
pub struct ResolvedCallSite {
pub param_name: String,
pub decl_id: NodeId,
}
#[derive(Debug, Clone)]
pub struct HintLookup {
by_offset: HashMap<usize, CallSite>,
by_name: HashMap<(String, usize), CallSite>,
}
impl HintLookup {
pub fn resolve_callsite_with_skip(
&self,
call_offset: usize,
func_name: &str,
arg_count: usize,
) -> Option<(NodeId, usize)> {
if let Some(site) = lookup_call_site(self, call_offset, func_name, arg_count) {
return Some((site.decl_id, site.info.skip));
}
self.by_name
.iter()
.find(|((name, _), _)| name == func_name)
.map(|(_, site)| (site.decl_id, site.info.skip))
}
pub fn resolve_callsite_param(
&self,
call_offset: usize,
func_name: &str,
arg_count: usize,
arg_index: usize,
) -> Option<ResolvedCallSite> {
let site = lookup_call_site(self, call_offset, func_name, arg_count)?;
let param_idx = arg_index + site.info.skip;
if param_idx >= site.info.names.len() {
return None;
}
let param_name = &site.info.names[param_idx];
if param_name.is_empty() {
return None;
}
Some(ResolvedCallSite {
param_name: param_name.clone(),
decl_id: site.decl_id,
})
}
}
pub type HintIndex = HashMap<AbsPath, HintLookup>;
#[derive(Debug, Clone)]
pub struct ConstructorInfo {
pub constructor_id: NodeId,
pub contract_name: String,
pub param_names: Vec<String>,
}
pub type ConstructorIndex = HashMap<NodeId, ConstructorInfo>;
pub fn build_constructor_index(
decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
) -> ConstructorIndex {
let mut index = HashMap::with_capacity(decl_index.len() / 8);
for (id, decl) in decl_index {
if decl.is_constructor()
&& let Some(scope) = decl.scope()
{
let scope = NodeId(scope);
let names = decl.param_names().unwrap_or_default();
let contract_name = decl_index
.get(&scope)
.map(|d| d.name().to_string())
.unwrap_or_default();
index.insert(
scope,
ConstructorInfo {
constructor_id: *id,
contract_name,
param_names: names,
},
);
}
}
index
}
pub fn build_hint_index(
sources: &Value,
decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
constructor_index: &ConstructorIndex,
) -> HintIndex {
let source_count = sources.as_object().map_or(0, |obj| obj.len());
let mut hint_index = HashMap::with_capacity(source_count);
if let Some(obj) = sources.as_object() {
for (_, source_data) in obj {
if let Some(ast) = source_data.get("ast")
&& let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str())
{
let lookup = build_hint_lookup(ast, decl_index, constructor_index);
hint_index.insert(AbsPath::new(abs_path), lookup);
}
}
}
hint_index
}
pub fn inlay_hints(
build: &CachedBuild,
uri: &Url,
range: Range,
live_source: &[u8],
) -> Vec<InlayHint> {
let path_str = match uri.to_file_path() {
Ok(p) => p.to_str().unwrap_or("").to_string(),
Err(_) => return vec![],
};
let abs = match build
.path_to_abs
.iter()
.find(|(k, _)| path_str.ends_with(k.as_str()))
{
Some((_, v)) => v.clone(),
None => return vec![],
};
let lookup = match build.hint_index.get(&abs) {
Some(l) => l,
None => return vec![],
};
let source_str = String::from_utf8_lossy(live_source);
let tree = match ts_parse(&source_str) {
Some(t) => t,
None => return vec![],
};
let mut hints = Vec::new();
collect_ts_hints(tree.root_node(), &source_str, &range, lookup, &mut hints);
hints
}
pub fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.expect("failed to load Solidity grammar");
parser.parse(source, None)
}
fn build_hint_lookup(
file_ast: &Value,
decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
constructor_index: &ConstructorIndex,
) -> HintLookup {
let mut lookup = HintLookup {
by_offset: HashMap::new(),
by_name: HashMap::new(),
};
collect_ast_calls(file_ast, decl_index, constructor_index, &mut lookup);
lookup
}
fn parse_src_offset(node: &Value) -> Option<usize> {
let src = node.get("src").and_then(|v| v.as_str())?;
SourceLoc::parse(src).map(|loc| loc.offset)
}
fn collect_ast_calls(
node: &Value,
decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
constructor_index: &ConstructorIndex,
lookup: &mut HintLookup,
) {
let node_type = node.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
match node_type {
"FunctionCall" => {
if let Some(call_info) = extract_call_info(node, decl_index, constructor_index) {
let arg_count = node
.get("arguments")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(0);
let site = CallSite {
info: ParamInfo {
names: call_info.params.names,
skip: call_info.params.skip,
},
name: call_info.name,
decl_id: call_info.decl_id,
};
if let Some(offset) = parse_src_offset(node) {
lookup.by_offset.insert(offset, site.clone());
}
lookup
.by_name
.entry((site.name.clone(), arg_count))
.or_insert(site);
}
}
"EmitStatement" => {
if let Some(event_call) = node.get("eventCall")
&& let Some(call_info) =
extract_call_info(event_call, decl_index, constructor_index)
{
let arg_count = event_call
.get("arguments")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(0);
let site = CallSite {
info: ParamInfo {
names: call_info.params.names,
skip: call_info.params.skip,
},
name: call_info.name,
decl_id: call_info.decl_id,
};
if let Some(offset) = parse_src_offset(node) {
lookup.by_offset.insert(offset, site.clone());
}
lookup
.by_name
.entry((site.name.clone(), arg_count))
.or_insert(site);
}
}
_ => {}
}
for key in crate::goto::CHILD_KEYS {
if let Some(child) = node.get(*key) {
if child.is_array() {
if let Some(arr) = child.as_array() {
for item in arr {
collect_ast_calls(item, decl_index, constructor_index, lookup);
}
}
} else if child.is_object() {
collect_ast_calls(child, decl_index, constructor_index, lookup);
}
}
}
}
struct CallInfo {
name: String,
params: ParamInfo,
decl_id: NodeId,
}
fn extract_call_info(
node: &Value,
decl_index: &HashMap<NodeId, crate::solc_ast::DeclNode>,
constructor_index: &ConstructorIndex,
) -> Option<CallInfo> {
let args = node.get("arguments")?.as_array()?;
if args.is_empty() {
return None;
}
let kind = node.get("kind").and_then(|v| v.as_str()).unwrap_or("");
if kind == "structConstructorCall"
&& node
.get("names")
.and_then(|v| v.as_array())
.is_some_and(|n| !n.is_empty())
{
return None;
}
let expr = node.get("expression")?;
let expr_type = expr.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
if expr_type == "NewExpression" {
return extract_new_expression_call_info(expr, args.len(), constructor_index);
}
let decl_id = NodeId(expr.get("referencedDeclaration").and_then(|v| v.as_i64())?);
let decl = decl_index.get(&decl_id)?;
let names = decl.param_names()?;
let func_name = extract_function_name(expr)?;
let arg_count = node
.get("arguments")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(0);
let skip = if is_member_access(expr) && arg_count < names.len() {
1
} else {
0
};
Some(CallInfo {
name: func_name,
params: ParamInfo { names, skip },
decl_id,
})
}
fn extract_new_expression_call_info(
new_expr: &Value,
_arg_count: usize,
constructor_index: &ConstructorIndex,
) -> Option<CallInfo> {
let type_name = new_expr.get("typeName")?;
let contract_id = NodeId(
type_name
.get("referencedDeclaration")
.and_then(|v| v.as_i64())?,
);
let info = constructor_index.get(&contract_id)?;
let contract_name = type_name
.get("pathNode")
.and_then(|p| p.get("name"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| info.contract_name.clone());
Some(CallInfo {
name: contract_name,
params: ParamInfo {
names: info.param_names.clone(),
skip: 0,
},
decl_id: info.constructor_id,
})
}
fn extract_function_name(expr: &Value) -> Option<String> {
let node_type = expr.get("nodeType").and_then(|v| v.as_str())?;
match node_type {
"Identifier" => expr.get("name").and_then(|v| v.as_str()).map(String::from),
"MemberAccess" => expr
.get("memberName")
.and_then(|v| v.as_str())
.map(String::from),
"NewExpression" => expr
.get("typeName")
.and_then(|t| {
t.get("pathNode")
.and_then(|p| p.get("name"))
.and_then(|v| v.as_str())
.or_else(|| t.get("name").and_then(|v| v.as_str()))
})
.map(String::from),
_ => None,
}
}
fn is_member_access(expr: &Value) -> bool {
expr.get("nodeType")
.and_then(|v| v.as_str())
.is_some_and(|t| t == "MemberAccess")
}
fn lookup_call_site<'a>(
lookup: &'a HintLookup,
offset: usize,
name: &str,
arg_count: usize,
) -> Option<&'a CallSite> {
if let Some(site) = lookup.by_offset.get(&offset)
&& site.name == name
{
return Some(site);
}
lookup.by_name.get(&(name.to_string(), arg_count))
}
fn collect_ts_hints(
node: Node,
source: &str,
range: &Range,
lookup: &HintLookup,
hints: &mut Vec<InlayHint>,
) {
let node_start = node.start_position();
let node_end = node.end_position();
if (node_end.row as u32) < range.start.line || (node_start.row as u32) > range.end.line {
return;
}
match node.kind() {
"call_expression" => {
emit_call_hints(node, source, lookup, hints);
}
"emit_statement" => {
emit_emit_hints(node, source, lookup, hints);
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_ts_hints(child, source, range, lookup, hints);
}
}
fn emit_call_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
let func_name = match ts_call_function_name(node, source) {
Some(n) => n,
None => return,
};
let args = ts_call_arguments(node);
if args.is_empty() {
return;
}
let site = match lookup_call_site(lookup, node.start_byte(), func_name, args.len()) {
Some(s) => s,
None => return,
};
emit_param_hints(&args, &site.info, hints);
}
fn emit_emit_hints(node: Node, source: &str, lookup: &HintLookup, hints: &mut Vec<InlayHint>) {
let event_name = match ts_emit_event_name(node, source) {
Some(n) => n,
None => return,
};
let args = ts_call_arguments(node);
if args.is_empty() {
return;
}
let site = match lookup_call_site(lookup, node.start_byte(), event_name, args.len()) {
Some(s) => s,
None => return,
};
emit_param_hints(&args, &site.info, hints);
}
fn emit_param_hints(args: &[Node], info: &ParamInfo, hints: &mut Vec<InlayHint>) {
for (i, arg) in args.iter().enumerate() {
let pi = i + info.skip;
if pi >= info.names.len() || info.names[pi].is_empty() {
continue;
}
let start = arg.start_position();
let position = Position::new(start.row as u32, start.column as u32);
hints.push(InlayHint {
position,
kind: Some(InlayHintKind::PARAMETER),
label: InlayHintLabel::String(format!("{}:", info.names[pi])),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: Some(true),
data: None,
});
}
}
fn ts_call_function_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
let func_expr = node.child_by_field_name("function")?;
let inner = first_named_child(func_expr)?;
extract_name_from_expr(inner, source)
}
fn extract_name_from_expr<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
match node.kind() {
"identifier" => Some(&source[node.byte_range()]),
"member_expression" => {
let prop = node.child_by_field_name("property")?;
Some(&source[prop.byte_range()])
}
"struct_expression" => {
let type_expr = node.child_by_field_name("type")?;
extract_name_from_expr(type_expr, source)
}
"new_expression" => {
ts_new_expression_name(node, source)
}
"expression" => {
let inner = first_named_child(node)?;
extract_name_from_expr(inner, source)
}
_ => None,
}
}
fn ts_emit_event_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
let name_expr = node.child_by_field_name("name")?;
let inner = first_named_child(name_expr)?;
match inner.kind() {
"identifier" => Some(&source[inner.byte_range()]),
"member_expression" => {
let prop = inner.child_by_field_name("property")?;
Some(&source[prop.byte_range()])
}
_ => None,
}
}
fn ts_new_expression_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
let name_node = node.child_by_field_name("name")?;
if name_node.kind() == "user_defined_type" || name_node.kind() == "type_name" {
let mut cursor = name_node.walk();
for child in name_node.children(&mut cursor) {
if child.kind() == "identifier" {
return Some(&source[child.byte_range()]);
}
}
Some(&source[name_node.byte_range()])
} else {
Some(&source[name_node.byte_range()])
}
}
fn ts_call_arguments(node: Node) -> Vec<Node> {
let mut args = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "call_argument" {
args.push(child);
}
}
args
}
fn first_named_child(node: Node) -> Option<Node> {
let mut cursor = node.walk();
node.children(&mut cursor).find(|c| c.is_named())
}
pub struct TsCallContext<'a> {
pub name: &'a str,
pub arg_index: usize,
pub arg_count: usize,
pub call_start_byte: usize,
pub is_index_access: bool,
}
pub fn ts_find_call_at_byte<'a>(
root: tree_sitter::Node<'a>,
source: &'a str,
byte_pos: usize,
) -> Option<TsCallContext<'a>> {
let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
loop {
if node.kind() == "call_argument" {
break;
}
node = node.parent()?;
}
let call_node = node.parent()?;
let args = ts_call_arguments(call_node);
let arg_index = args.iter().position(|a| a.id() == node.id())?;
match call_node.kind() {
"call_expression" => {
let name = ts_call_function_name(call_node, source)?;
Some(TsCallContext {
name,
arg_index,
arg_count: args.len(),
call_start_byte: call_node.start_byte(),
is_index_access: false,
})
}
"emit_statement" => {
let name = ts_emit_event_name(call_node, source)?;
Some(TsCallContext {
name,
arg_index,
arg_count: args.len(),
call_start_byte: call_node.start_byte(),
is_index_access: false,
})
}
_ => None,
}
}
pub fn ts_find_call_for_signature<'a>(
root: tree_sitter::Node<'a>,
source: &'a str,
byte_pos: usize,
) -> Option<TsCallContext<'a>> {
if let Some(ctx) = ts_find_call_at_byte(root, source, byte_pos) {
return Some(ctx);
}
let mut node = root.descendant_for_byte_range(byte_pos, byte_pos)?;
loop {
match node.kind() {
"call_expression" => {
let name = ts_call_function_name(node, source)?;
let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
let args = ts_call_arguments(node);
let arg_count = args.len().max(arg_index + 1);
return Some(TsCallContext {
name,
arg_index,
arg_count,
call_start_byte: node.start_byte(),
is_index_access: false,
});
}
"emit_statement" => {
let name = ts_emit_event_name(node, source)?;
let arg_index = count_commas_before(source, node.start_byte(), byte_pos);
let args = ts_call_arguments(node);
let arg_count = args.len().max(arg_index + 1);
return Some(TsCallContext {
name,
arg_index,
arg_count,
call_start_byte: node.start_byte(),
is_index_access: false,
});
}
"array_access" => {
let base_node = node.child_by_field_name("base")?;
let name_node = if base_node.kind() == "member_expression" {
base_node
.child_by_field_name("property")
.unwrap_or(base_node)
} else {
base_node
};
let name = &source[name_node.byte_range()];
return Some(TsCallContext {
name,
arg_index: 0,
arg_count: 1,
call_start_byte: node.start_byte(),
is_index_access: true,
});
}
"source_file" => break,
_ => {
node = node.parent()?;
}
}
}
if let Some(ctx) = find_call_by_text_scan(source, byte_pos) {
return Some(ctx);
}
find_index_by_text_scan(source, byte_pos)
}
fn find_call_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
let before = &source[..byte_pos.min(source.len())];
let mut depth: i32 = 0;
let mut paren_pos = None;
for (i, ch) in before.char_indices().rev() {
match ch {
')' => depth += 1,
'(' => {
if depth == 0 {
paren_pos = Some(i);
break;
}
depth -= 1;
}
_ => {}
}
}
let paren_pos = paren_pos?;
let mut scan_end = paren_pos;
let before_paren = source[..scan_end].trim_end();
if before_paren.ends_with('}') {
let mut brace_depth: i32 = 0;
for (i, ch) in before_paren.char_indices().rev() {
match ch {
'}' => brace_depth += 1,
'{' => {
brace_depth -= 1;
if brace_depth == 0 {
scan_end = i;
break;
}
}
_ => {}
}
}
}
let before_name = &source[..scan_end];
let name_end = before_name.trim_end().len();
let name_start = before_name[..name_end]
.rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
.map(|i| i + 1)
.unwrap_or(0);
let raw_name = &source[name_start..name_end];
let name = match raw_name.rfind('.') {
Some(dot) => &raw_name[dot + 1..],
None => raw_name,
};
if name.is_empty() || !name.chars().next()?.is_alphabetic() {
return None;
}
let arg_index = count_commas_before(source, paren_pos, byte_pos);
Some(TsCallContext {
name,
arg_index,
arg_count: arg_index + 1,
call_start_byte: name_start,
is_index_access: false,
})
}
fn find_index_by_text_scan<'a>(source: &'a str, byte_pos: usize) -> Option<TsCallContext<'a>> {
let before = &source[..byte_pos.min(source.len())];
let mut depth: i32 = 0;
let mut bracket_pos = None;
for (i, c) in before.char_indices().rev() {
match c {
']' => depth += 1,
'[' => {
if depth == 0 {
bracket_pos = Some(i);
break;
}
depth -= 1;
}
_ => {}
}
}
let bracket_pos = bracket_pos?;
let before_bracket = &source[..bracket_pos];
let name_end = before_bracket.trim_end().len();
let name_start = before_bracket[..name_end]
.rfind(|c: char| !c.is_alphanumeric() && c != '_')
.map(|i| i + 1)
.unwrap_or(0);
let name = &source[name_start..name_end];
if name.is_empty() || !name.chars().next()?.is_alphabetic() {
return None;
}
Some(TsCallContext {
name,
arg_index: 0,
arg_count: 1,
call_start_byte: name_start,
is_index_access: true,
})
}
fn count_commas_before(source: &str, start: usize, byte_pos: usize) -> usize {
let end = byte_pos.min(source.len());
let text = &source[start..end];
let mut count = 0;
let mut depth = 0;
let mut found_open = false;
for ch in text.chars() {
match ch {
'(' if !found_open => {
found_open = true;
depth = 1;
}
'(' => depth += 1,
')' => depth -= 1,
',' if found_open && depth == 1 => count += 1,
_ => {}
}
}
count
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ts_call_function_name() {
let source = r#"
contract Foo {
function bar(uint x) public {}
function test() public {
bar(42);
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut found = Vec::new();
find_calls(tree.root_node(), source, &mut found);
assert_eq!(found.len(), 1);
assert_eq!(found[0], "bar");
}
#[test]
fn test_ts_member_call_name() {
let source = r#"
contract Foo {
function test() public {
PRICE.addTax(TAX, TAX_BASE);
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut found = Vec::new();
find_calls(tree.root_node(), source, &mut found);
assert_eq!(found.len(), 1);
assert_eq!(found[0], "addTax");
}
#[test]
fn test_ts_call_with_value_modifier() {
let source = r#"
contract Foo {
function test() public {
router.swap{value: 100}(nativeKey, SWAP_PARAMS, testSettings, ZERO_BYTES);
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut found = Vec::new();
find_calls(tree.root_node(), source, &mut found);
assert_eq!(found.len(), 1, "should find one call");
assert_eq!(
found[0], "swap",
"should extract 'swap' through struct_expression"
);
}
#[test]
fn test_ts_call_simple_with_value_modifier() {
let source = r#"
contract Foo {
function test() public {
foo{value: 1 ether}(42);
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut found = Vec::new();
find_calls(tree.root_node(), source, &mut found);
assert_eq!(found.len(), 1, "should find one call");
assert_eq!(
found[0], "foo",
"should extract 'foo' through struct_expression"
);
}
#[test]
fn test_ts_call_with_gas_modifier() {
let source = r#"
contract Foo {
function test() public {
addr.call{gas: 5000, value: 1 ether}("");
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut found = Vec::new();
find_calls(tree.root_node(), source, &mut found);
assert_eq!(found.len(), 1, "should find one call");
assert_eq!(
found[0], "call",
"should extract 'call' through struct_expression"
);
}
#[test]
fn test_find_call_by_text_scan_with_value_modifier() {
let source = "router.swap{value: 100}(nativeKey, SWAP_PARAMS)";
let byte_pos = source.find("SWAP_PARAMS").unwrap();
let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
assert_eq!(ctx.name, "swap");
assert_eq!(ctx.arg_index, 1);
}
#[test]
fn test_find_call_by_text_scan_simple_value_modifier() {
let source = "foo{value: 1 ether}(42)";
let byte_pos = source.find("42").unwrap();
let ctx = find_call_by_text_scan(source, byte_pos).unwrap();
assert_eq!(ctx.name, "foo");
assert_eq!(ctx.arg_index, 0);
}
#[test]
fn test_ts_emit_event_name() {
let source = r#"
contract Foo {
event Purchase(address buyer, uint256 price);
function test() public {
emit Purchase(msg.sender, 100);
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut found = Vec::new();
find_emits(tree.root_node(), source, &mut found);
assert_eq!(found.len(), 1);
assert_eq!(found[0], "Purchase");
}
#[test]
fn test_ts_new_expression_name() {
let source = r#"
contract Token {
constructor(string memory _name, uint256 _supply) {}
}
contract Factory {
function create() public {
Token t = new Token("MyToken", 1000);
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut found = Vec::new();
find_new_exprs(tree.root_node(), source, &mut found);
assert_eq!(found.len(), 1);
assert_eq!(found[0], "Token");
}
#[test]
fn test_ts_new_expression_arguments() {
let source = r#"
contract Router {
constructor(address _manager, address _hook) {}
}
contract Factory {
function create() public {
Router r = new Router(address(this), address(0));
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut arg_counts = Vec::new();
find_call_arg_counts(tree.root_node(), &mut arg_counts);
assert_eq!(arg_counts, vec![2]);
}
#[test]
fn test_ts_find_call_at_byte_new_expression() {
let source = r#"
contract Token {
constructor(string memory _name, uint256 _supply) {}
}
contract Factory {
function create() public {
Token t = new Token("MyToken", 1000);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos = source.find("1000").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos).unwrap();
assert_eq!(ctx.name, "Token");
assert_eq!(ctx.arg_index, 1);
assert_eq!(ctx.arg_count, 2);
}
#[test]
fn test_ts_find_call_at_byte_new_expression_first_arg() {
let source = r#"
contract Token {
constructor(string memory _name, uint256 _supply) {}
}
contract Factory {
function create() public {
Token t = new Token("MyToken", 1000);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos = source.find("\"MyToken\"").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos).unwrap();
assert_eq!(ctx.name, "Token");
assert_eq!(ctx.arg_index, 0);
assert_eq!(ctx.arg_count, 2);
}
#[test]
fn test_extract_new_expression_call_info() {
let new_expr: Value = serde_json::json!({
"nodeType": "NewExpression",
"typeName": {
"nodeType": "UserDefinedTypeName",
"referencedDeclaration": 22,
"pathNode": {
"name": "Token"
}
}
});
let mut constructor_index = ConstructorIndex::new();
constructor_index.insert(
NodeId(22),
ConstructorInfo {
constructor_id: NodeId(21),
contract_name: "Token".to_string(),
param_names: vec!["_name".to_string(), "_supply".to_string()],
},
);
let info = extract_new_expression_call_info(&new_expr, 2, &constructor_index).unwrap();
assert_eq!(info.name, "Token");
assert_eq!(info.params.names, vec!["_name", "_supply"]);
assert_eq!(info.params.skip, 0);
assert_eq!(info.decl_id, NodeId(21));
}
#[test]
fn test_ts_call_arguments_count() {
let source = r#"
contract Foo {
function bar(uint x, uint y) public {}
function test() public {
bar(1, 2);
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut arg_counts = Vec::new();
find_call_arg_counts(tree.root_node(), &mut arg_counts);
assert_eq!(arg_counts, vec![2]);
}
#[test]
fn test_ts_argument_positions_follow_live_buffer() {
let source = r#"
contract Foo {
function bar(uint x, uint y) public {}
function test() public {
bar(
1,
2
);
}
}
"#;
let tree = ts_parse(source).unwrap();
let mut positions = Vec::new();
find_arg_positions(tree.root_node(), &mut positions);
assert_eq!(positions.len(), 2);
assert_eq!(positions[0].0, 5); assert_eq!(positions[1].0, 6); }
fn find_calls<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
if node.kind() == "call_expression"
&& let Some(name) = ts_call_function_name(node, source)
{
out.push(name);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
find_calls(child, source, out);
}
}
fn find_emits<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
if node.kind() == "emit_statement"
&& let Some(name) = ts_emit_event_name(node, source)
{
out.push(name);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
find_emits(child, source, out);
}
}
fn find_new_exprs<'a>(node: Node<'a>, source: &'a str, out: &mut Vec<&'a str>) {
if node.kind() == "new_expression"
&& let Some(name) = ts_new_expression_name(node, source)
{
out.push(name);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
find_new_exprs(child, source, out);
}
}
fn find_call_arg_counts(node: Node, out: &mut Vec<usize>) {
if node.kind() == "call_expression" {
out.push(ts_call_arguments(node).len());
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
find_call_arg_counts(child, out);
}
}
fn find_arg_positions(node: Node, out: &mut Vec<(usize, usize)>) {
if node.kind() == "call_expression" {
for arg in ts_call_arguments(node) {
let p = arg.start_position();
out.push((p.row, p.column));
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
find_arg_positions(child, out);
}
}
#[test]
fn test_ts_find_call_at_byte_first_arg() {
let source = r#"
contract Foo {
function bar(uint x, uint y) public {}
function test() public {
bar(42, 99);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos_42 = source.find("42").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
assert_eq!(ctx.name, "bar");
assert_eq!(ctx.arg_index, 0);
assert_eq!(ctx.arg_count, 2);
}
#[test]
fn test_ts_find_call_at_byte_second_arg() {
let source = r#"
contract Foo {
function bar(uint x, uint y) public {}
function test() public {
bar(42, 99);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos_99 = source.find("99").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
assert_eq!(ctx.name, "bar");
assert_eq!(ctx.arg_index, 1);
assert_eq!(ctx.arg_count, 2);
}
#[test]
fn test_ts_find_call_at_byte_outside_call_returns_none() {
let source = r#"
contract Foo {
function bar(uint x) public {}
function test() public {
uint z = 10;
bar(42);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos_10 = source.find("10").unwrap();
assert!(ts_find_call_at_byte(tree.root_node(), source, pos_10).is_none());
}
#[test]
fn test_ts_find_call_at_byte_member_call() {
let source = r#"
contract Foo {
function test() public {
PRICE.addTax(TAX, TAX_BASE);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos_tax = source.find("TAX,").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tax).unwrap();
assert_eq!(ctx.name, "addTax");
assert_eq!(ctx.arg_index, 0);
assert_eq!(ctx.arg_count, 2);
}
#[test]
fn test_ts_find_call_at_byte_emit_statement() {
let source = r#"
contract Foo {
event Purchase(address buyer, uint256 price);
function test() public {
emit Purchase(msg.sender, 100);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos_100 = source.find("100").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_100).unwrap();
assert_eq!(ctx.name, "Purchase");
assert_eq!(ctx.arg_index, 1);
assert_eq!(ctx.arg_count, 2);
}
#[test]
fn test_ts_find_call_at_byte_multiline() {
let source = r#"
contract Foo {
function bar(uint x, uint y, uint z) public {}
function test() public {
bar(
1,
2,
3
);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos_2 = source.find(" 2").unwrap() + 12; let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_2).unwrap();
assert_eq!(ctx.name, "bar");
assert_eq!(ctx.arg_index, 1);
assert_eq!(ctx.arg_count, 3);
}
#[test]
fn test_resolve_callsite_param_basic() {
let mut lookup = HintLookup {
by_offset: HashMap::new(),
by_name: HashMap::new(),
};
lookup.by_name.insert(
("transfer".to_string(), 2),
CallSite {
info: ParamInfo {
names: vec!["to".to_string(), "amount".to_string()],
skip: 0,
},
name: "transfer".to_string(),
decl_id: NodeId(42),
},
);
let result = lookup.resolve_callsite_param(0, "transfer", 2, 0).unwrap();
assert_eq!(result.param_name, "to");
assert_eq!(result.decl_id, NodeId(42));
let result = lookup.resolve_callsite_param(0, "transfer", 2, 1).unwrap();
assert_eq!(result.param_name, "amount");
assert_eq!(result.decl_id, NodeId(42));
}
#[test]
fn test_resolve_callsite_param_with_skip() {
let mut lookup = HintLookup {
by_offset: HashMap::new(),
by_name: HashMap::new(),
};
lookup.by_name.insert(
("addTax".to_string(), 2),
CallSite {
info: ParamInfo {
names: vec!["self".to_string(), "tax".to_string(), "base".to_string()],
skip: 1,
},
name: "addTax".to_string(),
decl_id: NodeId(99),
},
);
let result = lookup.resolve_callsite_param(0, "addTax", 2, 0).unwrap();
assert_eq!(result.param_name, "tax");
let result = lookup.resolve_callsite_param(0, "addTax", 2, 1).unwrap();
assert_eq!(result.param_name, "base");
}
#[test]
fn test_resolve_callsite_param_out_of_bounds() {
let mut lookup = HintLookup {
by_offset: HashMap::new(),
by_name: HashMap::new(),
};
lookup.by_name.insert(
("foo".to_string(), 1),
CallSite {
info: ParamInfo {
names: vec!["x".to_string()],
skip: 0,
},
name: "foo".to_string(),
decl_id: NodeId(1),
},
);
assert!(lookup.resolve_callsite_param(0, "foo", 1, 1).is_none());
}
#[test]
fn test_resolve_callsite_param_unknown_function() {
let lookup = HintLookup {
by_offset: HashMap::new(),
by_name: HashMap::new(),
};
assert!(lookup.resolve_callsite_param(0, "unknown", 1, 0).is_none());
}
#[test]
fn test_ts_find_call_at_byte_emit_member_access() {
let source = r#"
contract Foo {
event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
function test() public {
emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
}
}
"#;
let tree = ts_parse(source).unwrap();
let params_tick = source.find("params.tickLower,").unwrap();
let tick_lower_pos = params_tick + "params.".len();
let ctx = ts_find_call_at_byte(tree.root_node(), source, tick_lower_pos).unwrap();
assert_eq!(ctx.name, "ModifyLiquidity");
assert_eq!(
ctx.arg_index, 2,
"params.tickLower is the 3rd argument (index 2)"
);
assert_eq!(ctx.arg_count, 4);
}
#[test]
fn test_ts_find_call_at_byte_member_access_on_property() {
let source = r#"
contract Foo {
event Transfer(address from, address to);
function test() public {
emit Transfer(msg.sender, addr);
}
}
"#;
let tree = ts_parse(source).unwrap();
let sender_pos = source.find("sender").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, sender_pos).unwrap();
assert_eq!(ctx.name, "Transfer");
assert_eq!(ctx.arg_index, 0, "msg.sender is the 1st argument");
}
#[test]
fn test_ts_find_call_at_byte_emit_all_args() {
let source = r#"
contract Foo {
event ModifyLiquidity(uint id, address sender, int24 tickLower, int24 tickUpper);
function test() public {
emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos_id = source.find("(id,").unwrap() + 1;
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_id).unwrap();
assert_eq!(ctx.name, "ModifyLiquidity");
assert_eq!(ctx.arg_index, 0);
let pos_msg = source.find("msg.sender").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_msg).unwrap();
assert_eq!(ctx.arg_index, 1);
let pos_tl = source.find("params.tickLower").unwrap() + "params.".len();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tl).unwrap();
assert_eq!(ctx.arg_index, 2);
let pos_tu = source.find("params.tickUpper").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_tu).unwrap();
assert_eq!(ctx.arg_index, 3);
}
#[test]
fn test_ts_find_call_at_byte_nested_call_arg() {
let source = r#"
contract Foo {
function inner(uint x) public returns (uint) {}
function outer(uint a, uint b) public {}
function test() public {
outer(inner(42), 99);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos_42 = source.find("42").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_42).unwrap();
assert_eq!(ctx.name, "inner");
assert_eq!(ctx.arg_index, 0);
let pos_99 = source.find("99").unwrap();
let ctx = ts_find_call_at_byte(tree.root_node(), source, pos_99).unwrap();
assert_eq!(ctx.name, "outer");
assert_eq!(ctx.arg_index, 1);
}
#[test]
fn test_ts_find_call_for_signature_incomplete_call() {
let source = r#"
contract Foo {
function bar(uint x, uint y) public {}
function test() public {
bar(
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos = source.find("bar(").unwrap() + 4;
let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
assert_eq!(ctx.name, "bar");
assert_eq!(ctx.arg_index, 0);
}
#[test]
fn test_ts_find_call_for_signature_after_comma() {
let source = r#"
contract Foo {
function bar(uint x, uint y) public {}
function test() public {
bar(42,
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos = source.find("42,").unwrap() + 3;
let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
assert_eq!(ctx.name, "bar");
assert_eq!(ctx.arg_index, 1);
}
#[test]
fn test_ts_find_call_for_signature_complete_call() {
let source = r#"
contract Foo {
function bar(uint x, uint y) public {}
function test() public {
bar(42, 99);
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos = source.find("42").unwrap();
let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
assert_eq!(ctx.name, "bar");
assert_eq!(ctx.arg_index, 0);
}
#[test]
fn test_ts_find_call_for_signature_member_call() {
let source = r#"
contract Foo {
function test() public {
PRICE.addTax(
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos = source.find("addTax(").unwrap() + 7;
let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
assert_eq!(ctx.name, "addTax");
assert_eq!(ctx.arg_index, 0);
}
#[test]
fn test_ts_find_call_for_signature_array_access() {
let source = r#"
contract Foo {
mapping(bytes32 => uint256) public orders;
function test() public {
orders[orderId];
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos = source.find("[orderId]").unwrap() + 1;
let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
assert_eq!(ctx.name, "orders");
assert_eq!(ctx.arg_index, 0);
assert!(ctx.is_index_access);
}
#[test]
fn test_ts_find_call_for_signature_array_access_empty() {
let source = r#"
contract Foo {
mapping(bytes32 => uint256) public orders;
function test() public {
orders[
}
}
"#;
let tree = ts_parse(source).unwrap();
let pos = source.find("orders[").unwrap() + 7;
let ctx = ts_find_call_for_signature(tree.root_node(), source, pos).unwrap();
assert_eq!(ctx.name, "orders");
assert!(ctx.is_index_access);
}
}