1use std::collections::{BTreeMap, BTreeSet};
4
5use serde::Serialize;
6
7use crate::{ControlFlowAccessV1, ControlFlowGraphV1, ControlFlowProvenanceV1};
8
9pub const FUNCTION_SUMMARY_SCHEMA_VERSION: u32 = 1;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
13pub struct FunctionCallFactV1 {
14 pub caller: String,
15 pub callee: Option<String>,
16 pub provenance: ControlFlowProvenanceV1,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum FunctionCallCoverageV1 {
23 Complete,
24 Unavailable,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29pub struct FunctionCallFactsV1 {
30 pub coverage: FunctionCallCoverageV1,
31 pub calls: Vec<FunctionCallFactV1>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
36pub struct FunctionSummaryV1 {
37 pub id: String,
38 pub module_path: String,
39 pub direct_reads: Vec<ControlFlowAccessV1>,
40 pub direct_writes: Vec<ControlFlowAccessV1>,
41 pub direct_callees: Vec<String>,
42 pub has_direct_unknown_call: bool,
43 pub call_coverage: FunctionCallCoverageV1,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub transitive_reads: Option<Vec<ControlFlowAccessV1>>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub transitive_writes: Option<Vec<ControlFlowAccessV1>>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub transitive_callees: Option<Vec<String>>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub has_transitive_unknown_call: Option<bool>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
56pub struct FunctionSummaryGraphV1 {
57 pub schema_version: u32,
58 pub summaries: Vec<FunctionSummaryV1>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum FunctionSummaryErrorV1 {
64 UnknownCaller(String),
65 UnknownCallee { caller: String, callee: String },
66}
67
68impl std::fmt::Display for FunctionSummaryErrorV1 {
69 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 Self::UnknownCaller(caller) => {
72 write!(formatter, "call fact references unknown caller {caller}")
73 }
74 Self::UnknownCallee { caller, callee } => {
75 write!(
76 formatter,
77 "call fact from {caller} references unknown callee {callee}"
78 )
79 }
80 }
81 }
82}
83
84impl std::error::Error for FunctionSummaryErrorV1 {}
85
86pub fn build_function_summary_graph_v1(
88 control_flow: &ControlFlowGraphV1,
89 call_facts: &FunctionCallFactsV1,
90) -> Result<FunctionSummaryGraphV1, FunctionSummaryErrorV1> {
91 let functions = control_flow
92 .functions
93 .iter()
94 .map(|function| (function.id.clone(), function))
95 .collect::<BTreeMap<_, _>>();
96 let mut calls = functions
97 .keys()
98 .cloned()
99 .map(|id| (id, Vec::new()))
100 .collect::<BTreeMap<_, Vec<&FunctionCallFactV1>>>();
101 for fact in &call_facts.calls {
102 if !functions.contains_key(&fact.caller) {
103 return Err(FunctionSummaryErrorV1::UnknownCaller(fact.caller.clone()));
104 }
105 if let Some(callee) = &fact.callee {
106 if !functions.contains_key(callee) {
107 return Err(FunctionSummaryErrorV1::UnknownCallee {
108 caller: fact.caller.clone(),
109 callee: callee.clone(),
110 });
111 }
112 }
113 calls.entry(fact.caller.clone()).or_default().push(fact);
114 }
115
116 let direct = functions
117 .iter()
118 .map(|(id, function)| {
119 let reads = function
120 .blocks
121 .iter()
122 .flat_map(|block| block.reads.iter().cloned())
123 .collect::<BTreeSet<_>>();
124 let writes = function
125 .blocks
126 .iter()
127 .flat_map(|block| block.writes.iter().cloned())
128 .collect::<BTreeSet<_>>();
129 let callees = calls[id]
130 .iter()
131 .filter_map(|fact| fact.callee.clone())
132 .collect::<BTreeSet<_>>();
133 let unknown = calls[id].iter().any(|fact| fact.callee.is_none());
134 (
135 id.clone(),
136 SummaryAggregate {
137 reads,
138 writes,
139 callees,
140 unknown,
141 },
142 )
143 })
144 .collect::<BTreeMap<_, _>>();
145 let transitive = match call_facts.coverage {
146 FunctionCallCoverageV1::Complete => Some(derive_transitive_summaries(&direct)),
147 FunctionCallCoverageV1::Unavailable => None,
148 };
149 let summaries = functions
150 .iter()
151 .map(|(id, function)| {
152 let direct = &direct[id];
153 let transitive = transitive.as_ref().map(|summaries| &summaries[id]);
154 FunctionSummaryV1 {
155 id: id.clone(),
156 module_path: function.module_path.clone(),
157 direct_reads: direct.reads.iter().cloned().collect(),
158 direct_writes: direct.writes.iter().cloned().collect(),
159 direct_callees: direct.callees.iter().cloned().collect(),
160 has_direct_unknown_call: direct.unknown,
161 call_coverage: call_facts.coverage,
162 transitive_reads: transitive.map(|summary| summary.reads.iter().cloned().collect()),
163 transitive_writes: transitive
164 .map(|summary| summary.writes.iter().cloned().collect()),
165 transitive_callees: transitive
166 .map(|summary| summary.callees.iter().cloned().collect()),
167 has_transitive_unknown_call: transitive.map(|summary| summary.unknown),
168 }
169 })
170 .collect();
171 Ok(FunctionSummaryGraphV1 {
172 schema_version: FUNCTION_SUMMARY_SCHEMA_VERSION,
173 summaries,
174 })
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178struct SummaryAggregate {
179 reads: BTreeSet<ControlFlowAccessV1>,
180 writes: BTreeSet<ControlFlowAccessV1>,
181 callees: BTreeSet<String>,
182 unknown: bool,
183}
184
185fn derive_transitive_summaries(
186 direct: &BTreeMap<String, SummaryAggregate>,
187) -> BTreeMap<String, SummaryAggregate> {
188 let mut summaries = direct.clone();
189 let mut changed = true;
190 while changed {
191 changed = false;
192 for (id, direct_summary) in direct {
193 let mut next = direct_summary.clone();
194 for callee in &direct_summary.callees {
195 let callee_summary = &summaries[callee];
196 next.reads.extend(callee_summary.reads.iter().cloned());
197 next.writes.extend(callee_summary.writes.iter().cloned());
198 next.callees.extend(callee_summary.callees.iter().cloned());
199 next.unknown |= callee_summary.unknown;
200 }
201 if summaries.get(id) != Some(&next) {
202 summaries.insert(id.clone(), next);
203 changed = true;
204 }
205 }
206 }
207 summaries
208}
209
210#[cfg(test)]
211mod tests {
212 use crate::{
213 ControlFlowAccessKindV1, ControlFlowAccessV1, ControlFlowBlockV1,
214 ControlFlowCoverageStatusV1, ControlFlowCoverageV1, ControlFlowFunctionV1,
215 ControlFlowGraphV1, ControlFlowProvenanceV1,
216 };
217
218 use super::{
219 build_function_summary_graph_v1, FunctionCallCoverageV1, FunctionCallFactV1,
220 FunctionCallFactsV1,
221 };
222
223 fn provenance() -> ControlFlowProvenanceV1 {
224 ControlFlowProvenanceV1 {
225 path: "src/App.tsx".into(),
226 start: 0,
227 end: 1,
228 line: 1,
229 column: 1,
230 }
231 }
232
233 fn function(id: &str, module_path: &str, read: &str, write: &str) -> ControlFlowFunctionV1 {
234 ControlFlowFunctionV1 {
235 module_path: module_path.into(),
236 id: id.into(),
237 name: id.into(),
238 provenance: provenance(),
239 entry_block: format!("{id}/entry"),
240 blocks: vec![ControlFlowBlockV1 {
241 id: format!("{id}/entry"),
242 provenance: provenance(),
243 observable_instructions: Vec::new(),
244 reads: vec![ControlFlowAccessV1 {
245 kind: ControlFlowAccessKindV1::Storage,
246 id: read.into(),
247 }],
248 writes: vec![ControlFlowAccessV1 {
249 kind: ControlFlowAccessKindV1::Storage,
250 id: write.into(),
251 }],
252 }],
253 branch_edges: Vec::new(),
254 loops: Vec::new(),
255 coverage: ControlFlowCoverageV1 {
256 branch_topology: ControlFlowCoverageStatusV1::Available,
257 definite_dataflow: ControlFlowCoverageStatusV1::Available,
258 natural_loops: ControlFlowCoverageStatusV1::Available,
259 exception_paths: ControlFlowCoverageStatusV1::Unavailable,
260 async_suspension: ControlFlowCoverageStatusV1::Unavailable,
261 unknown_calls: ControlFlowCoverageStatusV1::Unavailable,
262 capture_escape: ControlFlowCoverageStatusV1::Unavailable,
263 resource_cancellation: ControlFlowCoverageStatusV1::Unavailable,
264 },
265 }
266 }
267
268 #[test]
269 fn summarizes_cross_module_calls_transitively_and_keeps_unknown_calls_conservative() {
270 let graph = ControlFlowGraphV1 {
271 schema_version: 1,
272 functions: vec![
273 function("app", "src/App.tsx", "app-read", "app-write"),
274 function("helper", "src/helper.ts", "helper-read", "helper-write"),
275 ],
276 };
277 let facts = FunctionCallFactsV1 {
278 coverage: FunctionCallCoverageV1::Complete,
279 calls: vec![
280 FunctionCallFactV1 {
281 caller: "app".into(),
282 callee: Some("helper".into()),
283 provenance: provenance(),
284 },
285 FunctionCallFactV1 {
286 caller: "helper".into(),
287 callee: None,
288 provenance: provenance(),
289 },
290 ],
291 };
292 let summary = build_function_summary_graph_v1(&graph, &facts).expect("valid call facts");
293 let app = summary
294 .summaries
295 .iter()
296 .find(|summary| summary.id == "app")
297 .unwrap();
298 assert_eq!(app.direct_callees, vec!["helper"]);
299 assert!(app
300 .transitive_reads
301 .as_ref()
302 .unwrap()
303 .iter()
304 .any(|access| access.id == "helper-read"));
305 assert!(app.has_transitive_unknown_call.unwrap());
306 }
307
308 #[test]
309 fn leaves_transitive_facts_unavailable_when_call_coverage_is_unavailable() {
310 let graph = ControlFlowGraphV1 {
311 schema_version: 1,
312 functions: vec![function("app", "src/App.tsx", "read", "write")],
313 };
314 let facts = FunctionCallFactsV1 {
315 coverage: FunctionCallCoverageV1::Unavailable,
316 calls: Vec::new(),
317 };
318 let summary = build_function_summary_graph_v1(&graph, &facts).expect("valid empty facts");
319 assert!(summary.summaries[0].transitive_reads.is_none());
320 assert!(summary.summaries[0].has_transitive_unknown_call.is_none());
321 }
322}