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