1use regex::Regex;
2
3macro_rules! static_regex {
4 ($pattern:expr) => {{
5 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
6 RE.get_or_init(|| {
7 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
8 })
9 }};
10}
11
12#[derive(Debug, Clone)]
13pub struct Signature {
14 pub kind: &'static str,
15 pub name: String,
16 pub params: String,
17 pub return_type: String,
18 pub is_async: bool,
19 pub is_exported: bool,
20 pub indent: usize,
21 pub start_line: Option<usize>,
22 pub end_line: Option<usize>,
23}
24
25impl Signature {
26 pub fn no_span() -> Self {
27 Self {
28 kind: "",
29 name: String::new(),
30 params: String::new(),
31 return_type: String::new(),
32 is_async: false,
33 is_exported: false,
34 indent: 0,
35 start_line: None,
36 end_line: None,
37 }
38 }
39
40 pub fn to_compact(&self) -> String {
41 let export = if self.is_exported { "⊛ " } else { "" };
42 let async_prefix = if self.is_async { "async " } else { "" };
43
44 match self.kind {
45 "fn" | "method" => {
46 let ret = if self.return_type.is_empty() {
47 String::new()
48 } else {
49 format!(" → {}", self.return_type)
50 };
51 let indent = " ".repeat(self.indent);
52 format!(
53 "{indent}fn {async_prefix}{export}{}({}){}",
54 self.name, self.params, ret
55 )
56 }
57 "class" | "struct" => format!("cl {export}{}", self.name),
58 "interface" | "trait" => format!("if {export}{}", self.name),
59 "type" => format!("ty {export}{}", self.name),
60 "enum" => format!("en {export}{}", self.name),
61 "const" | "let" | "var" => {
62 let ty = if self.return_type.is_empty() {
63 String::new()
64 } else {
65 format!(":{}", self.return_type)
66 };
67 format!("val {export}{}{ty}", self.name)
68 }
69 _ => format!("{} {}", self.kind, self.name),
70 }
71 }
72
73 pub fn to_tdd(&self) -> String {
74 let vis = if self.is_exported { "+" } else { "-" };
75 let a = if self.is_async { "~" } else { "" };
76
77 match self.kind {
78 "fn" | "method" => {
79 let ret = if self.return_type.is_empty() {
80 String::new()
81 } else {
82 format!("→{}", compact_type(&self.return_type))
83 };
84 let params = tdd_params(&self.params);
85 let indent = if self.indent > 0 { " " } else { "" };
86 format!("{indent}{a}λ{vis}{}({params}){ret}", self.name)
87 }
88 "class" | "struct" => format!("§{vis}{}", self.name),
89 "interface" | "trait" => format!("∂{vis}{}", self.name),
90 "type" => format!("τ{vis}{}", self.name),
91 "enum" => format!("ε{vis}{}", self.name),
92 "const" | "let" | "var" => {
93 let ty = if self.return_type.is_empty() {
94 String::new()
95 } else {
96 format!(":{}", compact_type(&self.return_type))
97 };
98 format!("ν{vis}{}{ty}", self.name)
99 }
100 _ => format!(
101 "{}{vis}{}",
102 self.kind.chars().next().unwrap_or('?'),
103 self.name
104 ),
105 }
106 }
107}
108
109fn fn_re() -> &'static Regex {
110 static_regex!(
111 r"^(\s*)(export\s+)?(async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\{]+))?\s*\{?"
112 )
113}
114
115fn class_re() -> &'static Regex {
116 static_regex!(r"^(\s*)(export\s+)?(abstract\s+)?class\s+(\w+)")
117}
118
119fn iface_re() -> &'static Regex {
120 static_regex!(r"^(\s*)(export\s+)?interface\s+(\w+)")
121}
122
123fn type_re() -> &'static Regex {
124 static_regex!(r"^(\s*)(export\s+)?type\s+(\w+)")
125}
126
127fn const_re() -> &'static Regex {
128 static_regex!(r"^(\s*)(export\s+)?(const|let|var)\s+(\w+)(?:\s*:\s*(\w+))?")
129}
130
131fn rust_fn_re() -> &'static Regex {
132 static_regex!(
133 r"^(\s*)(pub\s+)?(async\s+)?fn\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*->\s*([^\{]+))?\s*\{?"
134 )
135}
136
137fn rust_struct_re() -> &'static Regex {
138 static_regex!(r"^(\s*)(pub\s+)?struct\s+(\w+)")
139}
140
141fn rust_enum_re() -> &'static Regex {
142 static_regex!(r"^(\s*)(pub\s+)?enum\s+(\w+)")
143}
144
145fn rust_trait_re() -> &'static Regex {
146 static_regex!(r"^(\s*)(pub\s+)?trait\s+(\w+)")
147}
148
149fn rust_impl_re() -> &'static Regex {
150 static_regex!(r"^(\s*)impl\s+(?:(\w+)\s+for\s+)?(\w+)")
151}
152
153use std::sync::atomic::{AtomicU64, Ordering};
154
155static TREE_SITTER_HITS: AtomicU64 = AtomicU64::new(0);
156static REGEX_FALLBACK_HITS: AtomicU64 = AtomicU64::new(0);
157
158pub fn signature_backend_stats() -> (u64, u64) {
160 (
161 TREE_SITTER_HITS.load(Ordering::Relaxed),
162 REGEX_FALLBACK_HITS.load(Ordering::Relaxed),
163 )
164}
165
166pub fn extract_signatures(content: &str, file_ext: &str) -> Vec<Signature> {
167 #[cfg(feature = "tree-sitter")]
168 {
169 if let Some(sigs) = super::signatures_ts::extract_signatures_ts(content, file_ext) {
170 TREE_SITTER_HITS.fetch_add(1, Ordering::Relaxed);
171 return sigs;
172 }
173 }
174
175 REGEX_FALLBACK_HITS.fetch_add(1, Ordering::Relaxed);
176 match file_ext {
177 "rs" => extract_rust_signatures(content),
178 "ts" | "tsx" | "js" | "jsx" | "svelte" | "vue" => extract_ts_signatures(content),
179 "py" => extract_python_signatures(content),
180 "go" => extract_go_signatures(content),
181 _ => extract_generic_signatures(content),
182 }
183}
184
185pub fn extract_file_map(path: &str, content: &str) -> String {
186 let ext = std::path::Path::new(path)
187 .extension()
188 .and_then(|e| e.to_str())
189 .unwrap_or("rs");
190 let dep_info = super::deps::extract_deps(content, ext);
191 let sigs = extract_signatures(content, ext);
192 let mut parts = Vec::new();
193 if !dep_info.imports.is_empty() {
194 parts.push(dep_info.imports.join(","));
195 }
196 let key_sigs: Vec<String> = sigs
197 .iter()
198 .filter(|s| s.is_exported || s.indent == 0)
199 .map(Signature::to_compact)
200 .collect();
201 if !key_sigs.is_empty() {
202 parts.push(key_sigs.join("\n"));
203 }
204 parts.join("\n")
205}
206
207fn extract_ts_signatures(content: &str) -> Vec<Signature> {
208 let mut sigs = Vec::new();
209
210 for line in content.lines() {
211 let trimmed = line.trim();
212 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') {
213 continue;
214 }
215
216 if let Some(caps) = fn_re().captures(line) {
217 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
218 sigs.push(Signature {
219 kind: if indent > 0 { "method" } else { "fn" },
220 name: caps[4].to_string(),
221 params: compact_params(&caps[5]),
222 return_type: caps
223 .get(6)
224 .map_or(String::new(), |m| m.as_str().trim().to_string()),
225 is_async: caps.get(3).is_some(),
226 is_exported: caps.get(2).is_some(),
227 indent: if indent > 0 { 2 } else { 0 },
228 ..Signature::no_span()
229 });
230 } else if let Some(caps) = class_re().captures(line) {
231 sigs.push(Signature {
232 kind: "class",
233 name: caps[4].to_string(),
234 params: String::new(),
235 return_type: String::new(),
236 is_async: false,
237 is_exported: caps.get(2).is_some(),
238 indent: 0,
239 ..Signature::no_span()
240 });
241 } else if let Some(caps) = iface_re().captures(line) {
242 sigs.push(Signature {
243 kind: "interface",
244 name: caps[3].to_string(),
245 params: String::new(),
246 return_type: String::new(),
247 is_async: false,
248 is_exported: caps.get(2).is_some(),
249 indent: 0,
250 ..Signature::no_span()
251 });
252 } else if let Some(caps) = type_re().captures(line) {
253 sigs.push(Signature {
254 kind: "type",
255 name: caps[3].to_string(),
256 params: String::new(),
257 return_type: String::new(),
258 is_async: false,
259 is_exported: caps.get(2).is_some(),
260 indent: 0,
261 ..Signature::no_span()
262 });
263 } else if let Some(caps) = const_re().captures(line) {
264 if caps.get(2).is_some() {
265 sigs.push(Signature {
266 kind: "const",
267 name: caps[4].to_string(),
268 params: String::new(),
269 return_type: caps
270 .get(5)
271 .map_or(String::new(), |m| m.as_str().to_string()),
272 is_async: false,
273 is_exported: true,
274 indent: 0,
275 ..Signature::no_span()
276 });
277 }
278 }
279 }
280
281 sigs
282}
283
284fn extract_rust_signatures(content: &str) -> Vec<Signature> {
285 let mut sigs = Vec::new();
286
287 for line in content.lines() {
288 let trimmed = line.trim();
289 if trimmed.starts_with("//") || trimmed.starts_with("///") {
290 continue;
291 }
292
293 if let Some(caps) = rust_fn_re().captures(line) {
294 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
295 sigs.push(Signature {
296 kind: if indent > 0 { "method" } else { "fn" },
297 name: caps[4].to_string(),
298 params: compact_params(&caps[5]),
299 return_type: caps
300 .get(6)
301 .map_or(String::new(), |m| m.as_str().trim().to_string()),
302 is_async: caps.get(3).is_some(),
303 is_exported: caps.get(2).is_some(),
304 indent: if indent > 0 { 2 } else { 0 },
305 ..Signature::no_span()
306 });
307 } else if let Some(caps) = rust_struct_re().captures(line) {
308 sigs.push(Signature {
309 kind: "struct",
310 name: caps[3].to_string(),
311 params: String::new(),
312 return_type: String::new(),
313 is_async: false,
314 is_exported: caps.get(2).is_some(),
315 indent: 0,
316 ..Signature::no_span()
317 });
318 } else if let Some(caps) = rust_enum_re().captures(line) {
319 sigs.push(Signature {
320 kind: "enum",
321 name: caps[3].to_string(),
322 params: String::new(),
323 return_type: String::new(),
324 is_async: false,
325 is_exported: caps.get(2).is_some(),
326 indent: 0,
327 ..Signature::no_span()
328 });
329 } else if let Some(caps) = rust_trait_re().captures(line) {
330 sigs.push(Signature {
331 kind: "trait",
332 name: caps[3].to_string(),
333 params: String::new(),
334 return_type: String::new(),
335 is_async: false,
336 is_exported: caps.get(2).is_some(),
337 indent: 0,
338 ..Signature::no_span()
339 });
340 } else if let Some(caps) = rust_impl_re().captures(line) {
341 let trait_name = caps.get(2).map(|m| m.as_str());
342 let type_name = &caps[3];
343 let name = if let Some(t) = trait_name {
344 format!("{t} for {type_name}")
345 } else {
346 type_name.to_string()
347 };
348 sigs.push(Signature {
349 kind: "class",
350 name,
351 params: String::new(),
352 return_type: String::new(),
353 is_async: false,
354 is_exported: false,
355 indent: 0,
356 ..Signature::no_span()
357 });
358 }
359 }
360
361 sigs
362}
363
364fn extract_python_signatures(content: &str) -> Vec<Signature> {
365 let mut sigs = Vec::new();
366 let py_fn = static_regex!(r"^(\s*)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?");
367 let py_class = static_regex!(r"^(\s*)class\s+(\w+)");
368
369 for line in content.lines() {
370 if let Some(caps) = py_fn.captures(line) {
371 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
372 sigs.push(Signature {
373 kind: if indent > 0 { "method" } else { "fn" },
374 name: caps[3].to_string(),
375 params: compact_params(&caps[4]),
376 return_type: caps
377 .get(5)
378 .map_or(String::new(), |m| m.as_str().to_string()),
379 is_async: caps.get(2).is_some(),
380 is_exported: !caps[3].starts_with('_'),
381 indent: if indent > 0 { 2 } else { 0 },
382 ..Signature::no_span()
383 });
384 } else if let Some(caps) = py_class.captures(line) {
385 sigs.push(Signature {
386 kind: "class",
387 name: caps[2].to_string(),
388 params: String::new(),
389 return_type: String::new(),
390 is_async: false,
391 is_exported: !caps[2].starts_with('_'),
392 indent: 0,
393 ..Signature::no_span()
394 });
395 }
396 }
397
398 sigs
399}
400
401fn extract_go_signatures(content: &str) -> Vec<Signature> {
402 let mut sigs = Vec::new();
403 let go_fn = static_regex!(
404 r"^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|(\w+)))?\s*\{"
405 );
406 let go_type = static_regex!(r"^type\s+(\w+)\s+(struct|interface)");
407
408 for line in content.lines() {
409 if let Some(caps) = go_fn.captures(line) {
410 let is_method = caps.get(2).is_some();
411 sigs.push(Signature {
412 kind: if is_method { "method" } else { "fn" },
413 name: caps[3].to_string(),
414 params: compact_params(&caps[4]),
415 return_type: caps
416 .get(5)
417 .or(caps.get(6))
418 .map_or(String::new(), |m| m.as_str().to_string()),
419 is_async: false,
420 is_exported: caps[3].starts_with(char::is_uppercase),
421 indent: if is_method { 2 } else { 0 },
422 ..Signature::no_span()
423 });
424 } else if let Some(caps) = go_type.captures(line) {
425 sigs.push(Signature {
426 kind: if &caps[2] == "struct" {
427 "struct"
428 } else {
429 "interface"
430 },
431 name: caps[1].to_string(),
432 params: String::new(),
433 return_type: String::new(),
434 is_async: false,
435 is_exported: caps[1].starts_with(char::is_uppercase),
436 indent: 0,
437 ..Signature::no_span()
438 });
439 }
440 }
441
442 sigs
443}
444
445pub(crate) fn compact_params(params: &str) -> String {
446 if params.trim().is_empty() {
447 return String::new();
448 }
449 params
450 .split(',')
451 .map(|p| {
452 let p = p.trim();
453 if let Some((name, ty)) = p.split_once(':') {
454 let name = name.trim();
455 let ty = ty.trim();
456 let short = match ty {
457 "string" | "String" | "&str" | "str" => ":s",
458 "number" | "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" => ":n",
459 "boolean" | "bool" => ":b",
460 _ => return format!("{name}:{ty}"),
461 };
462 format!("{name}{short}")
463 } else {
464 p.to_string()
465 }
466 })
467 .collect::<Vec<_>>()
468 .join(", ")
469}
470
471fn compact_type(ty: &str) -> String {
472 match ty.trim() {
473 "String" | "string" | "&str" | "str" => "s".to_string(),
474 "bool" | "boolean" => "b".to_string(),
475 "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" | "number" => "n".to_string(),
476 "void" | "()" => "∅".to_string(),
477 other => {
478 if other.starts_with("Vec<") || other.starts_with("Array<") {
479 let inner = other
480 .trim_start_matches("Vec<")
481 .trim_start_matches("Array<")
482 .trim_end_matches('>');
483 format!("[{}]", compact_type(inner))
484 } else if other.starts_with("Option<") || other.starts_with("Maybe<") {
485 let inner = other
486 .trim_start_matches("Option<")
487 .trim_start_matches("Maybe<")
488 .trim_end_matches('>');
489 format!("?{}", compact_type(inner))
490 } else if other.starts_with("Result<") {
491 "R".to_string()
492 } else if other.starts_with("impl ") {
493 other.trim_start_matches("impl ").to_string()
494 } else {
495 other.to_string()
496 }
497 }
498 }
499}
500
501fn tdd_params(params: &str) -> String {
502 if params.trim().is_empty() {
503 return String::new();
504 }
505 params
506 .split(',')
507 .map(|p| {
508 let p = p.trim();
509 if p.starts_with('&') {
510 let rest = p.trim_start_matches("&mut ").trim_start_matches('&');
511 if let Some((name, ty)) = rest.split_once(':') {
512 format!("&{}:{}", name.trim(), compact_type(ty))
513 } else {
514 p.to_string()
515 }
516 } else if let Some((name, ty)) = p.split_once(':') {
517 format!("{}:{}", name.trim(), compact_type(ty))
518 } else if p == "self" || p == "&self" || p == "&mut self" {
519 "⊕".to_string()
520 } else {
521 p.to_string()
522 }
523 })
524 .collect::<Vec<_>>()
525 .join(",")
526}
527
528fn extract_generic_signatures(content: &str) -> Vec<Signature> {
529 let re_func = static_regex!(
530 r"^\s*(?:(?:public|private|protected|static|async|abstract|virtual|override|final|def|func|fun|fn)\s+)+(\w+)\s*\("
531 );
532 let re_class = static_regex!(
533 r"^\s*(?:(?:public|private|protected|abstract|final|sealed|partial)\s+)*(?:class|struct|enum|interface|trait|module|object|record)\s+(\w+)"
534 );
535
536 let mut sigs = Vec::new();
537 for line in content.lines() {
538 let trimmed = line.trim();
539 if trimmed.is_empty()
540 || trimmed.starts_with("//")
541 || trimmed.starts_with('#')
542 || trimmed.starts_with("/*")
543 || trimmed.starts_with('*')
544 {
545 continue;
546 }
547 if let Some(caps) = re_class.captures(trimmed) {
548 sigs.push(Signature {
549 kind: "type",
550 name: caps[1].to_string(),
551 params: String::new(),
552 return_type: String::new(),
553 is_async: false,
554 is_exported: true,
555 indent: 0,
556 ..Signature::no_span()
557 });
558 } else if let Some(caps) = re_func.captures(trimmed) {
559 sigs.push(Signature {
560 kind: "fn",
561 name: caps[1].to_string(),
562 params: String::new(),
563 return_type: String::new(),
564 is_async: trimmed.contains("async"),
565 is_exported: true,
566 indent: 0,
567 ..Signature::no_span()
568 });
569 }
570 }
571 sigs
572}