Skip to main content

foundry_evm_traces/debug/
sources.rs

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, 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    /// Maps contract name to (start, end) of the contract definition in the source code.
33    /// This is useful for determining which contract contains given function definition.
34    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                // Vyper contracts have the same name as the file name.
50                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    /// Finds name of contract that contains given loc.
78    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    /// Whether the contract was compiled through the via-IR pipeline, if the metadata says.
102    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            // Only parse source map if it's not empty.
114            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            // Only parse bytecode if it's not empty, stripping placeholders if necessary.
123            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/// Container with artifacts data useful for identifying individual execution steps.
151#[derive(Clone, Debug, Default)]
152pub struct ContractSources {
153    /// Map over build_id -> file_id -> (source code, language)
154    pub sources_by_id: HashMap<String, HashMap<u32, Arc<SourceData>>>,
155    /// Map over contract name -> Vec<(bytecode, build_id, file_id)>
156    pub artifacts_by_name: HashMap<String, Vec<ArtifactData>>,
157}
158
159impl ContractSources {
160    /// Collects the contract sources and artifacts from the project compile output.
161    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                    // Solc records `viaIR` in the metadata settings only when it is enabled.
191                    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        // Not all source files produce artifacts, so we are populating sources by using build
219        // infos.
220        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                    // Preprocessor deploy helpers are compiled from memory and never exist on disk.
226                    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    /// Replaces each source that the compiler saw with different text than the file on disk, such
271    /// as a test rewritten by dynamic test linking, with that text. Source maps point into it.
272    ///
273    /// The text comes from the full build infos (`build_info = true`) in `build_info_dir`. A build
274    /// without one keeps the files on disk.
275    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    /// Merges given contract sources.
301    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    /// Returns all sources for a contract by name.
309    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    /// Returns all (name, bytecode, source) sets.
323    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            // Solc indexes source maps by instruction counter, but Vyper indexes by program
347            // counter.
348            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            // if the source element has an index, find the sourcemap for that index
361            source_element
362                .index()
363                // if index matches current file_id, return current source code
364                .and_then(|index| {
365                    (index == artifact.file_id).then(|| (source_element.clone(), source))
366                })
367                .or_else(|| {
368                    // otherwise find the source code for the element's index
369                    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/// The compiler input of a build info, which only full build infos have.
379#[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        // Only `build` has a full build info, and it records a rewritten test file.
407        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        // The read stops before the output, so a truncated output does not matter.
420        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        // A malformed build info is skipped.
425        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}