Skip to main content

source_map_tauri/linker/
edges.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Result;
4use serde_json::Value;
5
6use crate::{
7    ids::document_id,
8    model::{ArtifactDoc, EdgeDoc, WarningDoc},
9};
10
11fn edge(
12    repo: &str,
13    edge_type: &str,
14    from: &ArtifactDoc,
15    to: &ArtifactDoc,
16    reason: &str,
17    confidence: f32,
18) -> EdgeDoc {
19    EdgeDoc {
20        id: document_id(
21            repo,
22            "edge",
23            from.source_path.as_deref(),
24            from.line_start,
25            Some(&format!("{edge_type}:{}:{}", from.id, to.id)),
26        ),
27        repo: repo.to_owned(),
28        kind: "edge".to_owned(),
29        edge_type: edge_type.to_owned(),
30        from_id: from.id.clone(),
31        from_kind: from.kind.clone(),
32        from_name: from.name.clone(),
33        to_id: to.id.clone(),
34        to_kind: to.kind.clone(),
35        to_name: to.name.clone(),
36        confidence,
37        reason: reason.to_owned(),
38        source_path: from.source_path.clone(),
39        line_start: from.line_start,
40        risk_level: from.risk_level.clone(),
41        updated_at: chrono::Utc::now().to_rfc3339(),
42    }
43}
44
45fn warning(
46    repo: &str,
47    warning_type: &str,
48    message: String,
49    related: Option<&ArtifactDoc>,
50    risk_level: &str,
51) -> WarningDoc {
52    WarningDoc {
53        id: document_id(
54            repo,
55            "warning",
56            related.and_then(|item| item.source_path.as_deref()),
57            related.and_then(|item| item.line_start),
58            Some(warning_type),
59        ),
60        repo: repo.to_owned(),
61        kind: "warning".to_owned(),
62        warning_type: warning_type.to_owned(),
63        severity: if risk_level == "critical" {
64            "error"
65        } else {
66            "warning"
67        }
68        .to_owned(),
69        message,
70        source_path: related.and_then(|item| item.source_path.clone()),
71        line_start: related.and_then(|item| item.line_start),
72        related_id: related.map(|item| item.id.clone()),
73        risk_level: risk_level.to_owned(),
74        remediation: None,
75        updated_at: chrono::Utc::now().to_rfc3339(),
76    }
77}
78
79pub fn link_all(
80    artifacts: &mut [ArtifactDoc],
81    warnings: &mut Vec<WarningDoc>,
82) -> Result<Vec<EdgeDoc>> {
83    let mut edges = Vec::new();
84    let mut by_name: BTreeMap<String, Vec<usize>> = BTreeMap::new();
85    let mut by_invoke: BTreeMap<String, usize> = BTreeMap::new();
86    let mut by_kind: BTreeMap<String, Vec<usize>> = BTreeMap::new();
87
88    for (index, artifact) in artifacts.iter().enumerate() {
89        if let Some(name) = &artifact.name {
90            by_name.entry(name.clone()).or_default().push(index);
91        }
92        if let Some(invoke_key) = artifact.data.get("invoke_key").and_then(Value::as_str) {
93            by_invoke.insert(invoke_key.to_owned(), index);
94        }
95        by_kind
96            .entry(artifact.kind.clone())
97            .or_default()
98            .push(index);
99    }
100
101    let mut test_links: BTreeMap<usize, BTreeSet<String>> = BTreeMap::new();
102
103    for artifact in artifacts.iter() {
104        if artifact.kind == "frontend_hook_use" {
105            if let Some(name) = artifact.data.get("hook_def_name").and_then(Value::as_str) {
106                if let Some(target_index) =
107                    by_kind.get("frontend_hook_def").and_then(|candidates| {
108                        candidates
109                            .iter()
110                            .copied()
111                            .find(|candidate| artifacts[*candidate].name.as_deref() == Some(name))
112                    })
113                {
114                    edges.push(edge(
115                        &artifact.repo,
116                        "uses_hook",
117                        artifact,
118                        &artifacts[target_index],
119                        "hook callsite name matches hook definition",
120                        0.95,
121                    ));
122                }
123            }
124        }
125
126        if artifact.kind == "tauri_invoke" {
127            if let Some(invoke_key) = artifact.data.get("invoke_key").and_then(Value::as_str) {
128                if let Some(target_index) = by_invoke
129                    .get(invoke_key)
130                    .copied()
131                    .filter(|candidate| artifacts[*candidate].kind == "tauri_plugin_command")
132                    .or_else(|| {
133                        let name = artifact.data.get("command_name").and_then(Value::as_str)?;
134                        by_kind.get("tauri_command").and_then(|candidates| {
135                            candidates.iter().copied().find(|candidate| {
136                                artifacts[*candidate].name.as_deref() == Some(name)
137                            })
138                        })
139                    })
140                {
141                    let edge_type = if artifacts[target_index].kind == "tauri_plugin_command" {
142                        "invokes_plugin_command"
143                    } else {
144                        "invokes"
145                    };
146                    edges.push(edge(
147                        &artifact.repo,
148                        edge_type,
149                        artifact,
150                        &artifacts[target_index],
151                        "invoke key matches command registration",
152                        0.98,
153                    ));
154                }
155            }
156        }
157
158        if artifact.kind == "tauri_plugin_binding" {
159            if let Some(invoke_key) = artifact.data.get("invoke_key").and_then(Value::as_str) {
160                if let Some(target_index) = by_invoke
161                    .get(invoke_key)
162                    .copied()
163                    .filter(|candidate| artifacts[*candidate].kind == "tauri_plugin_command")
164                {
165                    edges.push(edge(
166                        &artifact.repo,
167                        "invokes_plugin_command",
168                        artifact,
169                        &artifacts[target_index],
170                        "plugin binding invoke key matches plugin command",
171                        0.99,
172                    ));
173                }
174            }
175        }
176
177        if artifact.kind == "tauri_capability" {
178            let capability_permissions = artifact
179                .data
180                .get("permissions")
181                .and_then(Value::as_array)
182                .cloned()
183                .unwrap_or_default();
184            for permission in capability_permissions.iter().filter_map(Value::as_str) {
185                if let Some(target_index) = by_kind.get("tauri_permission").and_then(|candidates| {
186                    candidates.iter().copied().find(|candidate| {
187                        artifacts[*candidate]
188                            .name
189                            .as_deref()
190                            .map(|name| {
191                                name == permission || name.ends_with(&format!(":{permission}"))
192                            })
193                            .unwrap_or(false)
194                    })
195                }) {
196                    edges.push(edge(
197                        &artifact.repo,
198                        "capability_grants_permission",
199                        artifact,
200                        &artifacts[target_index],
201                        "capability permissions include permission identifier",
202                        0.9,
203                    ));
204                }
205            }
206        }
207
208        if artifact.kind == "frontend_test" {
209            let imports = artifact
210                .data
211                .get("imports")
212                .and_then(Value::as_array)
213                .cloned()
214                .unwrap_or_default();
215            let mocked_commands = artifact
216                .data
217                .get("mocked_commands")
218                .and_then(Value::as_array)
219                .cloned()
220                .unwrap_or_default();
221            for import in imports.iter().filter_map(Value::as_str) {
222                if let Some(candidates) = by_name.get(import) {
223                    for target_index in candidates {
224                        test_links
225                            .entry(*target_index)
226                            .or_default()
227                            .insert(artifact.source_path.clone().unwrap_or_default());
228                        edges.push(edge(
229                            &artifact.repo,
230                            "tested_by",
231                            &artifacts[*target_index],
232                            artifact,
233                            "frontend test imports symbol",
234                            0.9,
235                        ));
236                    }
237                }
238            }
239            for command in mocked_commands.iter().filter_map(Value::as_str) {
240                if let Some(target_index) = by_invoke.get(command).copied() {
241                    test_links
242                        .entry(target_index)
243                        .or_default()
244                        .insert(artifact.source_path.clone().unwrap_or_default());
245                    edges.push(edge(
246                        &artifact.repo,
247                        "mocked_by",
248                        &artifacts[target_index],
249                        artifact,
250                        "frontend test mocks invoke key",
251                        0.95,
252                    ));
253                }
254            }
255        }
256
257        if artifact.kind == "rust_test" {
258            let targets = artifact
259                .data
260                .get("targets")
261                .and_then(Value::as_array)
262                .cloned()
263                .unwrap_or_default();
264            for target in targets.iter().filter_map(Value::as_str) {
265                if let Some(candidates) = by_name.get(target) {
266                    for target_index in candidates {
267                        test_links
268                            .entry(*target_index)
269                            .or_default()
270                            .insert(artifact.source_path.clone().unwrap_or_default());
271                        edges.push(edge(
272                            &artifact.repo,
273                            "tested_by",
274                            &artifacts[*target_index],
275                            artifact,
276                            "rust test target name matches artifact name",
277                            0.85,
278                        ));
279                    }
280                }
281            }
282        }
283    }
284
285    for (index, related_tests) in test_links {
286        let artifact = &mut artifacts[index];
287        artifact.related_tests = related_tests.into_iter().collect();
288        artifact.has_related_tests = !artifact.related_tests.is_empty();
289    }
290
291    let warning_exists = |warning_type: &str, related_id: &str, warnings: &[WarningDoc]| {
292        warnings.iter().any(|item| {
293            item.warning_type == warning_type && item.related_id.as_deref() == Some(related_id)
294        })
295    };
296
297    for artifact in artifacts.iter() {
298        if (artifact.kind == "tauri_command" || artifact.kind == "tauri_plugin_command")
299            && artifact.risk_level != "low"
300            && artifact.related_tests.is_empty()
301        {
302            warnings.push(warning(
303                &artifact.repo,
304                "missing_related_test",
305                format!(
306                    "{} is high-risk and has no related tests",
307                    artifact.name.clone().unwrap_or_default()
308                ),
309                Some(artifact),
310                &artifact.risk_level,
311            ));
312        }
313
314        if artifact.kind == "tauri_plugin_command" {
315            let plugin_name = artifact.data.get("plugin_name").and_then(Value::as_str);
316            let command_name = artifact.name.as_deref().unwrap_or_default();
317            let has_permission = artifacts.iter().any(|candidate| {
318                candidate.kind == "tauri_permission"
319                    && candidate.data.get("plugin_name").and_then(Value::as_str) == plugin_name
320                    && candidate
321                        .data
322                        .get("commands_allow")
323                        .and_then(Value::as_array)
324                        .map(|items| items.iter().any(|item| item.as_str() == Some(command_name)))
325                        .unwrap_or(false)
326            });
327            if !has_permission
328                && !warning_exists(
329                    "plugin_command_without_permission_evidence",
330                    &artifact.id,
331                    warnings,
332                )
333            {
334                warnings.push(warning(
335                    &artifact.repo,
336                    "plugin_command_without_permission_evidence",
337                    format!(
338                        "{} has no permission evidence",
339                        artifact.name.clone().unwrap_or_default()
340                    ),
341                    Some(artifact),
342                    &artifact.risk_level,
343                ));
344            }
345        }
346
347        if artifact.kind == "tauri_command"
348            && !warning_exists(
349                "command_without_permission_evidence",
350                &artifact.id,
351                warnings,
352            )
353        {
354            warnings.push(warning(
355                &artifact.repo,
356                "command_without_permission_evidence",
357                format!(
358                    "{} has no permission evidence",
359                    artifact.name.clone().unwrap_or_default()
360                ),
361                Some(artifact),
362                &artifact.risk_level,
363            ));
364        }
365    }
366
367    Ok(edges)
368}