foundry_compilers/resolver/
parse.rs

1use foundry_compilers_core::utils;
2use semver::VersionReq;
3use solar_parse::{ast, interface::sym};
4use std::{
5    ops::Range,
6    path::{Path, PathBuf},
7};
8
9/// Represents various information about a Solidity file.
10#[derive(Clone, Debug)]
11#[non_exhaustive]
12pub struct SolData {
13    pub license: Option<Spanned<String>>,
14    pub version: Option<Spanned<String>>,
15    pub experimental: Option<Spanned<String>>,
16    pub imports: Vec<Spanned<SolImport>>,
17    pub version_req: Option<VersionReq>,
18    pub libraries: Vec<SolLibrary>,
19    pub contract_names: Vec<String>,
20    pub is_yul: bool,
21    pub parse_result: Result<(), String>,
22}
23
24impl SolData {
25    /// Returns the result of parsing the file.
26    pub fn parse_result(&self) -> crate::Result<()> {
27        self.parse_result.clone().map_err(crate::SolcError::ParseError)
28    }
29
30    #[allow(dead_code)]
31    pub fn fmt_version<W: std::fmt::Write>(
32        &self,
33        f: &mut W,
34    ) -> std::result::Result<(), std::fmt::Error> {
35        if let Some(version) = &self.version {
36            write!(f, "({})", version.data)?;
37        }
38        Ok(())
39    }
40
41    /// Extracts the useful data from a solidity source
42    ///
43    /// This will attempt to parse the solidity AST and extract the imports and version pragma. If
44    /// parsing fails, we'll fall back to extract that info via regex
45    #[instrument(name = "SolData::parse", skip_all)]
46    pub fn parse(content: &str, file: &Path) -> Self {
47        let is_yul = file.extension().is_some_and(|ext| ext == "yul");
48        let mut version = None;
49        let mut experimental = None;
50        let mut imports = Vec::<Spanned<SolImport>>::new();
51        let mut libraries = Vec::new();
52        let mut contract_names = Vec::new();
53        let mut parse_result = Ok(());
54
55        let result = crate::parse_one_source(content, file, |ast| {
56            for item in ast.items.iter() {
57                let loc = item.span.lo().to_usize()..item.span.hi().to_usize();
58                match &item.kind {
59                    ast::ItemKind::Pragma(pragma) => match &pragma.tokens {
60                        ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => {
61                            version = Some(Spanned::new(req.to_string(), loc));
62                        }
63                        ast::PragmaTokens::Custom(name, value)
64                            if name.as_str() == "experimental" =>
65                        {
66                            let value =
67                                value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default();
68                            experimental = Some(Spanned::new(value, loc));
69                        }
70                        _ => {}
71                    },
72
73                    ast::ItemKind::Import(import) => {
74                        let path = import.path.value.to_string();
75                        let aliases = match &import.items {
76                            ast::ImportItems::Plain(None) => &[][..],
77                            ast::ImportItems::Plain(Some(alias))
78                            | ast::ImportItems::Glob(alias) => &[(*alias, None)][..],
79                            ast::ImportItems::Aliases(aliases) => aliases,
80                        };
81                        let sol_import = SolImport::new(PathBuf::from(path)).set_aliases(
82                            aliases
83                                .iter()
84                                .map(|(id, alias)| match alias {
85                                    Some(al) => SolImportAlias::Contract(
86                                        al.name.to_string(),
87                                        id.name.to_string(),
88                                    ),
89                                    None => SolImportAlias::File(id.name.to_string()),
90                                })
91                                .collect(),
92                        );
93                        imports.push(Spanned::new(sol_import, loc));
94                    }
95
96                    ast::ItemKind::Contract(contract) => {
97                        if contract.kind.is_library() {
98                            libraries.push(SolLibrary { is_inlined: library_is_inlined(contract) });
99                        }
100                        contract_names.push(contract.name.to_string());
101                    }
102
103                    _ => {}
104                }
105            }
106        });
107        if let Err(e) = result {
108            let e = e.to_string();
109            trace!("failed parsing {file:?}: {e}");
110            parse_result = Err(e);
111
112            if version.is_none() {
113                version = utils::capture_outer_and_inner(
114                    content,
115                    &utils::RE_SOL_PRAGMA_VERSION,
116                    &["version"],
117                )
118                .first()
119                .map(|(cap, name)| Spanned::new(name.as_str().to_owned(), cap.range()));
120            }
121            if imports.is_empty() {
122                imports = capture_imports(content);
123            }
124            if contract_names.is_empty() {
125                utils::RE_CONTRACT_NAMES.captures_iter(content).for_each(|cap| {
126                    contract_names.push(cap[1].to_owned());
127                });
128            }
129        }
130        let license = content.lines().next().and_then(|line| {
131            utils::capture_outer_and_inner(
132                line,
133                &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER,
134                &["license"],
135            )
136            .first()
137            .map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range()))
138        });
139        let version_req = version.as_ref().and_then(|v| Self::parse_version_req(v.data()).ok());
140
141        Self {
142            version_req,
143            version,
144            experimental,
145            imports,
146            license,
147            libraries,
148            contract_names,
149            is_yul,
150            parse_result,
151        }
152    }
153
154    /// Parses the version pragma and returns the corresponding SemVer version requirement.
155    ///
156    /// See [`parse_version_req`](Self::parse_version_req).
157    pub fn parse_version_pragma(pragma: &str) -> Option<Result<VersionReq, semver::Error>> {
158        let version = utils::find_version_pragma(pragma)?.as_str();
159        Some(Self::parse_version_req(version))
160    }
161
162    /// Returns the corresponding SemVer version requirement for the solidity version.
163    ///
164    /// Note: This is a workaround for the fact that `VersionReq::parse` does not support whitespace
165    /// separators and requires comma separated operators. See [VersionReq].
166    pub fn parse_version_req(version: &str) -> Result<VersionReq, semver::Error> {
167        let version = version.replace(' ', ",");
168
169        // Somehow, Solidity semver without an operator is considered to be "exact",
170        // but lack of operator automatically marks the operator as Caret, so we need
171        // to manually patch it? :shrug:
172        let exact = !matches!(&version[0..1], "*" | "^" | "=" | ">" | "<" | "~");
173        let mut version = VersionReq::parse(&version)?;
174        if exact {
175            version.comparators[0].op = semver::Op::Exact;
176        }
177
178        Ok(version)
179    }
180}
181
182#[derive(Clone, Debug)]
183pub struct SolImport {
184    path: PathBuf,
185    aliases: Vec<SolImportAlias>,
186}
187
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub enum SolImportAlias {
190    File(String),
191    Contract(String, String),
192}
193
194impl SolImport {
195    pub fn new(path: PathBuf) -> Self {
196        Self { path, aliases: vec![] }
197    }
198
199    pub fn path(&self) -> &Path {
200        &self.path
201    }
202
203    pub fn aliases(&self) -> &[SolImportAlias] {
204        &self.aliases
205    }
206
207    fn set_aliases(mut self, aliases: Vec<SolImportAlias>) -> Self {
208        self.aliases = aliases;
209        self
210    }
211}
212
213/// Minimal representation of a contract inside a solidity file
214#[derive(Clone, Debug)]
215pub struct SolLibrary {
216    pub is_inlined: bool,
217}
218
219impl SolLibrary {
220    /// Returns `true` if all functions of this library will be inlined.
221    ///
222    /// This checks if all functions are either internal or private, because internal functions can
223    /// only be accessed from within the current contract or contracts deriving from it. They cannot
224    /// be accessed externally. Since they are not exposed to the outside through the contract’s
225    /// ABI, they can take parameters of internal types like mappings or storage references.
226    ///
227    /// See also <https://docs.soliditylang.org/en/latest/contracts.html#libraries>
228    pub fn is_inlined(&self) -> bool {
229        self.is_inlined
230    }
231}
232
233/// A spanned item.
234#[derive(Clone, Debug)]
235pub struct Spanned<T> {
236    /// The byte range of `data` in the file.
237    pub span: Range<usize>,
238    /// The data of the item.
239    pub data: T,
240}
241
242impl<T> Spanned<T> {
243    /// Creates a new data unit with the given data and location.
244    pub fn new(data: T, span: Range<usize>) -> Self {
245        Self { data, span }
246    }
247
248    /// Returns the underlying data.
249    pub fn data(&self) -> &T {
250        &self.data
251    }
252
253    /// Returns the location.
254    pub fn span(&self) -> Range<usize> {
255        self.span.clone()
256    }
257
258    /// Returns the location adjusted by an offset.
259    ///
260    /// Used to determine new position of the unit within the file after content manipulation.
261    pub fn loc_by_offset(&self, offset: isize) -> Range<usize> {
262        utils::range_by_offset(&self.span, offset)
263    }
264}
265
266fn library_is_inlined(contract: &ast::ItemContract<'_>) -> bool {
267    contract
268        .body
269        .iter()
270        .filter_map(|item| match &item.kind {
271            ast::ItemKind::Function(f) => Some(f),
272            _ => None,
273        })
274        .all(|f| {
275            !matches!(
276                f.header.visibility.map(|v| *v),
277                Some(ast::Visibility::Public | ast::Visibility::External)
278            )
279        })
280}
281
282/// Capture the import statement information together with aliases
283pub fn capture_imports(content: &str) -> Vec<Spanned<SolImport>> {
284    let mut imports = vec![];
285    for cap in utils::RE_SOL_IMPORT.captures_iter(content) {
286        if let Some(name_match) = ["p1", "p2", "p3", "p4"].iter().find_map(|name| cap.name(name)) {
287            let statement_match = cap.get(0).unwrap();
288            let mut aliases = vec![];
289            for alias_cap in utils::RE_SOL_IMPORT_ALIAS.captures_iter(statement_match.as_str()) {
290                if let Some(alias) = alias_cap.name("alias") {
291                    let alias = alias.as_str().to_owned();
292                    let import_alias = match alias_cap.name("target") {
293                        Some(target) => SolImportAlias::Contract(alias, target.as_str().to_owned()),
294                        None => SolImportAlias::File(alias),
295                    };
296                    aliases.push(import_alias);
297                }
298            }
299            let sol_import =
300                SolImport::new(PathBuf::from(name_match.as_str())).set_aliases(aliases);
301            imports.push(Spanned::new(sol_import, statement_match.range()));
302        }
303    }
304    imports
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[track_caller]
312    fn assert_version(version_req: Option<&str>, src: &str) {
313        let data = SolData::parse(src, "test.sol".as_ref());
314        assert_eq!(data.version_req, version_req.map(|v| v.parse().unwrap()), "src:\n{src}");
315    }
316
317    #[track_caller]
318    fn assert_contract_names(names: &[&str], src: &str) {
319        let data = SolData::parse(src, "test.sol".as_ref());
320        assert_eq!(data.contract_names, names, "src:\n{src}");
321    }
322
323    #[test]
324    fn soldata_parsing() {
325        assert_version(None, "");
326        assert_version(None, "contract C { }");
327
328        // https://github.com/foundry-rs/foundry/issues/9349
329        assert_version(
330            Some(">=0.4.22, <0.6"),
331            r#"
332pragma solidity >=0.4.22 <0.6;
333
334contract BugReport {
335    function() external payable {
336        deposit();
337    }
338    function deposit() public payable {}
339}
340        "#,
341        );
342
343        assert_contract_names(
344            &["A", "B69$_", "C_", "$D"],
345            r#"
346    contract A {}
347library B69$_ {}
348abstract contract C_ {} interface $D {}
349
350uint constant x = .1e10;
351uint constant y = .1 ether;
352        "#,
353        );
354    }
355
356    #[test]
357    fn can_capture_curly_imports() {
358        let content = r#"
359import { T } from "../Test.sol";
360import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
361import {DsTest} from "ds-test/test.sol";
362"#;
363
364        let captured_imports =
365            capture_imports(content).into_iter().map(|s| s.data.path).collect::<Vec<_>>();
366
367        let expected =
368            utils::find_import_paths(content).map(|m| m.as_str().into()).collect::<Vec<PathBuf>>();
369
370        assert_eq!(captured_imports, expected);
371
372        assert_eq!(
373            captured_imports,
374            vec![
375                PathBuf::from("../Test.sol"),
376                "@openzeppelin/contracts/utils/ReentrancyGuard.sol".into(),
377                "ds-test/test.sol".into(),
378            ],
379        );
380    }
381
382    #[test]
383    fn cap_capture_aliases() {
384        let content = r#"
385import * as T from "./Test.sol";
386import { DsTest as Test } from "ds-test/test.sol";
387import "ds-test/test.sol" as Test;
388import { FloatMath as Math, Math as FloatMath } from "./Math.sol";
389"#;
390
391        let caputred_imports =
392            capture_imports(content).into_iter().map(|s| s.data.aliases).collect::<Vec<_>>();
393        assert_eq!(
394            caputred_imports,
395            vec![
396                vec![SolImportAlias::File("T".into())],
397                vec![SolImportAlias::Contract("Test".into(), "DsTest".into())],
398                vec![SolImportAlias::File("Test".into())],
399                vec![
400                    SolImportAlias::Contract("Math".into(), "FloatMath".into()),
401                    SolImportAlias::Contract("FloatMath".into(), "Math".into()),
402                ],
403            ]
404        );
405    }
406}