source_map_tauri/frontend/
hooks.rs1use std::{collections::BTreeSet, path::Path};
2
3use anyhow::Result;
4use regex::Regex;
5use serde_json::{Map, Value};
6
7use crate::{
8 config::{normalize_path, ResolvedConfig},
9 discovery::RepoDiscovery,
10 ids::document_id,
11 model::ArtifactDoc,
12 security::apply_artifact_security,
13};
14
15fn line_number(text: &str, offset: usize) -> u32 {
16 text[..offset].bytes().filter(|byte| *byte == b'\n').count() as u32 + 1
17}
18
19fn base_artifact(
20 config: &ResolvedConfig,
21 path: &Path,
22 kind: &str,
23 name: &str,
24 line: u32,
25) -> ArtifactDoc {
26 let source_path = normalize_path(&config.root, path);
27 let mut doc = ArtifactDoc {
28 id: document_id(
29 &config.repo,
30 kind,
31 Some(&source_path),
32 Some(line),
33 Some(name),
34 ),
35 repo: config.repo.clone(),
36 kind: kind.to_owned(),
37 side: Some("frontend".to_owned()),
38 language: crate::frontend::language_for_path(path),
39 name: Some(name.to_owned()),
40 display_name: Some(name.to_owned()),
41 source_path: Some(source_path),
42 line_start: Some(line),
43 line_end: Some(line),
44 column_start: None,
45 column_end: None,
46 package_name: None,
47 comments: Vec::new(),
48 tags: Vec::new(),
49 related_symbols: Vec::new(),
50 related_tests: Vec::new(),
51 risk_level: "low".to_owned(),
52 risk_reasons: Vec::new(),
53 contains_phi: false,
54 has_related_tests: false,
55 updated_at: chrono::Utc::now().to_rfc3339(),
56 data: Map::new(),
57 };
58 apply_artifact_security(&mut doc);
59 doc
60}
61
62pub fn extract_components_and_hooks(
63 config: &ResolvedConfig,
64 path: &Path,
65 text: &str,
66 known_hooks: &BTreeSet<String>,
67) -> Vec<ArtifactDoc> {
68 let export_fn =
69 Regex::new(r"(?m)^\s*export\s+function\s+([A-Za-z0-9_]+)").expect("valid regex");
70 let hook_call = Regex::new(r"\b(use[A-Z][A-Za-z0-9_]*)\(").expect("valid regex");
71
72 let mut docs = Vec::new();
73 let mut component_names = Vec::new();
74
75 for capture in export_fn.captures_iter(text) {
76 let whole = capture.get(0).expect("match");
77 let name = capture.get(1).expect("name").as_str();
78 let line = line_number(text, whole.start());
79 if name.starts_with("use") {
80 let mut doc = base_artifact(config, path, "frontend_hook_def", name, line);
81 let hook_kind = if text.contains("new Channel") || text.contains("Channel<") {
82 "channel_stream"
83 } else if text.contains("listen(") || text.contains("once(") {
84 "event_subscription"
85 } else if text.contains("invoke(") {
86 "invoke_once"
87 } else {
88 "unknown"
89 };
90 doc.display_name = Some(format!("{name} hook"));
91 doc.tags = vec!["custom hook".to_owned()];
92 doc.data
93 .insert("hook_kind".to_owned(), Value::String(hook_kind.to_owned()));
94 doc.data.insert(
95 "requires_cleanup".to_owned(),
96 Value::Bool(text.contains("listen(") || text.contains("once(")),
97 );
98 doc.data.insert(
99 "cleanup_present".to_owned(),
100 Value::Bool(text.contains("return () =>") || text.contains("unlisten")),
101 );
102 apply_artifact_security(&mut doc);
103 docs.push(doc);
104 } else if name
105 .chars()
106 .next()
107 .map(|item| item.is_uppercase())
108 .unwrap_or(false)
109 {
110 let mut doc = base_artifact(config, path, "frontend_component", name, line);
111 doc.display_name = Some(format!("{name} component"));
112 doc.tags = vec!["component".to_owned()];
113 doc.data
114 .insert("component".to_owned(), Value::String(name.to_owned()));
115 apply_artifact_security(&mut doc);
116 component_names.push(name.to_owned());
117 docs.push(doc);
118 }
119 }
120
121 for capture in hook_call.captures_iter(text) {
122 let whole = capture.get(0).expect("match");
123 let name = capture.get(1).expect("name").as_str();
124 if !known_hooks.contains(name) {
125 continue;
126 }
127 if text[whole.start()..whole.end()].starts_with("function ") {
128 continue;
129 }
130 let line = line_number(text, whole.start());
131 let mut doc = base_artifact(config, path, "frontend_hook_use", name, line);
132 if let Some(component) = component_names.first() {
133 doc.data
134 .insert("component".to_owned(), Value::String(component.clone()));
135 doc.display_name = Some(format!("{component} uses {name}"));
136 }
137 doc.data
138 .insert("hook_kind".to_owned(), Value::String("unknown".to_owned()));
139 doc.data
140 .insert("hook_def_name".to_owned(), Value::String(name.to_owned()));
141 apply_artifact_security(&mut doc);
142 docs.push(doc);
143 }
144
145 docs
146}
147
148pub fn discover_hook_names(discovery: &RepoDiscovery) -> Result<BTreeSet<String>> {
149 let export_fn =
150 Regex::new(r"(?m)^\s*export\s+function\s+(use[A-Z][A-Za-z0-9_]*)").expect("valid regex");
151 let mut names = BTreeSet::new();
152
153 for path in &discovery.frontend_files {
154 let text = std::fs::read_to_string(path)?;
155 for capture in export_fn.captures_iter(&text) {
156 if let Some(name) = capture.get(1) {
157 names.insert(name.as_str().to_owned());
158 }
159 }
160 }
161
162 Ok(names)
163}