1pub 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 let comment_map = build_comment_map(&comments, source);
8
9 let mut contracts = Vec::new();
10 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 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 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 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 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 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 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
454fn 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 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; 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 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 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
491fn clean_doc_comment(text: &str) -> String {
493 text.lines()
494 .map(|line| {
495 let trimmed = line.trim();
496 if let Some(rest) = trimmed.strip_prefix("///") {
498 rest.trim().to_string()
499 } 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 } 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
516fn 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 if trimmed.starts_with('@') {
527 if let Some(tag) = current_tag {
529 save_tag_content(&mut doc, tag, ¤t_content);
530 }
531
532 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 if !current_content.is_empty() {
542 current_content.push(' ');
543 }
544 current_content.push_str(trimmed);
545 } else {
546 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 if let Some(tag) = current_tag {
558 save_tag_content(&mut doc, tag, ¤t_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 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 _ => {} }
592}
593
594fn find_preceding_doc(loc: &Loc, comment_map: &HashMap<usize, NatspecDocIR>) -> NatspecDocIR {
596 if let Loc::File(_, start, _) = loc {
597 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}