ghostscope_dwarf/analyzer/
source_resolution.rs1use super::{AddressQueryResult, DwarfAnalyzer};
2use crate::{
3 core::{ModuleAddress, Result},
4 path_match,
5};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone)]
9pub struct SourceLineAddressSearch {
10 pub file_path: Option<String>,
11 pub line_number: u32,
12 pub raw_address_count: usize,
13 pub addresses: Vec<ModuleAddress>,
14}
15
16#[derive(Debug, Clone)]
17pub struct SourceLineQuerySearch {
18 pub file_path: Option<String>,
19 pub line_number: u32,
20 pub raw_address_count: usize,
21 pub addresses: Vec<AddressQueryResult>,
22}
23
24impl DwarfAnalyzer {
25 pub fn source_line_candidates(&self, file_path: &str) -> Vec<String> {
31 let Ok(grouped) = self.get_grouped_file_info_by_module() else {
32 return vec![file_path.to_string()];
33 };
34
35 let mut exact = Vec::new();
36 let mut suffix_matches = Vec::new();
37 let mut basename_matches = Vec::new();
38 let mut seen_paths = HashSet::new();
39 let has_separator = path_match::has_path_separator(file_path);
40 let basename = path_match::file_name(file_path);
41
42 for (_module_path, files) in grouped {
43 for file in files {
44 if !seen_paths.insert(file.full_path.clone()) {
45 continue;
46 }
47
48 if file.full_path == file_path {
49 exact.push(file.full_path);
50 } else if has_separator
51 && (path_match::path_component_suffix_matches(&file.full_path, file_path)
52 || path_match::path_component_suffix_matches(file_path, &file.full_path))
53 {
54 suffix_matches.push(file.full_path);
55 } else if !has_separator && file.basename == basename {
56 basename_matches.push(file.full_path);
57 }
58 }
59 }
60
61 exact.sort();
62 suffix_matches.sort();
63 basename_matches.sort();
64 let basename_candidate = if basename_matches.len() == 1 {
65 basename_matches.pop()
66 } else {
67 None
68 };
69
70 let mut candidates = Vec::new();
71 let mut seen = HashSet::new();
72 for candidate in exact
73 .into_iter()
74 .chain(suffix_matches)
75 .chain(basename_candidate)
76 .chain([file_path.to_string()])
77 {
78 if !candidate.trim().is_empty() && seen.insert(candidate.clone()) {
79 candidates.push(candidate);
80 }
81 }
82 candidates
83 }
84
85 pub fn resolve_source_line_addresses_best_effort<I, S>(
91 &self,
92 candidates: I,
93 line_number: u32,
94 target_path: Option<&str>,
95 ) -> Result<SourceLineAddressSearch>
96 where
97 I: IntoIterator<Item = S>,
98 S: AsRef<str>,
99 {
100 let mut seen = HashSet::new();
101 let mut first_target_filtered: Option<SourceLineAddressSearch> = None;
102
103 for candidate in candidates {
104 let candidate = candidate.as_ref().trim();
105 if candidate.is_empty() || !seen.insert(candidate.to_string()) {
106 continue;
107 }
108
109 let module_addresses = self.lookup_addresses_by_source_line(candidate, line_number);
110 let raw_address_count = module_addresses.len();
111 if raw_address_count == 0 {
112 continue;
113 }
114
115 let addresses =
116 self.filter_module_addresses_to_target(module_addresses, target_path)?;
117 let search = SourceLineAddressSearch {
118 file_path: Some(candidate.to_string()),
119 line_number,
120 raw_address_count,
121 addresses,
122 };
123
124 if !search.addresses.is_empty() {
125 return Ok(search);
126 }
127 if first_target_filtered.is_none() {
128 first_target_filtered = Some(search);
129 }
130 }
131
132 Ok(first_target_filtered.unwrap_or(SourceLineAddressSearch {
133 file_path: None,
134 line_number,
135 raw_address_count: 0,
136 addresses: Vec::new(),
137 }))
138 }
139
140 pub fn resolve_source_line_query_best_effort<I, S>(
143 &self,
144 candidates: I,
145 line_number: u32,
146 target_path: Option<&str>,
147 ) -> Result<SourceLineQuerySearch>
148 where
149 I: IntoIterator<Item = S>,
150 S: AsRef<str>,
151 {
152 let address_search =
153 self.resolve_source_line_addresses_best_effort(candidates, line_number, target_path)?;
154 let Some(file_path) = address_search.file_path.clone() else {
155 return Ok(SourceLineQuerySearch {
156 file_path: None,
157 line_number,
158 raw_address_count: 0,
159 addresses: Vec::new(),
160 });
161 };
162
163 if address_search.addresses.is_empty() {
164 return Ok(SourceLineQuerySearch {
165 file_path: Some(file_path),
166 line_number,
167 raw_address_count: address_search.raw_address_count,
168 addresses: Vec::new(),
169 });
170 }
171
172 let addresses = self.query_module_addresses_for_source_line_best_effort(
173 address_search.addresses,
174 &file_path,
175 line_number,
176 &format!("source line '{file_path}:{line_number}'"),
177 )?;
178
179 Ok(SourceLineQuerySearch {
180 file_path: Some(file_path),
181 line_number,
182 raw_address_count: address_search.raw_address_count,
183 addresses,
184 })
185 }
186
187 pub fn describe_source_line_failure(&self, file_path: &str, line_number: u32) -> String {
189 let default_msg =
190 format!("No addresses resolved for source line {file_path}:{line_number}");
191
192 let grouped = match self.get_grouped_file_info_by_module() {
193 Ok(grouped) => grouped,
194 Err(_) => return default_msg,
195 };
196
197 let mut all_full_paths = Vec::new();
198 let mut same_basename_paths = Vec::new();
199 let has_sep = path_match::has_path_separator(file_path);
200 let basename = path_match::file_name(file_path);
201
202 for (_module, files) in &grouped {
203 for file in files {
204 all_full_paths.push(file.full_path.clone());
205 if file.basename == basename {
206 same_basename_paths.push(file.full_path.clone());
207 }
208 }
209 }
210
211 if all_full_paths.is_empty() {
212 return default_msg;
213 }
214
215 let exact_match = all_full_paths.iter().any(|path| path == file_path);
216 let suffix_matches: Vec<String> = all_full_paths
217 .iter()
218 .filter(|path| {
219 path_match::path_component_suffix_matches(path, file_path)
220 || path_match::path_component_suffix_matches(file_path, path)
221 })
222 .cloned()
223 .collect();
224
225 if same_basename_paths.is_empty() {
226 return format!(
227 "Source file not found in DWARF: {file_path}.\n- Tips: use 'srcpath map <dwarf_comp_dir> <local_dir>' or pass full DWARF path.\n- List files with: dwarf-tool source-files or 'info source-files'"
228 );
229 }
230
231 if !exact_match && suffix_matches.is_empty() && has_sep && same_basename_paths.len() > 1 {
232 let mut samples = same_basename_paths.clone();
233 samples.sort();
234 samples.dedup();
235 if samples.len() > 3 {
236 samples.truncate(3);
237 }
238 let sample_list = samples.join("\n - ");
239 return format!(
240 "Multiple files named '{basename}' found; the given path '{file_path}' did not uniquely match by suffix.\nTry a more specific path or add a path mapping (srcpath map).\nExamples:\n - {sample_list}"
241 );
242 }
243
244 let mut hit_candidates = Vec::new();
245 let probe_list = if !suffix_matches.is_empty() {
246 suffix_matches
247 } else {
248 let mut paths = same_basename_paths.clone();
249 paths.sort();
250 paths.dedup();
251 paths.truncate(20);
252 paths
253 };
254
255 for candidate in &probe_list {
256 if !self
257 .lookup_addresses_by_source_line(candidate, line_number)
258 .is_empty()
259 {
260 hit_candidates.push(candidate.clone());
261 if hit_candidates.len() >= 3 {
262 break;
263 }
264 }
265 }
266
267 if !hit_candidates.is_empty() {
268 let list = hit_candidates.join("\n - ");
269 return format!(
270 "Ambiguous path: '{file_path}' did not resolve, but found addresses for the same line in:\n - {list}\nPlease use a more specific path (full DWARF path) or add a mapping (srcpath map)."
271 );
272 }
273
274 format!(
275 "No executable addresses for {file_path}:{line_number} (file exists in DWARF but this line has no statement).\nTry a nearby line, or rebuild with debug info and lower optimization (e.g., -g -O0)."
276 )
277 }
278}