Skip to main content

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