foundry_evm_traces/debug/
sources.rs1use 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 std::{
15 collections::{BTreeMap, HashMap, HashSet},
16 fmt::Write,
17 ops::Range,
18 path::{Path, PathBuf},
19 sync::Arc,
20};
21
22#[derive(Clone, Debug)]
23pub struct SourceData {
24 pub source: Arc<String>,
25 pub language: MultiCompilerLanguage,
26 pub path: PathBuf,
27 contract_definitions: Vec<(String, Range<usize>)>,
30}
31
32impl SourceData {
33 pub fn new(
34 output: &ProjectCompileOutput,
35 source: Arc<String>,
36 language: MultiCompilerLanguage,
37 path: PathBuf,
38 root: &Path,
39 ) -> Self {
40 let mut contract_definitions = Vec::new();
41
42 match language {
43 MultiCompilerLanguage::Vyper(_) => {
44 if let Some(name) = path.file_stem().map(|s| s.to_string_lossy().to_string()) {
46 contract_definitions.push((name, 0..source.len()));
47 }
48 }
49 MultiCompilerLanguage::Solc(_) => {
50 let r = output.parser().solc().compiler().enter(|compiler| -> Option<()> {
51 let (_, source) = compiler.gcx().get_ast_source(root.join(&path))?;
52 for item in source.ast.as_ref()?.items.iter() {
53 if let solar::ast::ItemKind::Contract(contract) = &item.kind {
54 contract_definitions.push((
55 contract.name.to_string(),
56 compiler.sess().source_map().span_to_range(item.span).unwrap(),
57 ));
58 }
59 }
60 Some(())
61 });
62 if r.is_none() {
63 warn!("failed to parse contract definitions for {}", path.display());
64 }
65 }
66 }
67
68 Self { source, language, path, contract_definitions }
69 }
70
71 pub fn find_contract_name(&self, start: usize, end: usize) -> Option<&str> {
73 self.contract_definitions
74 .iter()
75 .find(|(_, r)| start >= r.start && end <= r.end)
76 .map(|(name, _)| name.as_str())
77 }
78}
79
80#[derive(Clone, Debug)]
81pub struct ArtifactData {
82 pub source_map: Option<SourceMap>,
83 pub source_map_runtime: Option<SourceMap>,
84 pub pc_ic_map: Option<PcIcMap>,
85 pub pc_ic_map_runtime: Option<PcIcMap>,
86 pub build_id: String,
87 pub file_id: u32,
88}
89
90impl ArtifactData {
91 fn new(bytecode: ContractBytecodeSome, build_id: String, file_id: u32) -> Result<Self> {
92 let parse = |b: &Bytecode, name: &str| {
93 let source_map = if b.source_map.as_ref().is_none_or(|s| s.is_empty()) {
95 Ok(None)
96 } else {
97 b.source_map().transpose().wrap_err_with(|| {
98 format!("failed to parse {name} source map of file {file_id} in {build_id}")
99 })
100 };
101
102 let pc_ic_map = if let Some(bytes) = strip_bytecode_placeholders(&b.object) {
104 (!bytes.is_empty()).then(|| PcIcMap::new(bytes.as_ref()))
105 } else {
106 None
107 };
108
109 source_map.map(|source_map| (source_map, pc_ic_map))
110 };
111 let (source_map, pc_ic_map) = parse(&bytecode.bytecode, "creation")?;
112 let (source_map_runtime, pc_ic_map_runtime) = bytecode
113 .deployed_bytecode
114 .bytecode
115 .map(|b| parse(&b, "runtime"))
116 .unwrap_or_else(|| Ok((None, None)))?;
117
118 Ok(Self { source_map, source_map_runtime, pc_ic_map, pc_ic_map_runtime, build_id, file_id })
119 }
120}
121
122#[derive(Clone, Debug, Default)]
124pub struct ContractSources {
125 pub sources_by_id: HashMap<String, HashMap<u32, Arc<SourceData>>>,
127 pub artifacts_by_name: HashMap<String, Vec<ArtifactData>>,
129}
130
131impl ContractSources {
132 pub fn from_project_output(
134 output: &ProjectCompileOutput,
135 root: &Path,
136 libraries: Option<&Libraries>,
137 ) -> Result<Self> {
138 let mut sources = Self::default();
139 sources.insert(output, root, libraries)?;
140 Ok(sources)
141 }
142
143 pub fn insert(
144 &mut self,
145 output: &ProjectCompileOutput,
146 root: &Path,
147 libraries: Option<&Libraries>,
148 ) -> Result<()> {
149 let link_data = libraries.map(|libraries| {
150 let linker = Linker::new(root, output.artifact_ids().collect());
151 (linker, libraries)
152 });
153
154 let artifacts: Vec<_> = output
155 .artifact_ids()
156 .collect::<Vec<_>>()
157 .par_iter()
158 .map(|(id, artifact)| {
159 let mut new_artifact = None;
160 if let Some(file_id) = artifact.id {
161 let artifact = if let Some((linker, libraries)) = link_data.as_ref() {
162 linker.link(id, libraries)?
163 } else {
164 artifact.get_contract_bytecode()
165 };
166 let bytecode = compact_to_contract(artifact.into_contract_bytecode())?;
167
168 new_artifact = Some((
169 id.name.clone(),
170 ArtifactData::new(bytecode, id.build_id.clone(), file_id)?,
171 ));
172 } else {
173 warn!(id = id.identifier(), "source not found");
174 };
175
176 Ok(new_artifact)
177 })
178 .collect::<Result<Vec<_>>>()?;
179
180 for (name, artifact) in artifacts.into_iter().flatten() {
181 self.artifacts_by_name.entry(name).or_default().push(artifact);
182 }
183
184 let mut files: BTreeMap<PathBuf, Arc<SourceData>> = BTreeMap::new();
187 let mut removed_files = HashSet::new();
188 for (build_id, build) in output.builds() {
189 for (source_id, path) in &build.source_id_to_path {
190 if !path.exists() {
191 removed_files.insert(path);
192 continue;
193 }
194
195 let source_data = match files.entry(path.clone()) {
196 std::collections::btree_map::Entry::Vacant(entry) => {
197 let source = Source::read(path).wrap_err_with(|| {
198 format!("failed to read artifact source file for `{}`", path.display())
199 })?;
200 let stripped = path.strip_prefix(root).unwrap_or(path).to_path_buf();
201 let source_data = Arc::new(SourceData::new(
202 output,
203 source.content.clone(),
204 build.language,
205 stripped,
206 root,
207 ));
208 entry.insert(source_data.clone());
209 source_data
210 }
211 std::collections::btree_map::Entry::Occupied(entry) => entry.get().clone(),
212 };
213 self.sources_by_id
214 .entry(build_id.clone())
215 .or_default()
216 .insert(*source_id, source_data);
217 }
218 }
219
220 if !removed_files.is_empty() {
221 let mut warning = "Detected artifacts built from source files that no longer exist. \
222 Run `forge clean` to make sure builds are in sync with project files."
223 .to_string();
224 for file in removed_files {
225 write!(warning, "\n - {}", file.display())?;
226 }
227 let _ = sh_warn!("{}", warning);
228 }
229
230 Ok(())
231 }
232
233 pub fn merge(&mut self, sources: Self) {
235 self.sources_by_id.extend(sources.sources_by_id);
236 for (name, artifacts) in sources.artifacts_by_name {
237 self.artifacts_by_name.entry(name).or_default().extend(artifacts);
238 }
239 }
240
241 pub fn get_sources(
243 &self,
244 name: &str,
245 ) -> Option<impl Iterator<Item = (&ArtifactData, &SourceData)>> {
246 self.artifacts_by_name.get(name).map(|artifacts| {
247 artifacts.iter().filter_map(|artifact| {
248 let source =
249 self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
250 Some((artifact, source.as_ref()))
251 })
252 })
253 }
254
255 pub fn entries(&self) -> impl Iterator<Item = (&str, &ArtifactData, &SourceData)> {
257 self.artifacts_by_name.iter().flat_map(|(name, artifacts)| {
258 artifacts.iter().filter_map(|artifact| {
259 let source =
260 self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
261 Some((name.as_str(), artifact, source.as_ref()))
262 })
263 })
264 }
265
266 pub fn find_source_mapping(
267 &self,
268 contract_name: &str,
269 pc: u32,
270 init_code: bool,
271 ) -> Option<(SourceElement, &SourceData)> {
272 self.get_sources(contract_name)?.find_map(|(artifact, source)| {
273 let source_map = if init_code {
274 artifact.source_map.as_ref()
275 } else {
276 artifact.source_map_runtime.as_ref()
277 }?;
278
279 let source_element = if matches!(source.language, MultiCompilerLanguage::Solc(_)) {
282 let pc_ic_map = if init_code {
283 artifact.pc_ic_map.as_ref()
284 } else {
285 artifact.pc_ic_map_runtime.as_ref()
286 }?;
287 let ic = pc_ic_map.get(pc)?;
288
289 source_map.get(ic as usize)
290 } else {
291 source_map.get(pc as usize)
292 }?;
293 source_element
295 .index()
296 .and_then(|index| {
298 (index == artifact.file_id).then(|| (source_element.clone(), source))
299 })
300 .or_else(|| {
301 self.sources_by_id
303 .get(&artifact.build_id)?
304 .get(&source_element.index()?)
305 .map(|source| (source_element.clone(), source.as_ref()))
306 })
307 })
308 }
309}