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>, pub version: Option>, pub experimental: Option>, pub imports: Vec>, pub version_req: Option, pub libraries: Vec, pub contracts: Vec, } impl SolData { #[allow(unused)] pub fn fmt_version( &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::>::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, pragma, 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(); let name = def.name.name; match def.ty { ContractTy::Contract(_) => { contracts.push(SolContract { name, functions }); } ContractTy::Library(_) => { libraries.push(SolLibrary { 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, } #[derive(Debug, Clone)] pub struct SolImport { path: PathBuf, aliases: Vec, } #[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 { &self.aliases } fn set_aliases(mut self, aliases: Vec) -> 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, } 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 contract’s /// ABI, they can take parameters of internal types like mappings or storage references. /// /// See also 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 { loc: Range, data: T, } /// Solidity Data Unit decorated with its location within the file impl SolDataUnit { pub fn new(data: T, loc: Range) -> 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 { 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 { 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: ®ex::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> { 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::>(); let expected = utils::find_import_paths(content).map(|m| m.as_str().into()).collect::>(); 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::>(); 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()), ], ] ); } }