Skip to main content

lean_ctx/core/
signatures.rs

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 {
41        let export = if self.is_exported { "⊛ " } else { "" };
42        let async_prefix = if self.is_async { "async " } else { "" };
43
44        match self.kind {
45            "fn" | "method" => {
46                let ret = if self.return_type.is_empty() {
47                    String::new()
48                } else {
49                    format!(" → {}", self.return_type)
50                };
51                let indent = " ".repeat(self.indent);
52                format!(
53                    "{indent}fn {async_prefix}{export}{}({}){}",
54                    self.name, self.params, ret
55                )
56            }
57            "class" | "struct" => format!("cl {export}{}", self.name),
58            "interface" | "trait" => format!("if {export}{}", self.name),
59            "type" => format!("ty {export}{}", self.name),
60            "enum" => format!("en {export}{}", self.name),
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!("val {export}{}{ty}", self.name)
68            }
69            _ => format!("{} {}", self.kind, self.name),
70        }
71    }
72
73    pub fn to_tdd(&self) -> String {
74        let vis = if self.is_exported { "+" } else { "-" };
75        let a = if self.is_async { "~" } else { "" };
76
77        match self.kind {
78            "fn" | "method" => {
79                let ret = if self.return_type.is_empty() {
80                    String::new()
81                } else {
82                    format!("→{}", compact_type(&self.return_type))
83                };
84                let params = tdd_params(&self.params);
85                let indent = if self.indent > 0 { " " } else { "" };
86                format!("{indent}{a}λ{vis}{}({params}){ret}", self.name)
87            }
88            "class" | "struct" => format!("§{vis}{}", self.name),
89            "interface" | "trait" => format!("∂{vis}{}", self.name),
90            "type" => format!("τ{vis}{}", self.name),
91            "enum" => format!("ε{vis}{}", self.name),
92            "const" | "let" | "var" => {
93                let ty = if self.return_type.is_empty() {
94                    String::new()
95                } else {
96                    format!(":{}", compact_type(&self.return_type))
97                };
98                format!("ν{vis}{}{ty}", self.name)
99            }
100            _ => format!(
101                "{}{vis}{}",
102                self.kind.chars().next().unwrap_or('?'),
103                self.name
104            ),
105        }
106    }
107}
108
109fn fn_re() -> &'static Regex {
110    static_regex!(
111        r"^(\s*)(export\s+)?(async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\{]+))?\s*\{?"
112    )
113}
114
115fn class_re() -> &'static Regex {
116    static_regex!(r"^(\s*)(export\s+)?(abstract\s+)?class\s+(\w+)")
117}
118
119fn iface_re() -> &'static Regex {
120    static_regex!(r"^(\s*)(export\s+)?interface\s+(\w+)")
121}
122
123fn type_re() -> &'static Regex {
124    static_regex!(r"^(\s*)(export\s+)?type\s+(\w+)")
125}
126
127fn const_re() -> &'static Regex {
128    static_regex!(r"^(\s*)(export\s+)?(const|let|var)\s+(\w+)(?:\s*:\s*(\w+))?")
129}
130
131fn rust_fn_re() -> &'static Regex {
132    static_regex!(
133        r"^(\s*)(pub\s+)?(async\s+)?fn\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*->\s*([^\{]+))?\s*\{?"
134    )
135}
136
137fn rust_struct_re() -> &'static Regex {
138    static_regex!(r"^(\s*)(pub\s+)?struct\s+(\w+)")
139}
140
141fn rust_enum_re() -> &'static Regex {
142    static_regex!(r"^(\s*)(pub\s+)?enum\s+(\w+)")
143}
144
145fn rust_trait_re() -> &'static Regex {
146    static_regex!(r"^(\s*)(pub\s+)?trait\s+(\w+)")
147}
148
149fn rust_impl_re() -> &'static Regex {
150    static_regex!(r"^(\s*)impl\s+(?:(\w+)\s+for\s+)?(\w+)")
151}
152
153use std::sync::atomic::{AtomicU64, Ordering};
154
155static TREE_SITTER_HITS: AtomicU64 = AtomicU64::new(0);
156static REGEX_FALLBACK_HITS: AtomicU64 = AtomicU64::new(0);
157
158/// Returns (tree_sitter_hits, regex_fallback_hits) since process start.
159pub fn signature_backend_stats() -> (u64, u64) {
160    (
161        TREE_SITTER_HITS.load(Ordering::Relaxed),
162        REGEX_FALLBACK_HITS.load(Ordering::Relaxed),
163    )
164}
165
166pub fn extract_signatures(content: &str, file_ext: &str) -> Vec<Signature> {
167    #[cfg(feature = "tree-sitter")]
168    {
169        if let Some(sigs) = super::signatures_ts::extract_signatures_ts(content, file_ext) {
170            TREE_SITTER_HITS.fetch_add(1, Ordering::Relaxed);
171            return sigs;
172        }
173    }
174
175    REGEX_FALLBACK_HITS.fetch_add(1, Ordering::Relaxed);
176    match file_ext {
177        "rs" => extract_rust_signatures(content),
178        "ts" | "tsx" | "js" | "jsx" | "svelte" | "vue" => extract_ts_signatures(content),
179        "py" => extract_python_signatures(content),
180        "go" => extract_go_signatures(content),
181        _ => extract_generic_signatures(content),
182    }
183}
184
185pub fn extract_file_map(path: &str, content: &str) -> String {
186    let ext = std::path::Path::new(path)
187        .extension()
188        .and_then(|e| e.to_str())
189        .unwrap_or("rs");
190    let dep_info = super::deps::extract_deps(content, ext);
191    let sigs = extract_signatures(content, ext);
192    let mut parts = Vec::new();
193    if !dep_info.imports.is_empty() {
194        parts.push(dep_info.imports.join(","));
195    }
196    let key_sigs: Vec<String> = sigs
197        .iter()
198        .filter(|s| s.is_exported || s.indent == 0)
199        .map(Signature::to_compact)
200        .collect();
201    if !key_sigs.is_empty() {
202        parts.push(key_sigs.join("\n"));
203    }
204    parts.join("\n")
205}
206
207fn extract_ts_signatures(content: &str) -> Vec<Signature> {
208    let mut sigs = Vec::new();
209
210    for line in content.lines() {
211        let trimmed = line.trim();
212        if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') {
213            continue;
214        }
215
216        if let Some(caps) = fn_re().captures(line) {
217            let indent = caps.get(1).map_or(0, |m| m.as_str().len());
218            sigs.push(Signature {
219                kind: if indent > 0 { "method" } else { "fn" },
220                name: caps[4].to_string(),
221                params: compact_params(&caps[5]),
222                return_type: caps
223                    .get(6)
224                    .map_or(String::new(), |m| m.as_str().trim().to_string()),
225                is_async: caps.get(3).is_some(),
226                is_exported: caps.get(2).is_some(),
227                indent: if indent > 0 { 2 } else { 0 },
228                ..Signature::no_span()
229            });
230        } else if let Some(caps) = class_re().captures(line) {
231            sigs.push(Signature {
232                kind: "class",
233                name: caps[4].to_string(),
234                params: String::new(),
235                return_type: String::new(),
236                is_async: false,
237                is_exported: caps.get(2).is_some(),
238                indent: 0,
239                ..Signature::no_span()
240            });
241        } else if let Some(caps) = iface_re().captures(line) {
242            sigs.push(Signature {
243                kind: "interface",
244                name: caps[3].to_string(),
245                params: String::new(),
246                return_type: String::new(),
247                is_async: false,
248                is_exported: caps.get(2).is_some(),
249                indent: 0,
250                ..Signature::no_span()
251            });
252        } else if let Some(caps) = type_re().captures(line) {
253            sigs.push(Signature {
254                kind: "type",
255                name: caps[3].to_string(),
256                params: String::new(),
257                return_type: String::new(),
258                is_async: false,
259                is_exported: caps.get(2).is_some(),
260                indent: 0,
261                ..Signature::no_span()
262            });
263        } else if let Some(caps) = const_re().captures(line) {
264            if caps.get(2).is_some() {
265                sigs.push(Signature {
266                    kind: "const",
267                    name: caps[4].to_string(),
268                    params: String::new(),
269                    return_type: caps
270                        .get(5)
271                        .map_or(String::new(), |m| m.as_str().to_string()),
272                    is_async: false,
273                    is_exported: true,
274                    indent: 0,
275                    ..Signature::no_span()
276                });
277            }
278        }
279    }
280
281    sigs
282}
283
284fn extract_rust_signatures(content: &str) -> Vec<Signature> {
285    let mut sigs = Vec::new();
286
287    for line in content.lines() {
288        let trimmed = line.trim();
289        if trimmed.starts_with("//") || trimmed.starts_with("///") {
290            continue;
291        }
292
293        if let Some(caps) = rust_fn_re().captures(line) {
294            let indent = caps.get(1).map_or(0, |m| m.as_str().len());
295            sigs.push(Signature {
296                kind: if indent > 0 { "method" } else { "fn" },
297                name: caps[4].to_string(),
298                params: compact_params(&caps[5]),
299                return_type: caps
300                    .get(6)
301                    .map_or(String::new(), |m| m.as_str().trim().to_string()),
302                is_async: caps.get(3).is_some(),
303                is_exported: caps.get(2).is_some(),
304                indent: if indent > 0 { 2 } else { 0 },
305                ..Signature::no_span()
306            });
307        } else if let Some(caps) = rust_struct_re().captures(line) {
308            sigs.push(Signature {
309                kind: "struct",
310                name: caps[3].to_string(),
311                params: String::new(),
312                return_type: String::new(),
313                is_async: false,
314                is_exported: caps.get(2).is_some(),
315                indent: 0,
316                ..Signature::no_span()
317            });
318        } else if let Some(caps) = rust_enum_re().captures(line) {
319            sigs.push(Signature {
320                kind: "enum",
321                name: caps[3].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                ..Signature::no_span()
328            });
329        } else if let Some(caps) = rust_trait_re().captures(line) {
330            sigs.push(Signature {
331                kind: "trait",
332                name: caps[3].to_string(),
333                params: String::new(),
334                return_type: String::new(),
335                is_async: false,
336                is_exported: caps.get(2).is_some(),
337                indent: 0,
338                ..Signature::no_span()
339            });
340        } else if let Some(caps) = rust_impl_re().captures(line) {
341            let trait_name = caps.get(2).map(|m| m.as_str());
342            let type_name = &caps[3];
343            let name = if let Some(t) = trait_name {
344                format!("{t} for {type_name}")
345            } else {
346                type_name.to_string()
347            };
348            sigs.push(Signature {
349                kind: "class",
350                name,
351                params: String::new(),
352                return_type: String::new(),
353                is_async: false,
354                is_exported: false,
355                indent: 0,
356                ..Signature::no_span()
357            });
358        }
359    }
360
361    sigs
362}
363
364fn extract_python_signatures(content: &str) -> Vec<Signature> {
365    let mut sigs = Vec::new();
366    let py_fn = static_regex!(r"^(\s*)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?");
367    let py_class = static_regex!(r"^(\s*)class\s+(\w+)");
368
369    for line in content.lines() {
370        if let Some(caps) = py_fn.captures(line) {
371            let indent = caps.get(1).map_or(0, |m| m.as_str().len());
372            sigs.push(Signature {
373                kind: if indent > 0 { "method" } else { "fn" },
374                name: caps[3].to_string(),
375                params: compact_params(&caps[4]),
376                return_type: caps
377                    .get(5)
378                    .map_or(String::new(), |m| m.as_str().to_string()),
379                is_async: caps.get(2).is_some(),
380                is_exported: !caps[3].starts_with('_'),
381                indent: if indent > 0 { 2 } else { 0 },
382                ..Signature::no_span()
383            });
384        } else if let Some(caps) = py_class.captures(line) {
385            sigs.push(Signature {
386                kind: "class",
387                name: caps[2].to_string(),
388                params: String::new(),
389                return_type: String::new(),
390                is_async: false,
391                is_exported: !caps[2].starts_with('_'),
392                indent: 0,
393                ..Signature::no_span()
394            });
395        }
396    }
397
398    sigs
399}
400
401fn extract_go_signatures(content: &str) -> Vec<Signature> {
402    let mut sigs = Vec::new();
403    let go_fn = static_regex!(
404        r"^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|(\w+)))?\s*\{"
405    );
406    let go_type = static_regex!(r"^type\s+(\w+)\s+(struct|interface)");
407
408    for line in content.lines() {
409        if let Some(caps) = go_fn.captures(line) {
410            let is_method = caps.get(2).is_some();
411            sigs.push(Signature {
412                kind: if is_method { "method" } else { "fn" },
413                name: caps[3].to_string(),
414                params: compact_params(&caps[4]),
415                return_type: caps
416                    .get(5)
417                    .or(caps.get(6))
418                    .map_or(String::new(), |m| m.as_str().to_string()),
419                is_async: false,
420                is_exported: caps[3].starts_with(char::is_uppercase),
421                indent: if is_method { 2 } else { 0 },
422                ..Signature::no_span()
423            });
424        } else if let Some(caps) = go_type.captures(line) {
425            sigs.push(Signature {
426                kind: if &caps[2] == "struct" {
427                    "struct"
428                } else {
429                    "interface"
430                },
431                name: caps[1].to_string(),
432                params: String::new(),
433                return_type: String::new(),
434                is_async: false,
435                is_exported: caps[1].starts_with(char::is_uppercase),
436                indent: 0,
437                ..Signature::no_span()
438            });
439        }
440    }
441
442    sigs
443}
444
445pub(crate) fn compact_params(params: &str) -> String {
446    if params.trim().is_empty() {
447        return String::new();
448    }
449    params
450        .split(',')
451        .map(|p| {
452            let p = p.trim();
453            if let Some((name, ty)) = p.split_once(':') {
454                let name = name.trim();
455                let ty = ty.trim();
456                let short = match ty {
457                    "string" | "String" | "&str" | "str" => ":s",
458                    "number" | "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" => ":n",
459                    "boolean" | "bool" => ":b",
460                    _ => return format!("{name}:{ty}"),
461                };
462                format!("{name}{short}")
463            } else {
464                p.to_string()
465            }
466        })
467        .collect::<Vec<_>>()
468        .join(", ")
469}
470
471fn compact_type(ty: &str) -> String {
472    match ty.trim() {
473        "String" | "string" | "&str" | "str" => "s".to_string(),
474        "bool" | "boolean" => "b".to_string(),
475        "i32" | "i64" | "u32" | "u64" | "usize" | "f32" | "f64" | "number" => "n".to_string(),
476        "void" | "()" => "∅".to_string(),
477        other => {
478            if other.starts_with("Vec<") || other.starts_with("Array<") {
479                let inner = other
480                    .trim_start_matches("Vec<")
481                    .trim_start_matches("Array<")
482                    .trim_end_matches('>');
483                format!("[{}]", compact_type(inner))
484            } else if other.starts_with("Option<") || other.starts_with("Maybe<") {
485                let inner = other
486                    .trim_start_matches("Option<")
487                    .trim_start_matches("Maybe<")
488                    .trim_end_matches('>');
489                format!("?{}", compact_type(inner))
490            } else if other.starts_with("Result<") {
491                "R".to_string()
492            } else if other.starts_with("impl ") {
493                other.trim_start_matches("impl ").to_string()
494            } else {
495                other.to_string()
496            }
497        }
498    }
499}
500
501fn tdd_params(params: &str) -> String {
502    if params.trim().is_empty() {
503        return String::new();
504    }
505    params
506        .split(',')
507        .map(|p| {
508            let p = p.trim();
509            if p.starts_with('&') {
510                let rest = p.trim_start_matches("&mut ").trim_start_matches('&');
511                if let Some((name, ty)) = rest.split_once(':') {
512                    format!("&{}:{}", name.trim(), compact_type(ty))
513                } else {
514                    p.to_string()
515                }
516            } else if let Some((name, ty)) = p.split_once(':') {
517                format!("{}:{}", name.trim(), compact_type(ty))
518            } else if p == "self" || p == "&self" || p == "&mut self" {
519                "⊕".to_string()
520            } else {
521                p.to_string()
522            }
523        })
524        .collect::<Vec<_>>()
525        .join(",")
526}
527
528fn extract_generic_signatures(content: &str) -> Vec<Signature> {
529    let re_func = static_regex!(
530        r"^\s*(?:(?:public|private|protected|static|async|abstract|virtual|override|final|def|func|fun|fn)\s+)+(\w+)\s*\("
531    );
532    let re_class = static_regex!(
533        r"^\s*(?:(?:public|private|protected|abstract|final|sealed|partial)\s+)*(?:class|struct|enum|interface|trait|module|object|record)\s+(\w+)"
534    );
535
536    let mut sigs = Vec::new();
537    for line in content.lines() {
538        let trimmed = line.trim();
539        if trimmed.is_empty()
540            || trimmed.starts_with("//")
541            || trimmed.starts_with('#')
542            || trimmed.starts_with("/*")
543            || trimmed.starts_with('*')
544        {
545            continue;
546        }
547        if let Some(caps) = re_class.captures(trimmed) {
548            sigs.push(Signature {
549                kind: "type",
550                name: caps[1].to_string(),
551                params: String::new(),
552                return_type: String::new(),
553                is_async: false,
554                is_exported: true,
555                indent: 0,
556                ..Signature::no_span()
557            });
558        } else if let Some(caps) = re_func.captures(trimmed) {
559            sigs.push(Signature {
560                kind: "fn",
561                name: caps[1].to_string(),
562                params: String::new(),
563                return_type: String::new(),
564                is_async: trimmed.contains("async"),
565                is_exported: true,
566                indent: 0,
567                ..Signature::no_span()
568            });
569        }
570    }
571    sigs
572}