neo_solidity/frontend/
frontend_parse.rs

1/// Parse Solidity source into [`ContractIR`] values.
2pub fn parse_source(source: &str) -> Result<Vec<ContractIR>, FrontendError> {
3    let (source_unit, comments) = parse(source, 0)
4        .map_err(|diags| FrontendError::Parse(format_diagnostics(source, &diags)))?;
5
6    // Build a map of end positions to preceding doc comments
7    let comment_map = build_comment_map(&comments, source);
8
9    let mut contracts = Vec::new();
10    // Collect file-level `type X is Y` definitions so they can be injected into all contracts.
11    let mut file_level_type_aliases: std::collections::HashMap<String, String> =
12        std::collections::HashMap::new();
13
14    for part in source_unit.0.into_iter() {
15        match part {
16            SourceUnitPart::PragmaDirective(pragma) => {
17                enforce_supported_pragma(&pragma)?;
18            }
19            SourceUnitPart::ContractDefinition(contract) => {
20                contracts.push(convert_contract(*contract, &comment_map));
21            }
22            SourceUnitPart::TypeDefinition(td) => {
23                let underlying = format!("{}", td.ty);
24                file_level_type_aliases.insert(td.name.name, underlying);
25            }
26            _ => {}
27        }
28    }
29
30    // Inject file-level type aliases into every contract in the file.
31    if !file_level_type_aliases.is_empty() {
32        for contract in &mut contracts {
33            for (name, underlying) in &file_level_type_aliases {
34                contract
35                    .type_aliases
36                    .entry(name.clone())
37                    .or_insert_with(|| underlying.clone());
38            }
39        }
40    }
41
42    Ok(contracts)
43}
44
45fn enforce_supported_pragma(
46    pragma: &solang_parser::pt::PragmaDirective,
47) -> Result<(), FrontendError> {
48    use solang_parser::pt::PragmaDirective;
49
50    let PragmaDirective::Version(_, ident, comparators) = pragma else {
51        return Ok(());
52    };
53
54    if ident.name != "solidity" {
55        return Ok(());
56    }
57
58    let spec = comparators
59        .iter()
60        .map(std::string::ToString::to_string)
61        .collect::<Vec<_>>()
62        .join(" ");
63
64    // Compiler support tracks Solidity 0.8.x syntax/features.
65    // A strict semver solver is unnecessary here; block obviously incompatible majors
66    // and allow wildcard/compound comparators that include 0.8.x.
67    if pragma_supports_0_8(spec.as_str()) {
68        Ok(())
69    } else {
70        Err(FrontendError::UnsupportedVersion(spec))
71    }
72}
73
74fn pragma_supports_0_8(spec: &str) -> bool {
75    let normalized = spec.replace(' ', "").to_lowercase();
76
77    if normalized.is_empty() {
78        return true;
79    }
80
81    // Accept if any OR-branch can include a 0.8.x version.
82    normalized.split("||").any(branch_supports_0_8)
83}
84
85fn branch_supports_0_8(branch: &str) -> bool {
86    if branch.is_empty() {
87        return false;
88    }
89
90    let comparators = split_comparators(branch);
91    if comparators.is_empty() {
92        return false;
93    }
94
95    let mut lower = Bound::Unbounded;
96    let mut upper = Bound::Unbounded;
97
98    for comparator in comparators {
99        if comparator == "*" {
100            continue;
101        }
102
103        if let Some((start, end)) = parse_hyphen_range(&comparator) {
104            lower = lower.max(Bound::Inclusive(start));
105            upper = upper.min(Bound::Inclusive(end));
106            continue;
107        }
108
109        if let Some((version, level)) = parse_caret(&comparator) {
110            let upper_version = match level {
111                0 => Version {
112                    major: version.major.saturating_add(1),
113                    minor: 0,
114                    patch: 0,
115                },
116                _ => Version {
117                    major: version.major,
118                    minor: version.minor.saturating_add(1),
119                    patch: 0,
120                },
121            };
122            lower = lower.max(Bound::Inclusive(version));
123            upper = upper.min(Bound::Exclusive(upper_version));
124            continue;
125        }
126
127        if let Some(version) = parse_tilde(&comparator) {
128            let upper_version = Version {
129                major: version.major,
130                minor: version.minor.saturating_add(1),
131                patch: 0,
132            };
133            lower = lower.max(Bound::Inclusive(version));
134            upper = upper.min(Bound::Exclusive(upper_version));
135            continue;
136        }
137
138        if let Some((op, version)) = parse_operator_version(&comparator) {
139            match op {
140                ComparatorOp::Greater => lower = lower.max(Bound::Exclusive(version)),
141                ComparatorOp::GreaterEq => lower = lower.max(Bound::Inclusive(version)),
142                ComparatorOp::Less => upper = upper.min(Bound::Exclusive(version)),
143                ComparatorOp::LessEq => upper = upper.min(Bound::Inclusive(version)),
144                ComparatorOp::Exact => {
145                    lower = lower.max(Bound::Inclusive(version));
146                    upper = upper.min(Bound::Inclusive(version));
147                }
148            }
149            continue;
150        }
151
152        if let Some(version) = parse_plain_version(&comparator) {
153            lower = lower.max(Bound::Inclusive(version));
154            upper = upper.min(Bound::Inclusive(version));
155            continue;
156        }
157
158        // Unknown comparator format: reject conservatively.
159        return false;
160    }
161
162    intersects_0_8(lower, upper)
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
166struct Version {
167    major: u64,
168    minor: u64,
169    patch: u64,
170}
171
172#[derive(Clone, Copy, Debug)]
173enum Bound {
174    Unbounded,
175    Inclusive(Version),
176    Exclusive(Version),
177}
178
179impl Bound {
180    fn max(self, other: Self) -> Self {
181        use Bound::{Exclusive, Inclusive, Unbounded};
182
183        match (self, other) {
184            (Unbounded, x) | (x, Unbounded) => x,
185            (Inclusive(a), Inclusive(b)) => {
186                if a >= b {
187                    Inclusive(a)
188                } else {
189                    Inclusive(b)
190                }
191            }
192            (Exclusive(a), Exclusive(b)) => {
193                if a >= b {
194                    Exclusive(a)
195                } else {
196                    Exclusive(b)
197                }
198            }
199            (Inclusive(a), Exclusive(b)) => {
200                if a > b {
201                    Inclusive(a)
202                } else if b > a {
203                    Exclusive(b)
204                } else {
205                    Exclusive(a)
206                }
207            }
208            (Exclusive(a), Inclusive(b)) => {
209                if a > b {
210                    Exclusive(a)
211                } else if b > a {
212                    Inclusive(b)
213                } else {
214                    Exclusive(a)
215                }
216            }
217        }
218    }
219
220    fn min(self, other: Self) -> Self {
221        use Bound::{Exclusive, Inclusive, Unbounded};
222
223        match (self, other) {
224            (Unbounded, x) | (x, Unbounded) => x,
225            (Inclusive(a), Inclusive(b)) => {
226                if a <= b {
227                    Inclusive(a)
228                } else {
229                    Inclusive(b)
230                }
231            }
232            (Exclusive(a), Exclusive(b)) => {
233                if a <= b {
234                    Exclusive(a)
235                } else {
236                    Exclusive(b)
237                }
238            }
239            (Inclusive(a), Exclusive(b)) => {
240                if a < b {
241                    Inclusive(a)
242                } else if b < a {
243                    Exclusive(b)
244                } else {
245                    Exclusive(a)
246                }
247            }
248            (Exclusive(a), Inclusive(b)) => {
249                if a < b {
250                    Exclusive(a)
251                } else if b < a {
252                    Inclusive(b)
253                } else {
254                    Exclusive(a)
255                }
256            }
257        }
258    }
259}
260
261#[derive(Clone, Copy)]
262enum ComparatorOp {
263    Greater,
264    GreaterEq,
265    Less,
266    LessEq,
267    Exact,
268}
269
270fn split_comparators(branch: &str) -> Vec<String> {
271    let mut tokens = Vec::new();
272    let chars: Vec<char> = branch.chars().collect();
273    let mut i = 0;
274
275    while i < chars.len() {
276        let ch = chars[i];
277        if ch == ',' {
278            i += 1;
279            continue;
280        }
281
282        if ch == '^' || ch == '~' {
283            let mut token = String::new();
284            token.push(ch);
285            i += 1;
286            while i < chars.len() {
287                let c = chars[i];
288                if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
289                    break;
290                }
291                token.push(c);
292                i += 1;
293            }
294            tokens.push(token);
295            continue;
296        }
297
298        if ch == '<' || ch == '>' || ch == '=' {
299            let mut token = String::new();
300            token.push(ch);
301            i += 1;
302            if i < chars.len() && chars[i] == '=' {
303                token.push('=');
304                i += 1;
305            }
306            while i < chars.len() {
307                let c = chars[i];
308                if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
309                    break;
310                }
311                token.push(c);
312                i += 1;
313            }
314            tokens.push(token);
315            continue;
316        }
317
318        // Plain version or hyphen range.
319        let mut token = String::new();
320        while i < chars.len() {
321            let c = chars[i];
322            if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
323                break;
324            }
325            token.push(c);
326            i += 1;
327        }
328        if !token.is_empty() {
329            tokens.push(token);
330        }
331    }
332
333    tokens
334}
335
336fn parse_hyphen_range(comparator: &str) -> Option<(Version, Version)> {
337    let (left, right) = comparator.split_once('-')?;
338    let start = parse_plain_version(left)?;
339    let end = parse_plain_version(right)?;
340    Some((start, end))
341}
342
343fn parse_caret(comparator: &str) -> Option<(Version, u8)> {
344    let raw = comparator.strip_prefix('^')?;
345    let dots = raw.matches('.').count() as u8;
346    let version = parse_plain_version(raw)?;
347    Some((version, dots))
348}
349
350fn parse_tilde(comparator: &str) -> Option<Version> {
351    let raw = comparator.strip_prefix('~')?;
352    parse_plain_version(raw)
353}
354
355fn parse_operator_version(comparator: &str) -> Option<(ComparatorOp, Version)> {
356    if let Some(raw) = comparator.strip_prefix(">=") {
357        return parse_plain_version(raw).map(|v| (ComparatorOp::GreaterEq, v));
358    }
359    if let Some(raw) = comparator.strip_prefix("<=") {
360        return parse_plain_version(raw).map(|v| (ComparatorOp::LessEq, v));
361    }
362    if let Some(raw) = comparator.strip_prefix('>') {
363        return parse_plain_version(raw).map(|v| (ComparatorOp::Greater, v));
364    }
365    if let Some(raw) = comparator.strip_prefix('<') {
366        return parse_plain_version(raw).map(|v| (ComparatorOp::Less, v));
367    }
368    if let Some(raw) = comparator.strip_prefix('=') {
369        return parse_plain_version(raw).map(|v| (ComparatorOp::Exact, v));
370    }
371    None
372}
373
374fn parse_plain_version(raw: &str) -> Option<Version> {
375    if raw.is_empty() || raw == "*" {
376        return None;
377    }
378
379    let mut parts = raw.split('.');
380    let major_raw = parts.next()?;
381    let minor_raw = parts.next().unwrap_or("0");
382    let patch_raw = parts.next().unwrap_or("0");
383
384    if parts.next().is_some() {
385        return None;
386    }
387
388    // Allow wildcard suffixes like 0.8.* or 0.8.x as "any patch".
389    let major = major_raw.parse::<u64>().ok()?;
390    let minor = if minor_raw == "*" || minor_raw == "x" {
391        0
392    } else {
393        minor_raw.parse::<u64>().ok()?
394    };
395    let patch = if patch_raw == "*" || patch_raw == "x" {
396        0
397    } else {
398        patch_raw.parse::<u64>().ok()?
399    };
400
401    Some(Version {
402        major,
403        minor,
404        patch,
405    })
406}
407
408fn intersects_0_8(lower: Bound, upper: Bound) -> bool {
409    let target_start = Version {
410        major: 0,
411        minor: 8,
412        patch: 0,
413    };
414    let target_end_exclusive = Version {
415        major: 0,
416        minor: 9,
417        patch: 0,
418    };
419
420    let effective_start = match lower {
421        Bound::Unbounded => target_start,
422        Bound::Inclusive(v) => v,
423        Bound::Exclusive(v) => next_patch(v),
424    };
425
426    let effective_end_exclusive = match upper {
427        Bound::Unbounded => target_end_exclusive,
428        Bound::Inclusive(v) => next_patch(v),
429        Bound::Exclusive(v) => v,
430    };
431
432    let range_start = if effective_start > target_start {
433        effective_start
434    } else {
435        target_start
436    };
437    let range_end = if effective_end_exclusive < target_end_exclusive {
438        effective_end_exclusive
439    } else {
440        target_end_exclusive
441    };
442
443    range_start < range_end
444}
445
446fn next_patch(version: Version) -> Version {
447    Version {
448        major: version.major,
449        minor: version.minor,
450        patch: version.patch.saturating_add(1),
451    }
452}
453
454/// Build a map from source positions to their preceding Natspec comments.
455fn build_comment_map(comments: &[Comment], _source: &str) -> HashMap<usize, NatspecDocIR> {
456    let mut map = HashMap::new();
457    let mut last_doc_comment: Option<(usize, String)> = None;
458
459    for comment in comments {
460        match comment {
461            Comment::DocLine(loc, text) | Comment::DocBlock(loc, text) => {
462                if let Loc::File(_, _, end) = loc {
463                    // Accumulate doc comments - update end position to latest
464                    let clean_text = clean_doc_comment(text);
465                    if let Some((ref mut end_pos, ref mut existing)) = last_doc_comment {
466                        *end_pos = *end; // Update to latest end position
467                        existing.push('\n');
468                        existing.push_str(&clean_text);
469                    } else {
470                        last_doc_comment = Some((*end, clean_text));
471                    }
472                }
473            }
474            Comment::Line(_loc, _) | Comment::Block(_loc, _) => {
475                // Regular comments break doc comment sequences
476                if let Some((end_pos, doc_text)) = last_doc_comment.take() {
477                    map.insert(end_pos, parse_natspec(&doc_text));
478                }
479            }
480        }
481    }
482
483    // Handle trailing doc comment
484    if let Some((end_pos, doc_text)) = last_doc_comment {
485        map.insert(end_pos, parse_natspec(&doc_text));
486    }
487
488    map
489}
490
491/// Remove comment delimiters and leading asterisks/slashes
492fn clean_doc_comment(text: &str) -> String {
493    text.lines()
494        .map(|line| {
495            let trimmed = line.trim();
496            // Remove /// prefix from line doc comments
497            if let Some(rest) = trimmed.strip_prefix("///") {
498                rest.trim().to_string()
499            // Remove /** and */ from block doc comments
500            } else if let Some(rest) = trimmed.strip_prefix("/**") {
501                rest.trim_end_matches("*/").trim().to_string()
502            } else if let Some(rest) = trimmed.strip_suffix("*/") {
503                rest.trim().to_string()
504            // Remove leading * from block comment lines
505            } else if let Some(rest) = trimmed.strip_prefix('*') {
506                rest.trim().to_string()
507            } else {
508                trimmed.to_string()
509            }
510        })
511        .filter(|line| !line.is_empty())
512        .collect::<Vec<_>>()
513        .join("\n")
514}
515
516/// Parse Natspec tags from a documentation comment
517fn parse_natspec(text: &str) -> NatspecDocIR {
518    let mut doc = NatspecDocIR::default();
519    let mut current_tag: Option<&str> = None;
520    let mut current_content = String::new();
521
522    for line in text.lines() {
523        let trimmed = line.trim();
524
525        // Check for tag at start of line
526        if trimmed.starts_with('@') {
527            // Save previous tag content
528            if let Some(tag) = current_tag {
529                save_tag_content(&mut doc, tag, &current_content);
530            }
531
532            // Parse new tag
533            let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect();
534            current_tag = Some(parts[0]);
535            current_content = parts
536                .get(1)
537                .map(|s| s.trim().to_string())
538                .unwrap_or_default();
539        } else if current_tag.is_some() {
540            // Continue previous tag content
541            if !current_content.is_empty() {
542                current_content.push(' ');
543            }
544            current_content.push_str(trimmed);
545        } else {
546            // No tag yet - treat as @notice
547            if doc.notice.is_none() && !trimmed.is_empty() {
548                doc.notice = Some(trimmed.to_string());
549            } else if let Some(ref mut notice) = doc.notice {
550                notice.push(' ');
551                notice.push_str(trimmed);
552            }
553        }
554    }
555
556    // Save final tag
557    if let Some(tag) = current_tag {
558        save_tag_content(&mut doc, tag, &current_content);
559    }
560
561    doc
562}
563
564fn save_tag_content(doc: &mut NatspecDocIR, tag: &str, content: &str) {
565    let content = content.trim().to_string();
566    if content.is_empty() {
567        return;
568    }
569
570    match tag {
571        "@title" => doc.title = Some(content),
572        "@author" => doc.author = Some(content),
573        "@notice" => doc.notice = Some(content),
574        "@dev" => doc.dev = Some(content),
575        "@param" => {
576            // Format: @param name description
577            let parts: Vec<&str> = content.splitn(2, char::is_whitespace).collect();
578            if parts.len() >= 2 {
579                doc.params
580                    .push((parts[0].to_string(), parts[1].trim().to_string()));
581            } else if !parts.is_empty() {
582                doc.params.push((parts[0].to_string(), String::new()));
583            }
584        }
585        "@return" => doc.returns.push(content),
586        tag if tag.starts_with("@custom:") => {
587            let custom_tag = tag.strip_prefix("@custom:").unwrap_or("");
588            doc.custom.push((custom_tag.to_string(), content));
589        }
590        _ => {} // Ignore unknown tags
591    }
592}
593
594/// Find the doc comment that precedes a given source location
595fn find_preceding_doc(loc: &Loc, comment_map: &HashMap<usize, NatspecDocIR>) -> NatspecDocIR {
596    if let Loc::File(_, start, _) = loc {
597        // Look for a doc comment that ends near this start position
598        // Allow some whitespace between comment end and definition start
599        for offset in 0..100 {
600            if let Some(pos) = start.checked_sub(offset) {
601                if let Some(doc) = comment_map.get(&pos) {
602                    return doc.clone();
603                }
604            } else {
605                break;
606            }
607        }
608    }
609    NatspecDocIR::default()
610}