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