foundry_compilers/resolver/
parse.rs

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