Skip to main content

source_map_tauri/frontend/
tauri_calls.rs

1use std::path::Path;
2
3use regex::Regex;
4use serde_json::{Map, Value};
5
6use crate::{
7    config::{normalize_path, ResolvedConfig},
8    ids::document_id,
9    model::{ArtifactDoc, WarningDoc},
10    security::apply_artifact_security,
11};
12
13fn line_number(text: &str, offset: usize) -> u32 {
14    text[..offset].bytes().filter(|byte| *byte == b'\n').count() as u32 + 1
15}
16
17fn new_artifact(
18    config: &ResolvedConfig,
19    path: &Path,
20    kind: &str,
21    name: &str,
22    line: u32,
23) -> ArtifactDoc {
24    let source_path = normalize_path(&config.root, path);
25    ArtifactDoc {
26        id: document_id(
27            &config.repo,
28            kind,
29            Some(&source_path),
30            Some(line),
31            Some(name),
32        ),
33        repo: config.repo.clone(),
34        kind: kind.to_owned(),
35        side: Some("frontend".to_owned()),
36        language: crate::frontend::language_for_path(path),
37        name: Some(name.to_owned()),
38        display_name: Some(name.to_owned()),
39        source_path: Some(source_path),
40        line_start: Some(line),
41        line_end: Some(line),
42        column_start: None,
43        column_end: None,
44        package_name: None,
45        comments: Vec::new(),
46        tags: Vec::new(),
47        related_symbols: Vec::new(),
48        related_tests: Vec::new(),
49        risk_level: "low".to_owned(),
50        risk_reasons: Vec::new(),
51        contains_phi: false,
52        has_related_tests: false,
53        updated_at: chrono::Utc::now().to_rfc3339(),
54        data: Map::new(),
55    }
56}
57
58fn warning(
59    config: &ResolvedConfig,
60    path: &Path,
61    warning_type: &str,
62    message: &str,
63    line: u32,
64) -> WarningDoc {
65    let source_path = normalize_path(&config.root, path);
66    WarningDoc {
67        id: document_id(
68            &config.repo,
69            "warning",
70            Some(&source_path),
71            Some(line),
72            Some(warning_type),
73        ),
74        repo: config.repo.clone(),
75        kind: "warning".to_owned(),
76        warning_type: warning_type.to_owned(),
77        severity: "warning".to_owned(),
78        message: message.to_owned(),
79        source_path: Some(source_path),
80        line_start: Some(line),
81        related_id: None,
82        risk_level: "medium".to_owned(),
83        remediation: None,
84        updated_at: chrono::Utc::now().to_rfc3339(),
85    }
86}
87
88pub fn extract_calls(
89    config: &ResolvedConfig,
90    path: &Path,
91    text: &str,
92    guest_binding: bool,
93) -> (Vec<ArtifactDoc>, Vec<WarningDoc>) {
94    let invoke_re = Regex::new(
95        r#"(?:\binvoke|\btauri\.invoke|\bwindow\.__TAURI__\.invoke)\(\s*['"]([^'"]+)['"]"#,
96    )
97    .expect("valid regex");
98    let event_re = Regex::new(r#"\b(listen|once|emit)\(\s*['"]([^'"]+)['"]"#).expect("valid regex");
99    let channel_re =
100        Regex::new(r"\b(?:const|let)\s+([A-Za-z0-9_]+)\s*=\s*new\s+Channel").expect("valid regex");
101    let export_fn =
102        Regex::new(r"(?m)^\s*export\s+async\s+function\s+([A-Za-z0-9_]+)").expect("valid regex");
103
104    let mut artifacts = Vec::new();
105    let mut warnings = Vec::new();
106
107    for capture in invoke_re.captures_iter(text) {
108        let whole = capture.get(0).expect("match");
109        let invoke_key = capture.get(1).expect("key").as_str();
110        let line = line_number(text, whole.start());
111        let name = invoke_key
112            .split('|')
113            .next_back()
114            .unwrap_or(invoke_key)
115            .split(':')
116            .next_back()
117            .unwrap_or(invoke_key);
118        let mut doc = new_artifact(config, path, "tauri_invoke", name, line);
119        doc.display_name = Some(format!("invoke {invoke_key}"));
120        doc.tags = vec!["tauri invoke".to_owned()];
121        doc.data.insert(
122            "invoke_key".to_owned(),
123            Value::String(invoke_key.to_owned()),
124        );
125        doc.data
126            .insert("command_name".to_owned(), Value::String(name.to_owned()));
127        if let Some(plugin_name) = invoke_key
128            .strip_prefix("plugin:")
129            .and_then(|value| value.split('|').next())
130        {
131            doc.data.insert(
132                "plugin_name".to_owned(),
133                Value::String(plugin_name.to_owned()),
134            );
135        }
136        apply_artifact_security(&mut doc);
137        artifacts.push(doc);
138    }
139
140    for capture in channel_re.captures_iter(text) {
141        let whole = capture.get(0).expect("match");
142        let channel_name = capture.get(1).expect("name").as_str();
143        let line = line_number(text, whole.start());
144        let mut doc = new_artifact(config, path, "tauri_channel", channel_name, line);
145        doc.display_name = Some(format!("Channel {channel_name}"));
146        doc.data.insert(
147            "channel_name".to_owned(),
148            Value::String(channel_name.to_owned()),
149        );
150        apply_artifact_security(&mut doc);
151        artifacts.push(doc);
152    }
153
154    for capture in event_re.captures_iter(text) {
155        let whole = capture.get(0).expect("match");
156        let verb = capture.get(1).expect("verb").as_str();
157        let event_name = capture.get(2).expect("event").as_str();
158        let line = line_number(text, whole.start());
159        let kind = match verb {
160            "emit" => "tauri_event_emit",
161            _ => "tauri_event_listener",
162        };
163        let mut doc = new_artifact(config, path, kind, event_name, line);
164        doc.data.insert(
165            "event_name".to_owned(),
166            Value::String(event_name.to_owned()),
167        );
168        doc.tags = vec!["event".to_owned()];
169        apply_artifact_security(&mut doc);
170        artifacts.push(doc);
171    }
172
173    if guest_binding {
174        for capture in export_fn.captures_iter(text) {
175            let whole = capture.get(0).expect("match");
176            let export_name = capture.get(1).expect("name").as_str();
177            let line = line_number(text, whole.start());
178            let mut doc = new_artifact(config, path, "tauri_plugin_binding", export_name, line);
179            doc.display_name = Some(format!("{export_name} plugin binding"));
180            doc.data.insert(
181                "plugin_export".to_owned(),
182                Value::String(export_name.to_owned()),
183            );
184            if let Some(call) = invoke_re.captures(text) {
185                let invoke_key = call.get(1).expect("invoke key").as_str();
186                doc.data.insert(
187                    "invoke_key".to_owned(),
188                    Value::String(invoke_key.to_owned()),
189                );
190                if let Some(plugin_name) = invoke_key
191                    .strip_prefix("plugin:")
192                    .and_then(|value| value.split('|').next())
193                {
194                    doc.data.insert(
195                        "plugin_name".to_owned(),
196                        Value::String(plugin_name.to_owned()),
197                    );
198                }
199            }
200            apply_artifact_security(&mut doc);
201            artifacts.push(doc);
202        }
203    }
204
205    let dynamic_invoke = Regex::new(
206        r"\b(?:invoke|tauri\.invoke|window\.__TAURI__\.invoke)\(\s*([A-Za-z_][A-Za-z0-9_]*)",
207    )
208    .expect("valid regex");
209    for capture in dynamic_invoke.captures_iter(text) {
210        let variable = capture.get(1).expect("name").as_str();
211        let line = line_number(text, capture.get(0).expect("match").start());
212        warnings.push(warning(
213            config,
214            path,
215            "dynamic_invoke",
216            &format!("Cannot statically resolve Tauri command name from {variable}"),
217            line,
218        ));
219    }
220
221    (artifacts, warnings)
222}