foundry_compilers/compilers/vyper/
parser.rs

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