ethers-rs/ethers-solc/src/resolver/parse.rs

347 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::{utils, Solc};
use semver::VersionReq;
use solang_parser::pt::{
ContractPart, ContractTy, FunctionAttribute, FunctionDefinition, Import, Loc, SourceUnitPart,
Visibility,
};
use std::{
ops::Range,
path::{Path, PathBuf},
};
/// Represents various information about a solidity file parsed via [solang_parser]
#[derive(Debug)]
#[allow(unused)]
pub struct SolData {
pub license: Option<SolDataUnit<String>>,
pub version: Option<SolDataUnit<String>>,
pub experimental: Option<SolDataUnit<String>>,
pub imports: Vec<SolDataUnit<SolImport>>,
pub version_req: Option<VersionReq>,
pub libraries: Vec<SolLibrary>,
pub contracts: Vec<SolContract>,
}
impl SolData {
#[allow(unused)]
pub fn fmt_version<W: std::fmt::Write>(
&self,
f: &mut W,
) -> std::result::Result<(), std::fmt::Error> {
if let Some(ref version) = self.version {
write!(f, "({})", version.data)?;
}
Ok(())
}
/// Extracts the useful data from a solidity source
///
/// This will attempt to parse the solidity AST and extract the imports and version pragma. If
/// parsing fails, we'll fall back to extract that info via regex
pub fn parse(content: &str, file: &Path) -> Self {
let mut version = None;
let mut experimental = None;
let mut imports = Vec::<SolDataUnit<SolImport>>::new();
let mut libraries = Vec::new();
let mut contracts = Vec::new();
match solang_parser::parse(content, 0) {
Ok((units, _)) => {
for unit in units.0 {
match unit {
SourceUnitPart::PragmaDirective(loc, Some(pragma), Some(value)) => {
if pragma.name == "solidity" {
// we're only interested in the solidity version pragma
version = Some(SolDataUnit::from_loc(value.string.clone(), loc));
}
if pragma.name == "experimental" {
experimental =
Some(SolDataUnit::from_loc(value.string.clone(), loc));
}
}
SourceUnitPart::ImportDirective(import) => {
let (import, ids, loc) = match import {
Import::Plain(s, l) => (s, vec![], l),
Import::GlobalSymbol(s, i, l) => (s, vec![(i, None)], l),
Import::Rename(s, i, l) => (s, i, l),
};
let sol_import = SolImport::new(PathBuf::from(import.string))
.set_aliases(
ids.into_iter()
.map(|(id, alias)| match alias {
Some(al) => SolImportAlias::Contract(al.name, id.name),
None => SolImportAlias::File(id.name),
})
.collect(),
);
imports.push(SolDataUnit::from_loc(sol_import, loc));
}
SourceUnitPart::ContractDefinition(def) => {
let functions = def
.parts
.into_iter()
.filter_map(|part| match part {
ContractPart::FunctionDefinition(f) => Some(*f),
_ => None,
})
.collect();
if let Some(name) = def.name {
match def.ty {
ContractTy::Contract(_) => {
contracts.push(SolContract { name: name.name, functions });
}
ContractTy::Library(_) => {
libraries.push(SolLibrary { name: name.name, functions });
}
_ => {}
}
}
}
_ => {}
}
}
}
Err(err) => {
tracing::trace!(
"failed to parse \"{}\" ast: \"{:?}\". Falling back to regex to extract data",
file.display(),
err
);
version =
capture_outer_and_inner(content, &utils::RE_SOL_PRAGMA_VERSION, &["version"])
.first()
.map(|(cap, name)| SolDataUnit::new(name.as_str().to_owned(), cap.range()));
imports = capture_imports(content);
}
};
let license = content.lines().next().and_then(|line| {
capture_outer_and_inner(line, &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER, &["license"])
.first()
.map(|(cap, l)| SolDataUnit::new(l.as_str().to_owned(), cap.range()))
});
let version_req = version.as_ref().and_then(|v| Solc::version_req(v.data()).ok());
Self { version_req, version, experimental, imports, license, libraries, contracts }
}
/// Returns `true` if the solidity file associated with this type contains a solidity library
/// that won't be inlined
pub fn has_link_references(&self) -> bool {
self.libraries.iter().any(|lib| !lib.is_inlined())
}
}
/// Minimal representation of a contract inside a solidity file
#[derive(Debug)]
pub struct SolContract {
pub name: String,
pub functions: Vec<FunctionDefinition>,
}
#[derive(Debug, Clone)]
pub struct SolImport {
path: PathBuf,
aliases: Vec<SolImportAlias>,
}
#[derive(Debug, Clone, 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
}
}
/// Minimal representation of a contract inside a solidity file
#[derive(Debug)]
pub struct SolLibrary {
pub name: String,
pub functions: Vec<FunctionDefinition>,
}
impl SolLibrary {
/// Returns `true` if all functions of this library will be inlined.
///
/// This checks if all functions are either internal or private, because internal functions can
/// only be accessed from within the current contract or contracts deriving from it. They cannot
/// be accessed externally. Since they are not exposed to the outside through the contracts
/// ABI, they can take parameters of internal types like mappings or storage references.
///
/// See also <https://docs.soliditylang.org/en/latest/contracts.html#libraries>
pub fn is_inlined(&self) -> bool {
for f in self.functions.iter() {
for attr in f.attributes.iter() {
if let FunctionAttribute::Visibility(vis) = attr {
match vis {
Visibility::External(_) | Visibility::Public(_) => return false,
_ => {}
}
}
}
}
true
}
}
/// Represents an item in a solidity file with its location in the file
#[derive(Debug, Clone)]
pub struct SolDataUnit<T> {
loc: Range<usize>,
data: T,
}
/// Solidity Data Unit decorated with its location within the file
impl<T> SolDataUnit<T> {
pub fn new(data: T, loc: Range<usize>) -> Self {
Self { data, loc }
}
pub fn from_loc(data: T, loc: Loc) -> Self {
Self {
data,
loc: match loc {
Loc::File(_, start, end) => Range { start, end: end + 1 },
_ => Range { start: 0, end: 0 },
},
}
}
/// Returns the underlying data for the unit
pub fn data(&self) -> &T {
&self.data
}
/// Returns the location of the given data unit
pub fn loc(&self) -> Range<usize> {
self.loc.clone()
}
/// Returns the location of the given data unit adjusted by an offset.
/// Used to determine new position of the unit within the file after
/// content manipulation.
pub fn loc_by_offset(&self, offset: isize) -> Range<usize> {
utils::range_by_offset(&self.loc, offset)
}
}
/// Given the regex and the target string, find all occurrences
/// of named groups within the string. This method returns
/// the tuple of matches `(a, b)` where `a` is the match for the
/// entire regex and `b` is the match for the first named group.
///
/// NOTE: This method will return the match for the first named
/// group, so the order of passed named groups matters.
fn capture_outer_and_inner<'a>(
content: &'a str,
regex: &regex::Regex,
names: &[&str],
) -> Vec<(regex::Match<'a>, regex::Match<'a>)> {
regex
.captures_iter(content)
.filter_map(|cap| {
let cap_match = names.iter().find_map(|name| cap.name(name));
cap_match.and_then(|m| cap.get(0).map(|outer| (outer.to_owned(), m)))
})
.collect()
}
/// Capture the import statement information together with aliases
pub fn capture_imports(content: &str) -> Vec<SolDataUnit<SolImport>> {
let mut imports = vec![];
for cap in utils::RE_SOL_IMPORT.captures_iter(content) {
if let Some(name_match) =
vec!["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(SolDataUnit::new(sol_import, statement_match.range()));
}
}
imports
}
#[cfg(test)]
mod tests {
use super::*;
#[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()),
],
]
);
}
}