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