use crate::{utils, Solc}; use regex::Match; use semver::VersionReq; use solang_parser::pt::{ ContractPart, ContractTy, FunctionAttribute, FunctionDefinition, Import, Loc, SourceUnitPart, Visibility, }; use std::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 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 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::new(value.string, loc.into())); } } SourceUnitPart::ImportDirective(_, import) => { let (import, loc) = match import { Import::Plain(s, l) => (s, l), Import::GlobalSymbol(s, _, l) => (s, l), Import::Rename(s, _, l) => (s, l), }; imports .push(SolDataUnit::new(PathBuf::from(import.string), loc.into())); } 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.to_owned().into()) }); 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.to_owned().into())) }); let version_req = version.as_ref().and_then(|v| Solc::version_req(v.data()).ok()); Self { version_req, version, 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, } /// 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: Location, data: T, } /// Location in a text file buffer #[derive(Debug, Clone)] pub struct Location { pub start: usize, pub end: usize, } /// Solidity Data Unit decorated with its location within the file impl SolDataUnit { pub fn new(data: T, loc: Location) -> Self { Self { data, loc } } /// 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) -> (usize, usize) { (self.loc.start, self.loc.end) } /// 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) -> (usize, usize) { ( offset.saturating_add(self.loc.start as isize) as usize, // make the end location exclusive offset.saturating_add(self.loc.end as isize + 1) as usize, ) } } impl From> for Location { fn from(src: Match) -> Self { Location { start: src.start(), end: src.end() } } } impl From for Location { fn from(src: Loc) -> Self { match src { Loc::File(_, start, end) => Location { start, end }, _ => Location { start: 0, end: 0 }, } } } /// 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() } pub fn capture_imports(content: &str) -> Vec> { capture_outer_and_inner(content, &utils::RE_SOL_IMPORT, &["p1", "p2", "p3", "p4"]) .iter() .map(|(cap, m)| SolDataUnit::new(PathBuf::from(m.as_str()), cap.to_owned().into())) .collect() } #[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).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(), ] ); } }