1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Visibility {
7 Public,
8 Internal,
9 External,
10 Private,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Mutability {
16 Pure,
17 View,
18 Nonpayable,
19 Payable,
20}
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum StatementKind {
25 ExternalCall,
27 StateWrite,
29 StateRead,
31 Guard,
33 FlowControl,
35 InternalCall,
37 Emit,
39 EventDef,
41 InlineAssembly,
43 Other,
45}
46
47#[derive(Debug, Clone)]
49pub struct Statement {
50 pub kind: StatementKind,
51 pub line: usize,
52 pub text: String,
53}
54
55#[derive(Debug, Clone)]
57pub struct StateVariable {
58 pub name: String,
59 pub type_name: String,
60 pub visibility: Visibility,
61 pub line: usize,
62}
63
64#[derive(Debug, Clone)]
66pub struct Param {
67 pub name: String,
68 pub type_name: String,
69 pub indexed: bool,
70}
71
72#[derive(Debug, Clone)]
74pub struct FunctionDef {
75 pub name: String,
76 pub visibility: Visibility,
77 pub mutability: Mutability,
78 pub modifiers: Vec<String>,
79 pub params: Vec<Param>,
80 pub return_params: Vec<Param>,
81 pub body: Vec<Statement>,
82 pub line: usize,
83 pub is_constructor: bool,
84 pub is_fallback: bool,
85 pub is_receive: bool,
86}
87
88impl FunctionDef {
89 pub fn modifies_state(&self) -> bool {
91 matches!(
92 self.mutability,
93 Mutability::Nonpayable | Mutability::Payable
94 )
95 }
96
97 pub fn has_reentrancy_guard(&self) -> bool {
99 self.modifiers.iter().any(|m| {
100 let m = m.to_lowercase();
101 m == "nonreentrant"
102 || m == "reentrancyguard"
103 || m.contains("nonreentrant")
104 || m == "reentrant"
105 })
106 }
107
108 pub fn has_access_control(&self, contract: &Contract) -> bool {
110 if self.modifiers.is_empty() {
111 return false;
112 }
113 for modifier in &self.modifiers {
114 let m = modifier.to_lowercase();
115 if m.contains("only")
117 || m == "auth"
118 || m.contains("role")
119 || m == "whennotpaused"
120 || m == "whenpaused"
121 || m == "nonreentrant"
122 {
123 return true;
124 }
125 if contract.has_modifier_named(modifier) {
127 return true;
128 }
129 }
130 false
131 }
132}
133
134#[derive(Debug, Clone)]
136pub struct ModifierDef {
137 pub name: String,
138 pub params: Vec<Param>,
139 pub body: Vec<Statement>,
140 pub line: usize,
141}
142
143#[derive(Debug, Clone)]
145pub struct EventDef {
146 pub name: String,
147 pub params: Vec<Param>,
148 pub line: usize,
149}
150
151#[derive(Debug, Clone)]
153pub struct Contract {
154 pub name: String,
155 pub inheritance: Vec<String>,
156 pub state_variables: Vec<StateVariable>,
157 pub functions: Vec<FunctionDef>,
158 pub modifiers_defs: Vec<ModifierDef>,
159 pub events: Vec<EventDef>,
160 pub kind: ContractKind,
161 pub line: usize,
162 pub structs: Vec<StructDef>,
163 pub errors: Vec<ErrorDef>,
164 pub using_for: Vec<UsingForDef>,
165}
166
167impl Contract {
168 pub fn inherits_access_control(&self) -> bool {
170 self.inheritance.iter().any(|i| {
171 let l = i.to_lowercase();
172 l.contains("ownable")
173 || l.contains("accesscontrol")
174 || l.contains("ownableupgradeable")
175 || l.contains("accesscontrolupgradeable")
176 || l.contains("auth")
177 })
178 }
179
180 pub fn has_modifier_named(&self, name: &str) -> bool {
182 self.modifiers_defs.iter().any(|m| m.name == name)
183 }
184
185 pub fn get_modifier(&self, name: &str) -> Option<&ModifierDef> {
187 self.modifiers_defs.iter().find(|m| m.name == name)
188 }
189
190 pub fn has_reentrancy_guard_modifier(&self) -> bool {
192 self.inheritance.iter().any(|i| {
193 let l = i.to_lowercase();
194 l.contains("reentrancyguard") || l.contains("reentrancyguardupgradeable")
195 }) || self.modifiers_defs.iter().any(|m| {
196 let l = m.name.to_lowercase();
197 l.contains("nonreentrant")
198 })
199 }
200
201 pub fn has_supports_interface(&self) -> bool {
203 self.functions.iter().any(|f| f.name == "supportsInterface")
204 }
205
206 pub fn is_erc721(&self) -> bool {
208 self.functions.iter().any(|f| f.name == "ownerOf")
209 && self.functions.iter().any(|f| f.name == "balanceOf")
210 }
211
212 pub fn is_erc1155(&self) -> bool {
214 self.functions.iter().any(|f| f.name == "balanceOfBatch")
215 }
216
217 pub fn get_function(&self, name: &str) -> Option<&FunctionDef> {
219 self.functions.iter().find(|f| f.name == name)
220 }
221
222 pub fn state_variable_by_name(&self, name: &str) -> Option<&StateVariable> {
224 self.state_variables.iter().find(|sv| sv.name == name)
225 }
226}
227
228#[derive(Debug, Clone)]
230pub struct StructDef {
231 pub name: String,
232 pub fields: Vec<StateVariable>,
233 pub line: usize,
234}
235
236#[derive(Debug, Clone)]
238pub struct ErrorDef {
239 pub name: String,
240 pub params: Vec<Param>,
241 pub line: usize,
242}
243
244#[derive(Debug, Clone)]
246pub struct UsingForDef {
247 pub type_name: String,
248 pub library_name: String,
249 pub line: usize,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum ContractKind {
255 Contract,
256 Interface,
257 Library,
258 Abstract,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub enum CallTarget {
264 Address,
266 Interface(String),
268 Named(String),
270 BuiltIn,
272 LocalVariable(String),
274 SelfCall,
276 Unknown,
278}
279
280#[derive(Debug, Clone)]
282pub struct SourceFile {
283 pub contracts: Vec<Contract>,
284 pub pragma: Option<String>,
285 pub imports: Vec<String>,
286}
287
288impl SourceFile {
289 pub fn get_contract(&self, name: &str) -> Option<&Contract> {
291 self.contracts.iter().find(|c| c.name == name)
292 }
293
294 pub fn inheritance_includes_access_control(&self, contract: &Contract) -> bool {
296 for parent_name in &contract.inheritance {
297 let parent_name = parent_name.trim();
298 let parent = self.get_contract(parent_name);
300 if parent.map(|p| p.inherits_access_control()).unwrap_or(false) {
301 return true;
302 }
303 let l = parent_name.to_lowercase();
305 if l.contains("ownable") || l.contains("accesscontrol") || l.contains("auth") {
306 return true;
307 }
308 }
309 false
310 }
311
312 pub fn inheritance_includes_reentrancy_guard(&self, contract: &Contract) -> bool {
314 for parent_name in &contract.inheritance {
315 let parent_name = parent_name.trim();
316 let parent = self.get_contract(parent_name);
317 if parent
318 .map(|p| p.has_reentrancy_guard_modifier())
319 .unwrap_or(false)
320 {
321 return true;
322 }
323 let l = parent_name.to_lowercase();
324 if l.contains("reentrancyguard") || l.contains("reentrancyguardupgradeable") {
325 return true;
326 }
327 }
328 false
329 }
330
331 pub fn all_state_variables(&self) -> Vec<&StateVariable> {
333 self.contracts
334 .iter()
335 .flat_map(|c| c.state_variables.iter())
336 .collect()
337 }
338}
339
340pub fn parse_source(content: &str) -> SourceFile {
342 let lines: Vec<&str> = content.lines().collect();
343 let cleaned = strip_comments(content);
344 let clean_lines: Vec<&str> = cleaned.lines().collect();
345
346 let mut contracts = Vec::new();
347 let mut pragma = None;
348 let mut imports = Vec::new();
349
350 for line in &clean_lines {
352 let t = line.trim();
353 if t.starts_with("pragma ") {
354 pragma = Some(t.to_string());
355 } else if t.starts_with("import ") {
356 imports.push(t.to_string());
357 }
358 }
359
360 let mut i = 0;
362 while i < clean_lines.len() {
363 let trimmed = clean_lines[i].trim();
364
365 if let Some(kind) = detect_top_level_kind(trimmed) {
366 let name = extract_name(trimmed).unwrap_or("Unknown");
367 let inheritance = extract_inheritance(trimmed);
368
369 let mut brace_start = i;
371 if trimmed.contains('{') {
372 } else {
374 let mut j = i + 1;
376 while j < clean_lines.len() && !clean_lines[j].trim().contains('{') {
377 j += 1;
378 }
379 if j < clean_lines.len() {
380 brace_start = j;
381 }
382 }
383
384 let (body_start, body_end) = find_body_range(&clean_lines, brace_start);
386 if body_end <= body_start {
387 i += 1;
388 continue;
389 }
390
391 let body_lines = &clean_lines[body_start..body_end];
392
393 let (state_vars, funcs, mods, evts, structs, errors, using_for) =
395 parse_contract_body(body_lines, &original_lines(&lines, body_start, body_end));
396
397 contracts.push(Contract {
398 name: name.to_string(),
399 inheritance,
400 state_variables: state_vars,
401 functions: funcs,
402 modifiers_defs: mods,
403 events: evts,
404 structs,
405 errors,
406 using_for,
407 kind,
408 line: i + 1,
409 });
410
411 i = body_end; }
413 i += 1;
414 }
415
416 SourceFile {
417 contracts,
418 pragma,
419 imports,
420 }
421}
422
423pub fn extract_call_target(text: &str) -> CallTarget {
425 let t = text.trim();
426
427 if t.starts_with("this.") {
429 return CallTarget::SelfCall;
430 }
431
432 if t.starts_with("address(") {
434 return CallTarget::Address;
435 }
436
437 if let Some(dot_pos) = t.find(".") {
439 let before_dot = t[..dot_pos].trim();
440 if before_dot.ends_with(')') {
442 if let Some(paren_pos) = before_dot.find('(') {
444 let name = before_dot[..paren_pos].trim();
445 if name.starts_with('I') && name[1..].starts_with(|c: char| c.is_uppercase()) {
447 return CallTarget::Interface(name.to_string());
448 }
449 if name == "address" {
450 return CallTarget::Address;
451 }
452 return CallTarget::Named(name.to_string());
453 }
454 return CallTarget::Address;
455 } else {
456 let lower = before_dot.to_lowercase();
458 if lower == "msg"
459 || lower == "tx"
460 || lower == "block"
461 || lower == "abi"
462 || lower == "gasleft"
463 {
464 return CallTarget::BuiltIn;
465 }
466 if before_dot.starts_with(|c: char| c.is_uppercase()) {
468 return CallTarget::Named(before_dot.to_string());
469 }
470 return CallTarget::LocalVariable(before_dot.to_string());
471 }
472 }
473
474 if t.contains("(") && !t.starts_with("if") && !t.starts_with("for") {
476 let name = t.split('(').next().unwrap_or("").trim();
478 if name.starts_with(|c: char| c.is_lowercase()) {
479 return CallTarget::LocalVariable(name.to_string());
480 }
481 }
482
483 CallTarget::Unknown
484}
485
486fn detect_top_level_kind(trimmed: &str) -> Option<ContractKind> {
492 if trimmed.starts_with("contract ") && !trimmed.ends_with(';') {
494 Some(ContractKind::Contract)
495 } else if trimmed.starts_with("interface ") && !trimmed.ends_with(';') {
496 Some(ContractKind::Interface)
497 } else if trimmed.starts_with("library ") && !trimmed.ends_with(';') {
498 Some(ContractKind::Library)
499 } else if trimmed.starts_with("abstract contract ") {
500 Some(ContractKind::Abstract)
501 } else {
502 None
503 }
504}
505
506fn extract_name(trimmed: &str) -> Option<&str> {
508 let stripped = trimmed
509 .strip_prefix("abstract contract ")
510 .or_else(|| trimmed.strip_prefix("contract "))
511 .or_else(|| trimmed.strip_prefix("interface "))
512 .or_else(|| trimmed.strip_prefix("library "))?;
513
514 let name = stripped.trim().split([' ', '(', '{']).next().unwrap_or("");
516 if name.is_empty() {
517 None
518 } else {
519 Some(name)
520 }
521}
522
523fn extract_inheritance(trimmed: &str) -> Vec<String> {
525 let stripped = trimmed
526 .strip_prefix("abstract contract ")
527 .or_else(|| trimmed.strip_prefix("contract "))
528 .or_else(|| trimmed.strip_prefix("interface "))
529 .or_else(|| trimmed.strip_prefix("library "));
530
531 let Some(after_kw) = stripped else {
532 return vec![];
533 };
534
535 let after_is = match after_kw.find(" is ") {
537 Some(pos) => &after_kw[pos + 4..],
538 None => return vec![],
539 };
540
541 let inherits_str = if let Some(brace_pos) = after_is.find('{') {
543 &after_is[..brace_pos]
544 } else {
545 after_is
546 };
547
548 inherits_str
549 .split(',')
550 .map(|s| s.trim().trim_end_matches([' ', ')']).trim().to_string())
551 .filter(|s| !s.is_empty())
552 .collect()
553}
554
555fn find_body_range(lines: &[&str], open_brace_line: usize) -> (usize, usize) {
558 let mut depth = 0u32;
559 let body_start = open_brace_line + 1;
560 let mut started = false;
561
562 for (i, line) in lines.iter().enumerate() {
563 if i < open_brace_line {
564 continue;
565 }
566 for ch in line.chars() {
567 match ch {
568 '{' => {
569 depth += 1;
570 started = true;
571 }
572 '}' => {
573 depth = depth.saturating_sub(1);
574 if started && depth == 0 {
575 return (body_start, i);
577 }
578 }
579 _ => {}
580 }
581 }
582 }
583
584 (open_brace_line + 1, open_brace_line + 1)
586}
587
588#[allow(clippy::type_complexity)]
591fn parse_contract_body(
592 body_lines: &[&str],
593 original_lines: &[&str],
594) -> (
595 Vec<StateVariable>,
596 Vec<FunctionDef>,
597 Vec<ModifierDef>,
598 Vec<EventDef>,
599 Vec<StructDef>,
600 Vec<ErrorDef>,
601 Vec<UsingForDef>,
602) {
603 let mut state_vars = Vec::new();
604 let mut functions = Vec::new();
605 let mut mods = Vec::new();
606 let mut events = Vec::new();
607 let mut structs = Vec::new();
608 let mut errors = Vec::new();
609 let mut using_for = Vec::new();
610
611 let mut i = 0;
612 while i < body_lines.len() {
613 let trimmed = body_lines[i].trim();
614
615 if trimmed.is_empty()
617 || trimmed.starts_with("//")
618 || trimmed.starts_with("/*")
619 || trimmed.starts_with("*")
620 {
621 i += 1;
622 continue;
623 }
624
625 if trimmed.starts_with("struct ") {
627 if let Some(s) = parse_struct_def(body_lines, i) {
628 structs.push(s);
629 }
630 i += 1;
631 continue;
632 }
633
634 if trimmed.starts_with("error ") {
636 if let Some(err) = parse_error_def(trimmed, i) {
637 errors.push(err);
638 }
639 i += 1;
640 continue;
641 }
642
643 if trimmed.starts_with("using ") && trimmed.contains(" for ") {
645 if let Some(uf) = parse_using_for(trimmed, i) {
646 using_for.push(uf);
647 }
648 i += 1;
649 continue;
650 }
651
652 if trimmed.starts_with("event ") {
654 if let Some(evt) = parse_event_line(trimmed, i) {
655 events.push(evt);
656 }
657 i += 1;
658 continue;
659 }
660
661 if trimmed.starts_with("modifier ") {
663 let (mod_def, consumed) = parse_modifier(body_lines, i, original_lines);
664 if let Some(m) = mod_def {
665 mods.push(m);
666 }
667 i += consumed;
668 continue;
669 }
670
671 if trimmed.starts_with("function ") || trimmed.contains("function(") {
673 let (func, consumed) = parse_function(body_lines, i, original_lines);
674 if let Some(f) = func {
675 functions.push(f);
676 }
677 i += consumed;
678 continue;
679 }
680
681 if trimmed.starts_with("constructor(") {
683 let (func, consumed) = parse_constructor(body_lines, i, original_lines);
684 if let Some(f) = func {
685 functions.push(f);
686 }
687 i += consumed;
688 continue;
689 }
690
691 if trimmed.starts_with("fallback(") || trimmed == "fallback()" {
693 let (func, consumed) = parse_fallback(body_lines, i, original_lines);
694 if let Some(f) = func {
695 functions.push(f);
696 }
697 i += consumed;
698 continue;
699 }
700
701 if trimmed.starts_with("receive()") || trimmed.starts_with("receive (") {
703 let (func, consumed) = parse_receive(body_lines, i, original_lines);
704 if let Some(f) = func {
705 functions.push(f);
706 }
707 i += consumed;
708 continue;
709 }
710
711 if !trimmed.starts_with("function ")
713 && !trimmed.starts_with("modifier ")
714 && !trimmed.starts_with("event ")
715 && !trimmed.starts_with("error ")
716 && !trimmed.starts_with("using ")
717 && !trimmed.starts_with("type ")
718 && trimmed.ends_with(';')
719 && !trimmed.starts_with("//")
720 {
721 if let Some(sv) = parse_state_variable(trimmed, i) {
722 state_vars.push(sv);
723 }
724 }
725
726 i += 1;
727 }
728
729 (
730 state_vars, functions, mods, events, structs, errors, using_for,
731 )
732}
733
734fn parse_struct_def(lines: &[&str], start: usize) -> Option<StructDef> {
736 let first_line = lines[start].trim();
737 let name = first_line
738 .strip_prefix("struct ")?
739 .trim()
740 .split([' ', '{', '('])
741 .next()?
742 .to_string();
743 if name.is_empty() {
744 return None;
745 }
746
747 let brace_line = if first_line.contains('{') {
749 start
750 } else {
751 let mut j = start + 1;
752 while j < lines.len() && !lines[j].trim().contains('{') {
753 j += 1;
754 }
755 if j >= lines.len() {
756 return None;
757 }
758 j
759 };
760
761 let (body_start, body_end) = find_body_range(lines, brace_line);
762 if body_end <= body_start {
763 return Some(StructDef {
764 name,
765 fields: Vec::new(),
766 line: start + 1,
767 });
768 }
769
770 let mut fields = Vec::new();
771 for line in &lines[body_start..body_end] {
772 let trimmed = line.trim();
773 if trimmed.is_empty() || trimmed.starts_with("//") {
774 continue;
775 }
776 if trimmed.ends_with(';') {
778 if let Some(sv) = parse_state_variable(trimmed, body_start) {
779 fields.push(sv);
780 }
781 }
782 }
783
784 Some(StructDef {
785 name,
786 fields,
787 line: start + 1,
788 })
789}
790
791fn parse_error_def(trimmed: &str, line_idx: usize) -> Option<ErrorDef> {
793 let content = trimmed.strip_prefix("error ")?;
794 let paren_pos = content.find('(')?;
795 let name = content[..paren_pos].trim().to_string();
796 if name.is_empty() {
797 return None;
798 }
799 let params_str = if let Some(close_paren) = content.rfind(')') {
800 let start = paren_pos + 1;
801 if start < close_paren {
802 &content[start..close_paren]
803 } else {
804 ""
805 }
806 } else {
807 ""
808 };
809 let params = parse_params(params_str);
810 Some(ErrorDef {
811 name,
812 params,
813 line: line_idx + 1,
814 })
815}
816
817fn parse_using_for(trimmed: &str, line_idx: usize) -> Option<UsingForDef> {
819 let content = trimmed.strip_prefix("using ")?;
820 let parts: Vec<&str> = content.splitn(2, " for ").collect();
821 if parts.len() != 2 {
822 return None;
823 }
824 let library_name = parts[0].trim().to_string();
825 let type_name = parts[1].trim().trim_end_matches(';').to_string();
826 if library_name.is_empty() || type_name.is_empty() {
827 return None;
828 }
829 Some(UsingForDef {
830 type_name,
831 library_name,
832 line: line_idx + 1,
833 })
834}
835
836fn original_lines<'a>(lines: &[&'a str], start: usize, end: usize) -> Vec<&'a str> {
838 if start < lines.len() && end <= lines.len() && start < end {
839 lines[start..end].to_vec()
840 } else {
841 Vec::new()
842 }
843}
844
845fn parse_event_line(trimmed: &str, _line_idx: usize) -> Option<EventDef> {
847 let content = trimmed.strip_prefix("event ")?;
848 let paren_pos = content.find('(')?;
850 let name = content[..paren_pos].trim().to_string();
851
852 if name.is_empty() {
853 return None;
854 }
855
856 let params_str = if let Some(close_paren) = content.rfind(')') {
858 let start = paren_pos + 1;
859 if start < close_paren {
860 &content[start..close_paren]
861 } else {
862 ""
863 }
864 } else {
865 ""
866 };
867
868 let params = parse_params(params_str);
869
870 Some(EventDef {
871 name,
872 params,
873 line: 0,
874 })
875}
876
877fn parse_state_variable(trimmed: &str, _line_idx: usize) -> Option<StateVariable> {
879 let mut visibility = Visibility::Internal; let mut cleaned = trimmed.to_string();
882
883 if cleaned.contains(" public ") || cleaned.starts_with("public ") {
884 visibility = Visibility::Public;
885 cleaned = cleaned.replace(" public ", " ");
886 } else if cleaned.contains(" internal ") || cleaned.starts_with("internal ") {
887 visibility = Visibility::Internal;
888 cleaned = cleaned.replace(" internal ", " ");
889 } else if cleaned.contains(" private ") || cleaned.starts_with("private ") {
890 visibility = Visibility::Private;
891 cleaned = cleaned.replace(" private ", " ");
892 } else if cleaned.contains(" external ") || cleaned.starts_with("external ") {
893 visibility = Visibility::External;
894 cleaned = cleaned.replace(" external ", " ");
895 }
896
897 for kw in &[" constant ", " immutable ", " override ", " virtual "] {
899 cleaned = cleaned.replace(kw, " ");
900 }
901
902 let eq_check = cleaned.replace("=>", " ");
904 if let Some(eq_pos) = eq_check.find('=') {
905 cleaned = cleaned[..eq_pos].trim().to_string();
906 }
907
908 cleaned = cleaned.trim_end_matches(';').trim().to_string();
910
911 let parts: Vec<&str> = cleaned.split_whitespace().collect();
913 if parts.len() < 2 {
914 return None;
915 }
916
917 let name = parts.last()?.to_string();
919 let type_name = parts[..parts.len() - 1].join(" ");
920
921 if name.is_empty() || type_name.is_empty() {
922 return None;
923 }
924
925 Some(StateVariable {
926 name,
927 type_name,
928 visibility,
929 line: 0,
930 })
931}
932
933fn parse_function(
935 lines: &[&str],
936 start: usize,
937 _original: &[&str],
938) -> (Option<FunctionDef>, usize) {
939 let first_line = lines[start].trim();
940 if !first_line.starts_with("function ") {
941 if !first_line.starts_with("function") {
943 return (None, 1);
944 }
945 }
946
947 let mut sig_lines = Vec::new();
949 let mut brace_line = start;
950 let mut found_brace = false;
951
952 for (offset, line) in lines[start..].iter().enumerate() {
953 sig_lines.push(line.to_string());
954 if line.contains('{') {
955 brace_line = start + offset;
956 found_brace = true;
957 break;
958 }
959 if line.trim().ends_with(';') && !line.trim().contains('{') {
961 let sig = sig_lines
963 .iter()
964 .map(|l| l.trim())
965 .collect::<Vec<_>>()
966 .join(" ");
967 return match build_function_from_sig(&sig, start + 1, true) {
968 Some(func) => (Some(func), offset + 1),
969 None => (None, offset + 1),
970 };
971 }
972 }
973
974 if !found_brace {
975 let sig = sig_lines
977 .iter()
978 .map(|l| l.trim())
979 .collect::<Vec<_>>()
980 .join(" ");
981 return match build_function_from_sig(&sig, start + 1, true) {
982 Some(func) => (Some(func), sig_lines.len()),
983 None => (None, sig_lines.len()),
984 };
985 }
986
987 let sig = sig_lines
989 .iter()
990 .map(|l| l.trim())
991 .collect::<Vec<_>>()
992 .join(" ");
993 let sig = sig.trim_end_matches('{').trim().to_string();
994
995 let (body_start, body_end) = find_body_range(lines, brace_line);
997
998 let body = if body_end > body_start {
1000 let body_slice = &lines[body_start..body_end];
1001 parse_body_statements(body_slice, start + body_start + 1)
1002 } else {
1003 Vec::new()
1004 };
1005
1006 let mut func = match build_function_from_sig(&sig, start + 1, false) {
1007 Some(f) => f,
1008 None => return (None, (body_end + 1).saturating_sub(start).max(1)),
1009 };
1010 func.body = body;
1011
1012 let consumed = (body_end + 1).saturating_sub(start).max(1);
1013
1014 (Some(func), consumed)
1015}
1016
1017fn parse_constructor(
1019 lines: &[&str],
1020 start: usize,
1021 _original: &[&str],
1022) -> (Option<FunctionDef>, usize) {
1023 let first_line = lines[start].trim();
1024 if !first_line.starts_with("constructor(") {
1025 return (None, 1);
1026 }
1027
1028 let sig = first_line.trim_end_matches('{').trim().to_string();
1029 let params_str = extract_params_str(&sig);
1030
1031 let (body_start, body_end) = find_body_range(lines, start);
1032
1033 let body = if body_end > body_start {
1034 parse_body_statements(&lines[body_start..body_end], body_start + 1)
1035 } else {
1036 Vec::new()
1037 };
1038
1039 let func = FunctionDef {
1040 name: "constructor".to_string(),
1041 visibility: Visibility::Internal,
1042 mutability: Mutability::Nonpayable,
1043 modifiers: Vec::new(),
1044 params: parse_params(¶ms_str),
1045 return_params: Vec::new(),
1046 body,
1047 line: start + 1,
1048 is_constructor: true,
1049 is_fallback: false,
1050 is_receive: false,
1051 };
1052
1053 let consumed = (body_end + 1).saturating_sub(start).max(1);
1054 (Some(func), consumed)
1055}
1056
1057fn parse_fallback(
1059 lines: &[&str],
1060 start: usize,
1061 _original: &[&str],
1062) -> (Option<FunctionDef>, usize) {
1063 let first_line = lines[start].trim();
1064 if !first_line.starts_with("fallback(") && first_line != "fallback()" {
1065 return (None, 1);
1066 }
1067
1068 let sig = first_line.trim_end_matches('{').trim().to_string();
1069 let (body_start, body_end) = find_body_range(lines, start);
1070
1071 let body = if body_end > body_start {
1072 parse_body_statements(&lines[body_start..body_end], body_start + 1)
1073 } else {
1074 Vec::new()
1075 };
1076
1077 let visibility = if sig.contains("external") {
1078 Visibility::External
1079 } else {
1080 Visibility::Public
1081 };
1082 let mutability = if sig.contains("payable") {
1083 Mutability::Payable
1084 } else if sig.contains("view") {
1085 Mutability::View
1086 } else {
1087 Mutability::Nonpayable
1088 };
1089 let modifiers = extract_modifiers(&sig);
1090
1091 let func = FunctionDef {
1092 name: "fallback".to_string(),
1093 visibility,
1094 mutability,
1095 modifiers,
1096 params: Vec::new(),
1097 return_params: Vec::new(),
1098 body,
1099 line: start + 1,
1100 is_constructor: false,
1101 is_fallback: true,
1102 is_receive: false,
1103 };
1104
1105 let consumed = (body_end + 1).saturating_sub(start).max(1);
1106 (Some(func), consumed)
1107}
1108
1109fn parse_receive(lines: &[&str], start: usize, _original: &[&str]) -> (Option<FunctionDef>, usize) {
1111 let first_line = lines[start].trim();
1112 if !first_line.starts_with("receive(") && !first_line.starts_with("receive (") {
1113 return (None, 1);
1114 }
1115
1116 let (body_start, body_end) = find_body_range(lines, start);
1117
1118 let body = if body_end > body_start {
1119 parse_body_statements(&lines[body_start..body_end], body_start + 1)
1120 } else {
1121 Vec::new()
1122 };
1123
1124 let func = FunctionDef {
1125 name: "receive".to_string(),
1126 visibility: Visibility::External,
1127 mutability: Mutability::Payable,
1128 modifiers: Vec::new(),
1129 params: Vec::new(),
1130 return_params: Vec::new(),
1131 body,
1132 line: start + 1,
1133 is_constructor: false,
1134 is_fallback: false,
1135 is_receive: true,
1136 };
1137
1138 let consumed = (body_end + 1).saturating_sub(start).max(1);
1139 (Some(func), consumed)
1140}
1141
1142fn parse_modifier(
1144 lines: &[&str],
1145 start: usize,
1146 _original: &[&str],
1147) -> (Option<ModifierDef>, usize) {
1148 let first_line = lines[start].trim();
1149 if !first_line.starts_with("modifier ") {
1150 return (None, 1);
1151 }
1152
1153 let mut brace_line = start;
1155 let mut found_brace = false;
1156
1157 for (offset, line) in lines[start..].iter().enumerate() {
1158 if line.contains('{') {
1159 brace_line = start + offset;
1160 found_brace = true;
1161 break;
1162 }
1163 if line.trim().ends_with(';') && offset > 0 {
1165 let name = first_line
1166 .strip_prefix("modifier ")
1167 .and_then(|s| s.trim().split('(').next())
1168 .unwrap_or("")
1169 .to_string();
1170 return (
1171 Some(ModifierDef {
1172 name,
1173 params: Vec::new(),
1174 body: Vec::new(),
1175 line: start + 1,
1176 }),
1177 offset + 1,
1178 );
1179 }
1180 }
1181
1182 if !found_brace {
1183 return (None, 1);
1184 }
1185
1186 let sig = first_line.to_string();
1187 let name = sig
1188 .strip_prefix("modifier ")
1189 .and_then(|s| s.trim().split('(').next())
1190 .unwrap_or("")
1191 .to_string();
1192
1193 let (body_start, body_end) = find_body_range(lines, brace_line);
1194 let body = if body_end > body_start {
1195 parse_body_statements(&lines[body_start..body_end], body_start + 1)
1196 } else {
1197 Vec::new()
1198 };
1199
1200 let consumed = (body_end + 1).saturating_sub(start).max(1);
1201 (
1202 Some(ModifierDef {
1203 name,
1204 params: Vec::new(),
1205 body,
1206 line: start + 1,
1207 }),
1208 consumed,
1209 )
1210}
1211
1212fn build_function_from_sig(sig: &str, line: usize, _is_abstract: bool) -> Option<FunctionDef> {
1214 let after_fn = sig.strip_prefix("function ")?;
1216 let name = after_fn.split('(').next().unwrap_or("").trim().to_string();
1217
1218 if name.is_empty() {
1219 return None;
1220 }
1221
1222 let params_str = extract_params_str(sig);
1223 let params = parse_params(¶ms_str);
1224
1225 let return_params = if let Some(returns_pos) = sig.find(" returns (") {
1227 let ret_str = &sig[returns_pos + 9..];
1228 let ret_str = ret_str.trim_end_matches(';').trim_end_matches(')');
1229 parse_params(ret_str)
1230 } else {
1231 Vec::new()
1232 };
1233
1234 let visibility = if sig.contains(" external ")
1236 || sig.ends_with(" external")
1237 || sig.starts_with("external ")
1238 {
1239 Visibility::External
1240 } else if sig.contains(" public ") || sig.ends_with(" public") || sig.starts_with("public ") {
1241 Visibility::Public
1242 } else if sig.contains(" internal ") || sig.ends_with(" internal") {
1243 Visibility::Internal
1244 } else if sig.contains(" private ") || sig.ends_with(" private") {
1245 Visibility::Private
1246 } else {
1247 Visibility::Public
1249 };
1250
1251 let mutability = if sig.contains(" pure ") {
1253 Mutability::Pure
1254 } else if sig.contains(" view ") {
1255 Mutability::View
1256 } else if sig.contains(" payable ") {
1257 Mutability::Payable
1258 } else {
1259 Mutability::Nonpayable
1260 };
1261
1262 let modifiers = extract_modifiers(sig);
1263
1264 Some(FunctionDef {
1265 name,
1266 visibility,
1267 mutability,
1268 modifiers,
1269 params,
1270 return_params,
1271 body: Vec::new(), line,
1273 is_constructor: false,
1274 is_fallback: false,
1275 is_receive: false,
1276 })
1277}
1278
1279fn extract_params_str(sig: &str) -> String {
1281 let open_paren = match sig.find('(') {
1282 Some(p) => p,
1283 None => return String::new(),
1284 };
1285 let mut depth = 0u32;
1286 let mut close_paren = sig.len();
1287 for (i, ch) in sig[open_paren..].char_indices() {
1288 match ch {
1289 '(' => depth += 1,
1290 ')' => {
1291 depth -= 1;
1292 if depth == 0 {
1293 close_paren = open_paren + i;
1294 break;
1295 }
1296 }
1297 _ => {}
1298 }
1299 }
1300 sig[open_paren + 1..close_paren].to_string()
1301}
1302
1303fn parse_params(params_str: &str) -> Vec<Param> {
1305 let mut params = Vec::new();
1306 let mut depth = 0u32;
1307 let mut current = String::new();
1308
1309 for ch in params_str.chars() {
1310 match ch {
1311 '(' | '<' => {
1312 depth += 1;
1313 current.push(ch);
1314 }
1315 ')' | '>' => {
1316 depth = depth.saturating_sub(1);
1317 current.push(ch);
1318 }
1319 ',' if depth == 0 => {
1320 if let Some(param) = parse_single_param(current.trim()) {
1321 params.push(param);
1322 }
1323 current.clear();
1324 }
1325 _ => current.push(ch),
1326 }
1327 }
1328
1329 if !current.trim().is_empty() {
1330 if let Some(param) = parse_single_param(current.trim()) {
1331 params.push(param);
1332 }
1333 }
1334
1335 params
1336}
1337
1338fn parse_single_param(s: &str) -> Option<Param> {
1340 let s = s.trim();
1341 if s.is_empty() {
1342 return None;
1343 }
1344
1345 let indexed = s.contains(" indexed ");
1346 let cleaned = s.replace(" indexed ", " ");
1347
1348 let parts: Vec<&str> = cleaned.split_whitespace().collect();
1349 if parts.is_empty() {
1350 return None;
1351 }
1352
1353 if parts.len() == 1 {
1354 return Some(Param {
1356 name: String::new(),
1357 type_name: parts[0].to_string(),
1358 indexed,
1359 });
1360 }
1361
1362 let name = parts.last()?.to_string();
1363 let type_name = parts[..parts.len() - 1].join(" ");
1364
1365 Some(Param {
1366 name,
1367 type_name,
1368 indexed,
1369 })
1370}
1371
1372fn extract_modifiers(sig: &str) -> Vec<String> {
1374 let cleaned = sig
1376 .replace(" public ", " ")
1377 .replace(" external ", " ")
1378 .replace(" internal ", " ")
1379 .replace(" private ", " ")
1380 .replace(" pure ", " ")
1381 .replace(" view ", " ")
1382 .replace(" payable ", " ")
1383 .replace(" virtual ", " ")
1384 .replace(" override ", " ");
1385
1386 let after_paren = match cleaned.find(')') {
1388 Some(p) => &cleaned[p + 1..],
1389 None => "",
1390 };
1391
1392 let before_returns = match after_paren.find(" returns ") {
1393 Some(p) => &after_paren[..p],
1394 None => after_paren,
1395 };
1396
1397 before_returns
1398 .split_whitespace()
1399 .map(|s| {
1400 s.trim()
1401 .trim_start_matches(['(', ')', ',', ' ', '\t'])
1402 .trim_end_matches(['(', ')', ',', ' '])
1403 .to_string()
1404 })
1405 .filter(|s| {
1406 !s.is_empty()
1407 && s != "public"
1408 && s != "external"
1409 && s != "internal"
1410 && s != "private"
1411 && s != "pure"
1412 && s != "view"
1413 && s != "payable"
1414 && s != "virtual"
1415 && s != "override"
1416 && s != "returns"
1417 })
1418 .collect()
1419}
1420
1421fn parse_body_statements(lines: &[&str], base_line: usize) -> Vec<Statement> {
1423 let mut statements = Vec::new();
1424 let mut stmt_buffer = String::new();
1425 let mut stmt_start = base_line;
1426
1427 for (offset, line) in lines.iter().enumerate() {
1428 let trimmed = line.trim();
1429 let abs_line = base_line + offset;
1430
1431 if trimmed.starts_with("//") {
1433 continue;
1434 }
1435
1436 if trimmed.starts_with("/*") || trimmed.starts_with("*") || trimmed.ends_with("*/") {
1438 continue;
1439 }
1440
1441 if trimmed.is_empty() {
1442 continue;
1443 }
1444
1445 if trimmed.starts_with("assembly ") || trimmed.starts_with("assembly{") {
1447 statements.push(Statement {
1448 kind: StatementKind::InlineAssembly,
1449 line: abs_line,
1450 text: trimmed.to_string(),
1451 });
1452 continue;
1453 }
1454
1455 stmt_buffer.push_str(trimmed);
1457 stmt_buffer.push(' ');
1458
1459 if trimmed.ends_with(';') || trimmed.ends_with("{\"") || trimmed.ends_with('}') {
1461 let text = stmt_buffer.trim().to_string();
1462 let kind = classify_statement(&text);
1463 statements.push(Statement {
1464 kind,
1465 line: stmt_start,
1466 text,
1467 });
1468 stmt_buffer.clear();
1469 stmt_start = abs_line + 1;
1470 } else if trimmed.contains('{') || trimmed.ends_with(')') || trimmed.ends_with("){") {
1471 if trimmed.contains('{') && !trimmed.contains('}') {
1473 let text = stmt_buffer.trim().to_string();
1475 let kind = classify_statement(&text);
1476 statements.push(Statement {
1477 kind,
1478 line: stmt_start,
1479 text,
1480 });
1481 stmt_buffer.clear();
1482 stmt_start = abs_line + 1;
1483 }
1484 }
1485 }
1486
1487 if !stmt_buffer.is_empty() {
1489 let text = stmt_buffer.trim().to_string();
1490 let kind = classify_statement(&text);
1491 statements.push(Statement {
1492 kind,
1493 line: stmt_start,
1494 text,
1495 });
1496 }
1497
1498 statements
1499}
1500
1501fn classify_statement(text: &str) -> StatementKind {
1503 let t = text.trim();
1504
1505 if t.starts_with("assembly ") || t.starts_with("assembly{") {
1507 return StatementKind::InlineAssembly;
1508 }
1509
1510 if t.starts_with("require(")
1512 || t.starts_with("require (")
1513 || t.starts_with("revert ")
1514 || t.starts_with("assert(")
1515 {
1516 return StatementKind::Guard;
1517 }
1518
1519 if t.starts_with("if ")
1521 || t.starts_with("if(")
1522 || t.starts_with("while ")
1523 || t.starts_with("while(")
1524 || t.starts_with("for (")
1525 || t.starts_with("for(")
1526 {
1527 return StatementKind::FlowControl;
1528 }
1529
1530 if t.starts_with("emit ") {
1532 return StatementKind::Emit;
1533 }
1534
1535 if t.contains(".call{") || t.contains(".call(") || t.contains(".delegatecall{")
1538 || t.contains(".delegatecall(") || t.contains(".staticcall{")
1539 || t.contains(".staticcall(") || t.contains(".transfer(")
1540 || t.contains(".send(")
1541 || (t.contains(").") && (t.contains(".call") || t.contains(".transfer") || t.contains(".send")))
1543 {
1544 return StatementKind::ExternalCall;
1545 }
1546
1547 if t.contains('=')
1549 || t.starts_with("delete ")
1550 || t.contains("++")
1551 || t.contains("--")
1552 || t.starts_with("mapping")
1553 || t.contains(" += ")
1554 || t.contains(" -= ")
1555 || t.contains(" *= ")
1556 || t.contains(" /= ")
1557 {
1558 return StatementKind::StateWrite;
1559 }
1560
1561 if t.contains('(') && !t.starts_with("//") && !t.contains("function") && !t.contains("modifier")
1563 {
1564 return StatementKind::InternalCall;
1566 }
1567
1568 StatementKind::Other
1569}
1570
1571fn strip_comments(content: &str) -> String {
1573 let mut result = String::with_capacity(content.len());
1574 let chars: Vec<char> = content.chars().collect();
1575 let len = chars.len();
1576 let mut i = 0;
1577 let mut in_multiline = false;
1578
1579 while i < len {
1580 if !in_multiline && i + 1 < len && chars[i] == '/' && chars[i + 1] == '/' {
1581 while i < len && chars[i] != '\n' {
1583 result.push(chars[i]);
1584 i += 1;
1585 }
1586 if i < len && chars[i] == '\n' {
1588 result.push('\n');
1589 i += 1;
1590 }
1591 } else if !in_multiline && i + 1 < len && chars[i] == '/' && chars[i + 1] == '*' {
1592 in_multiline = true;
1594 result.push_str("/*");
1596 i += 2;
1597 } else if in_multiline && i + 1 < len && chars[i] == '*' && chars[i + 1] == '/' {
1598 in_multiline = false;
1599 result.push_str("*/");
1600 i += 2;
1601 } else if in_multiline {
1602 if chars[i] == '\n' {
1604 result.push('\n');
1605 } else if chars[i] == '\r' {
1606 }
1608 i += 1;
1609 } else {
1610 result.push(chars[i]);
1611 i += 1;
1612 }
1613 }
1614
1615 result
1616}
1617
1618#[cfg(test)]
1619mod tests {
1620 use super::*;
1621
1622 #[test]
1623 fn test_simple_contract() {
1624 let source = r#"
1625// SPDX-License-Identifier: MIT
1626pragma solidity ^0.8.20;
1627
1628contract MyToken is Ownable {
1629 uint256 public totalSupply;
1630 mapping(address => uint256) public balances;
1631
1632 event Transfer(address indexed from, address indexed to, uint256 value);
1633
1634 function mint(address to, uint256 amount) external onlyOwner {
1635 balances[to] += amount;
1636 totalSupply += amount;
1637 emit Transfer(address(0), to, amount);
1638 }
1639
1640 function withdraw(uint256 amount) public {
1641 require(amount > 0, "Amount too low");
1642 balances[msg.sender] -= amount;
1643 totalSupply -= amount;
1644 payable(msg.sender).transfer(amount);
1645 }
1646}
1647"#;
1648
1649 let parsed = parse_source(source);
1650 assert_eq!(parsed.contracts.len(), 1);
1651 let c = &parsed.contracts[0];
1652 assert_eq!(c.name, "MyToken");
1653 assert_eq!(c.inheritance, vec!["Ownable"]);
1654 assert!(c.inherits_access_control());
1655 assert_eq!(c.state_variables.len(), 2);
1656 assert_eq!(c.functions.len(), 2);
1657 }
1658
1659 #[test]
1660 fn test_function_parsing() {
1661 let source = r#"
1662contract Test {
1663 function safeWithdraw(uint256 amount) external nonReentrant {
1664 uint256 balance = balances[msg.sender];
1665 require(balance >= amount, "Insufficient balance");
1666 balances[msg.sender] = balance - amount;
1667 payable(msg.sender).transfer(amount);
1668 }
1669}
1670"#;
1671 let parsed = parse_source(source);
1672 let func = &parsed.contracts[0].functions[0];
1673 assert_eq!(func.name, "safeWithdraw");
1674 assert_eq!(func.visibility, Visibility::External);
1675 assert_eq!(func.modifiers, vec!["nonReentrant"]);
1676 assert!(func.has_reentrancy_guard());
1677 assert_eq!(func.params.len(), 1);
1678 assert_eq!(func.params[0].name, "amount");
1679 }
1680
1681 #[test]
1682 fn test_cei_violation_detection() {
1683 let source = r#"
1684contract Vulnerable {
1685 mapping(address => uint256) public balances;
1686
1687 function withdraw(uint256 amount) public {
1688 require(amount > 0, "Invalid");
1689 payable(msg.sender).transfer(amount);
1690 balances[msg.sender] -= amount;
1691 }
1692}
1693"#;
1694 let parsed = parse_source(source);
1695 let func = &parsed.contracts[0].functions[0];
1696 assert_eq!(func.name, "withdraw");
1697
1698 let kinds: Vec<&StatementKind> = func.body.iter().map(|s| &s.kind).collect();
1700 assert!(kinds.contains(&&StatementKind::ExternalCall));
1704 assert!(kinds.contains(&&StatementKind::Guard));
1705 assert!(kinds.contains(&&StatementKind::StateWrite));
1706 }
1707
1708 #[test]
1709 fn test_access_control_detection() {
1710 let source = r#"
1711contract NoAccess {
1712 function withdrawAll() public {
1713 payable(msg.sender).transfer(address(this).balance);
1714 }
1715
1716 function setAdmin(address newAdmin) external {
1717 admin = newAdmin;
1718 }
1719}
1720"#;
1721 let parsed = parse_source(source);
1722 let contract = &parsed.contracts[0];
1723 assert!(!contract.inherits_access_control());
1724
1725 let withdraw_func = &contract.functions[0];
1726 assert!(!withdraw_func.has_access_control(contract));
1727
1728 let admin_func = &contract.functions[1];
1729 assert!(!admin_func.has_access_control(contract));
1730 }
1731
1732 #[test]
1733 fn test_proper_access_control() {
1734 let source = r#"
1735contract Secured is Ownable {
1736 function withdrawAll() public onlyOwner {
1737 payable(msg.sender).transfer(address(this).balance);
1738 }
1739}
1740"#;
1741 let parsed = parse_source(source);
1742 let contract = &parsed.contracts[0];
1743 assert!(contract.inherits_access_control());
1744
1745 let func = &contract.functions[0];
1746 assert!(func.has_access_control(contract));
1747 assert_eq!(func.modifiers[0], "onlyOwner");
1748 }
1749
1750 #[test]
1751 fn test_state_variable_parsing() {
1752 let source = r#"
1753contract Storage {
1754 uint256 public count;
1755 address private owner;
1756 mapping(address => uint256) internal balances;
1757 bool public initialized;
1758}
1759"#;
1760 let parsed = parse_source(source);
1761 let state_vars = &parsed.contracts[0].state_variables;
1762 assert_eq!(state_vars.len(), 4);
1763
1764 assert_eq!(state_vars[0].name, "count");
1765 assert_eq!(state_vars[0].type_name, "uint256");
1766 assert_eq!(state_vars[0].visibility, Visibility::Public);
1767
1768 assert_eq!(state_vars[1].name, "owner");
1769 assert_eq!(state_vars[1].visibility, Visibility::Private);
1770
1771 assert_eq!(state_vars[2].name, "balances");
1772 assert_eq!(state_vars[2].visibility, Visibility::Internal);
1773
1774 assert_eq!(state_vars[3].name, "initialized");
1775 assert_eq!(state_vars[3].visibility, Visibility::Public);
1776 }
1777
1778 #[test]
1779 fn test_event_parsing() {
1780 let source = r#"
1781contract Events {
1782 event Transfer(address indexed from, address indexed to, uint256 value);
1783 event Approval(address indexed owner, address indexed spender, uint256 value);
1784}
1785"#;
1786 let parsed = parse_source(source);
1787 let events = &parsed.contracts[0].events;
1788 assert_eq!(events.len(), 2);
1789 assert_eq!(events[0].name, "Transfer");
1790 assert_eq!(events[0].params[0].name, "from");
1791 assert!(events[0].params[0].indexed);
1792 }
1793
1794 #[test]
1795 fn test_comment_stripping() {
1796 let source = "\
1797// This is a comment
1798contract Test {
1799 // another comment
1800 uint256 x;
1801 /* inline */
1802 uint256 y;
1803}
1804";
1805 let cleaned = strip_comments(source);
1806 assert!(cleaned.contains("// This is a comment"));
1807 assert!(cleaned.contains("// another comment"));
1808 assert!(cleaned.contains("/*"));
1809 assert!(cleaned.contains("*/"));
1810
1811 let parsed = parse_source(&cleaned);
1812 assert_eq!(parsed.contracts.len(), 1);
1813 assert_eq!(parsed.contracts[0].state_variables.len(), 2);
1814 }
1815
1816 #[test]
1817 fn test_multiline_function_sig() {
1818 let source = r#"
1819contract Multi {
1820 function complex(
1821 address param1,
1822 uint256 param2,
1823 bytes calldata data
1824 ) external payable onlyOwner returns (bool success) {
1825 return true;
1826 }
1827}
1828"#;
1829 let parsed = parse_source(source);
1830 let func = &parsed.contracts[0].functions[0];
1831 assert_eq!(func.name, "complex");
1832 assert_eq!(func.params.len(), 3);
1833 assert!(func.modifiers.contains(&"onlyOwner".to_string()));
1834 assert_eq!(func.return_params.len(), 1);
1835 assert_eq!(func.return_params[0].name, "success");
1836 }
1837
1838 #[test]
1839 fn test_contract_with_no_functions() {
1840 let source = r#"
1841contract Empty {
1842 uint256 public constant VERSION = 1;
1843}
1844"#;
1845 let parsed = parse_source(source);
1846 assert_eq!(parsed.contracts[0].functions.len(), 0);
1847 assert_eq!(parsed.contracts[0].state_variables.len(), 1);
1848 }
1849
1850 #[test]
1851 fn test_interface_parsing() {
1852 let source = r#"
1853interface IERC20 {
1854 function transfer(address to, uint256 amount) external returns (bool);
1855 function balanceOf(address account) external view returns (uint256);
1856}
1857"#;
1858 let parsed = parse_source(source);
1859 eprintln!(
1860 "IFACE test: contracts={}, pragma={:?}, imports={:?}",
1861 parsed.contracts.len(),
1862 parsed.pragma,
1863 parsed.imports
1864 );
1865 for (ci, c) in parsed.contracts.iter().enumerate() {
1866 eprintln!(
1867 " contract[{}]: name={}, kind={:?}, functions={}",
1868 ci,
1869 c.name,
1870 c.kind,
1871 c.functions.len()
1872 );
1873 for fi in 0..c.functions.len() {
1874 eprintln!(
1875 " func[{}]: name={}, body_stmts={}",
1876 fi,
1877 c.functions[fi].name,
1878 c.functions[fi].body.len()
1879 );
1880 }
1881 }
1882 assert_eq!(parsed.contracts.len(), 1, "Should have 1 contract");
1883 assert_eq!(parsed.contracts[0].kind, ContractKind::Interface);
1884 assert_eq!(
1885 parsed.contracts[0].functions.len(),
1886 2,
1887 "Should have 2 functions, got {}",
1888 parsed.contracts[0].functions.len()
1889 );
1890 for func in &parsed.contracts[0].functions {
1892 assert!(func.body.is_empty());
1893 }
1894 }
1895
1896 #[test]
1897 fn test_inheritance_multiple() {
1898 let source = r#"
1899contract MyContract is Ownable, ReentrancyGuard, AccessControl {
1900 uint256 dummy;
1901}
1902"#;
1903 let parsed = parse_source(source);
1904 let c = &parsed.contracts[0];
1905 assert!(c.inheritance.contains(&"Ownable".to_string()));
1906 assert!(c.inheritance.contains(&"ReentrancyGuard".to_string()));
1907 assert!(c.inheritance.contains(&"AccessControl".to_string()));
1908 assert!(c.inherits_access_control());
1909 assert!(c.has_reentrancy_guard_modifier());
1910 }
1911}