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