source_map_tauri/frontend/
tauri_calls.rs1use 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}