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