foundry_compilers/resolver/
parse.rsuse foundry_compilers_core::utils;
use semver::VersionReq;
use solar_parse::{ast, interface::sym};
use std::{
ops::Range,
path::{Path, PathBuf},
};
#[derive(Clone, Debug)]
pub struct SolData {
pub license: Option<Spanned<String>>,
pub version: Option<Spanned<String>>,
pub experimental: Option<Spanned<String>>,
pub imports: Vec<Spanned<SolImport>>,
pub version_req: Option<VersionReq>,
pub libraries: Vec<SolLibrary>,
pub contract_names: Vec<String>,
pub is_yul: bool,
pub parse_result: Result<(), String>,
}
impl SolData {
pub fn parse_result(&self) -> crate::Result<()> {
self.parse_result.clone().map_err(crate::SolcError::ParseError)
}
#[allow(dead_code)]
pub fn fmt_version<W: std::fmt::Write>(
&self,
f: &mut W,
) -> std::result::Result<(), std::fmt::Error> {
if let Some(version) = &self.version {
write!(f, "({})", version.data)?;
}
Ok(())
}
pub fn parse(content: &str, file: &Path) -> Self {
let is_yul = file.extension().is_some_and(|ext| ext == "yul");
let mut version = None;
let mut experimental = None;
let mut imports = Vec::<Spanned<SolImport>>::new();
let mut libraries = Vec::new();
let mut contract_names = Vec::new();
let mut parse_result = Ok(());
let sess = solar_parse::interface::Session::builder()
.with_buffer_emitter(Default::default())
.build();
sess.enter(|| {
let arena = ast::Arena::new();
let filename = solar_parse::interface::source_map::FileName::Real(file.to_path_buf());
let Ok(mut parser) =
solar_parse::Parser::from_source_code(&sess, &arena, filename, content.to_string())
else {
return;
};
let Ok(ast) = parser.parse_file().map_err(|e| e.emit()) else { return };
for item in ast.items {
let loc = item.span.lo().to_usize()..item.span.hi().to_usize();
match &item.kind {
ast::ItemKind::Pragma(pragma) => match &pragma.tokens {
ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => {
version = Some(Spanned::new(req.to_string(), loc));
}
ast::PragmaTokens::Custom(name, value)
if name.as_str() == "experimental" =>
{
let value =
value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default();
experimental = Some(Spanned::new(value, loc));
}
_ => {}
},
ast::ItemKind::Import(import) => {
let path = import.path.value.to_string();
let aliases = match &import.items {
ast::ImportItems::Plain(None) | ast::ImportItems::Glob(None) => &[][..],
ast::ImportItems::Plain(Some(alias))
| ast::ImportItems::Glob(Some(alias)) => &[(*alias, None)][..],
ast::ImportItems::Aliases(aliases) => aliases,
};
let sol_import = SolImport::new(PathBuf::from(path)).set_aliases(
aliases
.iter()
.map(|(id, alias)| match alias {
Some(al) => SolImportAlias::Contract(
al.name.to_string(),
id.name.to_string(),
),
None => SolImportAlias::File(id.name.to_string()),
})
.collect(),
);
imports.push(Spanned::new(sol_import, loc));
}
ast::ItemKind::Contract(contract) => {
if contract.kind.is_library() {
libraries.push(SolLibrary { is_inlined: library_is_inlined(contract) });
}
contract_names.push(contract.name.to_string());
}
_ => {}
}
}
});
if let Err(e) = sess.emitted_errors().unwrap() {
let e = e.to_string();
trace!("failed parsing {file:?}: {e}");
parse_result = Err(e);
if version.is_none() {
version = utils::capture_outer_and_inner(
content,
&utils::RE_SOL_PRAGMA_VERSION,
&["version"],
)
.first()
.map(|(cap, name)| Spanned::new(name.as_str().to_owned(), cap.range()));
}
if imports.is_empty() {
imports = capture_imports(content);
}
if contract_names.is_empty() {
utils::RE_CONTRACT_NAMES.captures_iter(content).for_each(|cap| {
contract_names.push(cap[1].to_owned());
});
}
}
let license = content.lines().next().and_then(|line| {
utils::capture_outer_and_inner(
line,
&utils::RE_SOL_SDPX_LICENSE_IDENTIFIER,
&["license"],
)
.first()
.map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range()))
});
let version_req = version.as_ref().and_then(|v| Self::parse_version_req(v.data()).ok());
Self {
version_req,
version,
experimental,
imports,
license,
libraries,
contract_names,
is_yul,
parse_result,
}
}
pub fn parse_version_req(version: &str) -> Result<VersionReq, semver::Error> {
let version = version.replace(' ', ",");
let exact = !matches!(&version[0..1], "*" | "^" | "=" | ">" | "<" | "~");
let mut version = VersionReq::parse(&version)?;
if exact {
version.comparators[0].op = semver::Op::Exact;
}
Ok(version)
}
}
#[derive(Clone, Debug)]
pub struct SolImport {
path: PathBuf,
aliases: Vec<SolImportAlias>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SolImportAlias {
File(String),
Contract(String, String),
}
impl SolImport {
pub fn new(path: PathBuf) -> Self {
Self { path, aliases: vec![] }
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn aliases(&self) -> &Vec<SolImportAlias> {
&self.aliases
}
fn set_aliases(mut self, aliases: Vec<SolImportAlias>) -> Self {
self.aliases = aliases;
self
}
}
#[derive(Clone, Debug)]
pub struct SolLibrary {
pub is_inlined: bool,
}
impl SolLibrary {
pub fn is_inlined(&self) -> bool {
self.is_inlined
}
}
#[derive(Clone, Debug)]
pub struct Spanned<T> {
pub span: Range<usize>,
pub data: T,
}
impl<T> Spanned<T> {
pub fn new(data: T, span: Range<usize>) -> Self {
Self { data, span }
}
pub fn data(&self) -> &T {
&self.data
}
pub fn span(&self) -> Range<usize> {
self.span.clone()
}
pub fn loc_by_offset(&self, offset: isize) -> Range<usize> {
utils::range_by_offset(&self.span, offset)
}
}
fn library_is_inlined(contract: &ast::ItemContract<'_>) -> bool {
contract
.body
.iter()
.filter_map(|item| match &item.kind {
ast::ItemKind::Function(f) => Some(f),
_ => None,
})
.all(|f| {
!matches!(
f.header.visibility,
Some(ast::Visibility::Public | ast::Visibility::External)
)
})
}
pub fn capture_imports(content: &str) -> Vec<Spanned<SolImport>> {
let mut imports = vec![];
for cap in utils::RE_SOL_IMPORT.captures_iter(content) {
if let Some(name_match) = ["p1", "p2", "p3", "p4"].iter().find_map(|name| cap.name(name)) {
let statement_match = cap.get(0).unwrap();
let mut aliases = vec![];
for alias_cap in utils::RE_SOL_IMPORT_ALIAS.captures_iter(statement_match.as_str()) {
if let Some(alias) = alias_cap.name("alias") {
let alias = alias.as_str().to_owned();
let import_alias = match alias_cap.name("target") {
Some(target) => SolImportAlias::Contract(alias, target.as_str().to_owned()),
None => SolImportAlias::File(alias),
};
aliases.push(import_alias);
}
}
let sol_import =
SolImport::new(PathBuf::from(name_match.as_str())).set_aliases(aliases);
imports.push(Spanned::new(sol_import, statement_match.range()));
}
}
imports
}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn assert_version(version_req: Option<&str>, src: &str) {
let data = SolData::parse(src, "test.sol".as_ref());
assert_eq!(data.version_req, version_req.map(|v| v.parse().unwrap()), "src:\n{src}");
}
#[track_caller]
fn assert_contract_names(names: &[&str], src: &str) {
let data = SolData::parse(src, "test.sol".as_ref());
assert_eq!(data.contract_names, names, "src:\n{src}");
}
#[test]
fn soldata_parsing() {
assert_version(None, "");
assert_version(None, "contract C { }");
assert_version(
Some(">=0.4.22, <0.6"),
r#"
pragma solidity >=0.4.22 <0.6;
contract BugReport {
function() external payable {
deposit();
}
function deposit() public payable {}
}
"#,
);
assert_contract_names(
&["A", "B69$_", "C_", "$D"],
r#"
contract A {}
library B69$_ {}
abstract contract C_ {} interface $D {}
uint constant x = .1e10;
uint constant y = .1 ether;
"#,
);
}
#[test]
fn can_capture_curly_imports() {
let content = r#"
import { T } from "../Test.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {DsTest} from "ds-test/test.sol";
"#;
let captured_imports =
capture_imports(content).into_iter().map(|s| s.data.path).collect::<Vec<_>>();
let expected =
utils::find_import_paths(content).map(|m| m.as_str().into()).collect::<Vec<PathBuf>>();
assert_eq!(captured_imports, expected);
assert_eq!(
captured_imports,
vec![
PathBuf::from("../Test.sol"),
"@openzeppelin/contracts/utils/ReentrancyGuard.sol".into(),
"ds-test/test.sol".into(),
],
);
}
#[test]
fn cap_capture_aliases() {
let content = r#"
import * as T from "./Test.sol";
import { DsTest as Test } from "ds-test/test.sol";
import "ds-test/test.sol" as Test;
import { FloatMath as Math, Math as FloatMath } from "./Math.sol";
"#;
let caputred_imports =
capture_imports(content).into_iter().map(|s| s.data.aliases).collect::<Vec<_>>();
assert_eq!(
caputred_imports,
vec![
vec![SolImportAlias::File("T".into())],
vec![SolImportAlias::Contract("Test".into(), "DsTest".into())],
vec![SolImportAlias::File("Test".into())],
vec![
SolImportAlias::Contract("Math".into(), "FloatMath".into()),
SolImportAlias::Contract("FloatMath".into(), "Math".into()),
],
]
);
}
}