ethers_solc/resolver/
parse.rs

1use crate::{utils, Solc};
2use semver::VersionReq;
3use solang_parser::pt::{
4    ContractPart, ContractTy, FunctionAttribute, FunctionDefinition, Import, ImportPath, Loc,
5    SourceUnitPart, Visibility,
6};
7use std::{
8    ops::Range,
9    path::{Path, PathBuf},
10};
11
12/// Represents various information about a solidity file parsed via [solang_parser]
13#[derive(Debug)]
14#[allow(unused)]
15pub struct SolData {
16    pub license: Option<SolDataUnit<String>>,
17    pub version: Option<SolDataUnit<String>>,
18    pub experimental: Option<SolDataUnit<String>>,
19    pub imports: Vec<SolDataUnit<SolImport>>,
20    pub version_req: Option<VersionReq>,
21    pub libraries: Vec<SolLibrary>,
22    pub contracts: Vec<SolContract>,
23}
24
25impl SolData {
26    #[allow(unused)]
27    pub fn fmt_version<W: std::fmt::Write>(
28        &self,
29        f: &mut W,
30    ) -> std::result::Result<(), std::fmt::Error> {
31        if let Some(ref version) = self.version {
32            write!(f, "({})", version.data)?;
33        }
34        Ok(())
35    }
36
37    /// Extracts the useful data from a solidity source
38    ///
39    /// This will attempt to parse the solidity AST and extract the imports and version pragma. If
40    /// parsing fails, we'll fall back to extract that info via regex
41    pub fn parse(content: &str, file: &Path) -> Self {
42        let mut version = None;
43        let mut experimental = None;
44        let mut imports = Vec::<SolDataUnit<SolImport>>::new();
45        let mut libraries = Vec::new();
46        let mut contracts = Vec::new();
47
48        match solang_parser::parse(content, 0) {
49            Ok((units, _)) => {
50                for unit in units.0 {
51                    match unit {
52                        SourceUnitPart::PragmaDirective(loc, Some(pragma), Some(value)) => {
53                            if pragma.name == "solidity" {
54                                // we're only interested in the solidity version pragma
55                                version = Some(SolDataUnit::from_loc(value.string.clone(), loc));
56                            }
57
58                            if pragma.name == "experimental" {
59                                experimental = Some(SolDataUnit::from_loc(value.string, loc));
60                            }
61                        }
62                        SourceUnitPart::ImportDirective(import) => {
63                            let (import, ids, loc) = match import {
64                                Import::Plain(s, l) => (s, vec![], l),
65                                Import::GlobalSymbol(s, i, l) => (s, vec![(i, None)], l),
66                                Import::Rename(s, i, l) => (s, i, l),
67                            };
68                            let import = match import {
69                                ImportPath::Filename(s) => s.string.clone(),
70                                ImportPath::Path(p) => p.to_string(),
71                            };
72                            let sol_import = SolImport::new(PathBuf::from(import)).set_aliases(
73                                ids.into_iter()
74                                    .map(|(id, alias)| match alias {
75                                        Some(al) => SolImportAlias::Contract(al.name, id.name),
76                                        None => SolImportAlias::File(id.name),
77                                    })
78                                    .collect(),
79                            );
80                            imports.push(SolDataUnit::from_loc(sol_import, loc));
81                        }
82                        SourceUnitPart::ContractDefinition(def) => {
83                            let functions = def
84                                .parts
85                                .into_iter()
86                                .filter_map(|part| match part {
87                                    ContractPart::FunctionDefinition(f) => Some(*f),
88                                    _ => None,
89                                })
90                                .collect();
91                            if let Some(name) = def.name {
92                                match def.ty {
93                                    ContractTy::Contract(_) => {
94                                        contracts.push(SolContract { name: name.name, functions });
95                                    }
96                                    ContractTy::Library(_) => {
97                                        libraries.push(SolLibrary { name: name.name, functions });
98                                    }
99                                    _ => {}
100                                }
101                            }
102                        }
103                        _ => {}
104                    }
105                }
106            }
107            Err(err) => {
108                tracing::trace!(
109                    "failed to parse \"{}\" ast: \"{:?}\". Falling back to regex to extract data",
110                    file.display(),
111                    err
112                );
113                version =
114                    capture_outer_and_inner(content, &utils::RE_SOL_PRAGMA_VERSION, &["version"])
115                        .first()
116                        .map(|(cap, name)| SolDataUnit::new(name.as_str().to_owned(), cap.range()));
117                imports = capture_imports(content);
118            }
119        };
120        let license = content.lines().next().and_then(|line| {
121            capture_outer_and_inner(line, &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER, &["license"])
122                .first()
123                .map(|(cap, l)| SolDataUnit::new(l.as_str().to_owned(), cap.range()))
124        });
125        let version_req = version.as_ref().and_then(|v| Solc::version_req(v.data()).ok());
126
127        Self { version_req, version, experimental, imports, license, libraries, contracts }
128    }
129
130    /// Returns `true` if the solidity file associated with this type contains a solidity library
131    /// that won't be inlined
132    pub fn has_link_references(&self) -> bool {
133        self.libraries.iter().any(|lib| !lib.is_inlined())
134    }
135}
136
137/// Minimal representation of a contract inside a solidity file
138#[derive(Debug)]
139pub struct SolContract {
140    pub name: String,
141    pub functions: Vec<FunctionDefinition>,
142}
143
144#[derive(Debug, Clone)]
145pub struct SolImport {
146    path: PathBuf,
147    aliases: Vec<SolImportAlias>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum SolImportAlias {
152    File(String),
153    Contract(String, String),
154}
155
156impl SolImport {
157    pub fn new(path: PathBuf) -> Self {
158        Self { path, aliases: vec![] }
159    }
160
161    pub fn path(&self) -> &PathBuf {
162        &self.path
163    }
164
165    pub fn aliases(&self) -> &Vec<SolImportAlias> {
166        &self.aliases
167    }
168
169    fn set_aliases(mut self, aliases: Vec<SolImportAlias>) -> Self {
170        self.aliases = aliases;
171        self
172    }
173}
174
175/// Minimal representation of a contract inside a solidity file
176#[derive(Debug)]
177pub struct SolLibrary {
178    pub name: String,
179    pub functions: Vec<FunctionDefinition>,
180}
181
182impl SolLibrary {
183    /// Returns `true` if all functions of this library will be inlined.
184    ///
185    /// This checks if all functions are either internal or private, because internal functions can
186    /// only be accessed from within the current contract or contracts deriving from it. They cannot
187    /// be accessed externally. Since they are not exposed to the outside through the contract’s
188    /// ABI, they can take parameters of internal types like mappings or storage references.
189    ///
190    /// See also <https://docs.soliditylang.org/en/latest/contracts.html#libraries>
191    pub fn is_inlined(&self) -> bool {
192        for f in self.functions.iter() {
193            for attr in f.attributes.iter() {
194                if let FunctionAttribute::Visibility(vis) = attr {
195                    match vis {
196                        Visibility::External(_) | Visibility::Public(_) => return false,
197                        _ => {}
198                    }
199                }
200            }
201        }
202        true
203    }
204}
205
206/// Represents an item in a solidity file with its location in the file
207#[derive(Debug, Clone)]
208pub struct SolDataUnit<T> {
209    loc: Range<usize>,
210    data: T,
211}
212
213/// Solidity Data Unit decorated with its location within the file
214impl<T> SolDataUnit<T> {
215    pub fn new(data: T, loc: Range<usize>) -> Self {
216        Self { data, loc }
217    }
218
219    pub fn from_loc(data: T, loc: Loc) -> Self {
220        Self {
221            data,
222            loc: match loc {
223                Loc::File(_, start, end) => Range { start, end: end + 1 },
224                _ => Range { start: 0, end: 0 },
225            },
226        }
227    }
228
229    /// Returns the underlying data for the unit
230    pub fn data(&self) -> &T {
231        &self.data
232    }
233
234    /// Returns the location of the given data unit
235    pub fn loc(&self) -> Range<usize> {
236        self.loc.clone()
237    }
238
239    /// Returns the location of the given data unit adjusted by an offset.
240    /// Used to determine new position of the unit within the file after
241    /// content manipulation.
242    pub fn loc_by_offset(&self, offset: isize) -> Range<usize> {
243        utils::range_by_offset(&self.loc, offset)
244    }
245}
246
247/// Given the regex and the target string, find all occurrences
248/// of named groups within the string. This method returns
249/// the tuple of matches `(a, b)` where `a` is the match for the
250/// entire regex and `b` is the match for the first named group.
251///
252/// NOTE: This method will return the match for the first named
253/// group, so the order of passed named groups matters.
254fn capture_outer_and_inner<'a>(
255    content: &'a str,
256    regex: &regex::Regex,
257    names: &[&str],
258) -> Vec<(regex::Match<'a>, regex::Match<'a>)> {
259    regex
260        .captures_iter(content)
261        .filter_map(|cap| {
262            let cap_match = names.iter().find_map(|name| cap.name(name));
263            cap_match.and_then(|m| cap.get(0).map(|outer| (outer.to_owned(), m)))
264        })
265        .collect()
266}
267/// Capture the import statement information together with aliases
268pub fn capture_imports(content: &str) -> Vec<SolDataUnit<SolImport>> {
269    let mut imports = vec![];
270    for cap in utils::RE_SOL_IMPORT.captures_iter(content) {
271        if let Some(name_match) = ["p1", "p2", "p3", "p4"].iter().find_map(|name| cap.name(name)) {
272            let statement_match = cap.get(0).unwrap();
273            let mut aliases = vec![];
274            for alias_cap in utils::RE_SOL_IMPORT_ALIAS.captures_iter(statement_match.as_str()) {
275                if let Some(alias) = alias_cap.name("alias") {
276                    let alias = alias.as_str().to_owned();
277                    let import_alias = match alias_cap.name("target") {
278                        Some(target) => SolImportAlias::Contract(alias, target.as_str().to_owned()),
279                        None => SolImportAlias::File(alias),
280                    };
281                    aliases.push(import_alias);
282                }
283            }
284            let sol_import =
285                SolImport::new(PathBuf::from(name_match.as_str())).set_aliases(aliases);
286            imports.push(SolDataUnit::new(sol_import, statement_match.range()));
287        }
288    }
289    imports
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn can_capture_curly_imports() {
298        let content = r#"
299import { T } from "../Test.sol";
300import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
301import {DsTest} from "ds-test/test.sol";
302"#;
303
304        let captured_imports =
305            capture_imports(content).into_iter().map(|s| s.data.path).collect::<Vec<_>>();
306
307        let expected =
308            utils::find_import_paths(content).map(|m| m.as_str().into()).collect::<Vec<PathBuf>>();
309
310        assert_eq!(captured_imports, expected);
311
312        assert_eq!(
313            captured_imports,
314            vec![
315                PathBuf::from("../Test.sol"),
316                "@openzeppelin/contracts/utils/ReentrancyGuard.sol".into(),
317                "ds-test/test.sol".into(),
318            ]
319        );
320    }
321
322    #[test]
323    fn cap_capture_aliases() {
324        let content = r#"
325import * as T from "./Test.sol";
326import { DsTest as Test } from "ds-test/test.sol";
327import "ds-test/test.sol" as Test;
328import { FloatMath as Math, Math as FloatMath } from "./Math.sol";
329"#;
330
331        let caputred_imports =
332            capture_imports(content).into_iter().map(|s| s.data.aliases).collect::<Vec<_>>();
333        assert_eq!(
334            caputred_imports,
335            vec![
336                vec![SolImportAlias::File("T".into())],
337                vec![SolImportAlias::Contract("Test".into(), "DsTest".into())],
338                vec![SolImportAlias::File("Test".into())],
339                vec![
340                    SolImportAlias::Contract("Math".into(), "FloatMath".into()),
341                    SolImportAlias::Contract("FloatMath".into(), "Math".into()),
342                ],
343            ]
344        );
345    }
346}