1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Context as _;
4use itertools::Itertools as _;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 CrateName, Symbol, cap_rule::SymbolRules, reservoir_sample::ReservoirSampleExt as _,
9 rust_path::RustPath, symbol::FunctionOrPath,
10};
11
12pub type CapabilitySet = BTreeSet<Capability>;
13
14#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
17pub enum Capability {
18 #[serde(rename = "build.rs")]
23 BuildRs,
24
25 #[serde(rename = "alloc")]
27 Alloc,
28
29 #[serde(rename = "panic")]
31 Panic,
32
33 #[serde(rename = "time")]
35 Time,
36
37 #[serde(rename = "sysinfo")]
39 Sysinfo,
40
41 #[serde(rename = "stdio")]
43 Stdio,
44
45 #[serde(rename = "thread")]
47 Thread,
48
49 #[serde(rename = "net")]
51 Net,
52
53 #[serde(rename = "fs")]
55 FS,
56
57 #[serde(rename = "*")]
59 Any,
60}
61
62impl std::fmt::Display for Capability {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::BuildRs => write!(f, "build.rs"),
66 Self::Alloc => write!(f, "alloc"),
67 Self::Panic => write!(f, "panic"),
68 Self::Time => write!(f, "time"),
69 Self::Sysinfo => write!(f, "sysinfo"),
70 Self::Stdio => write!(f, "stdio"),
71 Self::Thread => write!(f, "thread"),
72 Self::Net => write!(f, "net"),
73 Self::FS => write!(f, "fs"),
74 Self::Any => write!(f, "any"),
75 }
76 }
77}
78
79impl Capability {
80 pub fn emoji(&self) -> &'static str {
81 match self {
82 Self::BuildRs => "๐ ๏ธ ",
83 Self::Alloc => "๐ฆ",
84 Self::Panic => "โ๏ธ",
85 Self::Time => "โฐ",
86 Self::Sysinfo => "๐ฅ๏ธ ",
87 Self::Stdio => "๐",
88 Self::Thread => "๐งต",
89 Self::Net => "๐",
90 Self::FS => "๐",
91 Self::Any => "โ ๏ธ ",
92 }
93 }
94}
95
96#[derive(Clone, Debug, Default)]
97pub struct DeducedCaps {
98 pub caps: BTreeMap<Capability, Reasons>,
100
101 pub unresolved_crates: BTreeMap<CrateName, BTreeSet<RustPath>>,
105}
106
107pub type Reasons = BTreeSet<Reason>;
109
110#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
111pub enum Reason {
112 PathMatchedRule(RustPath),
114
115 SymbolMatchedRule(Symbol),
117
118 SourceParseError(String),
120
121 UnmatchedSymbol(Symbol),
125
126 UmatchedStandardPath(RustPath),
128
129 Crate(CrateName),
131}
132
133impl std::fmt::Display for Reason {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 match self {
136 Self::PathMatchedRule(path) | Self::UmatchedStandardPath(path) => path.fmt(f),
137 Self::SourceParseError(err) => write!(f, "{err:#?}"),
138 Self::SymbolMatchedRule(symbol) | Self::UnmatchedSymbol(symbol) => {
139 write!(f, "{}", symbol.format(false))
140 }
141 Self::Crate(crate_name) => crate_name.fmt(f),
142 }
143 }
144}
145
146impl DeducedCaps {
147 pub fn from_symbols(
148 rules: &SymbolRules,
149 symbols: impl IntoIterator<Item = Symbol>,
150 ) -> anyhow::Result<Self> {
151 let mut slf = Self::default();
152 for symbol in symbols {
153 slf.add_symbol(rules, &symbol)?;
154 }
155 Ok(slf)
156 }
157
158 pub fn from_paths(
159 rules: &SymbolRules,
160 paths: impl IntoIterator<Item = RustPath>,
161 ) -> anyhow::Result<Self> {
162 let mut slf = Self::default();
163 for path in paths {
164 slf.add_path(rules, path)?;
165 }
166 Ok(slf)
167 }
168
169 fn add_symbol(&mut self, rules: &SymbolRules, symbol: &Symbol) -> anyhow::Result<()> {
171 for path in symbol.paths() {
172 match path {
173 FunctionOrPath::Function(fun_name) => {
174 let fun_name = fun_name.trim_start_matches('_');
175
176 if let Some(capabilities) = rules.match_symbol(fun_name) {
178 for &capability in capabilities {
179 self.caps
180 .entry(capability)
181 .or_default()
182 .insert(Reason::SymbolMatchedRule(symbol.clone()));
183 }
184 } else {
185 self.caps
186 .entry(Capability::Any)
187 .or_default()
188 .insert(Reason::UnmatchedSymbol(symbol.clone()));
189 }
190 }
191
192 FunctionOrPath::RustPath(rust_path) => {
193 let path_str = rust_path.to_string();
194 if let Some(capabilities) = rules.match_symbol(&path_str) {
196 for &capability in capabilities {
197 self.caps
198 .entry(capability)
199 .or_default()
200 .insert(Reason::PathMatchedRule(rust_path.clone()));
201 }
202 } else {
203 let segments = rust_path.segments();
206 let crate_name = CrateName::new(segments[0])
207 .with_context(|| format!("mangled: {:?}", symbol.mangled))
208 .with_context(|| format!("demangled: {:?}", symbol.demangled))?;
209
210 if crate_name.is_standard_crate() {
211 self.caps
212 .entry(Capability::Any)
213 .or_default()
214 .insert(Reason::UmatchedStandardPath(rust_path.clone()));
215 } else {
216 self.unresolved_crates
218 .entry(crate_name)
219 .or_default()
220 .insert(rust_path);
221 }
222 }
223 }
224 }
225 }
226
227 Ok(())
228 }
229
230 fn add_path(&mut self, rules: &SymbolRules, rust_path: RustPath) -> anyhow::Result<()> {
231 let path_str = rust_path.to_string();
232 if let Some(capabilities) = rules.match_symbol(&path_str) {
234 for &capability in capabilities {
235 self.caps
236 .entry(capability)
237 .or_default()
238 .insert(Reason::PathMatchedRule(rust_path.clone()));
239 }
240 } else {
241 let segments = rust_path.segments();
243
244 let crate_name = segments[0];
245 let crate_name =
246 CrateName::new(crate_name).with_context(|| format!("path: {rust_path}"))?;
247 self.unresolved_crates
248 .entry(crate_name)
249 .or_default()
250 .insert(rust_path);
251 }
252
253 Ok(())
254 }
255}
256
257pub fn format_reasons(reasons: &Reasons) -> String {
258 let mut crates = vec![];
259 let mut path_matched_rules = vec![];
260 let mut symbol_matched_rules = vec![];
261 let mut unmatched_paths = vec![];
262 let mut unmatched_symbols = vec![];
263 let mut source_parse_errors = vec![];
264
265 for reason in reasons {
266 match reason {
267 Reason::Crate(crate_name) => {
268 crates.push(crate_name);
269 }
270 Reason::UmatchedStandardPath(path) => {
271 unmatched_paths.push(path);
272 }
273 Reason::UnmatchedSymbol(symbol) => {
274 unmatched_symbols.push(symbol);
275 }
276 Reason::PathMatchedRule(rust_path) => {
277 path_matched_rules.push(rust_path);
278 }
279 Reason::SymbolMatchedRule(symbol) => {
280 symbol_matched_rules.push(symbol);
281 }
282 Reason::SourceParseError(error) => {
283 source_parse_errors.push(error);
284 }
285 }
286 }
287
288 fn format_long_list<T: std::fmt::Display>(header: &str, reasons: &[T]) -> String {
289 let max_width = 60;
290 let mut string = format!("{header}:");
291 let mut num_left = reasons.len();
292 for reason in reasons.iter().reservoir_sample(5) {
293 if string.len() < max_width {
294 string += &format!(" {reason}");
295 num_left -= 1;
296 } else {
297 string += &format!(" โฆ + {num_left} more");
298 break;
299 }
300 }
301 string
302 }
303
304 if !crates.is_empty() {
305 format_long_list("dependencies", &crates)
306 } else if !path_matched_rules.is_empty() {
307 format_long_list("rule for", &path_matched_rules)
308 } else if !symbol_matched_rules.is_empty() {
309 let symbol_matched_rules = symbol_matched_rules
310 .into_iter()
311 .map(|s| &s.demangled)
312 .collect_vec();
313 format_long_list("rule for", &symbol_matched_rules)
314 } else if !unmatched_paths.is_empty() {
315 format_long_list("unknown paths", &unmatched_paths)
316 } else if !unmatched_symbols.is_empty() {
317 let unmatched_symbols = unmatched_symbols
318 .into_iter()
319 .map(|s| &s.demangled)
320 .collect_vec();
321 format_long_list("unknown symbols", &unmatched_symbols)
322 } else if !source_parse_errors.is_empty() {
323 format_long_list("source parse error", &source_parse_errors)
324 } else {
325 unreachable!()
326 }
327}