Skip to main content

ghostscope_dwarf/analyzer/
source_resolution.rs

1use 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    /// Build DWARF source path candidates for a user-provided file path.
26    ///
27    /// This keeps DWARF path matching rules in the analyzer: exact paths first,
28    /// then component-boundary suffix matches, then unique basename matches
29    /// for bare filenames, and finally the original query.
30    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    /// Probe source-line candidates until a candidate resolves to addresses.
86    ///
87    /// If a candidate resolves before target filtering but all addresses are
88    /// filtered out, the first filtered match is returned with an empty address
89    /// list so callers can produce a target-scoped error.
90    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    /// Probe source-line candidates and return rich debug information for the
141    /// first candidate with address results.
142    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    /// Explain why a source-line lookup did not resolve to executable addresses.
188    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}