1use eyre::{Context, Result};
2use foundry_common::{compact_to_contract, strip_bytecode_placeholders};
3use foundry_compilers::{
4 Artifact, ProjectCompileOutput,
5 artifacts::{
6 Bytecode, ContractBytecodeSome, Libraries, Source,
7 sourcemap::{SourceElement, SourceMap},
8 },
9 multi::MultiCompilerLanguage,
10};
11use foundry_evm_core::ic::PcIcMap;
12use foundry_linking::Linker;
13use rayon::prelude::*;
14use solar::{ast, interface::SpannedOption};
15use std::{
16 collections::{BTreeMap, HashMap, HashSet},
17 fmt::Write,
18 ops::Range,
19 path::{Path, PathBuf},
20 sync::Arc,
21};
22
23#[derive(Clone, Debug)]
24pub struct SourceData {
25 pub source: Arc<String>,
26 pub language: MultiCompilerLanguage,
27 pub path: PathBuf,
28 pub contract_definitions: Vec<(String, Range<usize>)>,
31 pub debug_scopes: Vec<DebugSourceScope>,
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct DebugSourceScope {
37 pub contract_name: String,
38 pub function_name: String,
39 pub range: Range<usize>,
40 pub body_range: Range<usize>,
41 pub parameters_src: String,
42 pub returns_src: Option<String>,
43 pub parameters: Vec<DebugVariable>,
44 pub returns: Vec<DebugVariable>,
45 pub locals: Vec<DebugVariable>,
46}
47
48impl DebugSourceScope {
49 pub fn visible_locals(&self, offset: usize) -> impl Iterator<Item = &DebugVariable> {
50 self.locals.iter().filter(move |local| {
51 local.declaration.end <= offset
52 && offset >= local.scope.start
53 && offset <= local.scope.end
54 })
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct DebugVariable {
60 pub name: Option<String>,
61 pub declaration: Range<usize>,
62 pub scope: Range<usize>,
63}
64
65impl SourceData {
66 pub fn new(
67 output: &ProjectCompileOutput,
68 source: Arc<String>,
69 language: MultiCompilerLanguage,
70 path: PathBuf,
71 root: &Path,
72 ) -> Self {
73 let mut contract_definitions = Vec::new();
74 let mut debug_scopes = Vec::new();
75
76 match language {
77 MultiCompilerLanguage::Vyper(_) => {
78 if let Some(name) = path.file_stem().map(|s| s.to_string_lossy().to_string()) {
80 contract_definitions.push((name, 0..source.len()));
81 }
82 }
83 MultiCompilerLanguage::Solc(_) => {
84 let r = output.parser().solc().compiler().enter(|compiler| -> Option<()> {
85 let (_, source) = compiler.gcx().get_ast_source(root.join(&path))?;
86 let source_map = compiler.sess().source_map();
87 for item in source.ast.as_ref()?.items.iter() {
88 if let solar::ast::ItemKind::Contract(contract) = &item.kind {
89 let Some(contract_range) = source_map.span_to_range(item.span).ok()
90 else {
91 continue;
92 };
93 contract_definitions
94 .push((contract.name.to_string(), contract_range.clone()));
95 collect_contract_debug_scopes(
96 source_map,
97 contract,
98 contract_range,
99 &mut debug_scopes,
100 );
101 }
102 }
103 Some(())
104 });
105 if r.is_none() {
106 warn!("failed to parse contract definitions for {}", path.display());
107 }
108 }
109 }
110
111 Self { source, language, path, contract_definitions, debug_scopes }
112 }
113
114 pub fn find_contract_name(&self, start: usize, end: usize) -> Option<&str> {
116 self.contract_definitions
117 .iter()
118 .find(|(_, r)| start >= r.start && end <= r.end)
119 .map(|(name, _)| name.as_str())
120 }
121
122 pub fn find_debug_scope(&self, start: usize, end: usize) -> Option<&DebugSourceScope> {
124 self.debug_scopes
125 .iter()
126 .filter(|scope| start >= scope.range.start && end <= scope.range.end)
127 .min_by_key(|scope| scope.range.end.saturating_sub(scope.range.start))
128 }
129}
130
131fn collect_contract_debug_scopes(
132 source_map: &solar::interface::source_map::SourceMap,
133 contract: &ast::ItemContract<'_>,
134 contract_range: Range<usize>,
135 out: &mut Vec<DebugSourceScope>,
136) {
137 let mut scopes = Vec::new();
138 for item in contract.body.iter() {
139 let ast::ItemKind::Function(func) = &item.kind else { continue };
140 if !func.is_implemented() {
141 continue;
142 }
143
144 let Some(function_range) = span_to_range(source_map, item.span) else { continue };
145 let body_range =
146 span_to_range(source_map, func.body_span).unwrap_or_else(|| function_range.clone());
147 let function_name = function_name(func);
148 let parameters_src =
149 source_map.span_to_snippet(func.header.parameters.span).unwrap_or_default();
150 let returns_src = func
151 .header
152 .returns
153 .as_ref()
154 .and_then(|returns| source_map.span_to_snippet(returns.span).ok());
155
156 let mut locals = Vec::new();
157 if let Some(body) = &func.body {
158 collect_block_locals(source_map, body, body_range.clone(), &mut locals);
159 }
160
161 scopes.push(DebugSourceScope {
162 contract_name: contract.name.to_string(),
163 function_name,
164 range: function_range.clone(),
165 body_range: body_range.clone(),
166 parameters_src,
167 returns_src,
168 parameters: variables_from_list(
169 source_map,
170 &func.header.parameters,
171 function_range.clone(),
172 ),
173 returns: func
174 .header
175 .returns
176 .as_ref()
177 .map(|returns| variables_from_list(source_map, returns, function_range.clone()))
178 .unwrap_or_default(),
179 locals,
180 });
181 }
182
183 scopes.sort_by_key(|scope| {
185 (
186 scope.range.start,
187 scope.range.end.saturating_sub(scope.range.start),
188 scope.contract_name.clone(),
189 scope.function_name.clone(),
190 )
191 });
192
193 scopes.retain(|scope| {
195 scope.range.start >= contract_range.start && scope.range.end <= contract_range.end
196 });
197 out.extend(scopes);
198}
199
200fn function_name(func: &ast::ItemFunction<'_>) -> String {
201 match func.kind {
202 ast::FunctionKind::Constructor => "constructor".to_string(),
203 ast::FunctionKind::Fallback => "fallback".to_string(),
204 ast::FunctionKind::Receive => "receive".to_string(),
205 ast::FunctionKind::Modifier => {
206 func.header.name.as_ref().map(|n| n.as_str()).unwrap_or("modifier").to_string()
207 }
208 ast::FunctionKind::Function => {
209 func.header.name.as_ref().map(|n| n.as_str()).unwrap_or("function").to_string()
210 }
211 }
212}
213
214fn variables_from_list(
215 source_map: &solar::interface::source_map::SourceMap,
216 vars: &[ast::VariableDefinition<'_>],
217 scope: Range<usize>,
218) -> Vec<DebugVariable> {
219 vars.iter().filter_map(|var| variable_from_definition(source_map, var, scope.clone())).collect()
220}
221
222fn collect_block_locals(
223 source_map: &solar::interface::source_map::SourceMap,
224 block: &ast::Block<'_>,
225 fallback_scope: Range<usize>,
226 out: &mut Vec<DebugVariable>,
227) {
228 let scope = span_to_range(source_map, block.span).unwrap_or(fallback_scope);
229 for stmt in block.stmts.iter() {
230 collect_stmt_locals(source_map, stmt, scope.clone(), out);
231 }
232}
233
234fn collect_stmt_locals(
235 source_map: &solar::interface::source_map::SourceMap,
236 stmt: &ast::Stmt<'_>,
237 scope: Range<usize>,
238 out: &mut Vec<DebugVariable>,
239) {
240 match &stmt.kind {
241 ast::StmtKind::DeclSingle(var) => {
242 if let Some(var) = variable_from_definition(source_map, var, scope) {
243 out.push(var);
244 }
245 }
246 ast::StmtKind::DeclMulti(vars, _) => {
247 for var in vars.iter() {
248 let SpannedOption::Some(var) = var else { continue };
249 if let Some(var) = variable_from_definition(source_map, var, scope.clone()) {
250 out.push(var);
251 }
252 }
253 }
254 ast::StmtKind::Block(block) | ast::StmtKind::UncheckedBlock(block) => {
255 collect_block_locals(source_map, block, scope, out);
256 }
257 ast::StmtKind::If(_, then_stmt, else_stmt) => {
258 let then_scope =
259 span_to_range(source_map, then_stmt.span).unwrap_or_else(|| scope.clone());
260 collect_stmt_locals(source_map, then_stmt, then_scope, out);
261 if let Some(else_stmt) = else_stmt {
262 let else_scope =
263 span_to_range(source_map, else_stmt.span).unwrap_or_else(|| scope.clone());
264 collect_stmt_locals(source_map, else_stmt, else_scope, out);
265 }
266 }
267 ast::StmtKind::For { init, body, .. } => {
268 let for_scope = span_to_range(source_map, stmt.span).unwrap_or_else(|| scope.clone());
269 if let Some(init) = init {
270 collect_stmt_locals(source_map, init, for_scope.clone(), out);
271 }
272 collect_stmt_locals(source_map, body, for_scope, out);
273 }
274 ast::StmtKind::While(_, body) | ast::StmtKind::DoWhile(body, _) => {
275 let stmt_scope = span_to_range(source_map, stmt.span).unwrap_or(scope);
276 collect_stmt_locals(source_map, body, stmt_scope, out);
277 }
278 ast::StmtKind::Try(try_stmt) => {
279 for clause in try_stmt.clauses.iter() {
280 let clause_scope =
281 span_to_range(source_map, clause.span).unwrap_or_else(|| scope.clone());
282 for arg in clause.args.iter() {
283 if let Some(arg) =
284 variable_from_definition(source_map, arg, clause_scope.clone())
285 {
286 out.push(arg);
287 }
288 }
289 collect_block_locals(source_map, &clause.block, clause_scope, out);
290 }
291 }
292 ast::StmtKind::Assembly(_)
293 | ast::StmtKind::Break
294 | ast::StmtKind::Continue
295 | ast::StmtKind::Emit(..)
296 | ast::StmtKind::Expr(_)
297 | ast::StmtKind::Return(_)
298 | ast::StmtKind::Revert(..)
299 | ast::StmtKind::Placeholder => {}
300 }
301}
302
303fn variable_from_definition(
304 source_map: &solar::interface::source_map::SourceMap,
305 var: &ast::VariableDefinition<'_>,
306 scope: Range<usize>,
307) -> Option<DebugVariable> {
308 Some(DebugVariable {
309 name: var.name.map(|name| name.to_string()),
310 declaration: span_to_range(source_map, var.span)?,
311 scope,
312 })
313}
314
315fn span_to_range(
316 source_map: &solar::interface::source_map::SourceMap,
317 span: solar::interface::Span,
318) -> Option<Range<usize>> {
319 source_map.span_to_range(span).ok()
320}
321
322#[derive(Clone, Debug)]
323pub struct ArtifactData {
324 pub source_map: Option<SourceMap>,
325 pub source_map_runtime: Option<SourceMap>,
326 pub pc_ic_map: Option<PcIcMap>,
327 pub pc_ic_map_runtime: Option<PcIcMap>,
328 pub build_id: String,
329 pub file_id: u32,
330}
331
332impl ArtifactData {
333 fn new(bytecode: ContractBytecodeSome, build_id: String, file_id: u32) -> Result<Self> {
334 let parse = |b: &Bytecode, name: &str| {
335 let source_map = if b.source_map.as_ref().is_none_or(|s| s.is_empty()) {
337 Ok(None)
338 } else {
339 b.source_map().transpose().wrap_err_with(|| {
340 format!("failed to parse {name} source map of file {file_id} in {build_id}")
341 })
342 };
343
344 let pc_ic_map = if let Some(bytes) = strip_bytecode_placeholders(&b.object) {
346 (!bytes.is_empty()).then(|| PcIcMap::new(bytes.as_ref()))
347 } else {
348 None
349 };
350
351 source_map.map(|source_map| (source_map, pc_ic_map))
352 };
353 let (source_map, pc_ic_map) = parse(&bytecode.bytecode, "creation")?;
354 let (source_map_runtime, pc_ic_map_runtime) = bytecode
355 .deployed_bytecode
356 .bytecode
357 .map(|b| parse(&b, "runtime"))
358 .unwrap_or_else(|| Ok((None, None)))?;
359
360 Ok(Self { source_map, source_map_runtime, pc_ic_map, pc_ic_map_runtime, build_id, file_id })
361 }
362}
363
364#[derive(Clone, Debug, Default)]
366pub struct ContractSources {
367 pub sources_by_id: HashMap<String, HashMap<u32, Arc<SourceData>>>,
369 pub artifacts_by_name: HashMap<String, Vec<ArtifactData>>,
371}
372
373impl ContractSources {
374 pub fn from_project_output(
376 output: &ProjectCompileOutput,
377 root: &Path,
378 libraries: Option<&Libraries>,
379 ) -> Result<Self> {
380 let mut sources = Self::default();
381 sources.insert(output, root, libraries)?;
382 Ok(sources)
383 }
384
385 pub fn insert(
386 &mut self,
387 output: &ProjectCompileOutput,
388 root: &Path,
389 libraries: Option<&Libraries>,
390 ) -> Result<()> {
391 let link_data = libraries.map(|libraries| {
392 let linker = Linker::new(root, output.artifact_ids().collect());
393 (linker, libraries)
394 });
395
396 let artifacts: Vec<_> = output
397 .artifact_ids()
398 .collect::<Vec<_>>()
399 .par_iter()
400 .map(|(id, artifact)| {
401 let mut new_artifact = None;
402 if let Some(file_id) = artifact.id {
403 let artifact = if let Some((linker, libraries)) = link_data.as_ref() {
404 linker.link(id, libraries)?
405 } else {
406 artifact.get_contract_bytecode()
407 };
408 let bytecode = compact_to_contract(artifact.into_contract_bytecode())?;
409
410 new_artifact = Some((
411 id.name.clone(),
412 ArtifactData::new(bytecode, id.build_id.clone(), file_id)?,
413 ));
414 } else {
415 warn!(id = id.identifier(), "source not found");
416 };
417
418 Ok(new_artifact)
419 })
420 .collect::<Result<Vec<_>>>()?;
421
422 for (name, artifact) in artifacts.into_iter().flatten() {
423 self.artifacts_by_name.entry(name).or_default().push(artifact);
424 }
425
426 let mut files: BTreeMap<PathBuf, Arc<SourceData>> = BTreeMap::new();
429 let mut removed_files = HashSet::new();
430 for (build_id, build) in output.builds() {
431 for (source_id, path) in &build.source_id_to_path {
432 if !path.exists() {
433 removed_files.insert(path);
434 continue;
435 }
436
437 let source_data = match files.entry(path.clone()) {
438 std::collections::btree_map::Entry::Vacant(entry) => {
439 let source = Source::read(path).wrap_err_with(|| {
440 format!("failed to read artifact source file for `{}`", path.display())
441 })?;
442 let stripped = path.strip_prefix(root).unwrap_or(path).to_path_buf();
443 let source_data = Arc::new(SourceData::new(
444 output,
445 source.content.clone(),
446 build.language,
447 stripped,
448 root,
449 ));
450 entry.insert(source_data.clone());
451 source_data
452 }
453 std::collections::btree_map::Entry::Occupied(entry) => entry.get().clone(),
454 };
455 self.sources_by_id
456 .entry(build_id.clone())
457 .or_default()
458 .insert(*source_id, source_data);
459 }
460 }
461
462 if !removed_files.is_empty() {
463 let mut warning = "Detected artifacts built from source files that no longer exist. \
464 Run `forge clean` to make sure builds are in sync with project files."
465 .to_string();
466 for file in removed_files {
467 write!(warning, "\n - {}", file.display())?;
468 }
469 let _ = sh_warn!("{}", warning);
470 }
471
472 Ok(())
473 }
474
475 pub fn merge(&mut self, sources: Self) {
477 self.sources_by_id.extend(sources.sources_by_id);
478 for (name, artifacts) in sources.artifacts_by_name {
479 self.artifacts_by_name.entry(name).or_default().extend(artifacts);
480 }
481 }
482
483 pub fn get_sources(
485 &self,
486 name: &str,
487 ) -> Option<impl Iterator<Item = (&ArtifactData, &SourceData)>> {
488 self.artifacts_by_name.get(name).map(|artifacts| {
489 artifacts.iter().filter_map(|artifact| {
490 let source =
491 self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
492 Some((artifact, source.as_ref()))
493 })
494 })
495 }
496
497 pub fn entries(&self) -> impl Iterator<Item = (&str, &ArtifactData, &SourceData)> {
499 self.artifacts_by_name.iter().flat_map(|(name, artifacts)| {
500 artifacts.iter().filter_map(|artifact| {
501 let source =
502 self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
503 Some((name.as_str(), artifact, source.as_ref()))
504 })
505 })
506 }
507
508 pub fn find_source_mapping(
509 &self,
510 contract_name: &str,
511 pc: u32,
512 init_code: bool,
513 ) -> Option<(SourceElement, &SourceData)> {
514 self.get_sources(contract_name)?.find_map(|(artifact, source)| {
515 let source_map = if init_code {
516 artifact.source_map.as_ref()
517 } else {
518 artifact.source_map_runtime.as_ref()
519 }?;
520
521 let source_element = if matches!(source.language, MultiCompilerLanguage::Solc(_)) {
524 let pc_ic_map = if init_code {
525 artifact.pc_ic_map.as_ref()
526 } else {
527 artifact.pc_ic_map_runtime.as_ref()
528 }?;
529 let ic = pc_ic_map.get(pc)?;
530
531 source_map.get(ic as usize)
532 } else {
533 source_map.get(pc as usize)
534 }?;
535 source_element
537 .index()
538 .and_then(|index| {
540 (index == artifact.file_id).then(|| (source_element.clone(), source))
541 })
542 .or_else(|| {
543 self.sources_by_id
545 .get(&artifact.build_id)?
546 .get(&source_element.index()?)
547 .map(|source| (source_element.clone(), source.as_ref()))
548 })
549 })
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 fn variable(name: &str, declaration: Range<usize>, scope: Range<usize>) -> DebugVariable {
558 DebugVariable { name: Some(name.to_string()), declaration, scope }
559 }
560
561 fn scope(locals: Vec<DebugVariable>) -> DebugSourceScope {
562 DebugSourceScope {
563 contract_name: "DebugMe".to_string(),
564 function_name: "foo".to_string(),
565 range: 0..100,
566 body_range: 10..90,
567 parameters_src: String::new(),
568 returns_src: None,
569 parameters: Vec::new(),
570 returns: Vec::new(),
571 locals,
572 }
573 }
574
575 #[test]
576 fn visible_locals_require_declaration_and_scope() {
577 let scope = scope(vec![
578 variable("before", 10..15, 10..90),
579 variable("after", 70..75, 10..90),
580 variable("nested", 20..25, 20..40),
581 ]);
582
583 let names = |offset| {
584 scope
585 .visible_locals(offset)
586 .map(|variable| variable.name.as_deref().unwrap())
587 .collect::<Vec<_>>()
588 };
589
590 assert_eq!(names(14), Vec::<&str>::new());
591 assert_eq!(names(30), ["before", "nested"]);
592 assert_eq!(names(50), ["before"]);
593 assert_eq!(names(80), ["before", "after"]);
594 }
595}