foundry_compilers/compilers/vyper/
parser.rs1use 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 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 let mut candidate_dirs = Vec::new();
83
84 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 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
150fn 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
163fn 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}