1use foundry_compilers_core::utils;
2use semver::VersionReq;
3use solar::{
4 parse::{ast, interface::sym},
5 sema::interface,
6};
7use std::{
8 ops::Range,
9 path::{Path, PathBuf},
10};
11
12#[derive(derive_more::Debug)]
23pub struct SolParser {
24 #[debug(ignore)]
25 pub(crate) compiler: solar::sema::Compiler,
26}
27
28impl Clone for SolParser {
29 fn clone(&self) -> Self {
30 Self {
31 compiler: solar::sema::Compiler::new(Self::session_with_opts(
32 self.compiler.sess().opts.clone(),
33 )),
34 }
35 }
36}
37
38impl SolParser {
39 pub fn compiler(&self) -> &solar::sema::Compiler {
41 &self.compiler
42 }
43
44 pub fn compiler_mut(&mut self) -> &mut solar::sema::Compiler {
46 &mut self.compiler
47 }
48
49 pub fn into_compiler(self) -> solar::sema::Compiler {
51 self.compiler
52 }
53
54 pub(crate) fn session_with_opts(
55 opts: solar::sema::interface::config::Opts,
56 ) -> solar::sema::interface::Session {
57 let sess = solar::sema::interface::Session::builder()
58 .with_buffer_emitter(Default::default())
59 .opts(opts)
60 .build();
61 sess.source_map().set_file_loader(FileLoader);
62 sess
63 }
64}
65
66struct FileLoader;
67impl interface::source_map::FileLoader for FileLoader {
68 fn canonicalize_path(&self, path: &Path) -> std::io::Result<PathBuf> {
69 interface::source_map::RealFileLoader.canonicalize_path(path)
70 }
71
72 fn load_stdin(&self) -> std::io::Result<String> {
73 interface::source_map::RealFileLoader.load_stdin()
74 }
75
76 fn load_file(&self, path: &Path) -> std::io::Result<String> {
77 interface::source_map::RealFileLoader.load_file(path).map(|s| {
78 if s.contains('\r') {
79 s.replace('\r', "")
80 } else {
81 s
82 }
83 })
84 }
85
86 fn load_binary_file(&self, path: &Path) -> std::io::Result<Vec<u8>> {
87 interface::source_map::RealFileLoader.load_binary_file(path)
88 }
89}
90
91#[derive(Clone, Debug)]
93#[non_exhaustive]
94pub struct SolData {
95 pub license: Option<Spanned<String>>,
96 pub version: Option<Spanned<String>>,
97 pub experimental: Option<Spanned<String>>,
98 pub imports: Vec<Spanned<SolImport>>,
99 pub version_req: Option<VersionReq>,
100 pub libraries: Vec<SolLibrary>,
101 pub contract_names: Vec<String>,
102 pub is_yul: bool,
103 pub parse_result: Result<(), String>,
104}
105
106impl SolData {
107 pub fn parse_result(&self) -> crate::Result<()> {
109 self.parse_result.clone().map_err(crate::SolcError::ParseError)
110 }
111
112 #[allow(dead_code)]
113 pub fn fmt_version<W: std::fmt::Write>(
114 &self,
115 f: &mut W,
116 ) -> std::result::Result<(), std::fmt::Error> {
117 if let Some(version) = &self.version {
118 write!(f, "({})", version.data)?;
119 }
120 Ok(())
121 }
122
123 #[instrument(name = "SolData::parse", skip_all)]
128 pub fn parse(content: &str, file: &Path) -> Self {
129 match crate::parse_one_source(content, file, |sess, ast| {
130 SolDataBuilder::parse(content, file, Ok((sess, ast)))
131 }) {
132 Ok(data) => data,
133 Err(e) => {
134 let e = e.to_string();
135 trace!("failed parsing {file:?}: {e}");
136 SolDataBuilder::parse(content, file, Err(Some(e)))
137 }
138 }
139 }
140
141 pub(crate) fn parse_from(
142 sess: &solar::sema::interface::Session,
143 s: &solar::sema::Source<'_>,
144 ) -> Self {
145 let content = s.file.src.as_str();
146 let file = s.file.name.as_real().unwrap();
147 let ast = s.ast.as_ref().map(|ast| (sess, ast)).ok_or(None);
148 SolDataBuilder::parse(content, file, ast)
149 }
150
151 pub fn parse_version_pragma(pragma: &str) -> Option<Result<VersionReq, semver::Error>> {
155 let version = utils::find_version_pragma(pragma)?.as_str();
156 Some(Self::parse_version_req(version))
157 }
158
159 pub fn parse_version_req(version: &str) -> Result<VersionReq, semver::Error> {
164 let version = version.replace(' ', ",");
165
166 let exact = !matches!(version.get(..1), Some("*" | "^" | "=" | ">" | "<" | "~"));
170 let mut version = VersionReq::parse(&version)?;
171 if exact {
172 version.comparators[0].op = semver::Op::Exact;
173 }
174
175 Ok(version)
176 }
177}
178
179#[derive(Default)]
180struct SolDataBuilder {
181 version: Option<Spanned<String>>,
182 experimental: Option<Spanned<String>>,
183 imports: Vec<Spanned<SolImport>>,
184 libraries: Vec<SolLibrary>,
185 contract_names: Vec<String>,
186 parse_err: Option<String>,
187}
188
189impl SolDataBuilder {
190 fn parse(
191 content: &str,
192 file: &Path,
193 ast: Result<
194 (&solar::sema::interface::Session, &solar::parse::ast::SourceUnit<'_>),
195 Option<String>,
196 >,
197 ) -> SolData {
198 let mut builder = Self::default();
199 match ast {
200 Ok((sess, ast)) => builder.parse_from_ast(sess, ast),
201 Err(err) => {
202 builder.parse_from_regex(content);
203 if let Some(err) = err {
204 builder.parse_err = Some(err);
205 }
206 }
207 }
208 builder.build(content, file)
209 }
210
211 fn parse_from_ast(
212 &mut self,
213 sess: &solar::sema::interface::Session,
214 ast: &solar::parse::ast::SourceUnit<'_>,
215 ) {
216 for item in ast.items.iter() {
217 let loc = sess.source_map().span_to_source(item.span).unwrap().1;
218 match &item.kind {
219 ast::ItemKind::Pragma(pragma) => match &pragma.tokens {
220 ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => {
221 self.version = Some(Spanned::new(req.to_string(), loc));
222 }
223 ast::PragmaTokens::Custom(name, value) if name.as_str() == "experimental" => {
224 let value =
225 value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default();
226 self.experimental = Some(Spanned::new(value, loc));
227 }
228 _ => {}
229 },
230
231 ast::ItemKind::Import(import) => {
232 let path = import.path.value.to_string();
233 let aliases = match &import.items {
234 ast::ImportItems::Plain(None) => &[][..],
235 ast::ImportItems::Plain(Some(alias)) | ast::ImportItems::Glob(alias) => {
236 &[(*alias, None)][..]
237 }
238 ast::ImportItems::Aliases(aliases) => aliases,
239 };
240 let sol_import = SolImport::new(PathBuf::from(path)).set_aliases(
241 aliases
242 .iter()
243 .map(|(id, alias)| match alias {
244 Some(al) => SolImportAlias::Contract(
245 al.name.to_string(),
246 id.name.to_string(),
247 ),
248 None => SolImportAlias::File(id.name.to_string()),
249 })
250 .collect(),
251 );
252 self.imports.push(Spanned::new(sol_import, loc));
253 }
254
255 ast::ItemKind::Contract(contract) => {
256 if contract.kind.is_library() {
257 self.libraries
258 .push(SolLibrary { is_inlined: library_is_inlined(contract) });
259 }
260 self.contract_names.push(contract.name.to_string());
261 }
262
263 _ => {}
264 }
265 }
266 }
267
268 fn parse_from_regex(&mut self, content: &str) {
269 if self.version.is_none() {
270 self.version = utils::capture_outer_and_inner(
271 content,
272 &utils::RE_SOL_PRAGMA_VERSION,
273 &["version"],
274 )
275 .first()
276 .map(|(cap, name)| Spanned::new(name.as_str().to_owned(), cap.range()));
277 }
278 if self.imports.is_empty() {
279 self.imports = capture_imports(content);
280 }
281 if self.contract_names.is_empty() {
282 utils::RE_CONTRACT_NAMES.captures_iter(content).for_each(|cap| {
283 self.contract_names.push(cap[1].to_owned());
284 });
285 }
286 }
287
288 fn build(self, content: &str, file: &Path) -> SolData {
289 let Self { version, experimental, imports, libraries, contract_names, parse_err } = self;
290 let license = content.lines().next().and_then(|line| {
291 utils::capture_outer_and_inner(
292 line,
293 &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER,
294 &["license"],
295 )
296 .first()
297 .map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range()))
298 });
299 let version_req = version.as_ref().and_then(|v| SolData::parse_version_req(v.data()).ok());
300 SolData {
301 license,
302 version,
303 experimental,
304 imports,
305 version_req,
306 libraries,
307 contract_names,
308 is_yul: file.extension().is_some_and(|ext| ext == "yul"),
309 parse_result: parse_err.map(Err).unwrap_or(Ok(())),
310 }
311 }
312}
313
314#[derive(Clone, Debug)]
315pub struct SolImport {
316 path: PathBuf,
317 aliases: Vec<SolImportAlias>,
318}
319
320#[derive(Clone, Debug, PartialEq, Eq)]
321pub enum SolImportAlias {
322 File(String),
323 Contract(String, String),
324}
325
326impl SolImport {
327 pub fn new(path: PathBuf) -> Self {
328 Self { path, aliases: vec![] }
329 }
330
331 pub fn path(&self) -> &Path {
332 &self.path
333 }
334
335 pub fn aliases(&self) -> &[SolImportAlias] {
336 &self.aliases
337 }
338
339 fn set_aliases(mut self, aliases: Vec<SolImportAlias>) -> Self {
340 self.aliases = aliases;
341 self
342 }
343}
344
345#[derive(Clone, Debug)]
347pub struct SolLibrary {
348 pub is_inlined: bool,
349}
350
351impl SolLibrary {
352 pub fn is_inlined(&self) -> bool {
361 self.is_inlined
362 }
363}
364
365#[derive(Clone, Debug)]
367pub struct Spanned<T> {
368 pub span: Range<usize>,
370 pub data: T,
372}
373
374impl<T> Spanned<T> {
375 pub fn new(data: T, span: Range<usize>) -> Self {
377 Self { data, span }
378 }
379
380 pub fn data(&self) -> &T {
382 &self.data
383 }
384
385 pub fn span(&self) -> Range<usize> {
387 self.span.clone()
388 }
389
390 pub fn loc_by_offset(&self, offset: isize) -> Range<usize> {
394 utils::range_by_offset(&self.span, offset)
395 }
396}
397
398fn library_is_inlined(contract: &ast::ItemContract<'_>) -> bool {
399 contract
400 .body
401 .iter()
402 .filter_map(|item| match &item.kind {
403 ast::ItemKind::Function(f) => Some(f),
404 _ => None,
405 })
406 .all(|f| {
407 !matches!(
408 f.header.visibility.map(|v| *v),
409 Some(ast::Visibility::Public | ast::Visibility::External)
410 )
411 })
412}
413
414pub fn capture_imports(content: &str) -> Vec<Spanned<SolImport>> {
416 let mut imports = vec![];
417 for cap in utils::RE_SOL_IMPORT.captures_iter(content) {
418 if let Some(name_match) = ["p1", "p2", "p3", "p4"].iter().find_map(|name| cap.name(name)) {
419 let statement_match = cap.get(0).unwrap();
420 let mut aliases = vec![];
421 for alias_cap in utils::RE_SOL_IMPORT_ALIAS.captures_iter(statement_match.as_str()) {
422 if let Some(alias) = alias_cap.name("alias") {
423 let alias = alias.as_str().to_owned();
424 let import_alias = match alias_cap.name("target") {
425 Some(target) => SolImportAlias::Contract(alias, target.as_str().to_owned()),
426 None => SolImportAlias::File(alias),
427 };
428 aliases.push(import_alias);
429 }
430 }
431 let sol_import =
432 SolImport::new(PathBuf::from(name_match.as_str())).set_aliases(aliases);
433 imports.push(Spanned::new(sol_import, statement_match.range()));
434 }
435 }
436 imports
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[track_caller]
444 fn assert_version(version_req: Option<&str>, src: &str) {
445 let data = SolData::parse(src, "test.sol".as_ref());
446 assert_eq!(data.version_req, version_req.map(|v| v.parse().unwrap()), "src:\n{src}");
447 }
448
449 #[track_caller]
450 fn assert_contract_names(names: &[&str], src: &str) {
451 let data = SolData::parse(src, "test.sol".as_ref());
452 assert_eq!(data.contract_names, names, "src:\n{src}");
453 }
454
455 #[test]
456 fn soldata_parsing() {
457 assert_version(None, "");
458 assert_version(None, "contract C { }");
459
460 assert_version(
462 Some(">=0.4.22, <0.6"),
463 r#"
464pragma solidity >=0.4.22 <0.6;
465
466contract BugReport {
467 function() external payable {
468 deposit();
469 }
470 function deposit() public payable {}
471}
472 "#,
473 );
474
475 assert_contract_names(
476 &["A", "B69$_", "C_", "$D"],
477 r#"
478 contract A {}
479library B69$_ {}
480abstract contract C_ {} interface $D {}
481
482uint constant x = .1e10;
483uint constant y = .1 ether;
484 "#,
485 );
486 }
487
488 #[test]
489 fn can_capture_curly_imports() {
490 let content = r#"
491import { T } from "../Test.sol";
492import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
493import {DsTest} from "ds-test/test.sol";
494"#;
495
496 let captured_imports =
497 capture_imports(content).into_iter().map(|s| s.data.path).collect::<Vec<_>>();
498
499 let expected =
500 utils::find_import_paths(content).map(|m| m.as_str().into()).collect::<Vec<PathBuf>>();
501
502 assert_eq!(captured_imports, expected);
503
504 assert_eq!(
505 captured_imports,
506 vec![
507 PathBuf::from("../Test.sol"),
508 "@openzeppelin/contracts/utils/ReentrancyGuard.sol".into(),
509 "ds-test/test.sol".into(),
510 ],
511 );
512 }
513
514 #[test]
515 fn cap_capture_aliases() {
516 let content = r#"
517import * as T from "./Test.sol";
518import { DsTest as Test } from "ds-test/test.sol";
519import "ds-test/test.sol" as Test;
520import { FloatMath as Math, Math as FloatMath } from "./Math.sol";
521"#;
522
523 let caputred_imports =
524 capture_imports(content).into_iter().map(|s| s.data.aliases).collect::<Vec<_>>();
525 assert_eq!(
526 caputred_imports,
527 vec![
528 vec![SolImportAlias::File("T".into())],
529 vec![SolImportAlias::Contract("Test".into(), "DsTest".into())],
530 vec![SolImportAlias::File("Test".into())],
531 vec![
532 SolImportAlias::Contract("Math".into(), "FloatMath".into()),
533 SolImportAlias::Contract("FloatMath".into(), "Math".into()),
534 ],
535 ]
536 );
537 }
538}