foundry_evm_traces/debug/
sources.rs1use 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, Sources,
10 sourcemap::{SourceElement, SourceMap},
11 },
12 multi::MultiCompilerLanguage,
13};
14use foundry_config::read_build_info_input;
15use foundry_evm_core::ic::PcIcMap;
16use foundry_linking::Linker;
17use rayon::prelude::*;
18use serde::Deserialize;
19use std::{
20 collections::{BTreeMap, HashMap, HashSet},
21 fmt::Write,
22 ops::Range,
23 path::{Path, PathBuf},
24 sync::Arc,
25};
26
27#[derive(Clone, Debug)]
28pub struct SourceData {
29 pub source: Arc<String>,
30 pub language: MultiCompilerLanguage,
31 pub path: PathBuf,
32 pub contract_definitions: Vec<(String, Range<usize>)>,
35}
36
37impl SourceData {
38 pub fn new(
39 output: &ProjectCompileOutput,
40 source: Arc<String>,
41 language: MultiCompilerLanguage,
42 path: PathBuf,
43 root: &Path,
44 ) -> Self {
45 let mut contract_definitions = Vec::new();
46
47 match language {
48 MultiCompilerLanguage::Vyper(_) => {
49 if let Some(name) = path.file_stem().map(|s| s.to_string_lossy().to_string()) {
51 contract_definitions.push((name, 0..source.len()));
52 }
53 }
54 MultiCompilerLanguage::Solc(_) => {
55 let r = output.parser().solc().compiler().enter(|compiler| -> Option<()> {
56 let (_, source) = compiler.gcx().get_ast_source(root.join(&path))?;
57 let source_map = compiler.sess().source_map();
58 for item in source.ast.as_ref()?.items.iter() {
59 if let solar::ast::ItemKind::Contract(contract) = &item.kind {
60 let Some(contract_range) = span_to_range(source_map, item.span) else {
61 continue;
62 };
63 contract_definitions.push((contract.name.to_string(), contract_range));
64 }
65 }
66 Some(())
67 });
68 if r.is_none() {
69 warn!("failed to parse contract definitions for {}", path.display());
70 }
71 }
72 }
73
74 Self { source, language, path, contract_definitions }
75 }
76
77 pub fn find_contract_name(&self, start: usize, end: usize) -> Option<&str> {
79 self.contract_definitions
80 .iter()
81 .find(|(_, r)| start >= r.start && end <= r.end)
82 .map(|(name, _)| name.as_str())
83 }
84}
85
86fn span_to_range(
87 source_map: &solar::interface::source_map::SourceMap,
88 span: solar::interface::Span,
89) -> Option<Range<usize>> {
90 source_map.span_to_range(span).ok()
91}
92
93#[derive(Clone, Debug)]
94pub struct ArtifactData {
95 pub source_map: Option<SourceMap>,
96 pub source_map_runtime: Option<SourceMap>,
97 pub pc_ic_map: Option<PcIcMap>,
98 pub pc_ic_map_runtime: Option<PcIcMap>,
99 pub build_id: String,
100 pub file_id: u32,
101 pub via_ir: Option<bool>,
103}
104
105impl ArtifactData {
106 fn new(
107 bytecode: ContractBytecodeSome,
108 build_id: String,
109 file_id: u32,
110 via_ir: Option<bool>,
111 ) -> Result<Self> {
112 let parse = |b: &Bytecode, name: &str| {
113 let source_map = if b.source_map.as_ref().is_none_or(|s| s.is_empty()) {
115 Ok(None)
116 } else {
117 b.source_map().transpose().wrap_err_with(|| {
118 format!("failed to parse {name} source map of file {file_id} in {build_id}")
119 })
120 };
121
122 let pc_ic_map = if let Some(bytes) = strip_bytecode_placeholders(&b.object) {
124 (!bytes.is_empty()).then(|| PcIcMap::new(bytes.as_ref()))
125 } else {
126 None
127 };
128
129 source_map.map(|source_map| (source_map, pc_ic_map))
130 };
131 let (source_map, pc_ic_map) = parse(&bytecode.bytecode, "creation")?;
132 let (source_map_runtime, pc_ic_map_runtime) = bytecode
133 .deployed_bytecode
134 .bytecode
135 .map(|b| parse(&b, "runtime"))
136 .unwrap_or_else(|| Ok((None, None)))?;
137
138 Ok(Self {
139 source_map,
140 source_map_runtime,
141 pc_ic_map,
142 pc_ic_map_runtime,
143 build_id,
144 file_id,
145 via_ir,
146 })
147 }
148}
149
150#[derive(Clone, Debug, Default)]
152pub struct ContractSources {
153 pub sources_by_id: HashMap<String, HashMap<u32, Arc<SourceData>>>,
155 pub artifacts_by_name: HashMap<String, Vec<ArtifactData>>,
157}
158
159impl ContractSources {
160 pub fn from_project_output(
162 output: &ProjectCompileOutput,
163 root: &Path,
164 libraries: Option<&Libraries>,
165 ) -> Result<Self> {
166 let mut sources = Self::default();
167 sources.insert(output, root, libraries)?;
168 Ok(sources)
169 }
170
171 pub fn insert(
172 &mut self,
173 output: &ProjectCompileOutput,
174 root: &Path,
175 libraries: Option<&Libraries>,
176 ) -> Result<()> {
177 let link_data = libraries.map(|libraries| {
178 let linker = Linker::new(root, output.artifact_ids().collect());
179 (linker, libraries)
180 });
181
182 let artifacts: Vec<_> = output
183 .artifact_ids()
184 .filter(|(id, _)| !is_external_artifact(&id.build_id))
185 .collect::<Vec<_>>()
186 .par_iter()
187 .map(|(id, artifact)| {
188 let mut new_artifact = None;
189 if let Some(file_id) = artifact.id {
190 let via_ir = artifact
192 .metadata
193 .as_ref()
194 .map(|metadata| metadata.settings.via_ir.unwrap_or_default());
195 let artifact = if let Some((linker, libraries)) = link_data.as_ref() {
196 linker.link(id, libraries)?
197 } else {
198 artifact.get_contract_bytecode()
199 };
200 let bytecode = compact_to_contract(artifact.into_contract_bytecode())?;
201
202 new_artifact = Some((
203 id.name.clone(),
204 ArtifactData::new(bytecode, id.build_id.clone(), file_id, via_ir)?,
205 ));
206 } else {
207 warn!(id = id.identifier(), "source not found");
208 };
209
210 Ok(new_artifact)
211 })
212 .collect::<Result<Vec<_>>>()?;
213
214 for (name, artifact) in artifacts.into_iter().flatten() {
215 self.artifacts_by_name.entry(name).or_default().push(artifact);
216 }
217
218 let mut files: BTreeMap<PathBuf, Arc<SourceData>> = BTreeMap::new();
221 let mut removed_files = HashSet::new();
222 for (build_id, build) in output.builds() {
223 for (source_id, path) in &build.source_id_to_path {
224 if !path.exists() {
225 if !is_deploy_helper_path(path) {
227 removed_files.insert(path);
228 }
229 continue;
230 }
231
232 let source_data = match files.entry(path.clone()) {
233 std::collections::btree_map::Entry::Vacant(entry) => {
234 let source = Source::read(path).wrap_err_with(|| {
235 format!("failed to read artifact source file for `{}`", path.display())
236 })?;
237 let stripped = path.strip_prefix(root).unwrap_or(path).to_path_buf();
238 let source_data = Arc::new(SourceData::new(
239 output,
240 source.content,
241 build.language,
242 stripped,
243 root,
244 ));
245 entry.insert(source_data.clone());
246 source_data
247 }
248 std::collections::btree_map::Entry::Occupied(entry) => entry.get().clone(),
249 };
250 self.sources_by_id
251 .entry(build_id.clone())
252 .or_default()
253 .insert(*source_id, source_data);
254 }
255 }
256
257 if !removed_files.is_empty() {
258 let mut warning = "Detected artifacts built from source files that no longer exist. \
259 Run `forge clean` to make sure builds are in sync with project files."
260 .to_string();
261 for file in removed_files {
262 write!(warning, "\n - {}", file.display())?;
263 }
264 let _ = sh_warn!("{}", warning);
265 }
266
267 Ok(())
268 }
269
270 pub fn insert_compiled_sources(&mut self, build_info_dir: &Path) {
276 for (build_id, sources) in &mut self.sources_by_id {
277 let path = build_info_dir.join(build_id).with_extension("json");
278 if !path.exists() {
279 continue;
280 }
281 let mut input = match read_build_info_input::<BuildInfoInput>(&path) {
282 Ok(Some(input)) => input,
283 Ok(None) => continue,
284 Err(error) => {
285 let _ = sh_warn!("skipping build info {}: {error}", path.display());
286 continue;
287 }
288 };
289 for source in sources.values_mut() {
290 if let Some(compiled) = input.sources.remove(&source.path)
291 && compiled.content != source.source
292 {
293 *source =
294 Arc::new(SourceData { source: compiled.content, ..(**source).clone() });
295 }
296 }
297 }
298 }
299
300 pub fn merge(&mut self, sources: Self) {
302 self.sources_by_id.extend(sources.sources_by_id);
303 for (name, artifacts) in sources.artifacts_by_name {
304 self.artifacts_by_name.entry(name).or_default().extend(artifacts);
305 }
306 }
307
308 pub fn get_sources(
310 &self,
311 name: &str,
312 ) -> Option<impl Iterator<Item = (&ArtifactData, &SourceData)>> {
313 self.artifacts_by_name.get(name).map(|artifacts| {
314 artifacts.iter().filter_map(|artifact| {
315 let source =
316 self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
317 Some((artifact, source.as_ref()))
318 })
319 })
320 }
321
322 pub fn entries(&self) -> impl Iterator<Item = (&str, &ArtifactData, &SourceData)> {
324 self.artifacts_by_name.iter().flat_map(|(name, artifacts)| {
325 artifacts.iter().filter_map(|artifact| {
326 let source =
327 self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
328 Some((name.as_str(), artifact, source.as_ref()))
329 })
330 })
331 }
332
333 pub fn find_source_mapping(
334 &self,
335 contract_name: &str,
336 pc: u32,
337 init_code: bool,
338 ) -> Option<(SourceElement, &SourceData)> {
339 self.get_sources(contract_name)?.find_map(|(artifact, source)| {
340 let source_map = if init_code {
341 artifact.source_map.as_ref()
342 } else {
343 artifact.source_map_runtime.as_ref()
344 }?;
345
346 let source_element = if matches!(source.language, MultiCompilerLanguage::Solc(_)) {
349 let pc_ic_map = if init_code {
350 artifact.pc_ic_map.as_ref()
351 } else {
352 artifact.pc_ic_map_runtime.as_ref()
353 }?;
354 let ic = pc_ic_map.get(pc)?;
355
356 source_map.get(ic as usize)
357 } else {
358 source_map.get(pc as usize)
359 }?;
360 source_element
362 .index()
363 .and_then(|index| {
365 (index == artifact.file_id).then(|| (source_element.clone(), source))
366 })
367 .or_else(|| {
368 self.sources_by_id
370 .get(&artifact.build_id)?
371 .get(&source_element.index()?)
372 .map(|source| (source_element.clone(), source.as_ref()))
373 })
374 })
375 }
376}
377
378#[derive(Deserialize)]
380struct BuildInfoInput {
381 sources: Sources,
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn compiled_sources_replace_rewritten_files() {
390 let source = |path: &str, text: &str| {
391 Arc::new(SourceData {
392 source: Arc::new(text.to_string()),
393 language: Default::default(),
394 path: PathBuf::from(path),
395 contract_definitions: Vec::new(),
396 })
397 };
398 let mut sources = ContractSources::default();
399 sources.sources_by_id.insert(
400 "build".to_string(),
401 HashMap::from([
402 (0, source("src/A.sol", "contract A {}")),
403 (1, source("test/A.t.sol", "contract T { A a = new A(); }")),
404 ]),
405 );
406 sources.sources_by_id.insert(
408 "other".to_string(),
409 HashMap::from([(0, source("src/A.sol", "contract A {}"))]),
410 );
411 let dir = tempfile::tempdir().unwrap();
412 let build_info = serde_json::json!({
413 "id": "build",
414 "input": { "sources": {
415 "src/A.sol": { "content": "contract A {}" },
416 "test/A.t.sol": { "content": "contract T { A a = A(deploy()); }" },
417 }},
418 });
419 let build_info = build_info.to_string();
421 let build_info =
422 format!(r#"{},"output":{{"contracts":"#, &build_info[..build_info.len() - 1]);
423 std::fs::write(dir.path().join("build.json"), build_info).unwrap();
424 std::fs::write(dir.path().join("other.json"), "{").unwrap();
426
427 sources.insert_compiled_sources(dir.path());
428
429 let text =
430 |build: &str, id: u32| sources.sources_by_id[build][&id].source.as_str().to_owned();
431 assert_eq!(text("build", 0), "contract A {}");
432 assert_eq!(text("build", 1), "contract T { A a = A(deploy()); }");
433 assert_eq!(sources.sources_by_id["build"][&1].path, PathBuf::from("test/A.t.sol"));
434 assert_eq!(text("other", 0), "contract A {}");
435 }
436}