1use regex::Regex;
18
19macro_rules! static_regex {
20 ($pattern:expr_2021) => {{
21 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
22 RE.get_or_init(|| {
23 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
24 })
25 }};
26}
27
28#[derive(Debug, Clone)]
29pub struct Signature {
30 pub kind: &'static str,
31 pub name: String,
32 pub params: String,
33 pub return_type: String,
34 pub is_async: bool,
35 pub is_exported: bool,
36 pub indent: usize,
37 pub start_line: Option<usize>,
38 pub end_line: Option<usize>,
39}
40
41#[must_use]
48pub fn exports_not_in_signatures<'a>(
49 exports: &'a [String],
50 key_sigs: &[&Signature],
51) -> Vec<&'a str> {
52 let api_names: std::collections::HashSet<&str> =
53 key_sigs.iter().map(|s| s.name.as_str()).collect();
54 exports
55 .iter()
56 .map(String::as_str)
57 .filter(|e| !api_names.contains(e))
58 .collect()
59}
60
61impl Signature {
62 pub fn no_span() -> Self {
63 Self {
64 kind: "",
65 name: String::new(),
66 params: String::new(),
67 return_type: String::new(),
68 is_async: false,
69 is_exported: false,
70 indent: 0,
71 start_line: None,
72 end_line: None,
73 }
74 }
75
76 pub fn to_compact(&self) -> String {
81 let export = if self.is_exported { "pub " } else { "" };
82 let async_prefix = if self.is_async { "async " } else { "" };
83
84 match self.kind {
85 "fn" | "method" => {
86 let ret = if self.return_type.is_empty() {
87 String::new()
88 } else {
89 format!(" → {}", self.return_type)
90 };
91 let indent = " ".repeat(self.indent);
92 format!(
93 "{indent}fn {async_prefix}{export}{}({}){}",
94 self.name, self.params, ret
95 )
96 }
97 "const" | "let" | "var" => {
98 let ty = if self.return_type.is_empty() {
99 String::new()
100 } else {
101 format!(":{}", self.return_type)
102 };
103 format!("{} {export}{}{ty}", self.kind, self.name)
104 }
105 _ => format!("{} {export}{}", self.kind, self.name),
108 }
109 }
110
111 pub fn to_tdd(&self) -> String {
112 let vis = if self.is_exported { "+" } else { "-" };
113 let a = if self.is_async { "~" } else { "" };
114
115 match self.kind {
116 "fn" | "method" => {
117 let ret = if self.return_type.is_empty() {
118 String::new()
119 } else {
120 format!("→{}", compact_type(&self.return_type))
121 };
122 let params = tdd_params(&self.params);
123 let indent = if self.indent > 0 { " " } else { "" };
124 format!("{indent}{a}λ{vis}{}({params}){ret}", self.name)
125 }
126 "class" | "struct" => format!("§{vis}{}", self.name),
127 "interface" | "trait" => format!("∂{vis}{}", self.name),
128 "type" => format!("τ{vis}{}", self.name),
129 "enum" => format!("ε{vis}{}", self.name),
130 "const" | "let" | "var" => {
131 let ty = if self.return_type.is_empty() {
132 String::new()
133 } else {
134 format!(":{}", compact_type(&self.return_type))
135 };
136 format!("ν{vis}{}{ty}", self.name)
137 }
138 _ => format!(
139 "{}{vis}{}",
140 self.kind.chars().next().unwrap_or('?'),
141 self.name
142 ),
143 }
144 }
145
146 pub fn line_suffix(&self) -> String {
150 match (self.start_line, self.end_line) {
151 (Some(start), Some(end)) if start > 0 && end > start => format!(" @L{start}-{end}"),
152 (Some(start), _) if start > 0 => format!(" @L{start}"),
153 _ => String::new(),
154 }
155 }
156
157 pub fn to_compact_located(&self) -> String {
160 format!("{}{}", self.to_compact(), self.line_suffix())
161 }
162
163 pub fn to_tdd_located(&self) -> String {
165 format!("{}{}", self.to_tdd(), self.line_suffix())
166 }
167}
168
169pub fn tdd_legend<'a>(sigs: &[&'a Signature]) -> String {
174 if sigs.is_empty() {
175 return String::new();
176 }
177 let mut parts: Vec<&str> = Vec::new();
178 let has = |pred: &dyn Fn(&'a Signature) -> bool| sigs.iter().any(|s| pred(s));
179
180 if has(&|s| matches!(s.kind, "fn" | "method")) {
181 parts.push("λ=fn");
182 }
183 if has(&|s| matches!(s.kind, "class" | "struct")) {
184 parts.push("§=class");
185 }
186 if has(&|s| matches!(s.kind, "interface" | "trait")) {
187 parts.push("∂=trait");
188 }
189 if has(&|s| s.kind == "type") {
190 parts.push("τ=type");
191 }
192 if has(&|s| s.kind == "enum") {
193 parts.push("ε=enum");
194 }
195 if has(&|s| matches!(s.kind, "const" | "let" | "var")) {
196 parts.push("ν=val");
197 }
198 if has(&|s| s.is_exported) {
199 parts.push("+=pub");
200 }
201 if has(&|s| s.is_async) {
202 parts.push("~=async");
203 }
204 if parts.is_empty() {
205 String::new()
206 } else {
207 format!("[{}]", parts.join(" "))
208 }
209}
210
211fn fn_re() -> &'static Regex {
212 static_regex!(
213 r"^(\s*)(export\s+)?(async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\{]+))?\s*\{?"
214 )
215}
216
217fn class_re() -> &'static Regex {
218 static_regex!(r"^(\s*)(export\s+)?(abstract\s+)?class\s+(\w+)")
219}
220
221fn iface_re() -> &'static Regex {
222 static_regex!(r"^(\s*)(export\s+)?interface\s+(\w+)")
223}
224
225fn type_re() -> &'static Regex {
226 static_regex!(r"^(\s*)(export\s+)?type\s+(\w+)")
227}
228
229fn const_re() -> &'static Regex {
230 static_regex!(r"^(\s*)(export\s+)?(const|let|var)\s+(\w+)(?:\s*:\s*(\w+))?")
231}
232
233fn rust_fn_re() -> &'static Regex {
234 static_regex!(
235 r"^(\s*)(pub\s+)?(async\s+)?fn\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*->\s*([^\{]+))?\s*\{?"
236 )
237}
238
239fn rust_struct_re() -> &'static Regex {
240 static_regex!(r"^(\s*)(pub\s+)?struct\s+(\w+)")
241}
242
243fn rust_enum_re() -> &'static Regex {
244 static_regex!(r"^(\s*)(pub\s+)?enum\s+(\w+)")
245}
246
247fn rust_trait_re() -> &'static Regex {
248 static_regex!(r"^(\s*)(pub\s+)?trait\s+(\w+)")
249}
250
251fn rust_impl_re() -> &'static Regex {
252 static_regex!(r"^(\s*)impl\s+(?:(\w+)\s+for\s+)?(\w+)")
253}
254
255use std::sync::atomic::{AtomicU64, Ordering};
256
257static TREE_SITTER_HITS: AtomicU64 = AtomicU64::new(0);
258static REGEX_FALLBACK_HITS: AtomicU64 = AtomicU64::new(0);
259
260pub fn signature_backend_stats() -> (u64, u64) {
262 (
263 TREE_SITTER_HITS.load(Ordering::Relaxed),
264 REGEX_FALLBACK_HITS.load(Ordering::Relaxed),
265 )
266}
267
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum SigBackend {
275 TreeSitter,
276 Regex,
277}
278
279impl SigBackend {
280 #[must_use]
281 pub fn as_str(self) -> &'static str {
282 match self {
283 SigBackend::TreeSitter => "tree-sitter",
284 SigBackend::Regex => "regex",
285 }
286 }
287}
288
289#[cfg(feature = "tree-sitter")]
293mod ts_blocklist {
294 use std::collections::HashSet;
295 use std::sync::Mutex;
296
297 static BLOCKED: Mutex<Option<HashSet<String>>> = Mutex::new(None);
298
299 fn lock() -> std::sync::MutexGuard<'static, Option<HashSet<String>>> {
300 match BLOCKED.lock() {
301 Ok(g) => g,
302 Err(poisoned) => poisoned.into_inner(),
303 }
304 }
305
306 pub(super) fn is_blocked(ext: &str) -> bool {
307 lock().as_ref().is_some_and(|set| set.contains(ext))
308 }
309
310 pub(super) fn block(ext: &str) {
311 lock()
312 .get_or_insert_with(HashSet::new)
313 .insert(ext.to_string());
314 }
315}
316
317#[cfg(feature = "tree-sitter")]
318use ts_blocklist::{block as block_ts_language, is_blocked as is_ts_language_blocked};
319
320pub fn extract_signatures_counted(content: &str, file_ext: &str) -> (Vec<Signature>, SigBackend) {
324 let (sigs, backend) = extract_signatures_with_backend(content, file_ext);
325 match backend {
326 SigBackend::TreeSitter => TREE_SITTER_HITS.fetch_add(1, Ordering::Relaxed),
327 SigBackend::Regex => REGEX_FALLBACK_HITS.fetch_add(1, Ordering::Relaxed),
328 };
329 (sigs, backend)
330}
331
332pub fn extract_signatures_with_backend(
337 content: &str,
338 file_ext: &str,
339) -> (Vec<Signature>, SigBackend) {
340 #[cfg(feature = "tree-sitter")]
341 {
342 if !is_ts_language_blocked(file_ext) {
353 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
354 super::signatures_ts::extract_signatures_ts(content, file_ext)
355 })) {
356 Ok(Some(sigs)) if !sigs.is_empty() => {
357 return (sigs, SigBackend::TreeSitter);
358 }
359 Err(_) => {
360 block_ts_language(file_ext);
361 eprintln!(
362 "[lean-ctx] tree-sitter panicked for .{file_ext} — using regex fallback"
363 );
364 }
365 _ => {}
366 }
367 }
368 }
369
370 let sigs = match file_ext {
371 "rs" => extract_rust_signatures(content),
372 "ts" | "tsx" | "js" | "jsx" | "svelte" | "vue" => extract_ts_signatures(content),
373 "py" => extract_python_signatures(content),
374 "go" => extract_go_signatures(content),
375 _ => extract_generic_signatures(content),
376 };
377 (sigs, SigBackend::Regex)
378}
379
380fn extract_ts_signatures(content: &str) -> Vec<Signature> {
381 let mut sigs = Vec::new();
382
383 for (line_idx, line) in content.lines().enumerate() {
384 let line_no = line_idx + 1;
385 let trimmed = line.trim();
386 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') {
387 continue;
388 }
389
390 if let Some(caps) = fn_re().captures(line) {
391 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
392 sigs.push(Signature {
393 kind: if indent > 0 { "method" } else { "fn" },
394 name: caps[4].to_string(),
395 params: compact_params(&caps[5]),
396 return_type: caps
397 .get(6)
398 .map_or(String::new(), |m| m.as_str().trim().to_string()),
399 is_async: caps.get(3).is_some(),
400 is_exported: caps.get(2).is_some(),
401 indent: if indent > 0 { 2 } else { 0 },
402 start_line: Some(line_no),
403 end_line: Some(line_no),
404 });
405 } else if let Some(caps) = class_re().captures(line) {
406 sigs.push(Signature {
407 kind: "class",
408 name: caps[4].to_string(),
409 params: String::new(),
410 return_type: String::new(),
411 is_async: false,
412 is_exported: caps.get(2).is_some(),
413 indent: 0,
414 start_line: Some(line_no),
415 end_line: Some(line_no),
416 });
417 } else if let Some(caps) = iface_re().captures(line) {
418 sigs.push(Signature {
419 kind: "interface",
420 name: caps[3].to_string(),
421 params: String::new(),
422 return_type: String::new(),
423 is_async: false,
424 is_exported: caps.get(2).is_some(),
425 indent: 0,
426 start_line: Some(line_no),
427 end_line: Some(line_no),
428 });
429 } else if let Some(caps) = type_re().captures(line) {
430 sigs.push(Signature {
431 kind: "type",
432 name: caps[3].to_string(),
433 params: String::new(),
434 return_type: String::new(),
435 is_async: false,
436 is_exported: caps.get(2).is_some(),
437 indent: 0,
438 start_line: Some(line_no),
439 end_line: Some(line_no),
440 });
441 } else if let Some(caps) = const_re().captures(line)
442 && caps.get(2).is_some()
443 {
444 sigs.push(Signature {
445 kind: "const",
446 name: caps[4].to_string(),
447 params: String::new(),
448 return_type: caps
449 .get(5)
450 .map_or(String::new(), |m| m.as_str().to_string()),
451 is_async: false,
452 is_exported: true,
453 indent: 0,
454 start_line: Some(line_no),
455 end_line: Some(line_no),
456 });
457 }
458 }
459
460 sigs
461}
462
463fn extract_rust_signatures(content: &str) -> Vec<Signature> {
464 let mut sigs = Vec::new();
465
466 for (line_idx, line) in content.lines().enumerate() {
467 let line_no = line_idx + 1;
468 let trimmed = line.trim();
469 if trimmed.starts_with("//") || trimmed.starts_with("///") {
470 continue;
471 }
472
473 if let Some(caps) = rust_fn_re().captures(line) {
474 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
475 sigs.push(Signature {
476 kind: if indent > 0 { "method" } else { "fn" },
477 name: caps[4].to_string(),
478 params: compact_params(&caps[5]),
479 return_type: caps
480 .get(6)
481 .map_or(String::new(), |m| m.as_str().trim().to_string()),
482 is_async: caps.get(3).is_some(),
483 is_exported: caps.get(2).is_some(),
484 indent: if indent > 0 { 2 } else { 0 },
485 start_line: Some(line_no),
486 end_line: Some(line_no),
487 });
488 } else if let Some(caps) = rust_struct_re().captures(line) {
489 sigs.push(Signature {
490 kind: "struct",
491 name: caps[3].to_string(),
492 params: String::new(),
493 return_type: String::new(),
494 is_async: false,
495 is_exported: caps.get(2).is_some(),
496 indent: 0,
497 start_line: Some(line_no),
498 end_line: Some(line_no),
499 });
500 } else if let Some(caps) = rust_enum_re().captures(line) {
501 sigs.push(Signature {
502 kind: "enum",
503 name: caps[3].to_string(),
504 params: String::new(),
505 return_type: String::new(),
506 is_async: false,
507 is_exported: caps.get(2).is_some(),
508 indent: 0,
509 start_line: Some(line_no),
510 end_line: Some(line_no),
511 });
512 } else if let Some(caps) = rust_trait_re().captures(line) {
513 sigs.push(Signature {
514 kind: "trait",
515 name: caps[3].to_string(),
516 params: String::new(),
517 return_type: String::new(),
518 is_async: false,
519 is_exported: caps.get(2).is_some(),
520 indent: 0,
521 start_line: Some(line_no),
522 end_line: Some(line_no),
523 });
524 } else if let Some(caps) = rust_impl_re().captures(line) {
525 let trait_name = caps.get(2).map(|m| m.as_str());
526 let type_name = &caps[3];
527 let name = if let Some(t) = trait_name {
528 format!("{t} for {type_name}")
529 } else {
530 type_name.to_string()
531 };
532 sigs.push(Signature {
533 kind: "impl",
534 name,
535 params: String::new(),
536 return_type: String::new(),
537 is_async: false,
538 is_exported: false,
539 indent: 0,
540 start_line: Some(line_no),
541 end_line: Some(line_no),
542 });
543 }
544 }
545
546 sigs
547}
548
549fn extract_python_signatures(content: &str) -> Vec<Signature> {
550 let mut sigs = Vec::new();
551 let py_fn = static_regex!(r"^(\s*)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?");
552 let py_class = static_regex!(r"^(\s*)class\s+(\w+)");
553
554 for (line_idx, line) in content.lines().enumerate() {
555 let line_no = line_idx + 1;
556 if let Some(caps) = py_fn.captures(line) {
557 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
558 sigs.push(Signature {
559 kind: if indent > 0 { "method" } else { "fn" },
560 name: caps[3].to_string(),
561 params: compact_params(&caps[4]),
562 return_type: caps
563 .get(5)
564 .map_or(String::new(), |m| m.as_str().to_string()),
565 is_async: caps.get(2).is_some(),
566 is_exported: !caps[3].starts_with('_'),
567 indent: if indent > 0 { 2 } else { 0 },
568 start_line: Some(line_no),
569 end_line: Some(line_no),
570 });
571 } else if let Some(caps) = py_class.captures(line) {
572 sigs.push(Signature {
573 kind: "class",
574 name: caps[2].to_string(),
575 params: String::new(),
576 return_type: String::new(),
577 is_async: false,
578 is_exported: !caps[2].starts_with('_'),
579 indent: 0,
580 start_line: Some(line_no),
581 end_line: Some(line_no),
582 });
583 }
584 }
585
586 sigs
587}
588
589fn extract_go_signatures(content: &str) -> Vec<Signature> {
590 let mut sigs = Vec::new();
591 let go_fn = static_regex!(
592 r"^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|(\w+)))?\s*\{"
593 );
594 let go_type = static_regex!(r"^type\s+(\w+)\s+(struct|interface)");
595
596 for (line_idx, line) in content.lines().enumerate() {
597 let line_no = line_idx + 1;
598 if let Some(caps) = go_fn.captures(line) {
599 let is_method = caps.get(2).is_some();
600 sigs.push(Signature {
601 kind: if is_method { "method" } else { "fn" },
602 name: caps[3].to_string(),
603 params: compact_params(&caps[4]),
604 return_type: caps
605 .get(5)
606 .or(caps.get(6))
607 .map_or(String::new(), |m| m.as_str().to_string()),
608 is_async: false,
609 is_exported: caps[3].starts_with(char::is_uppercase),
610 indent: if is_method { 2 } else { 0 },
611 start_line: Some(line_no),
612 end_line: Some(line_no),
613 });
614 } else if let Some(caps) = go_type.captures(line) {
615 sigs.push(Signature {
616 kind: if &caps[2] == "struct" {
617 "struct"
618 } else {
619 "interface"
620 },
621 name: caps[1].to_string(),
622 params: String::new(),
623 return_type: String::new(),
624 is_async: false,
625 is_exported: caps[1].starts_with(char::is_uppercase),
626 indent: 0,
627 start_line: Some(line_no),
628 end_line: Some(line_no),
629 });
630 }
631 }
632
633 sigs
634}
635
636pub(crate) fn compact_params(params: &str) -> String {
641 if params.trim().is_empty() {
642 return String::new();
643 }
644 params
645 .split(',')
646 .map(|p| {
647 let p = p.trim();
648 if let Some((name, ty)) = p.split_once(':') {
649 format!("{}:{}", name.trim(), ty.trim())
650 } else {
651 p.to_string()
652 }
653 })
654 .collect::<Vec<_>>()
655 .join(", ")
656}
657
658fn compact_type(ty: &str) -> String {
659 match ty.trim() {
660 "String" | "string" | "&str" | "str" => "s".to_string(),
661 "bool" | "boolean" => "b".to_string(),
662 "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" | "number" => "n".to_string(),
663 "void" | "()" => "∅".to_string(),
664 other => {
665 if other.starts_with("Vec<") || other.starts_with("Array<") {
666 let inner = other
667 .trim_start_matches("Vec<")
668 .trim_start_matches("Array<")
669 .trim_end_matches('>');
670 format!("[{}]", compact_type(inner))
671 } else if other.starts_with("Option<") || other.starts_with("Maybe<") {
672 let inner = other
673 .trim_start_matches("Option<")
674 .trim_start_matches("Maybe<")
675 .trim_end_matches('>');
676 format!("?{}", compact_type(inner))
677 } else if other.starts_with("Result<") {
678 "R".to_string()
679 } else if other.starts_with("impl ") {
680 other.trim_start_matches("impl ").to_string()
681 } else {
682 other.to_string()
683 }
684 }
685 }
686}
687
688fn tdd_params(params: &str) -> String {
689 if params.trim().is_empty() {
690 return String::new();
691 }
692 params
693 .split(',')
694 .map(|p| {
695 let p = p.trim();
696 if p.starts_with('&') {
697 let rest = p.trim_start_matches("&mut ").trim_start_matches('&');
698 if let Some((name, ty)) = rest.split_once(':') {
699 format!("&{}:{}", name.trim(), compact_type(ty))
700 } else {
701 p.to_string()
702 }
703 } else if let Some((name, ty)) = p.split_once(':') {
704 format!("{}:{}", name.trim(), compact_type(ty))
705 } else if p == "self" || p == "&self" || p == "&mut self" {
706 "⊕".to_string()
707 } else {
708 p.to_string()
709 }
710 })
711 .collect::<Vec<_>>()
712 .join(",")
713}
714
715fn extract_generic_signatures(content: &str) -> Vec<Signature> {
716 let re_func = static_regex!(
717 r"^\s*(?:(?:public|private|protected|static|async|abstract|virtual|override|final|def|func|fun|fn)\s+)+(\w+)\s*\("
718 );
719 let re_ps_func = static_regex!(r"^\s*function\s+([\w-]+)");
723 let re_class = static_regex!(
724 r"^\s*(?:(?:public|private|protected|abstract|final|sealed|partial)\s+)*(?:class|struct|enum|interface|trait|module|object|record)\s+(\w+)"
725 );
726
727 let mut sigs = Vec::new();
728 for (line_idx, line) in content.lines().enumerate() {
729 let line_no = line_idx + 1;
730 let trimmed = line.trim();
731 if trimmed.is_empty()
732 || trimmed.starts_with("//")
733 || trimmed.starts_with('#')
734 || trimmed.starts_with("/*")
735 || trimmed.starts_with('*')
736 {
737 continue;
738 }
739 if let Some(caps) = re_class.captures(trimmed) {
740 sigs.push(Signature {
741 kind: "type",
742 name: caps[1].to_string(),
743 params: String::new(),
744 return_type: String::new(),
745 is_async: false,
746 is_exported: true,
747 indent: 0,
748 start_line: Some(line_no),
749 end_line: Some(line_no),
750 });
751 } else if let Some(caps) = re_func.captures(trimmed) {
752 sigs.push(Signature {
753 kind: "fn",
754 name: caps[1].to_string(),
755 params: String::new(),
756 return_type: String::new(),
757 is_async: trimmed.contains("async"),
758 is_exported: true,
759 indent: 0,
760 start_line: Some(line_no),
761 end_line: Some(line_no),
762 });
763 } else if let Some(caps) = re_ps_func.captures(trimmed) {
764 sigs.push(Signature {
765 kind: "fn",
766 name: caps[1].to_string(),
767 params: String::new(),
768 return_type: String::new(),
769 is_async: false,
770 is_exported: true,
771 indent: 0,
772 start_line: Some(line_no),
773 end_line: Some(line_no),
774 });
775 }
776 }
777 sigs
778}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783
784 fn sample_fn() -> Signature {
785 Signature {
786 kind: "fn",
787 name: "run".to_string(),
788 params: "id:usize".to_string(),
789 return_type: "bool".to_string(),
790 is_async: false,
791 is_exported: true,
792 indent: 0,
793 start_line: None,
794 end_line: None,
795 }
796 }
797
798 #[test]
799 fn line_suffix_formats_known_spans() {
800 let mut sig = sample_fn();
801 assert_eq!(sig.line_suffix(), "");
802
803 sig.start_line = Some(42);
804 sig.end_line = Some(42);
805 assert_eq!(sig.line_suffix(), " @L42");
806
807 sig.end_line = Some(57);
808 assert_eq!(sig.line_suffix(), " @L42-57");
809 }
810
811 #[test]
812 fn base_renderers_stay_suffix_free() {
813 let mut sig = sample_fn();
816 sig.start_line = Some(3);
817 sig.end_line = Some(9);
818 assert_eq!(sig.to_compact(), "fn pub run(id:usize) → bool");
819 assert_eq!(sig.to_tdd(), "λ+run(id:n)→b");
820 }
821
822 #[test]
823 fn located_renderers_append_line_suffix() {
824 let mut sig = sample_fn();
825 assert_eq!(sig.to_compact_located(), "fn pub run(id:usize) → bool");
827 assert_eq!(sig.to_tdd_located(), "λ+run(id:n)→b");
828
829 sig.start_line = Some(3);
830 sig.end_line = Some(5);
831 assert_eq!(
832 sig.to_compact_located(),
833 "fn pub run(id:usize) → bool @L3-5"
834 );
835 assert_eq!(sig.to_tdd_located(), "λ+run(id:n)→b @L3-5");
836 }
837
838 #[test]
839 fn plain_notation_is_self_describing() {
840 let mut sig = sample_fn();
842 sig.kind = "struct";
843 assert_eq!(sig.to_compact(), "struct pub run");
844 sig.kind = "trait";
845 assert_eq!(sig.to_compact(), "trait pub run");
846 sig.kind = "enum";
847 sig.is_exported = false;
848 assert_eq!(sig.to_compact(), "enum run");
849 sig.kind = "const";
850 sig.return_type = "u32".to_string();
851 assert_eq!(sig.to_compact(), "const run:u32");
852 let (extracted, _) =
854 extract_signatures_with_backend("fn f0(a: i32, s: &str) -> i32 {\n}\n", "rs");
855 assert_eq!(extracted[0].to_compact(), "fn f0(a:i32, s:&str) → i32");
856 }
857
858 #[test]
859 fn tdd_legend_explains_only_present_symbols() {
860 let f = sample_fn();
862 let mut s = sample_fn();
863 s.kind = "struct";
864 s.is_exported = false;
865
866 let legend = tdd_legend(&[&f, &s]);
867 assert_eq!(legend, "[λ=fn §=class +=pub]");
868 assert!(
870 lean_ctx_patterns::tokens::count_tokens(&legend) <= 15,
871 "{legend}"
872 );
873
874 assert_eq!(tdd_legend(&[]), "");
875 }
876
877 #[test]
878 fn regex_fallback_assigns_declaration_line_spans() {
879 let src = "\npublic class Service {}\n\npublic fn run() {\n}\n";
880 let sigs = extract_generic_signatures(src);
881
882 let service = sigs.iter().find(|s| s.name == "Service").unwrap();
883 assert_eq!(service.start_line, Some(2));
884 assert_eq!(service.end_line, Some(2));
885
886 let run = sigs.iter().find(|s| s.name == "run").unwrap();
887 assert_eq!(run.start_line, Some(4));
888 assert_eq!(run.end_line, Some(4));
889 }
890
891 #[test]
895 fn regex_fallback_matches_powershell_functions() {
896 let src = "function Get-CargoBinDir {\n}\nfunction Install($x) {\n}\n# function Commented-Out {\n";
897 let sigs = extract_generic_signatures(src);
898 let names: Vec<&str> = sigs.iter().map(|s| s.name.as_str()).collect();
899 assert!(names.contains(&"Get-CargoBinDir"), "got {names:?}");
900 assert!(names.contains(&"Install"), "got {names:?}");
901 assert!(
902 !names.contains(&"Commented-Out"),
903 "comment must be skipped; got {names:?}"
904 );
905 }
906}