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