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 #[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 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 let mut candidate_dirs = Vec::new();
84
85 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 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
151fn 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
164fn 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}