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