pub fn parse_source(source: &str) -> Result<Vec<ContractIR>, FrontendError> {
let (source_unit, comments) = parse_solidity_guarded(source)
.map_err(|diags| FrontendError::ParseDiagnostics(collect_parse_diagnostics(source, &diags)))?;
let comment_map = build_comment_map(&comments, source);
let mut contracts = Vec::new();
let mut file_level_type_aliases: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut file_level_structs: Vec<StructIR> = Vec::new();
let mut file_level_enums: Vec<EnumIR> = Vec::new();
let mut file_level_errors: Vec<ErrorIR> = Vec::new();
let mut file_level_free_functions: Vec<FunctionIR> = Vec::new();
let mut file_level_usings: Vec<Using> = Vec::new();
let mut pragma_min_version: Option<Version> = None;
for part in source_unit.0 {
match part {
SourceUnitPart::PragmaDirective(pragma) => {
if let Some(min) = enforce_supported_pragma(&pragma)? {
pragma_min_version = match pragma_min_version {
Some(existing) if existing >= min => Some(existing),
_ => Some(min),
};
}
}
SourceUnitPart::ContractDefinition(contract) => {
contracts.push(convert_contract(*contract, &comment_map));
}
SourceUnitPart::TypeDefinition(td) => {
let underlying = format!("{}", td.ty);
file_level_type_aliases.insert(td.name.name, underlying);
}
SourceUnitPart::StructDefinition(def) => {
file_level_structs.push(convert_struct(*def));
}
SourceUnitPart::EnumDefinition(def) => {
file_level_enums.push(convert_enum(*def));
}
SourceUnitPart::ErrorDefinition(def) => {
file_level_errors.push(convert_error(*def));
}
SourceUnitPart::FunctionDefinition(def) => {
let mut fn_ir = convert_function(*def, &comment_map);
fn_ir.visibility = VisibilityKind::Internal;
fn_ir.ty = FunctionTy::Function;
file_level_free_functions.push(fn_ir);
}
SourceUnitPart::Using(using) => {
file_level_usings.push(*using);
}
_ => {}
}
}
enforce_feature_version_gates(source, pragma_min_version)?;
if !file_level_type_aliases.is_empty() {
for contract in &mut contracts {
for (name, underlying) in &file_level_type_aliases {
contract
.type_aliases
.entry(name.clone())
.or_insert_with(|| underlying.clone());
}
}
}
if !file_level_structs.is_empty() {
for contract in &mut contracts {
for file_struct in &file_level_structs {
if !contract
.structs
.iter()
.any(|existing| existing.name == file_struct.name)
{
contract.structs.push(file_struct.clone());
}
}
}
}
if !file_level_enums.is_empty() {
for contract in &mut contracts {
for file_enum in &file_level_enums {
if !contract
.enums
.iter()
.any(|existing| existing.name == file_enum.name)
{
contract.enums.push(file_enum.clone());
}
}
}
}
if !file_level_errors.is_empty() {
for contract in &mut contracts {
for file_error in &file_level_errors {
if !contract
.errors
.iter()
.any(|existing| existing.name == file_error.name)
{
contract.errors.push(file_error.clone());
}
}
}
}
if !file_level_free_functions.is_empty() {
for contract in &mut contracts {
for free_fn in &file_level_free_functions {
if !contract
.functions
.iter()
.any(|existing| existing.name == free_fn.name)
{
contract.functions.push(free_fn.clone());
}
}
}
}
if !file_level_usings.is_empty() {
for contract in &mut contracts {
if matches!(contract.kind, ContractKind::Library) {
continue;
}
for using in &file_level_usings {
apply_file_level_using(contract, using);
}
}
}
Ok(contracts)
}
fn enforce_supported_pragma(
pragma: &solang_parser::pt::PragmaDirective,
) -> Result<Option<Version>, FrontendError> {
use solang_parser::pt::PragmaDirective;
let PragmaDirective::Version(_, ident, comparators) = pragma else {
return Ok(None);
};
if ident.name != "solidity" {
return Ok(None);
}
let spec = comparators
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
if pragma_supports_neo_devpack_solidity(spec.as_str()) {
Ok(pragma_min_version(spec.as_str()))
} else {
Err(FrontendError::UnsupportedVersion(spec))
}
}
fn pragma_min_version(spec: &str) -> Option<Version> {
let normalized = spec.replace(' ', "").to_lowercase();
if normalized.is_empty() {
return None;
}
let mut best: Option<Version> = None;
for branch in normalized.split("||") {
let Some(v) = branch_min_version(branch) else {
continue;
};
best = match best {
Some(existing) if existing <= v => Some(existing),
_ => Some(v),
};
}
best
}
fn branch_min_version(branch: &str) -> Option<Version> {
let comparators = split_comparators(branch);
let mut lower: Option<Version> = None;
let mut update = |candidate: Version| {
lower = match lower {
Some(existing) if existing >= candidate => Some(existing),
_ => Some(candidate),
};
};
for comparator in comparators {
if comparator == "*" {
continue;
}
if let Some((start, _)) = parse_hyphen_range(&comparator) {
update(start);
continue;
}
if let Some((version, _)) = parse_caret(&comparator) {
update(version);
continue;
}
if let Some(version) = parse_tilde(&comparator) {
update(version);
continue;
}
if let Some((op, version)) = parse_operator_version(&comparator) {
match op {
ComparatorOp::Greater => update(next_patch(version)),
ComparatorOp::GreaterEq | ComparatorOp::Exact => update(version),
_ => {}
}
continue;
}
if let Some(version) = parse_plain_version(&comparator) {
update(version);
}
}
lower
}
const FEATURE_STRING_CONCAT_MIN: Version = Version {
major: 0,
minor: 8,
patch: 12,
};
const FEATURE_BYTES_CONCAT_MIN: Version = Version {
major: 0,
minor: 8,
patch: 4,
};
fn enforce_feature_version_gates(
source: &str,
pragma_min: Option<Version>,
) -> Result<(), FrontendError> {
let Some(min) = pragma_min else {
return Ok(());
};
let stripped = strip_comments_and_strings(source);
if min < FEATURE_STRING_CONCAT_MIN && contains_builtin_call(&stripped, "string.concat(") {
return Err(FrontendError::Parse(format!(
"feature `string.concat` requires pragma >= 0.8.12; declared pragma allows {}.{}.{}",
min.major, min.minor, min.patch
)));
}
if min < FEATURE_BYTES_CONCAT_MIN && contains_builtin_call(&stripped, "bytes.concat(") {
return Err(FrontendError::Parse(format!(
"feature `bytes.concat` requires pragma >= 0.8.4; declared pragma allows {}.{}.{}",
min.major, min.minor, min.patch
)));
}
Ok(())
}
fn contains_builtin_call(haystack: &str, needle: &str) -> bool {
let bytes = haystack.as_bytes();
let mut start = 0usize;
while let Some(pos) = haystack[start..].find(needle) {
let abs = start + pos;
let boundary_ok = abs == 0
|| {
let prev = bytes[abs - 1];
!(prev.is_ascii_alphanumeric() || prev == b'_')
};
if boundary_ok {
return true;
}
start = abs + 1;
}
false
}
fn strip_comments_and_strings(source: &str) -> String {
let bytes = source.as_bytes();
let mut out = String::with_capacity(source.len());
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
while i < bytes.len() && bytes[i] != b'\n' {
out.push(' ');
i += 1;
}
continue;
}
if c == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
out.push_str(" ");
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
out.push(' ');
i += 1;
}
if i + 1 < bytes.len() {
out.push_str(" ");
i += 2;
}
continue;
}
if c == b'"' || c == b'\'' {
let quote = c;
out.push(' ');
i += 1;
while i < bytes.len() && bytes[i] != quote {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
out.push_str(" ");
i += 2;
continue;
}
out.push(' ');
i += 1;
}
if i < bytes.len() {
out.push(' ');
i += 1;
}
continue;
}
out.push(c as char);
i += 1;
}
out
}
fn pragma_supports_neo_devpack_solidity(spec: &str) -> bool {
let normalized = spec.replace(' ', "").to_lowercase();
if normalized.is_empty() {
return true;
}
normalized
.split("||")
.any(branch_supports_neo_devpack_solidity)
}
fn branch_supports_neo_devpack_solidity(branch: &str) -> bool {
if branch.is_empty() {
return false;
}
let comparators = split_comparators(branch);
if comparators.is_empty() {
return false;
}
let mut lower = Bound::Unbounded;
let mut upper = Bound::Unbounded;
for comparator in comparators {
if comparator == "*" {
continue;
}
if let Some((start, end)) = parse_hyphen_range(&comparator) {
lower = lower.max(Bound::Inclusive(start));
upper = upper.min(Bound::Inclusive(end));
continue;
}
if let Some((version, level)) = parse_caret(&comparator) {
let upper_version = match level {
0 => Version {
major: version.major.saturating_add(1),
minor: 0,
patch: 0,
},
_ => Version {
major: version.major,
minor: version.minor.saturating_add(1),
patch: 0,
},
};
lower = lower.max(Bound::Inclusive(version));
upper = upper.min(Bound::Exclusive(upper_version));
continue;
}
if let Some(version) = parse_tilde(&comparator) {
let upper_version = Version {
major: version.major,
minor: version.minor.saturating_add(1),
patch: 0,
};
lower = lower.max(Bound::Inclusive(version));
upper = upper.min(Bound::Exclusive(upper_version));
continue;
}
if let Some((op, version)) = parse_operator_version(&comparator) {
match op {
ComparatorOp::Greater => lower = lower.max(Bound::Exclusive(version)),
ComparatorOp::GreaterEq => lower = lower.max(Bound::Inclusive(version)),
ComparatorOp::Less => upper = upper.min(Bound::Exclusive(version)),
ComparatorOp::LessEq => upper = upper.min(Bound::Inclusive(version)),
ComparatorOp::Exact => {
lower = lower.max(Bound::Inclusive(version));
upper = upper.min(Bound::Inclusive(version));
}
}
continue;
}
if let Some(version) = parse_plain_version(&comparator) {
lower = lower.max(Bound::Inclusive(version));
upper = upper.min(Bound::Inclusive(version));
continue;
}
return false;
}
intersects_supported_neo_range(lower, upper)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u64,
minor: u64,
patch: u64,
}
#[derive(Clone, Copy, Debug)]
enum Bound {
Unbounded,
Inclusive(Version),
Exclusive(Version),
}
impl Bound {
fn max(self, other: Self) -> Self {
use Bound::{Exclusive, Inclusive, Unbounded};
match (self, other) {
(Unbounded, x) | (x, Unbounded) => x,
(Inclusive(a), Inclusive(b)) => {
if a >= b {
Inclusive(a)
} else {
Inclusive(b)
}
}
(Exclusive(a), Exclusive(b)) => {
if a >= b {
Exclusive(a)
} else {
Exclusive(b)
}
}
(Inclusive(a), Exclusive(b)) => {
if a > b {
Inclusive(a)
} else if b > a {
Exclusive(b)
} else {
Exclusive(a)
}
}
(Exclusive(a), Inclusive(b)) => {
if a > b {
Exclusive(a)
} else if b > a {
Inclusive(b)
} else {
Exclusive(a)
}
}
}
}
fn min(self, other: Self) -> Self {
use Bound::{Exclusive, Inclusive, Unbounded};
match (self, other) {
(Unbounded, x) | (x, Unbounded) => x,
(Inclusive(a), Inclusive(b)) => {
if a <= b {
Inclusive(a)
} else {
Inclusive(b)
}
}
(Exclusive(a), Exclusive(b)) => {
if a <= b {
Exclusive(a)
} else {
Exclusive(b)
}
}
(Inclusive(a), Exclusive(b)) => {
if a < b {
Inclusive(a)
} else if b < a {
Exclusive(b)
} else {
Exclusive(a)
}
}
(Exclusive(a), Inclusive(b)) => {
if a < b {
Exclusive(a)
} else if b < a {
Inclusive(b)
} else {
Exclusive(a)
}
}
}
}
}
#[derive(Clone, Copy)]
enum ComparatorOp {
Greater,
GreaterEq,
Less,
LessEq,
Exact,
}
fn split_comparators(branch: &str) -> Vec<String> {
let mut tokens = Vec::new();
let chars: Vec<char> = branch.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
if ch == ',' {
i += 1;
continue;
}
if ch == '^' || ch == '~' {
let mut token = String::new();
token.push(ch);
i += 1;
while i < chars.len() {
let c = chars[i];
if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
break;
}
token.push(c);
i += 1;
}
tokens.push(token);
continue;
}
if ch == '<' || ch == '>' || ch == '=' {
let mut token = String::new();
token.push(ch);
i += 1;
if i < chars.len() && chars[i] == '=' {
token.push('=');
i += 1;
}
while i < chars.len() {
let c = chars[i];
if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
break;
}
token.push(c);
i += 1;
}
tokens.push(token);
continue;
}
let mut token = String::new();
while i < chars.len() {
let c = chars[i];
if c == ',' || c == '^' || c == '~' || c == '<' || c == '>' || c == '=' {
break;
}
token.push(c);
i += 1;
}
if !token.is_empty() {
tokens.push(token);
}
}
tokens
}
fn parse_hyphen_range(comparator: &str) -> Option<(Version, Version)> {
let (left, right) = comparator.split_once('-')?;
let start = parse_plain_version(left)?;
let end = parse_plain_version(right)?;
Some((start, end))
}
fn parse_caret(comparator: &str) -> Option<(Version, u8)> {
let raw = comparator.strip_prefix('^')?;
let dots = raw.matches('.').count() as u8;
let version = parse_plain_version(raw)?;
Some((version, dots))
}
fn parse_tilde(comparator: &str) -> Option<Version> {
let raw = comparator.strip_prefix('~')?;
parse_plain_version(raw)
}
fn parse_operator_version(comparator: &str) -> Option<(ComparatorOp, Version)> {
if let Some(raw) = comparator.strip_prefix(">=") {
return parse_plain_version(raw).map(|v| (ComparatorOp::GreaterEq, v));
}
if let Some(raw) = comparator.strip_prefix("<=") {
return parse_plain_version(raw).map(|v| (ComparatorOp::LessEq, v));
}
if let Some(raw) = comparator.strip_prefix('>') {
return parse_plain_version(raw).map(|v| (ComparatorOp::Greater, v));
}
if let Some(raw) = comparator.strip_prefix('<') {
return parse_plain_version(raw).map(|v| (ComparatorOp::Less, v));
}
if let Some(raw) = comparator.strip_prefix('=') {
return parse_plain_version(raw).map(|v| (ComparatorOp::Exact, v));
}
None
}
fn parse_plain_version(raw: &str) -> Option<Version> {
if raw.is_empty() || raw == "*" {
return None;
}
let mut parts = raw.split('.');
let major_raw = parts.next()?;
let minor_raw = parts.next().unwrap_or("0");
let patch_raw = parts.next().unwrap_or("0");
if parts.next().is_some() {
return None;
}
let major = major_raw.parse::<u64>().ok()?;
let minor = if minor_raw == "*" || minor_raw == "x" {
0
} else {
minor_raw.parse::<u64>().ok()?
};
let patch = if patch_raw == "*" || patch_raw == "x" {
0
} else {
patch_raw.parse::<u64>().ok()?
};
Some(Version {
major,
minor,
patch,
})
}
fn intersects_supported_neo_range(lower: Bound, upper: Bound) -> bool {
(5u64..=8).any(|minor| {
intersects_semver_window(
lower,
upper,
Version {
major: 0,
minor,
patch: 0,
},
Version {
major: 0,
minor: minor + 1,
patch: 0,
},
)
})
}
fn intersects_semver_window(
lower: Bound,
upper: Bound,
target_start: Version,
target_end_exclusive: Version,
) -> bool {
let effective_start = match lower {
Bound::Unbounded => target_start,
Bound::Inclusive(v) => v,
Bound::Exclusive(v) => next_patch(v),
};
let effective_end_exclusive = match upper {
Bound::Unbounded => target_end_exclusive,
Bound::Inclusive(v) => next_patch(v),
Bound::Exclusive(v) => v,
};
let range_start = if effective_start > target_start {
effective_start
} else {
target_start
};
let range_end = if effective_end_exclusive < target_end_exclusive {
effective_end_exclusive
} else {
target_end_exclusive
};
range_start < range_end
}
fn next_patch(version: Version) -> Version {
Version {
major: version.major,
minor: version.minor,
patch: version.patch.saturating_add(1),
}
}
fn skip_whitespace_forward(bytes: &[u8], mut pos: usize) -> usize {
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
pos
}
fn build_comment_map(comments: &[Comment], source: &str) -> HashMap<usize, NatspecDocIR> {
let mut map = HashMap::new();
let bytes = source.as_bytes();
let mut pending: Option<(usize, String)> = None;
for comment in comments {
match comment {
Comment::DocLine(loc, text) | Comment::DocBlock(loc, text) => {
if let Loc::File(_, start, end) = loc {
let clean_text = clean_doc_comment(text);
let continues = match &pending {
Some((prev_end, _)) => bytes
.get(*prev_end..*start)
.is_some_and(|gap| gap.iter().all(u8::is_ascii_whitespace)),
None => false,
};
if continues {
if let Some((prev_end, existing)) = pending.as_mut() {
*prev_end = *end;
existing.push('\n');
existing.push_str(&clean_text);
}
} else {
if let Some((prev_end, doc_text)) = pending.take() {
map.insert(
skip_whitespace_forward(bytes, prev_end),
parse_natspec(&doc_text),
);
}
pending = Some((*end, clean_text));
}
}
}
Comment::Line(_loc, _) | Comment::Block(_loc, _) => {
if let Some((prev_end, doc_text)) = pending.take() {
map.insert(
skip_whitespace_forward(bytes, prev_end),
parse_natspec(&doc_text),
);
}
}
}
}
if let Some((prev_end, doc_text)) = pending.take() {
map.insert(
skip_whitespace_forward(bytes, prev_end),
parse_natspec(&doc_text),
);
}
map
}
fn clean_doc_comment(text: &str) -> String {
text.lines()
.map(|line| {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("///") {
rest.trim().to_string()
} else if let Some(rest) = trimmed.strip_prefix("/**") {
rest.trim_end_matches("*/").trim().to_string()
} else if let Some(rest) = trimmed.strip_suffix("*/") {
rest.trim().to_string()
} else if let Some(rest) = trimmed.strip_prefix('*') {
rest.trim().to_string()
} else {
trimmed.to_string()
}
})
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
fn parse_natspec(text: &str) -> NatspecDocIR {
let mut doc = NatspecDocIR::default();
let mut current_tag: Option<&str> = None;
let mut current_content = String::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with('@') {
if let Some(tag) = current_tag {
save_tag_content(&mut doc, tag, ¤t_content);
}
let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect();
current_tag = Some(parts[0]);
current_content = parts
.get(1)
.map(|s| s.trim().to_string())
.unwrap_or_default();
} else if current_tag.is_some() {
if !current_content.is_empty() {
current_content.push(' ');
}
current_content.push_str(trimmed);
} else {
if doc.notice.is_none() && !trimmed.is_empty() {
doc.notice = Some(trimmed.to_string());
} else if let Some(ref mut notice) = doc.notice {
notice.push(' ');
notice.push_str(trimmed);
}
}
}
if let Some(tag) = current_tag {
save_tag_content(&mut doc, tag, ¤t_content);
}
doc
}
fn save_tag_content(doc: &mut NatspecDocIR, tag: &str, content: &str) {
let content = content.trim().to_string();
if content.is_empty() {
return;
}
match tag {
"@title" => doc.title = Some(content),
"@author" => doc.author = Some(content),
"@notice" => doc.notice = Some(content),
"@dev" => doc.dev = Some(content),
"@param" => {
let parts: Vec<&str> = content.splitn(2, char::is_whitespace).collect();
if parts.len() >= 2 {
doc.params
.push((parts[0].to_string(), parts[1].trim().to_string()));
} else if !parts.is_empty() {
doc.params.push((parts[0].to_string(), String::new()));
}
}
"@return" => doc.returns.push(content),
tag if tag.starts_with("@custom:") => {
let custom_tag = tag.strip_prefix("@custom:").unwrap_or("");
doc.custom.push((custom_tag.to_string(), content));
}
_ => {} }
}
fn find_preceding_doc(loc: &Loc, comment_map: &HashMap<usize, NatspecDocIR>) -> NatspecDocIR {
if let Loc::File(_, start, _) = loc {
if let Some(doc) = comment_map.get(start) {
return doc.clone();
}
}
NatspecDocIR::default()
}