1use std::collections::BTreeSet;
8
9use llmy_types::error::LLMYError;
10use tree_sitter::Node;
11
12use crate::extract::{
13 FileExtraction, GrammarSet, NodeUtil, RawCallSite, RawCallable, RawModule, RawState,
14 RawStateRef, SourceFile,
15};
16use crate::model::{CallableKind, Language, ModuleKind, StateKind};
17
18const CONTAINER_KINDS: [&str; 3] = [
19 "contract_declaration",
20 "interface_declaration",
21 "library_declaration",
22];
23const CALLABLE_KINDS: [&str; 4] = [
24 "function_definition",
25 "constructor_definition",
26 "modifier_definition",
27 "fallback_receive_definition",
28];
29const WRITE_KINDS: [&str; 3] = [
30 "assignment_expression",
31 "augmented_assignment_expression",
32 "update_expression",
33];
34
35pub struct SolidityExtractor;
36
37impl SolidityExtractor {
38 pub fn extract(file: &SourceFile) -> Result<FileExtraction, LLMYError> {
39 let tree = GrammarSet::parse(Language::Solidity, &file.content)?;
40 let root = tree.root_node();
41 let parse_errors = GrammarSet::count_errors(root);
42
43 let modules = root
44 .descendants_of_kinds(&CONTAINER_KINDS, false)
45 .into_iter()
46 .map(|node| Self::extract_container(node, &file.content))
47 .collect();
48
49 Ok(FileExtraction {
50 file: file.relative.clone(),
51 language: Language::Solidity,
52 modules,
53 parse_errors,
54 })
55 }
56
57 fn extract_container(node: Node<'_>, source: &str) -> RawModule {
58 let kind = match node.kind() {
59 "interface_declaration" => ModuleKind::Interface,
60 "library_declaration" => ModuleKind::Library,
61 _ => ModuleKind::Contract,
62 };
63 let name = node
64 .child_of_kind("identifier")
65 .map(|n| n.text_of(source))
66 .unwrap_or_else(|| "<anonymous>".to_string());
67 let parents = node
68 .children_of_kind("inheritance_specifier")
69 .into_iter()
70 .filter_map(|spec| spec.first_identifier(source))
71 .collect();
72
73 let mut states = vec![];
74 let mut callables = vec![];
75 if let Some(body) = node.child_of_kind("contract_body") {
76 for declaration in body.children_of_kind("state_variable_declaration") {
77 let Some(state_name) = declaration
78 .child_of_kind("identifier")
79 .map(|n| n.text_of(source))
80 else {
81 continue;
82 };
83 let type_text = declaration
84 .child_of_kind("type_name")
85 .map(|n| n.text_of(source))
86 .unwrap_or_default();
87 states.push(RawState {
88 name: state_name,
89 kind: StateKind::StateVariable,
90 type_text,
91 span: declaration.line_span(),
92 });
93 }
94
95 let mut cursor = body.walk();
96 for child in body.children(&mut cursor) {
97 if CALLABLE_KINDS.contains(&child.kind()) {
98 callables.push(Self::extract_callable(child, source));
99 }
100 }
101 }
102
103 RawModule {
104 name,
105 kind,
106 span: node.line_span(),
107 parents,
108 callables,
109 states,
110 }
111 }
112
113 fn extract_callable(node: Node<'_>, source: &str) -> RawCallable {
114 let (name, kind) = match node.kind() {
115 "constructor_definition" => ("constructor".to_string(), CallableKind::Constructor),
116 "modifier_definition" => (
117 node.child_of_kind("identifier")
118 .map(|n| n.text_of(source))
119 .unwrap_or_else(|| "<modifier>".to_string()),
120 CallableKind::Modifier,
121 ),
122 "fallback_receive_definition" => {
123 let head = node.text_of(source);
124 if head.trim_start().starts_with("receive") {
125 ("receive".to_string(), CallableKind::Receive)
126 } else {
127 ("fallback".to_string(), CallableKind::Fallback)
128 }
129 }
130 _ => (
131 node.child_of_kind("identifier")
132 .map(|n| n.text_of(source))
133 .unwrap_or_else(|| "<function>".to_string()),
134 CallableKind::Function,
135 ),
136 };
137
138 let signature = node.signature_head(&["function_body"], source);
139 let mut calls = vec![];
140
141 for invocation in node.children_of_kind("modifier_invocation") {
144 if let Some(modifier_name) = invocation.first_identifier(source) {
145 calls.push(RawCallSite {
146 text: modifier_name.clone(),
147 name: modifier_name,
148 qualifier: None,
149 line: invocation.line_span().start_line,
150 });
151 }
152 }
153
154 let mut state_refs = vec![];
155 if let Some(body) = node.child_of_kind("function_body") {
156 for call in body.descendants_of_kinds(&["call_expression"], true) {
157 if let Some(site) = Self::call_target(call, source) {
158 calls.push(site);
159 }
160 }
161
162 let locals = Self::collect_locals(node, body, source);
163 state_refs = Self::collect_state_refs(body, source, &locals);
164 }
165
166 RawCallable {
167 name,
168 kind,
169 signature,
170 span: node.line_span(),
171 calls,
172 state_refs,
173 }
174 }
175
176 fn call_target(call: Node<'_>, source: &str) -> Option<RawCallSite> {
180 let target = Self::unwrap_expression(call.named_child(0)?);
181 let line = call.line_span().start_line;
182 match target.kind() {
183 "identifier" => {
184 let name = target.text_of(source);
185 Some(RawCallSite {
186 text: name.clone(),
187 name,
188 qualifier: None,
189 line,
190 })
191 }
192 "member_expression" => {
193 let mut cursor = target.walk();
194 let identifiers: Vec<Node<'_>> = target
195 .children(&mut cursor)
196 .filter(|c| c.is_named())
197 .collect();
198 let property = identifiers.last()?;
199 if property.kind() != "identifier" {
200 return None;
201 }
202 let name = property.text_of(source);
203 let qualifier = identifiers
204 .first()
205 .filter(|object| object.id() != property.id())
206 .map(|object| object.text_of(source));
207 Some(RawCallSite {
208 text: target.text_of(source),
209 name,
210 qualifier,
211 line,
212 })
213 }
214 _ => None,
215 }
216 }
217
218 fn unwrap_expression(node: Node<'_>) -> Node<'_> {
219 let mut current = node;
220 while current.kind() == "expression" {
221 match current.named_child(0) {
222 Some(inner) => current = inner,
223 None => break,
224 }
225 }
226 current
227 }
228
229 fn collect_locals(declaration: Node<'_>, body: Node<'_>, source: &str) -> BTreeSet<String> {
232 let mut locals = BTreeSet::new();
233 for parameter in declaration.descendants_of_kinds(&["parameter"], false) {
234 if let Some(identifier) = parameter.child_of_kind("identifier") {
235 locals.insert(identifier.text_of(source));
236 }
237 }
238 for declaration in body.descendants_of_kinds(&["variable_declaration"], false) {
239 if let Some(identifier) = declaration.child_of_kind("identifier") {
240 locals.insert(identifier.text_of(source));
241 }
242 }
243 locals
244 }
245
246 fn collect_state_refs(
250 body: Node<'_>,
251 source: &str,
252 locals: &BTreeSet<String>,
253 ) -> Vec<RawStateRef> {
254 let mut refs = vec![];
255
256 for write in body.descendants_of_kinds(&WRITE_KINDS, true) {
259 let Some(lhs) = write.named_child(0) else {
260 continue;
261 };
262 if let Some(name) = Self::base_identifier(Self::unwrap_expression(lhs), source)
263 && !locals.contains(&name)
264 {
265 refs.push(RawStateRef {
266 name,
267 write: true,
268 line: write.line_span().start_line,
269 });
270 }
271 }
272
273 for identifier in body.descendants_of_kinds(&["identifier"], false) {
275 let text = identifier.text_of(source);
276 if locals.contains(&text) {
277 continue;
278 }
279 if let Some(parent) = identifier.parent()
280 && parent.kind() == "member_expression"
281 && parent
282 .named_child(0)
283 .map(|first| first.id() != identifier.id())
284 .unwrap_or(false)
285 {
286 continue;
287 }
288 refs.push(RawStateRef {
289 name: text,
290 write: false,
291 line: identifier.line_span().start_line,
292 });
293 }
294
295 refs
296 }
297
298 fn base_identifier(node: Node<'_>, source: &str) -> Option<String> {
301 let mut current = node;
302 loop {
303 match current.kind() {
304 "identifier" => return Some(current.text_of(source)),
305 "expression"
306 | "array_access"
307 | "member_expression"
308 | "tuple_expression"
309 | "parenthesized_expression" => {
310 current = current.named_child(0)?;
311 }
312 _ => return None,
313 }
314 }
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use std::path::PathBuf;
322
323 fn extract(source: &str) -> FileExtraction {
324 SolidityExtractor::extract(&SourceFile {
325 relative: PathBuf::from("test.sol"),
326 content: source.to_string(),
327 })
328 .expect("extract")
329 }
330
331 const SAMPLE: &str = r#"
332pragma solidity ^0.8.0;
333contract Counter is Ownable {
334 uint256 public count;
335 mapping(address => uint256) balances;
336 modifier onlyPositive() { require(count > 0); _; }
337 constructor(uint256 start) { count = start; }
338 function increment() public onlyPositive {
339 count += 1;
340 balances[msg.sender] = count;
341 helper();
342 token.transfer(msg.sender, 1);
343 }
344 function helper() internal view returns (uint256) {
345 uint256 local = count;
346 return local;
347 }
348}
349interface IToken {
350 function transfer(address to, uint256 amount) external returns (bool);
351}
352"#;
353
354 #[test]
355 fn containers_states_and_inheritance_are_extracted() {
356 let extraction = extract(SAMPLE);
357 assert_eq!(extraction.parse_errors, 0);
358 assert_eq!(extraction.modules.len(), 2);
359
360 let counter = &extraction.modules[0];
361 assert_eq!(counter.name, "Counter");
362 assert_eq!(counter.kind, ModuleKind::Contract);
363 assert_eq!(counter.parents, vec!["Ownable".to_string()]);
364 let state_names: Vec<_> = counter.states.iter().map(|s| s.name.as_str()).collect();
365 assert_eq!(state_names, vec!["count", "balances"]);
366 assert_eq!(counter.states[1].type_text, "mapping(address => uint256)");
367
368 let token = &extraction.modules[1];
369 assert_eq!(token.kind, ModuleKind::Interface);
370 assert_eq!(token.callables.len(), 1);
371 assert!(token.callables[0].signature.contains("function transfer"));
372 }
373
374 #[test]
375 fn callables_carry_calls_and_modifier_invocations() {
376 let extraction = extract(SAMPLE);
377 let counter = &extraction.modules[0];
378 let names: Vec<_> = counter.callables.iter().map(|c| c.name.as_str()).collect();
379 assert_eq!(
380 names,
381 vec!["onlyPositive", "constructor", "increment", "helper"]
382 );
383
384 let increment = &counter.callables[2];
385 assert_eq!(increment.kind, CallableKind::Function);
386 let call_names: Vec<_> = increment.calls.iter().map(|c| c.name.as_str()).collect();
387 assert_eq!(call_names, vec!["onlyPositive", "helper", "transfer"]);
388 let transfer = &increment.calls[2];
389 assert_eq!(transfer.qualifier.as_deref(), Some("token"));
390 assert_eq!(transfer.text, "token.transfer");
391 }
392
393 #[test]
394 fn state_refs_classify_reads_and_writes_and_skip_locals() {
395 let extraction = extract(SAMPLE);
396 let counter = &extraction.modules[0];
397
398 let increment = &counter.callables[2];
399 let writes: Vec<_> = increment
400 .state_refs
401 .iter()
402 .filter(|r| r.write)
403 .map(|r| r.name.as_str())
404 .collect();
405 assert!(writes.contains(&"count"), "{writes:?}");
406 assert!(writes.contains(&"balances"), "{writes:?}");
407
408 let helper = &counter.callables[3];
409 assert!(
410 helper
411 .state_refs
412 .iter()
413 .any(|r| r.name == "count" && !r.write)
414 );
415 assert!(!helper.state_refs.iter().any(|r| r.name == "local"));
417 assert!(!increment.state_refs.iter().any(|r| r.name == "sender"));
419 }
420}