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