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 match backend {
290 SigBackend::TreeSitter => TREE_SITTER_HITS.fetch_add(1, Ordering::Relaxed),
291 SigBackend::Regex => REGEX_FALLBACK_HITS.fetch_add(1, Ordering::Relaxed),
292 };
293 sigs
294}
295
296pub fn extract_signatures_with_backend(
301 content: &str,
302 file_ext: &str,
303) -> (Vec<Signature>, SigBackend) {
304 #[cfg(feature = "tree-sitter")]
305 {
306 if let Some(sigs) = super::signatures_ts::extract_signatures_ts(content, file_ext)
312 && !sigs.is_empty()
313 {
314 return (sigs, SigBackend::TreeSitter);
315 }
316 }
317
318 let sigs = match file_ext {
319 "rs" => extract_rust_signatures(content),
320 "ts" | "tsx" | "js" | "jsx" | "svelte" | "vue" => extract_ts_signatures(content),
321 "py" => extract_python_signatures(content),
322 "go" => extract_go_signatures(content),
323 _ => extract_generic_signatures(content),
324 };
325 (sigs, SigBackend::Regex)
326}
327
328pub fn extract_file_map(path: &str, content: &str) -> String {
329 let ext = std::path::Path::new(path)
330 .extension()
331 .and_then(|e| e.to_str())
332 .unwrap_or("rs");
333 let dep_info = super::deps::extract_deps(content, ext);
334 let sigs = extract_signatures(content, ext);
335 let mut parts = Vec::new();
336 if !dep_info.imports.is_empty() {
337 parts.push(dep_info.imports.join(","));
338 }
339 let key_sigs: Vec<String> = sigs
340 .iter()
341 .filter(|s| s.is_exported || s.indent == 0)
342 .map(Signature::to_compact_located)
343 .collect();
344 if !key_sigs.is_empty() {
345 parts.push(key_sigs.join("\n"));
346 }
347 parts.join("\n")
348}
349
350fn extract_ts_signatures(content: &str) -> Vec<Signature> {
351 let mut sigs = Vec::new();
352
353 for (line_idx, line) in content.lines().enumerate() {
354 let line_no = line_idx + 1;
355 let trimmed = line.trim();
356 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') {
357 continue;
358 }
359
360 if let Some(caps) = fn_re().captures(line) {
361 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
362 sigs.push(Signature {
363 kind: if indent > 0 { "method" } else { "fn" },
364 name: caps[4].to_string(),
365 params: compact_params(&caps[5]),
366 return_type: caps
367 .get(6)
368 .map_or(String::new(), |m| m.as_str().trim().to_string()),
369 is_async: caps.get(3).is_some(),
370 is_exported: caps.get(2).is_some(),
371 indent: if indent > 0 { 2 } else { 0 },
372 start_line: Some(line_no),
373 end_line: Some(line_no),
374 });
375 } else if let Some(caps) = class_re().captures(line) {
376 sigs.push(Signature {
377 kind: "class",
378 name: caps[4].to_string(),
379 params: String::new(),
380 return_type: String::new(),
381 is_async: false,
382 is_exported: caps.get(2).is_some(),
383 indent: 0,
384 start_line: Some(line_no),
385 end_line: Some(line_no),
386 });
387 } else if let Some(caps) = iface_re().captures(line) {
388 sigs.push(Signature {
389 kind: "interface",
390 name: caps[3].to_string(),
391 params: String::new(),
392 return_type: String::new(),
393 is_async: false,
394 is_exported: caps.get(2).is_some(),
395 indent: 0,
396 start_line: Some(line_no),
397 end_line: Some(line_no),
398 });
399 } else if let Some(caps) = type_re().captures(line) {
400 sigs.push(Signature {
401 kind: "type",
402 name: caps[3].to_string(),
403 params: String::new(),
404 return_type: String::new(),
405 is_async: false,
406 is_exported: caps.get(2).is_some(),
407 indent: 0,
408 start_line: Some(line_no),
409 end_line: Some(line_no),
410 });
411 } else if let Some(caps) = const_re().captures(line)
412 && caps.get(2).is_some()
413 {
414 sigs.push(Signature {
415 kind: "const",
416 name: caps[4].to_string(),
417 params: String::new(),
418 return_type: caps
419 .get(5)
420 .map_or(String::new(), |m| m.as_str().to_string()),
421 is_async: false,
422 is_exported: true,
423 indent: 0,
424 start_line: Some(line_no),
425 end_line: Some(line_no),
426 });
427 }
428 }
429
430 sigs
431}
432
433fn extract_rust_signatures(content: &str) -> Vec<Signature> {
434 let mut sigs = Vec::new();
435
436 for (line_idx, line) in content.lines().enumerate() {
437 let line_no = line_idx + 1;
438 let trimmed = line.trim();
439 if trimmed.starts_with("//") || trimmed.starts_with("///") {
440 continue;
441 }
442
443 if let Some(caps) = rust_fn_re().captures(line) {
444 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
445 sigs.push(Signature {
446 kind: if indent > 0 { "method" } else { "fn" },
447 name: caps[4].to_string(),
448 params: compact_params(&caps[5]),
449 return_type: caps
450 .get(6)
451 .map_or(String::new(), |m| m.as_str().trim().to_string()),
452 is_async: caps.get(3).is_some(),
453 is_exported: caps.get(2).is_some(),
454 indent: if indent > 0 { 2 } else { 0 },
455 start_line: Some(line_no),
456 end_line: Some(line_no),
457 });
458 } else if let Some(caps) = rust_struct_re().captures(line) {
459 sigs.push(Signature {
460 kind: "struct",
461 name: caps[3].to_string(),
462 params: String::new(),
463 return_type: String::new(),
464 is_async: false,
465 is_exported: caps.get(2).is_some(),
466 indent: 0,
467 start_line: Some(line_no),
468 end_line: Some(line_no),
469 });
470 } else if let Some(caps) = rust_enum_re().captures(line) {
471 sigs.push(Signature {
472 kind: "enum",
473 name: caps[3].to_string(),
474 params: String::new(),
475 return_type: String::new(),
476 is_async: false,
477 is_exported: caps.get(2).is_some(),
478 indent: 0,
479 start_line: Some(line_no),
480 end_line: Some(line_no),
481 });
482 } else if let Some(caps) = rust_trait_re().captures(line) {
483 sigs.push(Signature {
484 kind: "trait",
485 name: caps[3].to_string(),
486 params: String::new(),
487 return_type: String::new(),
488 is_async: false,
489 is_exported: caps.get(2).is_some(),
490 indent: 0,
491 start_line: Some(line_no),
492 end_line: Some(line_no),
493 });
494 } else if let Some(caps) = rust_impl_re().captures(line) {
495 let trait_name = caps.get(2).map(|m| m.as_str());
496 let type_name = &caps[3];
497 let name = if let Some(t) = trait_name {
498 format!("{t} for {type_name}")
499 } else {
500 type_name.to_string()
501 };
502 sigs.push(Signature {
503 kind: "impl",
504 name,
505 params: String::new(),
506 return_type: String::new(),
507 is_async: false,
508 is_exported: false,
509 indent: 0,
510 start_line: Some(line_no),
511 end_line: Some(line_no),
512 });
513 }
514 }
515
516 sigs
517}
518
519fn extract_python_signatures(content: &str) -> Vec<Signature> {
520 let mut sigs = Vec::new();
521 let py_fn = static_regex!(r"^(\s*)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?");
522 let py_class = static_regex!(r"^(\s*)class\s+(\w+)");
523
524 for (line_idx, line) in content.lines().enumerate() {
525 let line_no = line_idx + 1;
526 if let Some(caps) = py_fn.captures(line) {
527 let indent = caps.get(1).map_or(0, |m| m.as_str().len());
528 sigs.push(Signature {
529 kind: if indent > 0 { "method" } else { "fn" },
530 name: caps[3].to_string(),
531 params: compact_params(&caps[4]),
532 return_type: caps
533 .get(5)
534 .map_or(String::new(), |m| m.as_str().to_string()),
535 is_async: caps.get(2).is_some(),
536 is_exported: !caps[3].starts_with('_'),
537 indent: if indent > 0 { 2 } else { 0 },
538 start_line: Some(line_no),
539 end_line: Some(line_no),
540 });
541 } else if let Some(caps) = py_class.captures(line) {
542 sigs.push(Signature {
543 kind: "class",
544 name: caps[2].to_string(),
545 params: String::new(),
546 return_type: String::new(),
547 is_async: false,
548 is_exported: !caps[2].starts_with('_'),
549 indent: 0,
550 start_line: Some(line_no),
551 end_line: Some(line_no),
552 });
553 }
554 }
555
556 sigs
557}
558
559fn extract_go_signatures(content: &str) -> Vec<Signature> {
560 let mut sigs = Vec::new();
561 let go_fn = static_regex!(
562 r"^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|(\w+)))?\s*\{"
563 );
564 let go_type = static_regex!(r"^type\s+(\w+)\s+(struct|interface)");
565
566 for (line_idx, line) in content.lines().enumerate() {
567 let line_no = line_idx + 1;
568 if let Some(caps) = go_fn.captures(line) {
569 let is_method = caps.get(2).is_some();
570 sigs.push(Signature {
571 kind: if is_method { "method" } else { "fn" },
572 name: caps[3].to_string(),
573 params: compact_params(&caps[4]),
574 return_type: caps
575 .get(5)
576 .or(caps.get(6))
577 .map_or(String::new(), |m| m.as_str().to_string()),
578 is_async: false,
579 is_exported: caps[3].starts_with(char::is_uppercase),
580 indent: if is_method { 2 } else { 0 },
581 start_line: Some(line_no),
582 end_line: Some(line_no),
583 });
584 } else if let Some(caps) = go_type.captures(line) {
585 sigs.push(Signature {
586 kind: if &caps[2] == "struct" {
587 "struct"
588 } else {
589 "interface"
590 },
591 name: caps[1].to_string(),
592 params: String::new(),
593 return_type: String::new(),
594 is_async: false,
595 is_exported: caps[1].starts_with(char::is_uppercase),
596 indent: 0,
597 start_line: Some(line_no),
598 end_line: Some(line_no),
599 });
600 }
601 }
602
603 sigs
604}
605
606pub(crate) fn compact_params(params: &str) -> String {
607 if params.trim().is_empty() {
608 return String::new();
609 }
610 params
611 .split(',')
612 .map(|p| {
613 let p = p.trim();
614 if let Some((name, ty)) = p.split_once(':') {
615 let name = name.trim();
616 let ty = ty.trim();
617 let short = match ty {
618 "string" | "String" | "&str" | "str" => ":s",
619 "number" | "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" => ":n",
620 "boolean" | "bool" => ":b",
621 _ => return format!("{name}:{ty}"),
622 };
623 format!("{name}{short}")
624 } else {
625 p.to_string()
626 }
627 })
628 .collect::<Vec<_>>()
629 .join(", ")
630}
631
632fn compact_type(ty: &str) -> String {
633 match ty.trim() {
634 "String" | "string" | "&str" | "str" => "s".to_string(),
635 "bool" | "boolean" => "b".to_string(),
636 "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" | "number" => "n".to_string(),
637 "void" | "()" => "∅".to_string(),
638 other => {
639 if other.starts_with("Vec<") || other.starts_with("Array<") {
640 let inner = other
641 .trim_start_matches("Vec<")
642 .trim_start_matches("Array<")
643 .trim_end_matches('>');
644 format!("[{}]", compact_type(inner))
645 } else if other.starts_with("Option<") || other.starts_with("Maybe<") {
646 let inner = other
647 .trim_start_matches("Option<")
648 .trim_start_matches("Maybe<")
649 .trim_end_matches('>');
650 format!("?{}", compact_type(inner))
651 } else if other.starts_with("Result<") {
652 "R".to_string()
653 } else if other.starts_with("impl ") {
654 other.trim_start_matches("impl ").to_string()
655 } else {
656 other.to_string()
657 }
658 }
659 }
660}
661
662fn tdd_params(params: &str) -> String {
663 if params.trim().is_empty() {
664 return String::new();
665 }
666 params
667 .split(',')
668 .map(|p| {
669 let p = p.trim();
670 if p.starts_with('&') {
671 let rest = p.trim_start_matches("&mut ").trim_start_matches('&');
672 if let Some((name, ty)) = rest.split_once(':') {
673 format!("&{}:{}", name.trim(), compact_type(ty))
674 } else {
675 p.to_string()
676 }
677 } else if let Some((name, ty)) = p.split_once(':') {
678 format!("{}:{}", name.trim(), compact_type(ty))
679 } else if p == "self" || p == "&self" || p == "&mut self" {
680 "⊕".to_string()
681 } else {
682 p.to_string()
683 }
684 })
685 .collect::<Vec<_>>()
686 .join(",")
687}
688
689fn extract_generic_signatures(content: &str) -> Vec<Signature> {
690 let re_func = static_regex!(
691 r"^\s*(?:(?:public|private|protected|static|async|abstract|virtual|override|final|def|func|fun|fn)\s+)+(\w+)\s*\("
692 );
693 let re_class = static_regex!(
694 r"^\s*(?:(?:public|private|protected|abstract|final|sealed|partial)\s+)*(?:class|struct|enum|interface|trait|module|object|record)\s+(\w+)"
695 );
696
697 let mut sigs = Vec::new();
698 for (line_idx, line) in content.lines().enumerate() {
699 let line_no = line_idx + 1;
700 let trimmed = line.trim();
701 if trimmed.is_empty()
702 || trimmed.starts_with("//")
703 || trimmed.starts_with('#')
704 || trimmed.starts_with("/*")
705 || trimmed.starts_with('*')
706 {
707 continue;
708 }
709 if let Some(caps) = re_class.captures(trimmed) {
710 sigs.push(Signature {
711 kind: "type",
712 name: caps[1].to_string(),
713 params: String::new(),
714 return_type: String::new(),
715 is_async: false,
716 is_exported: true,
717 indent: 0,
718 start_line: Some(line_no),
719 end_line: Some(line_no),
720 });
721 } else if let Some(caps) = re_func.captures(trimmed) {
722 sigs.push(Signature {
723 kind: "fn",
724 name: caps[1].to_string(),
725 params: String::new(),
726 return_type: String::new(),
727 is_async: trimmed.contains("async"),
728 is_exported: true,
729 indent: 0,
730 start_line: Some(line_no),
731 end_line: Some(line_no),
732 });
733 }
734 }
735 sigs
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741
742 fn sample_fn() -> Signature {
743 Signature {
744 kind: "fn",
745 name: "run".to_string(),
746 params: "id:usize".to_string(),
747 return_type: "bool".to_string(),
748 is_async: false,
749 is_exported: true,
750 indent: 0,
751 start_line: None,
752 end_line: None,
753 }
754 }
755
756 #[test]
757 fn line_suffix_formats_known_spans() {
758 let mut sig = sample_fn();
759 assert_eq!(sig.line_suffix(), "");
760
761 sig.start_line = Some(42);
762 sig.end_line = Some(42);
763 assert_eq!(sig.line_suffix(), " @L42");
764
765 sig.end_line = Some(57);
766 assert_eq!(sig.line_suffix(), " @L42-57");
767 }
768
769 #[test]
770 fn base_renderers_stay_suffix_free() {
771 let mut sig = sample_fn();
774 sig.start_line = Some(3);
775 sig.end_line = Some(9);
776 assert_eq!(sig.to_compact(), "fn pub run(id:usize) → bool");
777 assert_eq!(sig.to_tdd(), "λ+run(id:n)→b");
778 }
779
780 #[test]
781 fn located_renderers_append_line_suffix() {
782 let mut sig = sample_fn();
783 assert_eq!(sig.to_compact_located(), "fn pub run(id:usize) → bool");
785 assert_eq!(sig.to_tdd_located(), "λ+run(id:n)→b");
786
787 sig.start_line = Some(3);
788 sig.end_line = Some(5);
789 assert_eq!(
790 sig.to_compact_located(),
791 "fn pub run(id:usize) → bool @L3-5"
792 );
793 assert_eq!(sig.to_tdd_located(), "λ+run(id:n)→b @L3-5");
794 }
795
796 #[test]
797 fn plain_notation_is_self_describing() {
798 let mut sig = sample_fn();
800 sig.kind = "struct";
801 assert_eq!(sig.to_compact(), "struct pub run");
802 sig.kind = "trait";
803 assert_eq!(sig.to_compact(), "trait pub run");
804 sig.kind = "enum";
805 sig.is_exported = false;
806 assert_eq!(sig.to_compact(), "enum run");
807 sig.kind = "const";
808 sig.return_type = "u32".to_string();
809 assert_eq!(sig.to_compact(), "const run:u32");
810 }
811
812 #[test]
813 fn tdd_legend_explains_only_present_symbols() {
814 let f = sample_fn();
816 let mut s = sample_fn();
817 s.kind = "struct";
818 s.is_exported = false;
819
820 let legend = tdd_legend(&[&f, &s]);
821 assert_eq!(legend, "[λ=fn §=class +=pub]");
822 assert!(crate::core::tokens::count_tokens(&legend) <= 15, "{legend}");
824
825 assert_eq!(tdd_legend(&[]), "");
826 }
827
828 #[test]
829 fn regex_fallback_assigns_declaration_line_spans() {
830 let src = "\npublic class Service {}\n\npublic fn run() {\n}\n";
831 let sigs = extract_generic_signatures(src);
832
833 let service = sigs.iter().find(|s| s.name == "Service").unwrap();
834 assert_eq!(service.start_line, Some(2));
835 assert_eq!(service.end_line, Some(2));
836
837 let run = sigs.iter().find(|s| s.name == "run").unwrap();
838 assert_eq!(run.start_line, Some(4));
839 assert_eq!(run.end_line, Some(4));
840 }
841}