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