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