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 pub fn line_suffix(&self) -> String {
112 match (self.start_line, self.end_line) {
113 (Some(start), Some(end)) if start > 0 && end > start => format!(" @L{start}-{end}"),
114 (Some(start), _) if start > 0 => format!(" @L{start}"),
115 _ => String::new(),
116 }
117 }
118
119 pub fn to_compact_located(&self) -> String {
122 format!("{}{}", self.to_compact(), self.line_suffix())
123 }
124
125 pub fn to_tdd_located(&self) -> String {
127 format!("{}{}", self.to_tdd(), self.line_suffix())
128 }
129}
130
131fn fn_re() -> &'static Regex {
132 static_regex!(
133 r"^(\s*)(export\s+)?(async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\{]+))?\s*\{?"
134 )
135}
136
137fn class_re() -> &'static Regex {
138 static_regex!(r"^(\s*)(export\s+)?(abstract\s+)?class\s+(\w+)")
139}
140
141fn iface_re() -> &'static Regex {
142 static_regex!(r"^(\s*)(export\s+)?interface\s+(\w+)")
143}
144
145fn type_re() -> &'static Regex {
146 static_regex!(r"^(\s*)(export\s+)?type\s+(\w+)")
147}
148
149fn const_re() -> &'static Regex {
150 static_regex!(r"^(\s*)(export\s+)?(const|let|var)\s+(\w+)(?:\s*:\s*(\w+))?")
151}
152
153fn rust_fn_re() -> &'static Regex {
154 static_regex!(
155 r"^(\s*)(pub\s+)?(async\s+)?fn\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*->\s*([^\{]+))?\s*\{?"
156 )
157}
158
159fn rust_struct_re() -> &'static Regex {
160 static_regex!(r"^(\s*)(pub\s+)?struct\s+(\w+)")
161}
162
163fn rust_enum_re() -> &'static Regex {
164 static_regex!(r"^(\s*)(pub\s+)?enum\s+(\w+)")
165}
166
167fn rust_trait_re() -> &'static Regex {
168 static_regex!(r"^(\s*)(pub\s+)?trait\s+(\w+)")
169}
170
171fn rust_impl_re() -> &'static Regex {
172 static_regex!(r"^(\s*)impl\s+(?:(\w+)\s+for\s+)?(\w+)")
173}
174
175use std::sync::atomic::{AtomicU64, Ordering};
176
177static TREE_SITTER_HITS: AtomicU64 = AtomicU64::new(0);
178static REGEX_FALLBACK_HITS: AtomicU64 = AtomicU64::new(0);
179
180pub fn signature_backend_stats() -> (u64, u64) {
182 (
183 TREE_SITTER_HITS.load(Ordering::Relaxed),
184 REGEX_FALLBACK_HITS.load(Ordering::Relaxed),
185 )
186}
187
188pub fn extract_signatures(content: &str, file_ext: &str) -> Vec<Signature> {
189 #[cfg(feature = "tree-sitter")]
190 {
191 if let Some(sigs) = super::signatures_ts::extract_signatures_ts(content, file_ext) {
192 TREE_SITTER_HITS.fetch_add(1, Ordering::Relaxed);
193 return sigs;
194 }
195 }
196
197 REGEX_FALLBACK_HITS.fetch_add(1, Ordering::Relaxed);
198 match file_ext {
199 "rs" => extract_rust_signatures(content),
200 "ts" | "tsx" | "js" | "jsx" | "svelte" | "vue" => extract_ts_signatures(content),
201 "py" => extract_python_signatures(content),
202 "go" => extract_go_signatures(content),
203 _ => extract_generic_signatures(content),
204 }
205}
206
207pub fn extract_file_map(path: &str, content: &str) -> String {
208 let ext = std::path::Path::new(path)
209 .extension()
210 .and_then(|e| e.to_str())
211 .unwrap_or("rs");
212 let dep_info = super::deps::extract_deps(content, ext);
213 let sigs = extract_signatures(content, ext);
214 let mut parts = Vec::new();
215 if !dep_info.imports.is_empty() {
216 parts.push(dep_info.imports.join(","));
217 }
218 let key_sigs: Vec<String> = sigs
219 .iter()
220 .filter(|s| s.is_exported || s.indent == 0)
221 .map(Signature::to_compact_located)
222 .collect();
223 if !key_sigs.is_empty() {
224 parts.push(key_sigs.join("\n"));
225 }
226 parts.join("\n")
227}
228
229fn extract_ts_signatures(content: &str) -> Vec<Signature> {
230 let mut sigs = Vec::new();
231
232 for (line_idx, line) in content.lines().enumerate() {
233 let line_no = line_idx + 1;
234 let trimmed = line.trim();
235 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') {
236 continue;
237 }
238
239 if let Some(caps) = fn_re().captures(line) {
240 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
241 sigs.push(Signature {
242 kind: if indent > 0 { "method" } else { "fn" },
243 name: caps[4].to_string(),
244 params: compact_params(&caps[5]),
245 return_type: caps
246 .get(6)
247 .map_or(String::new(), |m| m.as_str().trim().to_string()),
248 is_async: caps.get(3).is_some(),
249 is_exported: caps.get(2).is_some(),
250 indent: if indent > 0 { 2 } else { 0 },
251 start_line: Some(line_no),
252 end_line: Some(line_no),
253 });
254 } else if let Some(caps) = class_re().captures(line) {
255 sigs.push(Signature {
256 kind: "class",
257 name: caps[4].to_string(),
258 params: String::new(),
259 return_type: String::new(),
260 is_async: false,
261 is_exported: caps.get(2).is_some(),
262 indent: 0,
263 start_line: Some(line_no),
264 end_line: Some(line_no),
265 });
266 } else if let Some(caps) = iface_re().captures(line) {
267 sigs.push(Signature {
268 kind: "interface",
269 name: caps[3].to_string(),
270 params: String::new(),
271 return_type: String::new(),
272 is_async: false,
273 is_exported: caps.get(2).is_some(),
274 indent: 0,
275 start_line: Some(line_no),
276 end_line: Some(line_no),
277 });
278 } else if let Some(caps) = type_re().captures(line) {
279 sigs.push(Signature {
280 kind: "type",
281 name: caps[3].to_string(),
282 params: String::new(),
283 return_type: String::new(),
284 is_async: false,
285 is_exported: caps.get(2).is_some(),
286 indent: 0,
287 start_line: Some(line_no),
288 end_line: Some(line_no),
289 });
290 } else if let Some(caps) = const_re().captures(line) {
291 if caps.get(2).is_some() {
292 sigs.push(Signature {
293 kind: "const",
294 name: caps[4].to_string(),
295 params: String::new(),
296 return_type: caps
297 .get(5)
298 .map_or(String::new(), |m| m.as_str().to_string()),
299 is_async: false,
300 is_exported: true,
301 indent: 0,
302 start_line: Some(line_no),
303 end_line: Some(line_no),
304 });
305 }
306 }
307 }
308
309 sigs
310}
311
312fn extract_rust_signatures(content: &str) -> Vec<Signature> {
313 let mut sigs = Vec::new();
314
315 for (line_idx, line) in content.lines().enumerate() {
316 let line_no = line_idx + 1;
317 let trimmed = line.trim();
318 if trimmed.starts_with("//") || trimmed.starts_with("///") {
319 continue;
320 }
321
322 if let Some(caps) = rust_fn_re().captures(line) {
323 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
324 sigs.push(Signature {
325 kind: if indent > 0 { "method" } else { "fn" },
326 name: caps[4].to_string(),
327 params: compact_params(&caps[5]),
328 return_type: caps
329 .get(6)
330 .map_or(String::new(), |m| m.as_str().trim().to_string()),
331 is_async: caps.get(3).is_some(),
332 is_exported: caps.get(2).is_some(),
333 indent: if indent > 0 { 2 } else { 0 },
334 start_line: Some(line_no),
335 end_line: Some(line_no),
336 });
337 } else if let Some(caps) = rust_struct_re().captures(line) {
338 sigs.push(Signature {
339 kind: "struct",
340 name: caps[3].to_string(),
341 params: String::new(),
342 return_type: String::new(),
343 is_async: false,
344 is_exported: caps.get(2).is_some(),
345 indent: 0,
346 start_line: Some(line_no),
347 end_line: Some(line_no),
348 });
349 } else if let Some(caps) = rust_enum_re().captures(line) {
350 sigs.push(Signature {
351 kind: "enum",
352 name: caps[3].to_string(),
353 params: String::new(),
354 return_type: String::new(),
355 is_async: false,
356 is_exported: caps.get(2).is_some(),
357 indent: 0,
358 start_line: Some(line_no),
359 end_line: Some(line_no),
360 });
361 } else if let Some(caps) = rust_trait_re().captures(line) {
362 sigs.push(Signature {
363 kind: "trait",
364 name: caps[3].to_string(),
365 params: String::new(),
366 return_type: String::new(),
367 is_async: false,
368 is_exported: caps.get(2).is_some(),
369 indent: 0,
370 start_line: Some(line_no),
371 end_line: Some(line_no),
372 });
373 } else if let Some(caps) = rust_impl_re().captures(line) {
374 let trait_name = caps.get(2).map(|m| m.as_str());
375 let type_name = &caps[3];
376 let name = if let Some(t) = trait_name {
377 format!("{t} for {type_name}")
378 } else {
379 type_name.to_string()
380 };
381 sigs.push(Signature {
382 kind: "class",
383 name,
384 params: String::new(),
385 return_type: String::new(),
386 is_async: false,
387 is_exported: false,
388 indent: 0,
389 start_line: Some(line_no),
390 end_line: Some(line_no),
391 });
392 }
393 }
394
395 sigs
396}
397
398fn extract_python_signatures(content: &str) -> Vec<Signature> {
399 let mut sigs = Vec::new();
400 let py_fn = static_regex!(r"^(\s*)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?");
401 let py_class = static_regex!(r"^(\s*)class\s+(\w+)");
402
403 for (line_idx, line) in content.lines().enumerate() {
404 let line_no = line_idx + 1;
405 if let Some(caps) = py_fn.captures(line) {
406 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
407 sigs.push(Signature {
408 kind: if indent > 0 { "method" } else { "fn" },
409 name: caps[3].to_string(),
410 params: compact_params(&caps[4]),
411 return_type: caps
412 .get(5)
413 .map_or(String::new(), |m| m.as_str().to_string()),
414 is_async: caps.get(2).is_some(),
415 is_exported: !caps[3].starts_with('_'),
416 indent: if indent > 0 { 2 } else { 0 },
417 start_line: Some(line_no),
418 end_line: Some(line_no),
419 });
420 } else if let Some(caps) = py_class.captures(line) {
421 sigs.push(Signature {
422 kind: "class",
423 name: caps[2].to_string(),
424 params: String::new(),
425 return_type: String::new(),
426 is_async: false,
427 is_exported: !caps[2].starts_with('_'),
428 indent: 0,
429 start_line: Some(line_no),
430 end_line: Some(line_no),
431 });
432 }
433 }
434
435 sigs
436}
437
438fn extract_go_signatures(content: &str) -> Vec<Signature> {
439 let mut sigs = Vec::new();
440 let go_fn = static_regex!(
441 r"^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|(\w+)))?\s*\{"
442 );
443 let go_type = static_regex!(r"^type\s+(\w+)\s+(struct|interface)");
444
445 for (line_idx, line) in content.lines().enumerate() {
446 let line_no = line_idx + 1;
447 if let Some(caps) = go_fn.captures(line) {
448 let is_method = caps.get(2).is_some();
449 sigs.push(Signature {
450 kind: if is_method { "method" } else { "fn" },
451 name: caps[3].to_string(),
452 params: compact_params(&caps[4]),
453 return_type: caps
454 .get(5)
455 .or(caps.get(6))
456 .map_or(String::new(), |m| m.as_str().to_string()),
457 is_async: false,
458 is_exported: caps[3].starts_with(char::is_uppercase),
459 indent: if is_method { 2 } else { 0 },
460 start_line: Some(line_no),
461 end_line: Some(line_no),
462 });
463 } else if let Some(caps) = go_type.captures(line) {
464 sigs.push(Signature {
465 kind: if &caps[2] == "struct" {
466 "struct"
467 } else {
468 "interface"
469 },
470 name: caps[1].to_string(),
471 params: String::new(),
472 return_type: String::new(),
473 is_async: false,
474 is_exported: caps[1].starts_with(char::is_uppercase),
475 indent: 0,
476 start_line: Some(line_no),
477 end_line: Some(line_no),
478 });
479 }
480 }
481
482 sigs
483}
484
485pub(crate) fn compact_params(params: &str) -> String {
486 if params.trim().is_empty() {
487 return String::new();
488 }
489 params
490 .split(',')
491 .map(|p| {
492 let p = p.trim();
493 if let Some((name, ty)) = p.split_once(':') {
494 let name = name.trim();
495 let ty = ty.trim();
496 let short = match ty {
497 "string" | "String" | "&str" | "str" => ":s",
498 "number" | "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" => ":n",
499 "boolean" | "bool" => ":b",
500 _ => return format!("{name}:{ty}"),
501 };
502 format!("{name}{short}")
503 } else {
504 p.to_string()
505 }
506 })
507 .collect::<Vec<_>>()
508 .join(", ")
509}
510
511fn compact_type(ty: &str) -> String {
512 match ty.trim() {
513 "String" | "string" | "&str" | "str" => "s".to_string(),
514 "bool" | "boolean" => "b".to_string(),
515 "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" | "number" => "n".to_string(),
516 "void" | "()" => "∅".to_string(),
517 other => {
518 if other.starts_with("Vec<") || other.starts_with("Array<") {
519 let inner = other
520 .trim_start_matches("Vec<")
521 .trim_start_matches("Array<")
522 .trim_end_matches('>');
523 format!("[{}]", compact_type(inner))
524 } else if other.starts_with("Option<") || other.starts_with("Maybe<") {
525 let inner = other
526 .trim_start_matches("Option<")
527 .trim_start_matches("Maybe<")
528 .trim_end_matches('>');
529 format!("?{}", compact_type(inner))
530 } else if other.starts_with("Result<") {
531 "R".to_string()
532 } else if other.starts_with("impl ") {
533 other.trim_start_matches("impl ").to_string()
534 } else {
535 other.to_string()
536 }
537 }
538 }
539}
540
541fn tdd_params(params: &str) -> String {
542 if params.trim().is_empty() {
543 return String::new();
544 }
545 params
546 .split(',')
547 .map(|p| {
548 let p = p.trim();
549 if p.starts_with('&') {
550 let rest = p.trim_start_matches("&mut ").trim_start_matches('&');
551 if let Some((name, ty)) = rest.split_once(':') {
552 format!("&{}:{}", name.trim(), compact_type(ty))
553 } else {
554 p.to_string()
555 }
556 } else if let Some((name, ty)) = p.split_once(':') {
557 format!("{}:{}", name.trim(), compact_type(ty))
558 } else if p == "self" || p == "&self" || p == "&mut self" {
559 "⊕".to_string()
560 } else {
561 p.to_string()
562 }
563 })
564 .collect::<Vec<_>>()
565 .join(",")
566}
567
568fn extract_generic_signatures(content: &str) -> Vec<Signature> {
569 let re_func = static_regex!(
570 r"^\s*(?:(?:public|private|protected|static|async|abstract|virtual|override|final|def|func|fun|fn)\s+)+(\w+)\s*\("
571 );
572 let re_class = static_regex!(
573 r"^\s*(?:(?:public|private|protected|abstract|final|sealed|partial)\s+)*(?:class|struct|enum|interface|trait|module|object|record)\s+(\w+)"
574 );
575
576 let mut sigs = Vec::new();
577 for (line_idx, line) in content.lines().enumerate() {
578 let line_no = line_idx + 1;
579 let trimmed = line.trim();
580 if trimmed.is_empty()
581 || trimmed.starts_with("//")
582 || trimmed.starts_with('#')
583 || trimmed.starts_with("/*")
584 || trimmed.starts_with('*')
585 {
586 continue;
587 }
588 if let Some(caps) = re_class.captures(trimmed) {
589 sigs.push(Signature {
590 kind: "type",
591 name: caps[1].to_string(),
592 params: String::new(),
593 return_type: String::new(),
594 is_async: false,
595 is_exported: true,
596 indent: 0,
597 start_line: Some(line_no),
598 end_line: Some(line_no),
599 });
600 } else if let Some(caps) = re_func.captures(trimmed) {
601 sigs.push(Signature {
602 kind: "fn",
603 name: caps[1].to_string(),
604 params: String::new(),
605 return_type: String::new(),
606 is_async: trimmed.contains("async"),
607 is_exported: true,
608 indent: 0,
609 start_line: Some(line_no),
610 end_line: Some(line_no),
611 });
612 }
613 }
614 sigs
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620
621 fn sample_fn() -> Signature {
622 Signature {
623 kind: "fn",
624 name: "run".to_string(),
625 params: "id:usize".to_string(),
626 return_type: "bool".to_string(),
627 is_async: false,
628 is_exported: true,
629 indent: 0,
630 start_line: None,
631 end_line: None,
632 }
633 }
634
635 #[test]
636 fn line_suffix_formats_known_spans() {
637 let mut sig = sample_fn();
638 assert_eq!(sig.line_suffix(), "");
639
640 sig.start_line = Some(42);
641 sig.end_line = Some(42);
642 assert_eq!(sig.line_suffix(), " @L42");
643
644 sig.end_line = Some(57);
645 assert_eq!(sig.line_suffix(), " @L42-57");
646 }
647
648 #[test]
649 fn base_renderers_stay_suffix_free() {
650 let mut sig = sample_fn();
653 sig.start_line = Some(3);
654 sig.end_line = Some(9);
655 assert_eq!(sig.to_compact(), "fn ⊛ run(id:usize) → bool");
656 assert_eq!(sig.to_tdd(), "λ+run(id:n)→b");
657 }
658
659 #[test]
660 fn located_renderers_append_line_suffix() {
661 let mut sig = sample_fn();
662 assert_eq!(sig.to_compact_located(), "fn ⊛ run(id:usize) → bool");
664 assert_eq!(sig.to_tdd_located(), "λ+run(id:n)→b");
665
666 sig.start_line = Some(3);
667 sig.end_line = Some(5);
668 assert_eq!(sig.to_compact_located(), "fn ⊛ run(id:usize) → bool @L3-5");
669 assert_eq!(sig.to_tdd_located(), "λ+run(id:n)→b @L3-5");
670 }
671
672 #[test]
673 fn regex_fallback_assigns_declaration_line_spans() {
674 let src = "\npublic class Service {}\n\npublic fn run() {\n}\n";
675 let sigs = extract_generic_signatures(src);
676
677 let service = sigs.iter().find(|s| s.name == "Service").unwrap();
678 assert_eq!(service.start_line, Some(2));
679 assert_eq!(service.end_line, Some(2));
680
681 let run = sigs.iter().find(|s| s.name == "run").unwrap();
682 assert_eq!(run.start_line, Some(4));
683 assert_eq!(run.end_line, Some(4));
684 }
685}