foundry_compilers/resolver/
parse.rs1use foundry_compilers_core::utils;
2use semver::VersionReq;
3use solar_parse::{ast, interface::sym};
4use std::{
5 ops::Range,
6 path::{Path, PathBuf},
7};
8
9#[derive(Clone, Debug)]
11pub struct SolData {
12 pub license: Option<Spanned<String>>,
13 pub version: Option<Spanned<String>>,
14 pub experimental: Option<Spanned<String>>,
15 pub imports: Vec<Spanned<SolImport>>,
16 pub version_req: Option<VersionReq>,
17 pub libraries: Vec<SolLibrary>,
18 pub contract_names: Vec<String>,
19 pub is_yul: bool,
20 pub parse_result: Result<(), String>,
21}
22
23impl SolData {
24 pub fn parse_result(&self) -> crate::Result<()> {
26 self.parse_result.clone().map_err(crate::SolcError::ParseError)
27 }
28
29 #[allow(dead_code)]
30 pub fn fmt_version<W: std::fmt::Write>(
31 &self,
32 f: &mut W,
33 ) -> std::result::Result<(), std::fmt::Error> {
34 if let Some(version) = &self.version {
35 write!(f, "({})", version.data)?;
36 }
37 Ok(())
38 }
39
40 pub fn parse(content: &str, file: &Path) -> Self {
45 let is_yul = file.extension().is_some_and(|ext| ext == "yul");
46 let mut version = None;
47 let mut experimental = None;
48 let mut imports = Vec::<Spanned<SolImport>>::new();
49 let mut libraries = Vec::new();
50 let mut contract_names = Vec::new();
51 let mut parse_result = Ok(());
52
53 let sess = solar_parse::interface::Session::builder()
54 .with_buffer_emitter(Default::default())
55 .build();
56 sess.enter(|| {
57 let arena = ast::Arena::new();
58 let filename = solar_parse::interface::source_map::FileName::Real(file.to_path_buf());
59 let Ok(mut parser) =
60 solar_parse::Parser::from_source_code(&sess, &arena, filename, content.to_string())
61 else {
62 return;
63 };
64 let Ok(ast) = parser.parse_file().map_err(|e| e.emit()) else { return };
65 for item in ast.items {
66 let loc = item.span.lo().to_usize()..item.span.hi().to_usize();
67 match &item.kind {
68 ast::ItemKind::Pragma(pragma) => match &pragma.tokens {
69 ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => {
70 version = Some(Spanned::new(req.to_string(), loc));
71 }
72 ast::PragmaTokens::Custom(name, value)
73 if name.as_str() == "experimental" =>
74 {
75 let value =
76 value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default();
77 experimental = Some(Spanned::new(value, loc));
78 }
79 _ => {}
80 },
81
82 ast::ItemKind::Import(import) => {
83 let path = import.path.value.to_string();
84 let aliases = match &import.items {
85 ast::ImportItems::Plain(None) | ast::ImportItems::Glob(None) => &[][..],
86 ast::ImportItems::Plain(Some(alias))
87 | ast::ImportItems::Glob(Some(alias)) => &[(*alias, None)][..],
88 ast::ImportItems::Aliases(aliases) => aliases,
89 };
90 let sol_import = SolImport::new(PathBuf::from(path)).set_aliases(
91 aliases
92 .iter()
93 .map(|(id, alias)| match alias {
94 Some(al) => SolImportAlias::Contract(
95 al.name.to_string(),
96 id.name.to_string(),
97 ),
98 None => SolImportAlias::File(id.name.to_string()),
99 })
100 .collect(),
101 );
102 imports.push(Spanned::new(sol_import, loc));
103 }
104
105 ast::ItemKind::Contract(contract) => {
106 if contract.kind.is_library() {
107 libraries.push(SolLibrary { is_inlined: library_is_inlined(contract) });
108 }
109 contract_names.push(contract.name.to_string());
110 }
111
112 _ => {}
113 }
114 }
115 });
116 if let Err(e) = sess.emitted_errors().unwrap() {
117 let e = e.to_string();
118 trace!("failed parsing {file:?}: {e}");
119 parse_result = Err(e);
120
121 if version.is_none() {
122 version = utils::capture_outer_and_inner(
123 content,
124 &utils::RE_SOL_PRAGMA_VERSION,
125 &["version"],
126 )
127 .first()
128 .map(|(cap, name)| Spanned::new(name.as_str().to_owned(), cap.range()));
129 }
130 if imports.is_empty() {
131 imports = capture_imports(content);
132 }
133 if contract_names.is_empty() {
134 utils::RE_CONTRACT_NAMES.captures_iter(content).for_each(|cap| {
135 contract_names.push(cap[1].to_owned());
136 });
137 }
138 }
139 let license = content.lines().next().and_then(|line| {
140 utils::capture_outer_and_inner(
141 line,
142 &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER,
143 &["license"],
144 )
145 .first()
146 .map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range()))
147 });
148 let version_req = version.as_ref().and_then(|v| Self::parse_version_req(v.data()).ok());
149
150 Self {
151 version_req,
152 version,
153 experimental,
154 imports,
155 license,
156 libraries,
157 contract_names,
158 is_yul,
159 parse_result,
160 }
161 }
162
163 pub fn parse_version_req(version: &str) -> Result<VersionReq, semver::Error> {
168 let version = version.replace(' ', ",");
169
170 let exact = !matches!(&version[0..1], "*" | "^" | "=" | ">" | "<" | "~");
174 let mut version = VersionReq::parse(&version)?;
175 if exact {
176 version.comparators[0].op = semver::Op::Exact;
177 }
178
179 Ok(version)
180 }
181}
182
183#[derive(Clone, Debug)]
184pub struct SolImport {
185 path: PathBuf,
186 aliases: Vec<SolImportAlias>,
187}
188
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub enum SolImportAlias {
191 File(String),
192 Contract(String, String),
193}
194
195impl SolImport {
196 pub fn new(path: PathBuf) -> Self {
197 Self { path, aliases: vec![] }
198 }
199
200 pub fn path(&self) -> &PathBuf {
201 &self.path
202 }
203
204 pub fn aliases(&self) -> &Vec<SolImportAlias> {
205 &self.aliases
206 }
207
208 fn set_aliases(mut self, aliases: Vec<SolImportAlias>) -> Self {
209 self.aliases = aliases;
210 self
211 }
212}
213
214#[derive(Clone, Debug)]
216pub struct SolLibrary {
217 pub is_inlined: bool,
218}
219
220impl SolLibrary {
221 pub fn is_inlined(&self) -> bool {
230 self.is_inlined
231 }
232}
233
234#[derive(Clone, Debug)]
236pub struct Spanned<T> {
237 pub span: Range<usize>,
239 pub data: T,
241}
242
243impl<T> Spanned<T> {
244 pub fn new(data: T, span: Range<usize>) -> Self {
246 Self { data, span }
247 }
248
249 pub fn data(&self) -> &T {
251 &self.data
252 }
253
254 pub fn span(&self) -> Range<usize> {
256 self.span.clone()
257 }
258
259 pub fn loc_by_offset(&self, offset: isize) -> Range<usize> {
263 utils::range_by_offset(&self.span, offset)
264 }
265}
266
267fn library_is_inlined(contract: &ast::ItemContract<'_>) -> bool {
268 contract
269 .body
270 .iter()
271 .filter_map(|item| match &item.kind {
272 ast::ItemKind::Function(f) => Some(f),
273 _ => None,
274 })
275 .all(|f| {
276 !matches!(
277 f.header.visibility,
278 Some(ast::Visibility::Public | ast::Visibility::External)
279 )
280 })
281}
282
283pub fn capture_imports(content: &str) -> Vec<Spanned<SolImport>> {
285 let mut imports = vec![];
286 for cap in utils::RE_SOL_IMPORT.captures_iter(content) {
287 if let Some(name_match) = ["p1", "p2", "p3", "p4"].iter().find_map(|name| cap.name(name)) {
288 let statement_match = cap.get(0).unwrap();
289 let mut aliases = vec![];
290 for alias_cap in utils::RE_SOL_IMPORT_ALIAS.captures_iter(statement_match.as_str()) {
291 if let Some(alias) = alias_cap.name("alias") {
292 let alias = alias.as_str().to_owned();
293 let import_alias = match alias_cap.name("target") {
294 Some(target) => SolImportAlias::Contract(alias, target.as_str().to_owned()),
295 None => SolImportAlias::File(alias),
296 };
297 aliases.push(import_alias);
298 }
299 }
300 let sol_import =
301 SolImport::new(PathBuf::from(name_match.as_str())).set_aliases(aliases);
302 imports.push(Spanned::new(sol_import, statement_match.range()));
303 }
304 }
305 imports
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[track_caller]
313 fn assert_version(version_req: Option<&str>, src: &str) {
314 let data = SolData::parse(src, "test.sol".as_ref());
315 assert_eq!(data.version_req, version_req.map(|v| v.parse().unwrap()), "src:\n{src}");
316 }
317
318 #[track_caller]
319 fn assert_contract_names(names: &[&str], src: &str) {
320 let data = SolData::parse(src, "test.sol".as_ref());
321 assert_eq!(data.contract_names, names, "src:\n{src}");
322 }
323
324 #[test]
325 fn soldata_parsing() {
326 assert_version(None, "");
327 assert_version(None, "contract C { }");
328
329 assert_version(
331 Some(">=0.4.22, <0.6"),
332 r#"
333pragma solidity >=0.4.22 <0.6;
334
335contract BugReport {
336 function() external payable {
337 deposit();
338 }
339 function deposit() public payable {}
340}
341 "#,
342 );
343
344 assert_contract_names(
345 &["A", "B69$_", "C_", "$D"],
346 r#"
347 contract A {}
348library B69$_ {}
349abstract contract C_ {} interface $D {}
350
351uint constant x = .1e10;
352uint constant y = .1 ether;
353 "#,
354 );
355 }
356
357 #[test]
358 fn can_capture_curly_imports() {
359 let content = r#"
360import { T } from "../Test.sol";
361import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
362import {DsTest} from "ds-test/test.sol";
363"#;
364
365 let captured_imports =
366 capture_imports(content).into_iter().map(|s| s.data.path).collect::<Vec<_>>();
367
368 let expected =
369 utils::find_import_paths(content).map(|m| m.as_str().into()).collect::<Vec<PathBuf>>();
370
371 assert_eq!(captured_imports, expected);
372
373 assert_eq!(
374 captured_imports,
375 vec![
376 PathBuf::from("../Test.sol"),
377 "@openzeppelin/contracts/utils/ReentrancyGuard.sol".into(),
378 "ds-test/test.sol".into(),
379 ],
380 );
381 }
382
383 #[test]
384 fn cap_capture_aliases() {
385 let content = r#"
386import * as T from "./Test.sol";
387import { DsTest as Test } from "ds-test/test.sol";
388import "ds-test/test.sol" as Test;
389import { FloatMath as Math, Math as FloatMath } from "./Math.sol";
390"#;
391
392 let caputred_imports =
393 capture_imports(content).into_iter().map(|s| s.data.aliases).collect::<Vec<_>>();
394 assert_eq!(
395 caputred_imports,
396 vec![
397 vec![SolImportAlias::File("T".into())],
398 vec![SolImportAlias::Contract("Test".into(), "DsTest".into())],
399 vec![SolImportAlias::File("Test".into())],
400 vec![
401 SolImportAlias::Contract("Math".into(), "FloatMath".into()),
402 SolImportAlias::Contract("FloatMath".into(), "Math".into()),
403 ],
404 ]
405 );
406 }
407}