use tree_sitter::{Node, Parser};
use crate::error::{CodegraphError, Result};
use crate::graph::types::{
Binding, BindingKind, ByteSpan, FileFacts, RefRole, Reference, Scope, ScopeId, ScopeKind,
Symbol, SymbolKind, TypeRefContext, Visibility,
};
use crate::lang::Language;
use crate::symbol::Descriptor;
use super::{
ExtractCtx, Extractor, MIN_REF_LEN, attach_reference_scopes, collect_call_references,
definition_bindings, field_text, import_bindings, innermost_scope, make_symbol, node_span,
node_text, one_line_signature, push_binding, push_ref, push_scope, push_type_ref,
};
const CALL_QUERY: &str = r#"
[
(call_expression (expression (identifier) @callee))
(call_expression (expression (member_expression property: (identifier) @callee)))
]
"#;
pub struct SolidityExtractor;
impl Extractor for SolidityExtractor {
fn lang(&self) -> Language {
Language::Solidity
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::solidity();
let mut parser = Parser::new();
parser
.set_language(&ts_language)
.map_err(|_| CodegraphError::Parse {
path: file.to_owned(),
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| CodegraphError::Parse {
path: file.to_owned(),
})?;
let root = tree.root_node();
let bytes = source.as_bytes();
let ctx = ExtractCtx {
bytes,
file,
lang: Language::Solidity,
};
let ns_strings = solidity_namespaces(file);
let ns_descriptors: Vec<Descriptor> = ns_strings
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
let mut defs = Vec::new();
collect_decls(root, &ns_descriptors, false, &ctx, &mut defs);
let def_bindings = definition_bindings(&defs);
let mut symbols = defs;
symbols.push(super::module_symbol(
Language::Solidity,
&ns_strings,
file,
source.len(),
));
let mut references = collect_call_references(
&root,
&ts_language,
CALL_QUERY,
Language::Solidity,
ctx.bytes,
ctx.file,
)?;
collect_inheritance(&root, ctx.bytes, ctx.file, &mut references);
collect_imports(&root, ctx.bytes, ctx.file, &mut references);
collect_type_references(&root, ctx.bytes, ctx.file, &mut references);
collect_read_references(&root, ctx.bytes, ctx.file, &mut references);
collect_write_references(&root, ctx.bytes, ctx.file, &mut references);
let scopes = collect_scopes(&root, source.len());
attach_reference_scopes(&mut references, &scopes);
let mut bindings = collect_bindings(&root, ctx.bytes, &scopes);
bindings.extend(def_bindings);
bindings.extend(import_bindings(&references, &scopes));
Ok(FileFacts {
file: file.to_owned(),
lang: Language::Solidity.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports: Vec::new(),
})
}
}
fn solidity_namespaces(file: &str) -> Vec<String> {
let p = file.strip_suffix(".sol").unwrap_or(file);
p.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn read_visibility(node: &Node, bytes: &[u8]) -> Visibility {
for child in node.children(&mut node.walk()) {
if child.kind() == "visibility" {
return match node_text(&child, bytes) {
"public" | "external" => Visibility::Public,
"internal" => Visibility::Internal,
"private" => Visibility::Private,
_ => Visibility::Unknown,
};
}
}
Visibility::Internal
}
fn push_symbol(
out: &mut Vec<Symbol>,
ctx: &ExtractCtx,
node: &Node,
name: String,
kind: SymbolKind,
visibility: Visibility,
descriptors: Vec<Descriptor>,
) {
let signature = one_line_signature(node_text(node, ctx.bytes), &['{', ';']);
out.push(make_symbol(
ctx,
node,
name,
kind,
visibility,
descriptors,
signature,
));
}
fn emit_container_and_body(
out: &mut Vec<Symbol>,
ctx: &ExtractCtx,
node: Node,
type_name: String,
kind: SymbolKind,
prefix: &[Descriptor],
) {
let mut type_descriptors = prefix.to_vec();
type_descriptors.push(Descriptor::Type(type_name.clone()));
push_symbol(
out,
ctx,
&node,
type_name,
kind,
Visibility::Public,
type_descriptors.clone(),
);
if let Some(body) = node.child_by_field_name("body") {
collect_decls(body, &type_descriptors, true, ctx, out);
}
}
fn collect_decls(
container: Node,
prefix: &[Descriptor],
inside_type: bool,
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let mut cursor = container.walk();
for child in container.children(&mut cursor) {
match child.kind() {
"contract_declaration" | "library_declaration" => {
handle_container(child, SymbolKind::Class, prefix, ctx, out);
}
"interface_declaration" => {
handle_container(child, SymbolKind::Interface, prefix, ctx, out);
}
"function_definition" => {
handle_function(child, prefix, inside_type, ctx, out);
}
"constructor_definition" => {
handle_constructor(child, prefix, ctx, out);
}
"modifier_definition" => {
handle_modifier(child, prefix, ctx, out);
}
"fallback_receive_definition" => {
handle_fallback_receive(child, prefix, ctx, out);
}
"state_variable_declaration" => {
handle_state_variable(child, prefix, ctx, out);
}
"constant_variable_declaration" => {
handle_constant_variable(child, prefix, ctx, out);
}
"event_definition" | "error_declaration" => {
handle_event_or_error(child, prefix, ctx, out);
}
"struct_declaration" => {
handle_struct(child, prefix, ctx, out);
}
"enum_declaration" => {
handle_enum(child, prefix, ctx, out);
}
"user_defined_type_definition" => {
handle_typedef(child, prefix, ctx, out);
}
_ => {}
}
}
}
fn handle_container(
node: Node,
kind: SymbolKind,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let type_name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
emit_container_and_body(out, ctx, node, type_name, kind, prefix);
}
fn handle_function(
node: Node,
prefix: &[Descriptor],
inside_type: bool,
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let kind = if inside_type {
SymbolKind::Method
} else {
SymbolKind::Function
};
let visibility = read_visibility(&node, ctx.bytes);
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
push_symbol(out, ctx, &node, name, kind, visibility, descriptors);
}
fn handle_constructor(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: "constructor".to_owned(),
disambiguator: String::new(),
});
push_symbol(
out,
ctx,
&node,
"constructor".to_owned(),
SymbolKind::Method,
Visibility::Public,
descriptors,
);
}
fn handle_modifier(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
push_symbol(
out,
ctx,
&node,
name,
SymbolKind::Method,
Visibility::Internal,
descriptors,
);
}
fn handle_fallback_receive(
node: Node,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let text = node_text(&node, ctx.bytes).trim_start();
let name = if text.starts_with("fallback") {
"fallback"
} else if text.starts_with("receive") {
"receive"
} else {
return;
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.to_owned(),
disambiguator: String::new(),
});
push_symbol(
out,
ctx,
&node,
name.to_owned(),
SymbolKind::Method,
Visibility::Public,
descriptors,
);
}
fn handle_state_variable(
node: Node,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let visibility = match node.child_by_field_name("visibility") {
Some(vis_node) => match node_text(&vis_node, ctx.bytes) {
"public" | "external" => Visibility::Public,
"internal" => Visibility::Internal,
"private" => Visibility::Private,
_ => Visibility::Unknown,
},
None => Visibility::Internal,
};
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let has_immutable = node
.children(&mut node.walk())
.any(|c| c.kind() == "immutable");
let text = node_text(&node, ctx.bytes);
let is_constant = has_immutable || text.split_whitespace().any(|w| w == "constant");
let kind = if is_constant {
SymbolKind::Const
} else {
SymbolKind::Static
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
push_symbol(out, ctx, &node, name, kind, visibility, descriptors);
}
fn handle_constant_variable(
node: Node,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
push_symbol(
out,
ctx,
&node,
name,
SymbolKind::Const,
Visibility::Public,
descriptors,
);
}
fn handle_event_or_error(
node: Node,
prefix: &[Descriptor],
ctx: &ExtractCtx,
out: &mut Vec<Symbol>,
) {
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
push_symbol(
out,
ctx,
&node,
name,
SymbolKind::Other,
Visibility::Public,
descriptors,
);
}
fn handle_struct(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let type_name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut type_descriptors = prefix.to_vec();
type_descriptors.push(Descriptor::Type(type_name.clone()));
push_symbol(
out,
ctx,
&node,
type_name,
SymbolKind::Struct,
Visibility::Public,
type_descriptors.clone(),
);
let body = match node.child_by_field_name("body") {
Some(b) => b,
None => return,
};
let mut cursor = body.walk();
for member in body.children(&mut cursor) {
if member.kind() != "struct_member" {
continue;
}
let member_name = match field_text(&member, "name", ctx.bytes) {
Some(n) => n,
None => continue,
};
let mut member_descriptors = type_descriptors.clone();
member_descriptors.push(Descriptor::Term(member_name.clone()));
push_symbol(
out,
ctx,
&member,
member_name,
SymbolKind::Static,
Visibility::Public,
member_descriptors,
);
}
}
fn handle_enum(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let type_name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut type_descriptors = prefix.to_vec();
type_descriptors.push(Descriptor::Type(type_name.clone()));
push_symbol(
out,
ctx,
&node,
type_name,
SymbolKind::Enum,
Visibility::Public,
type_descriptors.clone(),
);
let body = match node.child_by_field_name("body") {
Some(b) => b,
None => return,
};
let mut cursor = body.walk();
for value_node in body.children(&mut cursor) {
if value_node.kind() != "enum_value" {
continue;
}
let value_name = node_text(&value_node, ctx.bytes).to_owned();
if value_name.is_empty() {
continue;
}
let mut value_descriptors = type_descriptors.clone();
value_descriptors.push(Descriptor::Term(value_name.clone()));
push_symbol(
out,
ctx,
&value_node,
value_name,
SymbolKind::Const,
Visibility::Public,
value_descriptors,
);
}
}
fn handle_typedef(node: Node, prefix: &[Descriptor], ctx: &ExtractCtx, out: &mut Vec<Symbol>) {
let name = match field_text(&node, "name", ctx.bytes) {
Some(n) => n,
None => return,
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
push_symbol(
out,
ctx,
&node,
name,
SymbolKind::TypeAlias,
Visibility::Public,
descriptors,
);
}
fn type_leaf(
type_node: &Node,
bytes: &[u8],
file: &str,
ctx: TypeRefContext,
out: &mut Vec<Reference>,
) {
match type_node.kind() {
"user_defined_type" => {
let last_ident = type_node
.children(&mut type_node.walk())
.filter(|c| c.kind() == "identifier")
.last();
if let Some(ident) = last_ident {
let name = node_text(&ident, bytes);
push_type_ref(out, name, &ident, file, ctx);
}
}
"primitive_type" => {}
"mapping" => {
if let Some(key) = type_node.child_by_field_name("key") {
type_leaf(&key, bytes, file, ctx, out);
}
if let Some(value) = type_node.child_by_field_name("value") {
type_leaf(&value, bytes, file, ctx, out);
}
}
"array_type" => {
if let Some(elem) = type_node.named_children(&mut type_node.walk()).next() {
type_leaf(&elem, bytes, file, TypeRefContext::Other, out);
}
}
"type_name" => {
for child in type_node.named_children(&mut type_node.walk()) {
type_leaf(&child, bytes, file, ctx, out);
}
}
_ => {}
}
}
fn collect_type_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"parameter" => {
if let Some(type_node) = node.child_by_field_name("type") {
type_leaf(&type_node, bytes, file, TypeRefContext::ParameterType, out);
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
return;
}
"function_definition"
| "constructor_definition"
| "modifier_definition"
| "fallback_receive_definition" => {
for child in node.children(&mut node.walk()) {
match child.kind() {
"parameter" => {
if let Some(type_node) = child.child_by_field_name("type") {
type_leaf(&type_node, bytes, file, TypeRefContext::ParameterType, out);
}
}
"return_type_definition" => {
for ret_param in child.children(&mut child.walk()) {
if ret_param.kind() == "parameter" {
if let Some(type_node) = ret_param.child_by_field_name("type") {
type_leaf(
&type_node,
bytes,
file,
TypeRefContext::ReturnType,
out,
);
}
}
}
}
_ => {
collect_type_references(&child, bytes, file, out);
}
}
}
return; }
"struct_member" => {
if let Some(type_node) = node.child_by_field_name("type") {
type_leaf(&type_node, bytes, file, TypeRefContext::Field, out);
}
return;
}
"state_variable_declaration" => {
if let Some(type_node) = node.child_by_field_name("type") {
type_leaf(&type_node, bytes, file, TypeRefContext::Field, out);
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
return;
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
}
fn collect_scopes(root: &Node, source_len: usize) -> Vec<Scope> {
let mut scopes = Vec::new();
push_scope(
&mut scopes,
None,
ByteSpan {
start: 0,
end: source_len,
},
ScopeKind::Module,
);
for child in root.children(&mut root.walk()) {
scope_dfs(&child, 0, &mut scopes);
}
scopes
}
fn scope_dfs(node: &Node, parent_id: ScopeId, scopes: &mut Vec<Scope>) {
match node.kind() {
"contract_declaration" | "library_declaration" | "interface_declaration" => {
let type_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Type);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, type_id, scopes);
}
}
}
"function_definition"
| "modifier_definition"
| "constructor_definition"
| "fallback_receive_definition" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, fn_id, scopes);
}
}
}
"block_statement" => {
let block_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Block);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, block_id, scopes);
}
}
_ => {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, parent_id, scopes);
}
}
}
}
fn collect_bindings(root: &Node, bytes: &[u8], scopes: &[Scope]) -> Vec<Binding> {
let mut out = Vec::new();
collect_bindings_dfs(root, bytes, scopes, &mut out);
out
}
fn collect_bindings_dfs(node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
match node.kind() {
"function_definition"
| "modifier_definition"
| "constructor_definition"
| "fallback_receive_definition" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "parameter" {
if let Some(name_node) = child.child_by_field_name("name") {
let name = node_text(&name_node, bytes).to_owned();
if !name.is_empty() {
push_binding(
out,
name,
name_node.start_byte(),
BindingKind::Param,
scopes,
);
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"variable_declaration" => {
if let Some(name_node) = node.child_by_field_name("name") {
let name = node_text(&name_node, bytes).to_owned();
if !name.is_empty() {
let intro = name_node.start_byte();
if let Some(sid) = innermost_scope(intro, scopes) {
if matches!(scopes[sid].kind, ScopeKind::Function | ScopeKind::Block) {
push_binding(out, name, intro, BindingKind::Local, scopes);
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"variable_declaration_tuple" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "identifier" {
let name = node_text(&child, bytes).to_owned();
if !name.is_empty() {
let intro = child.start_byte();
if let Some(sid) = innermost_scope(intro, scopes) {
if matches!(scopes[sid].kind, ScopeKind::Function | ScopeKind::Block) {
push_binding(out, name, intro, BindingKind::Local, scopes);
}
}
}
} else {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
}
_ => {
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
}
}
fn collect_imports(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "import_directive" {
let mut cursor = node.walk();
for import_name_node in node.children_by_field_name("import_name", &mut cursor) {
let name = super::node_text(&import_name_node, bytes);
super::push_ref(out, name, &import_name_node, file, RefRole::Import);
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_imports(&child, bytes, file, out);
}
}
fn collect_inheritance(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"contract_declaration" | "interface_declaration" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "inheritance_specifier" {
if let Some(ancestor) = child.child_by_field_name("ancestor") {
super::push_ref(
out,
super::simple_type_name(node_text(&ancestor, bytes), "."),
&ancestor,
file,
RefRole::IsImplementation,
);
}
}
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_inheritance(&child, bytes, file, out);
}
}
fn is_non_read_position(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true, };
match parent.kind() {
"expression" => {
if let Some(grandparent) = parent.parent() {
match grandparent.kind() {
"call_expression" => {
if let Some(fn_field) = grandparent.child_by_field_name("function") {
if fn_field == parent {
return true; }
}
}
"assignment_expression" | "augmented_assignment_expression" => {
if let Some(left_field) = grandparent.child_by_field_name("left") {
if left_field == parent {
return true; }
}
}
_ => {}
}
}
false
}
"member_expression" => parent.child_by_field_name("property").as_ref() == Some(node),
"function_definition"
| "modifier_definition"
| "event_definition"
| "struct_declaration"
| "state_variable_declaration"
| "constant_variable_declaration"
| "constructor_definition"
| "error_declaration"
| "user_defined_type_definition"
| "enum_declaration" => parent.child_by_field_name("name").as_ref() == Some(node),
"variable_declaration" => parent.child_by_field_name("name").as_ref() == Some(node),
"parameter" => parent.child_by_field_name("name").as_ref() == Some(node),
"user_defined_type" => true,
"type_name" => {
let is_key = parent.child_by_field_name("key_identifier").as_ref() == Some(node);
let is_val = parent.child_by_field_name("value_identifier").as_ref() == Some(node);
is_key || is_val
}
"import_directive" => true,
"inheritance_specifier" => true,
"struct_field_assignment" => parent.child_by_field_name("name").as_ref() == Some(node),
_ => false,
}
}
fn collect_read_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "identifier" {
let name = node_text(node, bytes);
if name.len() >= MIN_REF_LEN && !is_non_read_position(node) {
push_ref(out, name, node, file, RefRole::Read);
}
return;
}
for child in node.children(&mut node.walk()) {
collect_read_references(&child, bytes, file, out);
}
}
fn collect_write_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if matches!(
node.kind(),
"assignment_expression" | "augmented_assignment_expression"
) {
if let Some(lhs) = node.child_by_field_name("left") {
let bare = if lhs.kind() == "expression" {
lhs.named_children(&mut lhs.walk()).next()
} else {
Some(lhs)
};
if let Some(bare_node) = bare {
if bare_node.kind() == "identifier" {
let name = node_text(&bare_node, bytes);
if name.len() >= MIN_REF_LEN {
push_ref(out, name, &bare_node, file, RefRole::Write);
}
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_write_references(&child, bytes, file, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn extract(src: &str, path: &str) -> FileFacts {
SolidityExtractor.extract(src, path).unwrap()
}
fn by_name(facts: &FileFacts, name: &str) -> Option<Symbol> {
facts.symbols.iter().find(|s| s.name == name).cloned()
}
#[test]
fn contract_function_visibility_tagged() {
let src = r#"
pragma solidity ^0.8.0;
contract Token {
function mint(address to) public {}
function _secret() private {}
}
"#;
let facts = extract(src, "contracts/Token.sol");
let token = by_name(&facts, "Token").unwrap();
assert_eq!(token.kind, SymbolKind::Class);
assert_eq!(
token.id.to_scip_string(),
"codegraph . . . contracts/Token/Token#"
);
let mint = by_name(&facts, "mint").unwrap();
assert_eq!(mint.kind, SymbolKind::Method);
assert_eq!(mint.visibility, Visibility::Public);
assert_eq!(
mint.id.to_scip_string(),
"codegraph . . . contracts/Token/Token#mint()."
);
let secret = by_name(&facts, "_secret").expect("private function must be emitted");
assert_eq!(secret.kind, SymbolKind::Method);
assert_eq!(
secret.visibility,
Visibility::Private,
"private function must carry Visibility::Private"
);
assert_eq!(
secret.id.to_scip_string(),
"codegraph . . . contracts/Token/Token#_secret()."
);
}
#[test]
fn function_public_visibility() {
let src = "contract C { function foo() public {} }";
let facts = extract(src, "contracts/C.sol");
let foo = by_name(&facts, "foo").expect("public fn must be emitted");
assert_eq!(
foo.visibility,
Visibility::Public,
"public function must carry Visibility::Public"
);
}
#[test]
fn function_external_visibility() {
let src = "contract C { function foo() external {} }";
let facts = extract(src, "contracts/C.sol");
let foo = by_name(&facts, "foo").expect("external fn must be emitted");
assert_eq!(
foo.visibility,
Visibility::Public,
"external function must carry Visibility::Public"
);
}
#[test]
fn function_internal_visibility() {
let src = "contract C { function foo() internal {} }";
let facts = extract(src, "contracts/C.sol");
let foo = by_name(&facts, "foo").expect("internal fn must be emitted");
assert_eq!(
foo.visibility,
Visibility::Internal,
"internal function must carry Visibility::Internal"
);
}
#[test]
fn function_private_visibility_emitted() {
let src = "contract C { function foo() private {} }";
let facts = extract(src, "contracts/C.sol");
let foo = by_name(&facts, "foo").expect("private fn must be emitted");
assert_eq!(
foo.visibility,
Visibility::Private,
"private function must carry Visibility::Private"
);
}
#[test]
fn function_no_visibility_defaults_to_internal() {
let src = "contract C { function helper() pure returns (uint) { return 0; } }";
let facts = extract(src, "contracts/C.sol");
let helper = by_name(&facts, "helper").expect("fn with no visibility must be emitted");
assert_eq!(
helper.visibility,
Visibility::Internal,
"function with no visibility keyword must default to Visibility::Internal"
);
}
#[test]
fn state_variable_private_visibility_emitted() {
let src = "contract C { uint256 private _balance; }";
let facts = extract(src, "contracts/C.sol");
let bal = by_name(&facts, "_balance").expect("private state var must be emitted");
assert_eq!(
bal.visibility,
Visibility::Private,
"private state variable must carry Visibility::Private"
);
}
#[test]
fn state_variable_public_visibility() {
let src = "contract C { uint256 public supply; }";
let facts = extract(src, "contracts/C.sol");
let supply = by_name(&facts, "supply").expect("public state var must be emitted");
assert_eq!(
supply.visibility,
Visibility::Public,
"public state variable must carry Visibility::Public"
);
}
#[test]
fn state_variable_no_visibility_defaults_to_internal() {
let src = "contract C { uint256 counter; }";
let facts = extract(src, "contracts/C.sol");
let counter =
by_name(&facts, "counter").expect("state var with no visibility must be emitted");
assert_eq!(
counter.visibility,
Visibility::Internal,
"state variable with no visibility keyword must default to Visibility::Internal"
);
}
#[test]
fn interface_and_library_kinds() {
let src = r#"
pragma solidity ^0.8.0;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) { return a + b; }
}
"#;
let facts = extract(src, "contracts/Defs.sol");
let iface = by_name(&facts, "IERC20").unwrap();
assert_eq!(iface.kind, SymbolKind::Interface);
assert_eq!(
iface.id.to_scip_string(),
"codegraph . . . contracts/Defs/IERC20#"
);
let lib = by_name(&facts, "SafeMath").unwrap();
assert_eq!(lib.kind, SymbolKind::Class);
assert_eq!(
lib.id.to_scip_string(),
"codegraph . . . contracts/Defs/SafeMath#"
);
}
#[test]
fn state_variable_kinds() {
let src = r#"
pragma solidity ^0.8.0;
contract Store {
uint256 public totalSupply;
uint256 public constant MAX_SUPPLY = 1000;
}
"#;
let facts = extract(src, "src/Store.sol");
let total = by_name(&facts, "totalSupply").unwrap();
assert_eq!(total.kind, SymbolKind::Static);
assert_eq!(
total.id.to_scip_string(),
"codegraph . . . src/Store/Store#totalSupply."
);
let max = by_name(&facts, "MAX_SUPPLY").unwrap();
assert_eq!(max.kind, SymbolKind::Const);
assert_eq!(
max.id.to_scip_string(),
"codegraph . . . src/Store/Store#MAX_SUPPLY."
);
}
#[test]
fn file_level_constant() {
let src = r#"
pragma solidity ^0.8.0;
uint256 constant VERSION = 1;
"#;
let facts = extract(src, "contracts/Const.sol");
let ver = by_name(&facts, "VERSION").unwrap();
assert_eq!(ver.kind, SymbolKind::Const);
assert_eq!(
ver.id.to_scip_string(),
"codegraph . . . contracts/Const/VERSION."
);
}
#[test]
fn struct_and_enum() {
let src = r#"
pragma solidity ^0.8.0;
contract Market {
struct Item {
uint256 price;
address seller;
}
enum Status { Active, Sold, Cancelled }
}
"#;
let facts = extract(src, "contracts/Market.sol");
let item = by_name(&facts, "Item").unwrap();
assert_eq!(item.kind, SymbolKind::Struct);
assert_eq!(
item.id.to_scip_string(),
"codegraph . . . contracts/Market/Market#Item#"
);
let price = by_name(&facts, "price").unwrap();
assert_eq!(price.kind, SymbolKind::Static);
assert_eq!(
price.id.to_scip_string(),
"codegraph . . . contracts/Market/Market#Item#price."
);
let seller = by_name(&facts, "seller").unwrap();
assert_eq!(seller.kind, SymbolKind::Static);
let status = by_name(&facts, "Status").unwrap();
assert_eq!(status.kind, SymbolKind::Enum);
assert_eq!(
status.id.to_scip_string(),
"codegraph . . . contracts/Market/Market#Status#"
);
let active = by_name(&facts, "Active").unwrap();
assert_eq!(active.kind, SymbolKind::Const);
assert_eq!(
active.id.to_scip_string(),
"codegraph . . . contracts/Market/Market#Status#Active."
);
}
#[test]
fn event_and_modifier() {
let src = r#"
pragma solidity ^0.8.0;
contract Vault {
event Deposit(address indexed sender, uint256 amount);
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
address owner;
}
"#;
let facts = extract(src, "contracts/Vault.sol");
let ev = by_name(&facts, "Deposit").unwrap();
assert_eq!(ev.kind, SymbolKind::Other);
assert_eq!(
ev.id.to_scip_string(),
"codegraph . . . contracts/Vault/Vault#Deposit."
);
let modifier = by_name(&facts, "onlyOwner").unwrap();
assert_eq!(modifier.kind, SymbolKind::Method);
assert_eq!(
modifier.id.to_scip_string(),
"codegraph . . . contracts/Vault/Vault#onlyOwner()."
);
}
#[test]
fn free_function_top_level() {
let src = r#"
pragma solidity ^0.8.0;
function computeHash(bytes memory data) pure returns (bytes32) {
return keccak256(data);
}
"#;
let facts = extract(src, "lib/Utils.sol");
let func = by_name(&facts, "computeHash").unwrap();
assert_eq!(func.kind, SymbolKind::Function);
assert_eq!(
func.id.to_scip_string(),
"codegraph . . . lib/Utils/computeHash()."
);
}
#[test]
fn call_references_captured() {
let src = r#"
pragma solidity ^0.8.0;
contract Caller {
function run() public {
foo();
x.bar();
}
}
"#;
let facts = extract(src, "contracts/Caller.sol");
let names: Vec<&str> = facts.references.iter().map(|r| r.name.as_str()).collect();
assert!(names.contains(&"foo"), "expected 'foo' in {names:?}");
assert!(names.contains(&"bar"), "expected 'bar' in {names:?}");
}
#[test]
fn lang_tag() {
let facts = extract("pragma solidity ^0.8.0;", "contracts/Foo.sol");
assert_eq!(facts.lang, "solidity");
}
#[test]
fn contract_multiple_inheritance() {
let src = "pragma solidity ^0.8.0; contract Foo is Bar, Baz {}";
let facts = extract(src, "contracts/Foo.sol");
let inherit: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(inherit.contains(&"Bar"), "expected 'Bar' in {inherit:?}");
assert!(inherit.contains(&"Baz"), "expected 'Baz' in {inherit:?}");
}
#[test]
fn interface_inheritance() {
let src = "pragma solidity ^0.8.0; interface I is J {}";
let facts = extract(src, "contracts/I.sol");
let inherit: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(inherit.contains(&"J"), "expected 'J' in {inherit:?}");
}
#[test]
fn dotted_parent_simple_name() {
let src = "pragma solidity ^0.8.0; contract C is Lib.Base {}";
let facts = extract(src, "contracts/C.sol");
let inherit: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit.contains(&"Base"),
"expected 'Base' (leaf of 'Lib.Base') in {inherit:?}"
);
assert!(
!inherit.contains(&"Lib.Base"),
"dotted form must not appear in {inherit:?}"
);
}
#[test]
fn import_single_named() {
let src = r#"pragma solidity ^0.8.0; import {ERC20} from "./ERC20.sol";"#;
let facts = extract(src, "contracts/Token.sol");
let imports: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
imports,
vec!["ERC20"],
"expected [\"ERC20\"] but got {imports:?}"
);
}
#[test]
fn import_multiple_named() {
let src = r#"pragma solidity ^0.8.0; import {A, B} from "x.sol";"#;
let facts = extract(src, "contracts/Multi.sol");
let imports: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(imports.contains(&"A"), "expected 'A' in {imports:?}");
assert!(imports.contains(&"B"), "expected 'B' in {imports:?}");
assert_eq!(
imports.len(),
2,
"expected exactly 2 import refs, got {imports:?}"
);
}
#[test]
fn import_aliased_emits_original_name() {
let src = r#"pragma solidity ^0.8.0; import {Foo as F} from "x.sol";"#;
let facts = extract(src, "contracts/Alias.sol");
let imports: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(imports.contains(&"Foo"), "expected 'Foo' in {imports:?}");
assert!(
!imports.contains(&"F"),
"alias 'F' must not appear in {imports:?}"
);
}
#[test]
fn import_whole_file_emits_nothing() {
let src = r#"pragma solidity ^0.8.0; import "./lib.sol";"#;
let facts = extract(src, "contracts/WF.sol");
let imports: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
imports.is_empty(),
"expected no import refs but got {imports:?}"
);
}
#[test]
fn params_emit_param_bindings() {
let src = "contract C { function add(uint256 a, uint256 b) public {} }";
let facts = extract(src, "contracts/C.sol");
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let mut param_names: Vec<(&str, ScopeId)> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| (b.name.as_str(), b.scope))
.collect();
param_names.sort_by_key(|(n, _)| *n);
assert_eq!(
param_names,
vec![("a", fn_scope_id), ("b", fn_scope_id)],
"expected Param bindings for a and b, got {param_names:?}"
);
}
#[test]
fn unnamed_param_skipped() {
let src = "contract C { function f(uint256) public {} }";
let facts = extract(src, "contracts/C.sol");
let params: Vec<&str> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| b.name.as_str())
.collect();
assert!(
params.is_empty(),
"unnamed param must not produce a Param binding, got {params:?}"
);
}
#[test]
fn modifier_params_emit_param_bindings() {
let src = "contract C { modifier onlyRole(bytes32 role) { _; } }";
let facts = extract(src, "contracts/C.sol");
let role = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "role")
.expect("expected Param binding for 'role'");
assert_eq!(
facts.scopes[role.scope].kind,
ScopeKind::Function,
"modifier param 'role' should be in a Function scope"
);
}
#[test]
fn local_var_emits_local_binding() {
let src = "contract C { function f() public { uint256 x = 0; } }";
let facts = extract(src, "contracts/C.sol");
let x = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "x")
.expect("expected a Local binding for 'x'");
assert_ne!(x.scope, 0, "local 'x' must NOT be in scope 0 (file root)");
assert!(
matches!(
facts.scopes[x.scope].kind,
ScopeKind::Function | ScopeKind::Block
),
"local 'x' scope must be Function or Block, got {:?}",
facts.scopes[x.scope].kind
);
}
#[test]
fn for_init_var_emits_local_binding() {
let src = "contract C { function f(uint256[] memory xs) public { for (uint256 i = 0; i < xs.length; i++) {} } }";
let facts = extract(src, "contracts/C.sol");
let i_binding = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "i")
.expect("expected a Local binding for 'i'");
assert_ne!(i_binding.scope, 0, "for-init 'i' must NOT be in scope 0");
}
#[test]
fn state_var_not_local_but_is_definition() {
let src = "contract C { uint256 public totalSupply; }";
let facts = extract(src, "contracts/C.sol");
assert!(
!facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Local && b.name == "totalSupply"),
"state variable 'totalSupply' must NOT be a Local binding"
);
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Definition && b.name == "totalSupply"),
"state variable 'totalSupply' must have a Definition binding"
);
}
#[test]
fn nesting_produces_type_and_function_scopes() {
let src = "contract C { function m() public {} }";
let facts = extract(src, "contracts/C.sol");
let has_type = facts.scopes.iter().any(|s| s.kind == ScopeKind::Type);
let has_fn = facts.scopes.iter().any(|s| s.kind == ScopeKind::Function);
assert!(has_type, "expected a Type scope for contract body");
assert!(has_fn, "expected a Function scope for method body");
}
#[test]
fn same_file_call_ref_has_non_zero_scope() {
let src = r#"
pragma solidity ^0.8.0;
contract C {
function helper() internal returns (uint256) { return 42; }
function compute() public returns (uint256) { return helper(); }
}
"#;
let facts = extract(src, "contracts/C.sol");
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Definition && b.name == "helper"),
"expected a Definition binding for 'helper'"
);
let call_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "helper")
.expect("expected a Call ref for 'helper'");
assert!(
call_ref.scope.is_some() && call_ref.scope != Some(0),
"helper() call ref must be in a non-zero scope, got {:?}",
call_ref.scope
);
}
#[test]
fn import_binding_emitted() {
let src = r#"pragma solidity ^0.8.0; import {ERC20} from "./ERC20.sol";"#;
let facts = extract(src, "contracts/Token.sol");
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Import && b.name == "ERC20"),
"expected an Import binding for 'ERC20', got {:?}",
facts.bindings
);
}
#[test]
fn type_ref_param_type_emitted() {
let src = "contract C { function f(Config c) public {} }";
let facts = extract(src, "contracts/C.sol");
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.expect("expected TypeRef ref for 'Config'");
assert_eq!(
r.type_ref_ctx,
Some(TypeRefContext::ParameterType),
"expected ParameterType ctx, got {:?}",
r.type_ref_ctx
);
}
#[test]
fn type_ref_return_type_emitted() {
let src = "contract C { function f() public returns (Config) {} }";
let facts = extract(src, "contracts/C.sol");
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.expect("expected TypeRef ref for 'Config'");
assert_eq!(
r.type_ref_ctx,
Some(TypeRefContext::ReturnType),
"expected ReturnType ctx, got {:?}",
r.type_ref_ctx
);
}
#[test]
fn type_ref_struct_field_emitted() {
let src = "contract C { struct T { Config conf; } }";
let facts = extract(src, "contracts/C.sol");
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.expect("expected TypeRef ref for 'Config'");
assert_eq!(
r.type_ref_ctx,
Some(TypeRefContext::Field),
"expected Field ctx, got {:?}",
r.type_ref_ctx
);
}
#[test]
fn type_ref_elementary_not_emitted() {
let src = "contract C { function f(uint n) public {} }";
let facts = extract(src, "contracts/C.sol");
let type_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::TypeRef)
.map(|r| r.name.as_str())
.collect();
assert!(
!type_refs.contains(&"uint"),
"elementary type 'uint' must NOT produce a TypeRef, got {type_refs:?}"
);
}
#[test]
fn read_ref_at_use_not_at_declaration() {
let src = r#"
contract C {
function f() public returns (uint) {
uint base = 1;
return base;
}
}
"#;
let facts = extract(src, "contracts/C.sol");
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "base")
.collect();
assert!(
!read_refs.is_empty(),
"expected at least one Read ref for 'base', got none; all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
let decl_byte = facts
.references
.iter()
.find(|r| r.role == RefRole::Read && r.name == "base")
.map(|r| r.occ.byte)
.unwrap();
assert!(
decl_byte > 40,
"Read ref for 'base' should be at the use site (byte > 40), got byte={}",
decl_byte
);
}
#[test]
fn write_ref_emitted_for_assignment() {
let src = r#"
contract C {
function f() public {
uint cnt = 0;
cnt = 5;
}
}
"#;
let facts = extract(src, "contracts/C.sol");
let write_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write && r.name == "cnt")
.collect();
assert!(
!write_refs.is_empty(),
"expected at least one Write ref for 'cnt', got none; all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
}
#[test]
fn call_not_also_read() {
let src = r#"
contract C {
function f() public {
helper();
}
}
"#;
let facts = extract(src, "contracts/C.sol");
let call_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Call && r.name == "helper")
.collect();
assert!(!call_refs.is_empty(), "expected a Call ref for 'helper'");
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "helper")
.collect();
assert!(
read_refs.is_empty(),
"helper() must NOT produce a Read ref; got: {read_refs:?}"
);
}
#[test]
fn member_access_reads_object_not_property() {
let src = r#"
contract C {
function f() public {
address who = msg.sender;
}
}
"#;
let facts = extract(src, "contracts/C.sol");
let sender_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "sender")
.collect();
assert!(
sender_reads.is_empty(),
"property 'sender' in member_expression must NOT be a Read ref; got: {sender_reads:?}"
);
}
}