foundry_compilers/compilers/vyper/
parser.rs1use 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 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 let mut candidate_dirs = Vec::new();
97
98 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 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
164fn 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
177fn 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}