foundry_compilers/compilers/vyper/
parser.rs

1use super::VyperLanguage;
2use crate::{
3    compilers::{vyper::VYPER_EXTENSIONS, ParsedSource},
4    ProjectPathsConfig,
5};
6use foundry_compilers_core::{
7    error::{Result, SolcError},
8    utils::{capture_outer_and_inner, RE_VYPER_VERSION},
9};
10use semver::VersionReq;
11use std::{
12    collections::BTreeSet,
13    path::{Path, PathBuf},
14};
15use winnow::{
16    ascii::space1,
17    combinator::{alt, opt, preceded},
18    token::{take_till, take_while},
19    ModalResult, Parser,
20};
21
22#[derive(Clone, Debug, PartialEq)]
23pub struct VyperImport {
24    pub level: usize,
25    pub path: Option<String>,
26    pub final_part: Option<String>,
27}
28
29#[derive(Clone, Debug)]
30pub struct VyperParsedSource {
31    path: PathBuf,
32    version_req: Option<VersionReq>,
33    imports: Vec<VyperImport>,
34}
35
36impl ParsedSource for VyperParsedSource {
37    type Language = VyperLanguage;
38
39    #[instrument(name = "VyperParsedSource::parse", skip_all)]
40    fn parse(content: &str, file: &Path) -> Result<Self> {
41        let version_req = capture_outer_and_inner(content, &RE_VYPER_VERSION, &["version"])
42            .first()
43            .and_then(|(cap, _)| VersionReq::parse(cap.as_str()).ok());
44
45        let imports = parse_imports(content);
46
47        let path = file.to_path_buf();
48
49        Ok(Self { path, version_req, imports })
50    }
51
52    fn version_req(&self) -> Option<&VersionReq> {
53        self.version_req.as_ref()
54    }
55
56    fn contract_names(&self) -> &[String] {
57        &[]
58    }
59
60    fn language(&self) -> Self::Language {
61        VyperLanguage
62    }
63
64    fn resolve_imports<C>(
65        &self,
66        paths: &ProjectPathsConfig<C>,
67        include_paths: &mut BTreeSet<PathBuf>,
68    ) -> Result<Vec<PathBuf>> {
69        let mut imports = Vec::new();
70        'outer: for import in &self.imports {
71            // skip built-in imports
72            if import.level == 0
73                && import
74                    .path
75                    .as_ref()
76                    .map(|path| path.starts_with("vyper.") || path.starts_with("ethereum.ercs"))
77                    .unwrap_or_default()
78            {
79                continue;
80            }
81
82            // Potential locations of imported source.
83            let mut candidate_dirs = Vec::new();
84
85            // For relative imports, vyper always checks only directory containing contract which
86            // includes given import.
87            if import.level > 0 {
88                let mut candidate_dir = Some(self.path.as_path());
89
90                for _ in 0..import.level {
91                    candidate_dir = candidate_dir.and_then(|dir| dir.parent());
92                }
93
94                let candidate_dir = candidate_dir.ok_or_else(|| {
95                    SolcError::msg(format!(
96                        "Could not go {} levels up for import at {}",
97                        import.level,
98                        self.path.display()
99                    ))
100                })?;
101
102                candidate_dirs.push(candidate_dir);
103            } else {
104                // For absolute imports, Vyper firstly checks current directory, and then root.
105                if let Some(parent) = self.path.parent() {
106                    candidate_dirs.push(parent);
107                }
108                candidate_dirs.push(paths.root.as_path());
109            }
110
111            candidate_dirs.extend(paths.libraries.iter().map(PathBuf::as_path));
112
113            let import_path = {
114                let mut path = PathBuf::new();
115
116                if let Some(import_path) = &import.path {
117                    path = path.join(import_path.replace('.', "/"));
118                }
119
120                if let Some(part) = &import.final_part {
121                    path = path.join(part);
122                }
123
124                path
125            };
126
127            for candidate_dir in candidate_dirs {
128                let candidate = candidate_dir.join(&import_path);
129                for extension in VYPER_EXTENSIONS {
130                    let candidate = candidate.clone().with_extension(extension);
131                    trace!("trying {}", candidate.display());
132                    if candidate.exists() {
133                        imports.push(candidate);
134                        include_paths.insert(candidate_dir.to_path_buf());
135                        continue 'outer;
136                    }
137                }
138            }
139
140            return Err(SolcError::msg(format!(
141                "failed to resolve import {}{} at {}",
142                ".".repeat(import.level),
143                import_path.display(),
144                self.path.display()
145            )));
146        }
147        Ok(imports)
148    }
149}
150
151/// Parses given source trying to find all import directives.
152fn parse_imports(content: &str) -> Vec<VyperImport> {
153    let mut imports = Vec::new();
154
155    for mut line in content.split('\n') {
156        if let Ok(parts) = parse_import(&mut line) {
157            imports.push(parts);
158        }
159    }
160
161    imports
162}
163
164/// Parses given input, trying to find (import|from) part1.part2.part3 (import part4)?
165fn parse_import(input: &mut &str) -> ModalResult<VyperImport> {
166    (
167        preceded(
168            (alt(["from", "import"]), space1),
169            (take_while(0.., |c| c == '.'), take_till(0.., [' '])),
170        ),
171        opt(preceded((space1, "import", space1), take_till(0.., [' ']))),
172    )
173        .parse_next(input)
174        .map(|((dots, path), last)| VyperImport {
175            level: dots.len(),
176            path: (!path.is_empty()).then(|| path.to_string()),
177            final_part: last.map(|p| p.to_string()),
178        })
179}
180
181#[cfg(test)]
182mod tests {
183    use super::{parse_import, VyperImport};
184    use winnow::Parser;
185
186    #[test]
187    fn can_parse_import() {
188        assert_eq!(
189            parse_import.parse("import one.two.three").unwrap(),
190            VyperImport { level: 0, path: Some("one.two.three".to_string()), final_part: None }
191        );
192        assert_eq!(
193            parse_import.parse("from one.two.three import four").unwrap(),
194            VyperImport {
195                level: 0,
196                path: Some("one.two.three".to_string()),
197                final_part: Some("four".to_string()),
198            }
199        );
200        assert_eq!(
201            parse_import.parse("from one import two").unwrap(),
202            VyperImport {
203                level: 0,
204                path: Some("one".to_string()),
205                final_part: Some("two".to_string()),
206            }
207        );
208        assert_eq!(
209            parse_import.parse("import one").unwrap(),
210            VyperImport { level: 0, path: Some("one".to_string()), final_part: None }
211        );
212        assert_eq!(
213            parse_import.parse("from . import one").unwrap(),
214            VyperImport { level: 1, path: None, final_part: Some("one".to_string()) }
215        );
216        assert_eq!(
217            parse_import.parse("from ... import two").unwrap(),
218            VyperImport { level: 3, path: None, final_part: Some("two".to_string()) }
219        );
220        assert_eq!(
221            parse_import.parse("from ...one.two import three").unwrap(),
222            VyperImport {
223                level: 3,
224                path: Some("one.two".to_string()),
225                final_part: Some("three".to_string())
226            }
227        );
228    }
229}