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