Skip to main content

forge_guard/parser/
mod.rs

1//! Solidity source file parser — parses source code into structured representations
2//! for deeper vulnerability analysis (CEI, access control, etc.).
3
4/// Visibility of a function or state variable.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Visibility {
7    Public,
8    Internal,
9    External,
10    Private,
11}
12
13/// State mutability of a function.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Mutability {
16    Pure,
17    View,
18    Nonpayable,
19    Payable,
20}
21
22/// Classification of a single logical statement within a function body.
23#[derive(Debug, Clone, PartialEq)]
24pub enum StatementKind {
25    /// An external call: .call{..., .transfer(), .send(), or calling an interface method
26    ExternalCall,
27    /// A state write: assignment (=, +=, -=, etc.), delete, push, pop
28    StateWrite,
29    /// A state read: reading a state variable
30    StateRead,
31    /// A require() or revert() — flow control
32    Guard,
33    /// An if/while/for condition — flow control
34    FlowControl,
35    /// An internal function call
36    InternalCall,
37    /// An emit statement
38    Emit,
39    /// An event definition (inside contract, not function)
40    EventDef,
41    /// Inner assembly block
42    InlineAssembly,
43    /// Other / unclassified
44    Other,
45}
46
47/// A statement with its content, line number, and kind.
48#[derive(Debug, Clone)]
49pub struct Statement {
50    pub kind: StatementKind,
51    pub line: usize,
52    pub text: String,
53}
54
55/// A single state variable definition.
56#[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/// A parameter in a function or event.
65#[derive(Debug, Clone)]
66pub struct Param {
67    pub name: String,
68    pub type_name: String,
69    pub indexed: bool,
70}
71
72/// A full function definition with parsed body.
73#[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    /// Whether this function can modify state.
90    pub fn modifies_state(&self) -> bool {
91        matches!(
92            self.mutability,
93            Mutability::Nonpayable | Mutability::Payable
94        )
95    }
96
97    /// Whether this function has a reentrancy guard modifier.
98    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    /// Whether this function has any access-control modifiers.
109    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            // Standard access control modifiers
116            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            // Check if modifier is defined in the contract (could be custom access control)
126            if contract.has_modifier_named(modifier) {
127                return true;
128            }
129        }
130        false
131    }
132}
133
134/// A modifier definition.
135#[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/// An event definition at the contract level.
144#[derive(Debug, Clone)]
145pub struct EventDef {
146    pub name: String,
147    pub params: Vec<Param>,
148    pub line: usize,
149}
150
151/// A contract, interface, or library definition.
152#[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    /// Check if contract (or its inheritance chain) likely has a specific capability.
169    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    /// Check if a modifier with the given name exists in this contract.
181    pub fn has_modifier_named(&self, name: &str) -> bool {
182        self.modifiers_defs.iter().any(|m| m.name == name)
183    }
184
185    /// Get a modifier by name.
186    pub fn get_modifier(&self, name: &str) -> Option<&ModifierDef> {
187        self.modifiers_defs.iter().find(|m| m.name == name)
188    }
189
190    /// Check if the contract has OpenZeppelin-style ReentrancyGuard.
191    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    /// Check if this contract defines ERC-165 `supportsInterface`.
202    pub fn has_supports_interface(&self) -> bool {
203        self.functions.iter().any(|f| f.name == "supportsInterface")
204    }
205
206    /// Check if this contract likely implements ERC-721 (has balanceOf, ownerOf).
207    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    /// Check if this contract likely implements ERC-1155 (has balanceOfBatch).
213    pub fn is_erc1155(&self) -> bool {
214        self.functions.iter().any(|f| f.name == "balanceOfBatch")
215    }
216
217    /// Find a function by name.
218    pub fn get_function(&self, name: &str) -> Option<&FunctionDef> {
219        self.functions.iter().find(|f| f.name == name)
220    }
221
222    /// Find all state variables initialized to a specific value pattern.
223    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/// A struct definition inside a contract.
229#[derive(Debug, Clone)]
230pub struct StructDef {
231    pub name: String,
232    pub fields: Vec<StateVariable>,
233    pub line: usize,
234}
235
236/// A custom error definition.
237#[derive(Debug, Clone)]
238pub struct ErrorDef {
239    pub name: String,
240    pub params: Vec<Param>,
241    pub line: usize,
242}
243
244/// A using-for directive.
245#[derive(Debug, Clone)]
246pub struct UsingForDef {
247    pub type_name: String,
248    pub library_name: String,
249    pub line: usize,
250}
251
252/// The kind of top-level type.
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum ContractKind {
255    Contract,
256    Interface,
257    Library,
258    Abstract,
259}
260
261/// Metadata about a function call target (which contract/interface is being called).
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub enum CallTarget {
264    /// Direct call to an address: address(...).call{...}
265    Address,
266    /// Call via an interface type: IERC20(token).transfer(...)
267    Interface(String),
268    /// Call via a known contract name
269    Named(String),
270    /// Call on msg.sender, tx.origin, etc.
271    BuiltIn,
272    /// Call on a local variable (could be anything)
273    LocalVariable(String),
274    /// Self-call: this.function()
275    SelfCall,
276    /// Unresolved target
277    Unknown,
278}
279
280/// A parsed Solidity source file.
281#[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    /// Find a contract by name (if defined in this file).
290    pub fn get_contract(&self, name: &str) -> Option<&Contract> {
291        self.contracts.iter().find(|c| c.name == name)
292    }
293
294    /// Resolve whether an inherited contract is known to have access control.
295    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            // Direct name check
299            let parent = self.get_contract(parent_name);
300            if parent.map(|p| p.inherits_access_control()).unwrap_or(false) {
301                return true;
302            }
303            // Check if the name itself is a known access-control base
304            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    /// Check if an inherited contract provides reentrancy guard.
313    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    /// Find all state variables across all contracts (useful for cross-contract analysis).
332    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
340/// Parse a full Solidity source string into a structured representation.
341pub 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    // First pass: collect pragma and imports
351    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    // Second pass: find top-level contract/interface/library definitions
361    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            // Find the opening brace of the contract body
370            let mut brace_start = i;
371            if trimmed.contains('{') {
372                // brace is on the same line
373            } else {
374                // find next non-commented line with {
375                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            // Find the closing brace with depth counting
385            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            // Parse contract body
394            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; // skip past the contract body
412        }
413        i += 1;
414    }
415
416    SourceFile {
417        contracts,
418        pragma,
419        imports,
420    }
421}
422
423/// Extract the call target from a statement text.
424pub fn extract_call_target(text: &str) -> CallTarget {
425    let t = text.trim();
426
427    // Check for self-call: this.function(...)
428    if t.starts_with("this.") {
429        return CallTarget::SelfCall;
430    }
431
432    // Check for address(...).call{...}
433    if t.starts_with("address(") {
434        return CallTarget::Address;
435    }
436
437    // Check for interface pattern: SomeInterface(address).func(...)
438    if let Some(dot_pos) = t.find(".") {
439        let before_dot = t[..dot_pos].trim();
440        // If it contains a parenthesis, it's likely Interface(address)...
441        if before_dot.ends_with(')') {
442            // Extract the interface/contract name before '(' if present
443            if let Some(paren_pos) = before_dot.find('(') {
444                let name = before_dot[..paren_pos].trim();
445                // Check if it looks like an interface (I prefix or known patterns)
446                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            // Direct property access: msg.sender, tx.origin, etc.
457            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 the part before dot is uppercase, it could be a contract name
467            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    // Direct call: IInterface(address).function(...)
475    if t.contains("(") && !t.starts_with("if") && !t.starts_with("for") {
476        // Could be a bare function call
477        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
486// ─────────────────────────────────────────────────────────────────
487// Internal helpers
488// ─────────────────────────────────────────────────────────────────
489
490/// Detect contract/interface/library/abstract
491fn detect_top_level_kind(trimmed: &str) -> Option<ContractKind> {
492    // Skip lines that are not top-level definitions
493    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
506/// Extract the contract/interface/library name from a definition line.
507fn 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    // Take the first word (before whitespace, '(', or '{')
515    let name = stripped.trim().split([' ', '(', '{']).next().unwrap_or("");
516    if name.is_empty() {
517        None
518    } else {
519        Some(name)
520    }
521}
522
523/// Extract inherited contract names.
524fn 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    // Find 'is' keyword after the name
536    let after_is = match after_kw.find(" is ") {
537        Some(pos) => &after_kw[pos + 4..],
538        None => return vec![],
539    };
540
541    // Take everything before '{' or end of line
542    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
555/// Find the range of lines that form the body of a contract (or function) given the opening brace line.
556/// Returns (body_start, body_end) where body_end is exclusive.
557fn 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                        // Found closing brace — body is from open_brace+1 to here
576                        return (body_start, i);
577                    }
578                }
579                _ => {}
580            }
581        }
582    }
583
584    // Fallback: unbalanced braces, return empty
585    (open_brace_line + 1, open_brace_line + 1)
586}
587
588/// Parse the body lines of a contract and extract state variables, functions, modifiers, events,
589/// structs, errors, and using-for directives.
590#[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        // Skip empty/comment-only lines
616        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        // Parse structs
626        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        // Parse errors
635        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        // Parse using-for directives
644        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        // Parse events
653        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        // Parse modifiers
662        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        // Parse functions
672        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        // Parse constructor
682        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        // Parse fallback
692        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        // Parse receive
702        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        // Parse state variables (lines ending with ;, not function-like)
712        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
734/// Parse a struct definition.
735fn 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    // Find opening brace
748    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        // Fields end with semicolon
777        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
791/// Parse an error definition: error Unauthorized(address caller);
792fn 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
817/// Parse a using-for directive: using Lib for Type;
818fn 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
836/// Get the original (uncommented) lines for the given range.
837fn 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
845/// Parse an event definition line like: event Transfer(address indexed from, address indexed to, uint256 value);
846fn parse_event_line(trimmed: &str, _line_idx: usize) -> Option<EventDef> {
847    let content = trimmed.strip_prefix("event ")?;
848    // Extract name before '('
849    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    // Extract params between '(' and ')'
857    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
877/// Parse a state variable declaration.
878fn parse_state_variable(trimmed: &str, _line_idx: usize) -> Option<StateVariable> {
879    // Strip modifiers like public, internal, private, constant, immutable
880    let mut visibility = Visibility::Internal; // default
881    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    // Remove constant/immutable/override/virtual keywords
898    for kw in &[" constant ", " immutable ", " override ", " virtual "] {
899        cleaned = cleaned.replace(kw, " ");
900    }
901
902    // Remove initializer ( = ... ) — but skip => arrow
903    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    // Remove trailing semicolon
909    cleaned = cleaned.trim_end_matches(';').trim().to_string();
910
911    // The last token before the semicolon is the variable name
912    let parts: Vec<&str> = cleaned.split_whitespace().collect();
913    if parts.len() < 2 {
914        return None;
915    }
916
917    // The last part is likely the name; everything before is the type
918    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
933/// Parse a function definition, extracting signature and body lines.
934fn 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        // Might be a function on multiple lines — check next few
942        if !first_line.starts_with("function") {
943            return (None, 1);
944        }
945    }
946
947    // Collect all lines until we hit the opening brace
948    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        // Semicolon-only lines (abstract/interface functions)
960        if line.trim().ends_with(';') && !line.trim().contains('{') {
961            // This is an abstract function declaration
962            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        // Abstract function — semicolon at end
976        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    // Reconstruct the full signature
988    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    // Find the body range (after the opening brace)
996    let (body_start, body_end) = find_body_range(lines, brace_line);
997
998    // Parse body statements
999    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
1017/// Parse a constructor.
1018fn 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(&params_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
1057/// Parse a fallback function.
1058fn 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
1109/// Parse a receive() function.
1110fn 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
1142/// Parse a modifier definition.
1143fn 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    // Collect signature lines until opening brace
1154    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        // Semicolon-only = abstract modifier
1164        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
1212/// Build a FunctionDef from a reconstructed signature string.
1213fn build_function_from_sig(sig: &str, line: usize, _is_abstract: bool) -> Option<FunctionDef> {
1214    // Extract function name
1215    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(&params_str);
1224
1225    // Extract return parameters
1226    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    // Determine visibility
1235    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        // Default visibility
1248        Visibility::Public
1249    };
1250
1251    // Determine mutability
1252    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(), // filled in by caller
1272        line,
1273        is_constructor: false,
1274        is_fallback: false,
1275        is_receive: false,
1276    })
1277}
1278
1279/// Extract parameters string from between parentheses (handles nesting).
1280fn 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
1303/// Parse parameter definitions from a comma-separated string.
1304fn 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
1338/// Parse a single parameter like "address indexed from" or "uint256 amount".
1339fn 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        // Only type, no name (common in events)
1355        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
1372/// Extract modifier names from a function signature.
1373fn extract_modifiers(sig: &str) -> Vec<String> {
1374    // Remove visibility and mutability keywords
1375    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    // Modifiers appear after the first ')', before 'returns'
1387    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
1421/// Parse body lines into classified statements.
1422fn 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        // Skip comment-only lines
1432        if trimmed.starts_with("//") {
1433            continue;
1434        }
1435
1436        // Handle multi-line comments
1437        if trimmed.starts_with("/*") || trimmed.starts_with("*") || trimmed.ends_with("*/") {
1438            continue;
1439        }
1440
1441        if trimmed.is_empty() {
1442            continue;
1443        }
1444
1445        // Check for inline assembly
1446        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        // Accumulate lines until we hit a semicolon or brace block
1456        stmt_buffer.push_str(trimmed);
1457        stmt_buffer.push(' ');
1458
1459        // Is this a complete statement?
1460        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            // Opening brace could be start of a block
1472            if trimmed.contains('{') && !trimmed.contains('}') {
1473                // Statement with opening brace - might be if/for/while
1474                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    // Flush any remaining buffer
1488    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
1501/// Classify a single statement line into a StatementKind.
1502fn classify_statement(text: &str) -> StatementKind {
1503    let t = text.trim();
1504
1505    // Inline assembly
1506    if t.starts_with("assembly ") || t.starts_with("assembly{") {
1507        return StatementKind::InlineAssembly;
1508    }
1509
1510    // Guard statements
1511    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    // Flow control
1520    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    // Emit
1531    if t.starts_with("emit ") {
1532        return StatementKind::Emit;
1533    }
1534
1535    // External calls: .call{, .delegatecall{, .staticcall{, .transfer(, .send(
1536    // Also detect interface calls: IERC20(token).transfer(...), etc.
1537    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        // Pattern: ContractName(...).functionName( or address(...).call(
1542        || (t.contains(").") && (t.contains(".call") || t.contains(".transfer") || t.contains(".send")))
1543    {
1544        return StatementKind::ExternalCall;
1545    }
1546
1547    // State writes
1548    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    // Emit via event call (older Solidity style without emit keyword)
1562    if t.contains('(') && !t.starts_with("//") && !t.contains("function") && !t.contains("modifier")
1563    {
1564        // Could be an event emit or internal call
1565        return StatementKind::InternalCall;
1566    }
1567
1568    StatementKind::Other
1569}
1570
1571/// Strip comments from Solidity source code, preserving line count.
1572fn 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            // Single-line comment: skip to end of line
1582            while i < len && chars[i] != '\n' {
1583                result.push(chars[i]);
1584                i += 1;
1585            }
1586            // keep the newline
1587            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            // Multi-line comment start
1593            in_multiline = true;
1594            // Preserve the /* to maintain line count
1595            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            // Preserve whitespace/newlines inside multi-line comments to keep line count
1603            if chars[i] == '\n' {
1604                result.push('\n');
1605            } else if chars[i] == '\r' {
1606                // skip
1607            }
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        // Find statement kinds
1699        let kinds: Vec<&StatementKind> = func.body.iter().map(|s| &s.kind).collect();
1700        // require -> guard
1701        // .transfer -> external call
1702        // -= -> state write (after external call = CEI violation)
1703        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        // Interface functions should have empty bodies
1891        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}