1pub fn parse_source(source: &str) -> Result<Vec<ContractIR>, FrontendError> {
3 let (source_unit, comments) = parse_solidity_guarded(source)
4 .map_err(|diags| FrontendError::ParseDiagnostics(collect_parse_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 let mut file_level_structs: Vec<StructIR> = Vec::new();
14 let mut file_level_enums: Vec<EnumIR> = Vec::new();
15 let mut file_level_errors: Vec<ErrorIR> = Vec::new();
19 let mut file_level_free_functions: Vec<FunctionIR> = Vec::new();
26 let mut file_level_usings: Vec<Using> = Vec::new();
35
36 let mut pragma_min_version: Option<Version> = None;
39
40 for part in source_unit.0 {
41 match part {
42 SourceUnitPart::PragmaDirective(pragma) => {
43 if let Some(min) = enforce_supported_pragma(&pragma)? {
44 pragma_min_version = match pragma_min_version {
56 Some(existing) if existing >= min => Some(existing),
57 _ => Some(min),
58 };
59 }
60 }
61 SourceUnitPart::ContractDefinition(contract) => {
62 contracts.push(convert_contract(*contract, &comment_map));
63 }
64 SourceUnitPart::TypeDefinition(td) => {
65 let underlying = format!("{}", td.ty);
66 file_level_type_aliases.insert(td.name.name, underlying);
67 }
68 SourceUnitPart::StructDefinition(def) => {
69 file_level_structs.push(convert_struct(*def));
70 }
71 SourceUnitPart::EnumDefinition(def) => {
72 file_level_enums.push(convert_enum(*def));
73 }
74 SourceUnitPart::ErrorDefinition(def) => {
75 file_level_errors.push(convert_error(*def));
76 }
77 SourceUnitPart::FunctionDefinition(def) => {
78 let mut fn_ir = convert_function(*def, &comment_map);
85 fn_ir.visibility = VisibilityKind::Internal;
86 fn_ir.ty = FunctionTy::Function;
87 file_level_free_functions.push(fn_ir);
88 }
89 SourceUnitPart::Using(using) => {
90 file_level_usings.push(*using);
93 }
94 _ => {}
103 }
104 }
105
106 enforce_feature_version_gates(source, pragma_min_version)?;
110
111 if !file_level_type_aliases.is_empty() {
113 for contract in &mut contracts {
114 for (name, underlying) in &file_level_type_aliases {
115 contract
116 .type_aliases
117 .entry(name.clone())
118 .or_insert_with(|| underlying.clone());
119 }
120 }
121 }
122
123 if !file_level_structs.is_empty() {
124 for contract in &mut contracts {
125 for file_struct in &file_level_structs {
126 if !contract
127 .structs
128 .iter()
129 .any(|existing| existing.name == file_struct.name)
130 {
131 contract.structs.push(file_struct.clone());
132 }
133 }
134 }
135 }
136
137 if !file_level_enums.is_empty() {
138 for contract in &mut contracts {
139 for file_enum in &file_level_enums {
140 if !contract
141 .enums
142 .iter()
143 .any(|existing| existing.name == file_enum.name)
144 {
145 contract.enums.push(file_enum.clone());
146 }
147 }
148 }
149 }
150
151 if !file_level_errors.is_empty() {
154 for contract in &mut contracts {
155 for file_error in &file_level_errors {
156 if !contract
157 .errors
158 .iter()
159 .any(|existing| existing.name == file_error.name)
160 {
161 contract.errors.push(file_error.clone());
162 }
163 }
164 }
165 }
166
167 if !file_level_free_functions.is_empty() {
176 for contract in &mut contracts {
177 for free_fn in &file_level_free_functions {
178 if !contract
179 .functions
180 .iter()
181 .any(|existing| existing.name == free_fn.name)
182 {
183 contract.functions.push(free_fn.clone());
184 }
185 }
186 }
187 }
188
189 if !file_level_usings.is_empty() {
201 for contract in &mut contracts {
202 if matches!(contract.kind, ContractKind::Library) {
203 continue;
204 }
205 for using in &file_level_usings {
206 apply_file_level_using(contract, using);
207 }
208 }
209 }
210
211 Ok(contracts)
212}
213
214fn enforce_supported_pragma(
215 pragma: &solang_parser::pt::PragmaDirective,
216) -> Result<Option<Version>, FrontendError> {
217 use solang_parser::pt::PragmaDirective;
218
219 let PragmaDirective::Version(_, ident, comparators) = pragma else {
220 return Ok(None);
221 };
222
223 if ident.name != "solidity" {
224 return Ok(None);
225 }
226
227 let spec = comparators
228 .iter()
229 .map(std::string::ToString::to_string)
230 .collect::<Vec<_>>()
231 .join(" ");
232
233 if pragma_supports_neo_devpack_solidity(spec.as_str()) {
236 Ok(pragma_min_version(spec.as_str()))
237 } else {
238 Err(FrontendError::UnsupportedVersion(spec))
239 }
240}
241
242fn pragma_min_version(spec: &str) -> Option<Version> {
249 let normalized = spec.replace(' ', "").to_lowercase();
250 if normalized.is_empty() {
251 return None;
252 }
253
254 let mut best: Option<Version> = None;
255 for branch in normalized.split("||") {
256 let Some(v) = branch_min_version(branch) else {
257 continue;
258 };
259 best = match best {
260 Some(existing) if existing <= v => Some(existing),
261 _ => Some(v),
262 };
263 }
264 best
265}
266
267fn branch_min_version(branch: &str) -> Option<Version> {
268 let comparators = split_comparators(branch);
269 let mut lower: Option<Version> = None;
270 let mut update = |candidate: Version| {
271 lower = match lower {
272 Some(existing) if existing >= candidate => Some(existing),
273 _ => Some(candidate),
274 };
275 };
276
277 for comparator in comparators {
278 if comparator == "*" {
279 continue;
280 }
281 if let Some((start, _)) = parse_hyphen_range(&comparator) {
282 update(start);
283 continue;
284 }
285 if let Some((version, _)) = parse_caret(&comparator) {
286 update(version);
287 continue;
288 }
289 if let Some(version) = parse_tilde(&comparator) {
290 update(version);
291 continue;
292 }
293 if let Some((op, version)) = parse_operator_version(&comparator) {
294 match op {
295 ComparatorOp::Greater => update(next_patch(version)),
296 ComparatorOp::GreaterEq | ComparatorOp::Exact => update(version),
297 _ => {}
298 }
299 continue;
300 }
301 if let Some(version) = parse_plain_version(&comparator) {
302 update(version);
303 }
304 }
305 lower
306}
307
308const FEATURE_STRING_CONCAT_MIN: Version = Version {
313 major: 0,
314 minor: 8,
315 patch: 12,
316};
317const FEATURE_BYTES_CONCAT_MIN: Version = Version {
318 major: 0,
319 minor: 8,
320 patch: 4,
321};
322
323fn enforce_feature_version_gates(
330 source: &str,
331 pragma_min: Option<Version>,
332) -> Result<(), FrontendError> {
333 let Some(min) = pragma_min else {
334 return Ok(());
335 };
336
337 let stripped = strip_comments_and_strings(source);
338
339 if min < FEATURE_STRING_CONCAT_MIN && contains_builtin_call(&stripped, "string.concat(") {
340 return Err(FrontendError::Parse(format!(
341 "feature `string.concat` requires pragma >= 0.8.12; declared pragma allows {}.{}.{}",
342 min.major, min.minor, min.patch
343 )));
344 }
345 if min < FEATURE_BYTES_CONCAT_MIN && contains_builtin_call(&stripped, "bytes.concat(") {
346 return Err(FrontendError::Parse(format!(
347 "feature `bytes.concat` requires pragma >= 0.8.4; declared pragma allows {}.{}.{}",
348 min.major, min.minor, min.patch
349 )));
350 }
351 Ok(())
352}
353
354fn contains_builtin_call(haystack: &str, needle: &str) -> bool {
360 let bytes = haystack.as_bytes();
361 let mut start = 0usize;
362 while let Some(pos) = haystack[start..].find(needle) {
363 let abs = start + pos;
364 let boundary_ok = abs == 0
365 || {
366 let prev = bytes[abs - 1];
367 !(prev.is_ascii_alphanumeric() || prev == b'_')
368 };
369 if boundary_ok {
370 return true;
371 }
372 start = abs + 1;
373 }
374 false
375}
376
377fn strip_comments_and_strings(source: &str) -> String {
383 let bytes = source.as_bytes();
384 let mut out = String::with_capacity(source.len());
385 let mut i = 0;
386 while i < bytes.len() {
387 let c = bytes[i];
388 if c == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
389 while i < bytes.len() && bytes[i] != b'\n' {
390 out.push(' ');
391 i += 1;
392 }
393 continue;
394 }
395 if c == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
396 out.push_str(" ");
397 i += 2;
398 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
399 out.push(' ');
400 i += 1;
401 }
402 if i + 1 < bytes.len() {
403 out.push_str(" ");
404 i += 2;
405 }
406 continue;
407 }
408 if c == b'"' || c == b'\'' {
409 let quote = c;
410 out.push(' ');
411 i += 1;
412 while i < bytes.len() && bytes[i] != quote {
413 if bytes[i] == b'\\' && i + 1 < bytes.len() {
414 out.push_str(" ");
415 i += 2;
416 continue;
417 }
418 out.push(' ');
419 i += 1;
420 }
421 if i < bytes.len() {
422 out.push(' ');
423 i += 1;
424 }
425 continue;
426 }
427 out.push(c as char);
428 i += 1;
429 }
430 out
431}
432
433fn pragma_supports_neo_devpack_solidity(spec: &str) -> bool {
434 let normalized = spec.replace(' ', "").to_lowercase();
435
436 if normalized.is_empty() {
437 return true;
438 }
439
440 normalized
442 .split("||")
443 .any(branch_supports_neo_devpack_solidity)
444}
445
446fn branch_supports_neo_devpack_solidity(branch: &str) -> bool {
447 if branch.is_empty() {
448 return false;
449 }
450
451 let comparators = split_comparators(branch);
452 if comparators.is_empty() {
453 return false;
454 }
455
456 let mut lower = Bound::Unbounded;
457 let mut upper = Bound::Unbounded;
458
459 for comparator in comparators {
460 if comparator == "*" {
461 continue;
462 }
463
464 if let Some((start, end)) = parse_hyphen_range(&comparator) {
465 lower = lower.max(Bound::Inclusive(start));
466 upper = upper.min(Bound::Inclusive(end));
467 continue;
468 }
469
470 if let Some((version, level)) = parse_caret(&comparator) {
471 let upper_version = match level {
472 0 => Version {
473 major: version.major.saturating_add(1),
474 minor: 0,
475 patch: 0,
476 },
477 _ => Version {
478 major: version.major,
479 minor: version.minor.saturating_add(1),
480 patch: 0,
481 },
482 };
483 lower = lower.max(Bound::Inclusive(version));
484 upper = upper.min(Bound::Exclusive(upper_version));
485 continue;
486 }
487
488 if let Some(version) = parse_tilde(&comparator) {
489 let upper_version = Version {
490 major: version.major,
491 minor: version.minor.saturating_add(1),
492 patch: 0,
493 };
494 lower = lower.max(Bound::Inclusive(version));
495 upper = upper.min(Bound::Exclusive(upper_version));
496 continue;
497 }
498
499 if let Some((op, version)) = parse_operator_version(&comparator) {
500 match op {
501 ComparatorOp::Greater => lower = lower.max(Bound::Exclusive(version)),
502 ComparatorOp::GreaterEq => lower = lower.max(Bound::Inclusive(version)),
503 ComparatorOp::Less => upper = upper.min(Bound::Exclusive(version)),
504 ComparatorOp::LessEq => upper = upper.min(Bound::Inclusive(version)),
505 ComparatorOp::Exact => {
506 lower = lower.max(Bound::Inclusive(version));
507 upper = upper.min(Bound::Inclusive(version));
508 }
509 }
510 continue;
511 }
512
513 if let Some(version) = parse_plain_version(&comparator) {
514 lower = lower.max(Bound::Inclusive(version));
515 upper = upper.min(Bound::Inclusive(version));
516 continue;
517 }
518
519 return false;
521 }
522
523 intersects_supported_neo_range(lower, upper)
524}
525
526#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
527struct Version {
528 major: u64,
529 minor: u64,
530 patch: u64,
531}
532
533#[derive(Clone, Copy, Debug)]
534enum Bound {
535 Unbounded,
536 Inclusive(Version),
537 Exclusive(Version),
538}
539
540impl Bound {
541 fn max(self, other: Self) -> Self {
542 use Bound::{Exclusive, Inclusive, Unbounded};
543
544 match (self, other) {
545 (Unbounded, x) | (x, Unbounded) => x,
546 (Inclusive(a), Inclusive(b)) => {
547 if a >= b {
548 Inclusive(a)
549 } else {
550 Inclusive(b)
551 }
552 }
553 (Exclusive(a), Exclusive(b)) => {
554 if a >= b {
555 Exclusive(a)
556 } else {
557 Exclusive(b)
558 }
559 }
560 (Inclusive(a), Exclusive(b)) => {
561 if a > b {
562 Inclusive(a)
563 } else if b > a {
564 Exclusive(b)
565 } else {
566 Exclusive(a)
567 }
568 }
569 (Exclusive(a), Inclusive(b)) => {
570 if a > b {
571 Exclusive(a)
572 } else if b > a {
573 Inclusive(b)
574 } else {
575 Exclusive(a)
576 }
577 }
578 }
579 }
580
581 fn min(self, other: Self) -> Self {
582 use Bound::{Exclusive, Inclusive, Unbounded};
583
584 match (self, other) {
585 (Unbounded, x) | (x, Unbounded) => x,
586 (Inclusive(a), Inclusive(b)) => {
587 if a <= b {
588 Inclusive(a)
589 } else {
590 Inclusive(b)
591 }
592 }
593 (Exclusive(a), Exclusive(b)) => {
594 if a <= b {
595 Exclusive(a)
596 } else {
597 Exclusive(b)
598 }
599 }
600 (Inclusive(a), Exclusive(b)) => {
601 if a < b {
602 Inclusive(a)
603 } else if b < a {
604 Exclusive(b)
605 } else {
606 Exclusive(a)
607 }
608 }
609 (Exclusive(a), Inclusive(b)) => {
610 if a < b {
611 Exclusive(a)
612 } else if b < a {
613 Inclusive(b)
614 } else {
615 Exclusive(a)
616 }
617 }
618 }
619 }
620}
621
622#[derive(Clone, Copy)]
623enum ComparatorOp {
624 Greater,
625 GreaterEq,
626 Less,
627 LessEq,
628 Exact,
629}
630
631fn split_comparators(branch: &str) -> Vec<String> {
632 let mut tokens = Vec::new();
633 let chars: Vec<char> = branch.chars().collect();
634 let mut i = 0;
635
636 while i < chars.len() {
637 let ch = chars[i];
638 if ch == ',' {
639 i += 1;
640 continue;
641 }
642
643 if ch == '^' || ch == '~' {
644 let mut token = String::new();
645 token.push(ch);
646 i += 1;
647 while i < chars.len() {
648 let c = chars[i];
649 if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
650 break;
651 }
652 token.push(c);
653 i += 1;
654 }
655 tokens.push(token);
656 continue;
657 }
658
659 if ch == '<' || ch == '>' || ch == '=' {
660 let mut token = String::new();
661 token.push(ch);
662 i += 1;
663 if i < chars.len() && chars[i] == '=' {
664 token.push('=');
665 i += 1;
666 }
667 while i < chars.len() {
668 let c = chars[i];
669 if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
670 break;
671 }
672 token.push(c);
673 i += 1;
674 }
675 tokens.push(token);
676 continue;
677 }
678
679 let mut token = String::new();
681 while i < chars.len() {
682 let c = chars[i];
683 if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
684 break;
685 }
686 token.push(c);
687 i += 1;
688 }
689 if !token.is_empty() {
690 tokens.push(token);
691 }
692 }
693
694 tokens
695}
696
697fn parse_hyphen_range(comparator: &str) -> Option<(Version, Version)> {
698 let (left, right) = comparator.split_once('-')?;
699 let start = parse_plain_version(left)?;
700 let end = parse_plain_version(right)?;
701 Some((start, end))
702}
703
704fn parse_caret(comparator: &str) -> Option<(Version, u8)> {
705 let raw = comparator.strip_prefix('^')?;
706 let dots = raw.matches('.').count() as u8;
707 let version = parse_plain_version(raw)?;
708 Some((version, dots))
709}
710
711fn parse_tilde(comparator: &str) -> Option<Version> {
712 let raw = comparator.strip_prefix('~')?;
713 parse_plain_version(raw)
714}
715
716fn parse_operator_version(comparator: &str) -> Option<(ComparatorOp, Version)> {
717 if let Some(raw) = comparator.strip_prefix(">=") {
718 return parse_plain_version(raw).map(|v| (ComparatorOp::GreaterEq, v));
719 }
720 if let Some(raw) = comparator.strip_prefix("<=") {
721 return parse_plain_version(raw).map(|v| (ComparatorOp::LessEq, v));
722 }
723 if let Some(raw) = comparator.strip_prefix('>') {
724 return parse_plain_version(raw).map(|v| (ComparatorOp::Greater, v));
725 }
726 if let Some(raw) = comparator.strip_prefix('<') {
727 return parse_plain_version(raw).map(|v| (ComparatorOp::Less, v));
728 }
729 if let Some(raw) = comparator.strip_prefix('=') {
730 return parse_plain_version(raw).map(|v| (ComparatorOp::Exact, v));
731 }
732 None
733}
734
735fn parse_plain_version(raw: &str) -> Option<Version> {
736 if raw.is_empty() || raw == "*" {
737 return None;
738 }
739
740 let mut parts = raw.split('.');
741 let major_raw = parts.next()?;
742 let minor_raw = parts.next().unwrap_or("0");
743 let patch_raw = parts.next().unwrap_or("0");
744
745 if parts.next().is_some() {
746 return None;
747 }
748
749 let major = major_raw.parse::<u64>().ok()?;
751 let minor = if minor_raw == "*" || minor_raw == "x" {
752 0
753 } else {
754 minor_raw.parse::<u64>().ok()?
755 };
756 let patch = if patch_raw == "*" || patch_raw == "x" {
757 0
758 } else {
759 patch_raw.parse::<u64>().ok()?
760 };
761
762 Some(Version {
763 major,
764 minor,
765 patch,
766 })
767}
768
769fn intersects_supported_neo_range(lower: Bound, upper: Bound) -> bool {
770 (5u64..=8).any(|minor| {
772 intersects_semver_window(
773 lower,
774 upper,
775 Version {
776 major: 0,
777 minor,
778 patch: 0,
779 },
780 Version {
781 major: 0,
782 minor: minor + 1,
783 patch: 0,
784 },
785 )
786 })
787}
788
789fn intersects_semver_window(
790 lower: Bound,
791 upper: Bound,
792 target_start: Version,
793 target_end_exclusive: Version,
794) -> bool {
795
796 let effective_start = match lower {
797 Bound::Unbounded => target_start,
798 Bound::Inclusive(v) => v,
799 Bound::Exclusive(v) => next_patch(v),
800 };
801
802 let effective_end_exclusive = match upper {
803 Bound::Unbounded => target_end_exclusive,
804 Bound::Inclusive(v) => next_patch(v),
805 Bound::Exclusive(v) => v,
806 };
807
808 let range_start = if effective_start > target_start {
809 effective_start
810 } else {
811 target_start
812 };
813 let range_end = if effective_end_exclusive < target_end_exclusive {
814 effective_end_exclusive
815 } else {
816 target_end_exclusive
817 };
818
819 range_start < range_end
820}
821
822fn next_patch(version: Version) -> Version {
823 Version {
824 major: version.major,
825 minor: version.minor,
826 patch: version.patch.saturating_add(1),
827 }
828}
829
830fn skip_whitespace_forward(bytes: &[u8], mut pos: usize) -> usize {
833 while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
834 pos += 1;
835 }
836 pos
837}
838
839fn build_comment_map(comments: &[Comment], source: &str) -> HashMap<usize, NatspecDocIR> {
848 let mut map = HashMap::new();
849 let bytes = source.as_bytes();
850 let mut pending: Option<(usize, String)> = None;
852
853 for comment in comments {
854 match comment {
855 Comment::DocLine(loc, text) | Comment::DocBlock(loc, text) => {
856 if let Loc::File(_, start, end) = loc {
857 let clean_text = clean_doc_comment(text);
858 let continues = match &pending {
859 Some((prev_end, _)) => bytes
860 .get(*prev_end..*start)
861 .is_some_and(|gap| gap.iter().all(u8::is_ascii_whitespace)),
862 None => false,
863 };
864 if continues {
865 if let Some((prev_end, existing)) = pending.as_mut() {
866 *prev_end = *end;
867 existing.push('\n');
868 existing.push_str(&clean_text);
869 }
870 } else {
871 if let Some((prev_end, doc_text)) = pending.take() {
872 map.insert(
873 skip_whitespace_forward(bytes, prev_end),
874 parse_natspec(&doc_text),
875 );
876 }
877 pending = Some((*end, clean_text));
878 }
879 }
880 }
881 Comment::Line(_loc, _) | Comment::Block(_loc, _) => {
882 if let Some((prev_end, doc_text)) = pending.take() {
884 map.insert(
885 skip_whitespace_forward(bytes, prev_end),
886 parse_natspec(&doc_text),
887 );
888 }
889 }
890 }
891 }
892
893 if let Some((prev_end, doc_text)) = pending.take() {
894 map.insert(
895 skip_whitespace_forward(bytes, prev_end),
896 parse_natspec(&doc_text),
897 );
898 }
899
900 map
901}
902
903fn clean_doc_comment(text: &str) -> String {
905 text.lines()
906 .map(|line| {
907 let trimmed = line.trim();
908 if let Some(rest) = trimmed.strip_prefix("///") {
910 rest.trim().to_string()
911 } else if let Some(rest) = trimmed.strip_prefix("/**") {
913 rest.trim_end_matches("*/").trim().to_string()
914 } else if let Some(rest) = trimmed.strip_suffix("*/") {
915 rest.trim().to_string()
916 } else if let Some(rest) = trimmed.strip_prefix('*') {
918 rest.trim().to_string()
919 } else {
920 trimmed.to_string()
921 }
922 })
923 .filter(|line| !line.is_empty())
924 .collect::<Vec<_>>()
925 .join("\n")
926}
927
928fn parse_natspec(text: &str) -> NatspecDocIR {
930 let mut doc = NatspecDocIR::default();
931 let mut current_tag: Option<&str> = None;
932 let mut current_content = String::new();
933
934 for line in text.lines() {
935 let trimmed = line.trim();
936
937 if trimmed.starts_with('@') {
939 if let Some(tag) = current_tag {
941 save_tag_content(&mut doc, tag, ¤t_content);
942 }
943
944 let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect();
946 current_tag = Some(parts[0]);
947 current_content = parts
948 .get(1)
949 .map(|s| s.trim().to_string())
950 .unwrap_or_default();
951 } else if current_tag.is_some() {
952 if !current_content.is_empty() {
954 current_content.push(' ');
955 }
956 current_content.push_str(trimmed);
957 } else {
958 if doc.notice.is_none() && !trimmed.is_empty() {
960 doc.notice = Some(trimmed.to_string());
961 } else if let Some(ref mut notice) = doc.notice {
962 notice.push(' ');
963 notice.push_str(trimmed);
964 }
965 }
966 }
967
968 if let Some(tag) = current_tag {
970 save_tag_content(&mut doc, tag, ¤t_content);
971 }
972
973 doc
974}
975
976fn save_tag_content(doc: &mut NatspecDocIR, tag: &str, content: &str) {
977 let content = content.trim().to_string();
978 if content.is_empty() {
979 return;
980 }
981
982 match tag {
983 "@title" => doc.title = Some(content),
984 "@author" => doc.author = Some(content),
985 "@notice" => doc.notice = Some(content),
986 "@dev" => doc.dev = Some(content),
987 "@param" => {
988 let parts: Vec<&str> = content.splitn(2, char::is_whitespace).collect();
990 if parts.len() >= 2 {
991 doc.params
992 .push((parts[0].to_string(), parts[1].trim().to_string()));
993 } else if !parts.is_empty() {
994 doc.params.push((parts[0].to_string(), String::new()));
995 }
996 }
997 "@return" => doc.returns.push(content),
998 tag if tag.starts_with("@custom:") => {
999 let custom_tag = tag.strip_prefix("@custom:").unwrap_or("");
1000 doc.custom.push((custom_tag.to_string(), content));
1001 }
1002 _ => {} }
1004}
1005
1006fn find_preceding_doc(loc: &Loc, comment_map: &HashMap<usize, NatspecDocIR>) -> NatspecDocIR {
1012 if let Loc::File(_, start, _) = loc {
1013 if let Some(doc) = comment_map.get(start) {
1014 return doc.clone();
1015 }
1016 }
1017 NatspecDocIR::default()
1018}