1use std::sync::Arc;
2
3use rustc_hash::FxHashSet;
4
5use crate::config::graph_filtering::GRAPH_FILTERING;
6use crate::types::{Fragment, FragmentId, FragmentKind};
7
8fn is_signature_eligible(kind: FragmentKind) -> bool {
9 matches!(
13 kind,
14 FragmentKind::Function
15 | FragmentKind::Class
16 | FragmentKind::Struct
17 | FragmentKind::Interface
18 | FragmentKind::Enum
19 | FragmentKind::Variable
20 )
21}
22
23fn signature_kind(kind: FragmentKind) -> FragmentKind {
24 match kind {
25 FragmentKind::Function => FragmentKind::FunctionSignature,
26 FragmentKind::Class => FragmentKind::ClassSignature,
27 FragmentKind::Struct => FragmentKind::StructSignature,
28 FragmentKind::Interface => FragmentKind::InterfaceSignature,
29 FragmentKind::Enum => FragmentKind::EnumSignature,
30 _ => FragmentKind::FunctionSignature,
31 }
32}
33
34fn count_brackets_outside_strings(line: &str) -> (i32, i32, i32, i32) {
35 let mut open_parens = 0i32;
36 let mut close_parens = 0i32;
37 let mut open_braces = 0i32;
38 let mut close_braces = 0i32;
39 let mut in_string: Option<char> = None;
40 let mut escaped = false;
41
42 for ch in line.chars() {
43 if let Some(quote) = in_string {
44 if escaped {
45 escaped = false;
46 } else if ch == '\\' {
47 escaped = true;
48 } else if ch == quote {
49 in_string = None;
50 }
51 continue;
52 }
53 match ch {
54 '\'' | '"' | '`' => {
55 in_string = Some(ch);
56 escaped = false;
57 }
58 '(' => open_parens += 1,
59 ')' => close_parens += 1,
60 '{' => open_braces += 1,
61 '}' => close_braces += 1,
62 _ => {}
63 }
64 }
65
66 (open_parens, close_parens, open_braces, close_braces)
67}
68
69fn decorator_prefix_len(lines: &[&str]) -> usize {
70 let mut i = 0;
71 let mut paren_depth = 0i32;
72 while i < lines.len() {
73 let trimmed = lines[i].trim_start();
74 let starts_decorator = trimmed.starts_with('@') || trimmed.starts_with("#[");
75 if paren_depth <= 0 && !starts_decorator {
76 break;
77 }
78 let (op, cp, _, _) = count_brackets_outside_strings(lines[i]);
79 paren_depth += op - cp;
80 i += 1;
81 }
82 if i >= lines.len() { 0 } else { i }
83}
84
85fn find_signature_end(lines: &[&str]) -> usize {
86 let mut paren_depth = 0i32;
87 let mut seen_open_paren = false;
88
89 for (i, line) in lines.iter().enumerate() {
90 let (op, cp, ob, cb) = count_brackets_outside_strings(line);
91 paren_depth += op - cp;
92 if op > 0 {
93 seen_open_paren = true;
94 }
95 if paren_depth <= 0 && ob - cb > 0 {
100 return i + 1;
101 }
102 if seen_open_paren && paren_depth <= 0 {
103 return i + 1;
104 }
105 }
106
107 2.min(lines.len())
108}
109
110pub fn generate_signature_variants(fragments: &[Fragment]) -> Vec<Fragment> {
111 let mut signatures: Vec<Fragment> = Vec::new();
112 let mut seen: FxHashSet<FragmentId> = FxHashSet::default();
113
114 for frag in fragments {
115 if !is_signature_eligible(frag.kind) {
116 continue;
117 }
118 if frag.line_count() < GRAPH_FILTERING.min_lines_for_signature {
119 continue;
120 }
121 let lines: Vec<&str> = frag.content.lines().collect();
122 let decorators = decorator_prefix_len(&lines);
123 let sig_end = decorators + find_signature_end(&lines[decorators..]);
124 let sig_content: String = lines[..sig_end].join("\n");
125 let sig_end_line = frag.start_line() + sig_end as u32 - 1;
126 let sig_id = FragmentId::new(frag.id.path.clone(), frag.start_line(), sig_end_line);
127
128 if seen.contains(&sig_id) {
129 continue;
130 }
131 seen.insert(sig_id.clone());
132
133 signatures.push(Fragment {
134 id: sig_id,
135 kind: signature_kind(frag.kind),
136 content: Arc::from(sig_content),
137 identifiers: frag.identifiers.clone(),
138 token_count: 0,
139 symbol_name: frag.symbol_name.clone(),
140 });
141 }
142
143 signatures
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn frag(kind: FragmentKind, start: u32, content: &str) -> Fragment {
151 let line_count = content.lines().count() as u32;
152 Fragment {
153 id: FragmentId::new(Arc::from("a.src"), start, start + line_count - 1),
154 kind,
155 content: Arc::from(content),
156 identifiers: FxHashSet::default(),
157 token_count: 100,
158 symbol_name: Some("target".into()),
159 }
160 }
161
162 fn stub_of(f: &Fragment) -> String {
163 let sigs = generate_signature_variants(std::slice::from_ref(f));
164 assert_eq!(sigs.len(), 1, "expected exactly one signature variant");
165 sigs[0].content.to_string()
166 }
167
168 #[test]
169 fn python_default_containing_braces_does_not_truncate_the_parameter_list() {
170 let f = frag(
173 FragmentKind::Function,
174 1,
175 "def f(\n x={},\n y=1,\n):\n body_one()\n body_two()\n",
176 );
177 let stub = stub_of(&f);
178 assert!(stub.contains("x={}"), "stub lost a parameter: {stub:?}");
179 assert!(stub.contains("y=1"), "stub lost a parameter: {stub:?}");
180 assert!(
181 stub.trim_end().ends_with("):"),
182 "stub is not closed: {stub:?}"
183 );
184 assert!(!stub.contains("body_one"), "stub leaked the body: {stub:?}");
185 }
186
187 #[test]
188 fn open_paren_inside_a_string_literal_does_not_extend_the_signature() {
189 let f = frag(
190 FragmentKind::Function,
191 1,
192 "def f(sep=\"a(b\"):\n one()\n two()\n three()\n four()\n",
193 );
194 let stub = stub_of(&f);
195 assert!(
196 stub.contains("sep=\"a(b\""),
197 "stub mangled the literal: {stub:?}"
198 );
199 assert!(!stub.contains("one()"), "stub leaked the body: {stub:?}");
200 }
201
202 #[test]
203 fn rust_attribute_prefix_is_kept_above_the_signature() {
204 let f = frag(
205 FragmentKind::Struct,
206 10,
207 "#[derive(Debug, Clone)]\npub struct S {\n a: u32,\n b: u32,\n c: u32,\n}\n",
208 );
209 let stub = stub_of(&f);
210 assert!(
211 stub.starts_with("#[derive(Debug, Clone)]"),
212 "lost the attribute: {stub:?}"
213 );
214 assert!(stub.contains("pub struct S {"), "lost the header: {stub:?}");
215 assert!(!stub.contains("a: u32"), "stub leaked the body: {stub:?}");
216 }
217
218 #[test]
219 fn multiline_decorator_prefix_is_kept_whole() {
220 let f = frag(
221 FragmentKind::Function,
222 1,
223 "@retry(\n times=3,\n)\ndef f(x):\n one()\n two()\n",
224 );
225 let stub = stub_of(&f);
226 assert!(stub.starts_with("@retry("), "lost the decorator: {stub:?}");
227 assert!(
228 stub.contains("times=3"),
229 "truncated the decorator: {stub:?}"
230 );
231 assert!(stub.contains("def f(x):"), "lost the signature: {stub:?}");
232 assert!(!stub.contains("one()"), "stub leaked the body: {stub:?}");
233 }
234
235 #[test]
236 fn signature_span_matches_the_emitted_line_count() {
237 let f = frag(
241 FragmentKind::Function,
242 42,
243 "def f(\n x,\n):\n one()\n two()\n three()\n",
244 );
245 let sigs = generate_signature_variants(std::slice::from_ref(&f));
246 let sig = &sigs[0];
247 assert_eq!(sig.id.start_line, 42);
248 assert_eq!(
249 sig.id.end_line - sig.id.start_line + 1,
250 sig.content.lines().count() as u32
251 );
252 }
253
254 #[test]
255 fn kind_maps_to_its_signature_variant_and_ineligible_kinds_are_skipped() {
256 let body = "\nline\nline\nline\nline\nline\n";
257 for (kind, expected) in [
258 (FragmentKind::Function, FragmentKind::FunctionSignature),
259 (FragmentKind::Class, FragmentKind::ClassSignature),
260 (FragmentKind::Struct, FragmentKind::StructSignature),
261 (FragmentKind::Interface, FragmentKind::InterfaceSignature),
262 (FragmentKind::Enum, FragmentKind::EnumSignature),
263 (FragmentKind::Variable, FragmentKind::FunctionSignature),
266 ] {
267 let sigs = generate_signature_variants(&[frag(kind, 1, body)]);
268 assert_eq!(sigs.len(), 1, "{kind:?} produced no signature");
269 assert_eq!(sigs[0].kind, expected, "{kind:?} mapped wrong");
270 }
271 assert!(generate_signature_variants(&[frag(FragmentKind::Chunk, 1, body)]).is_empty());
272 assert!(generate_signature_variants(&[frag(FragmentKind::Module, 1, body)]).is_empty());
273 }
274
275 #[test]
276 fn fragments_shorter_than_the_threshold_get_no_stub() {
277 let short = frag(FragmentKind::Function, 1, "def f():\n one()\n");
278 assert!(generate_signature_variants(&[short]).is_empty());
279 }
280
281 #[test]
282 fn duplicate_signature_spans_are_emitted_once() {
283 let a = frag(
284 FragmentKind::Function,
285 1,
286 "def f(x):\n a()\n b()\n c()\n d()\n",
287 );
288 let b = frag(
289 FragmentKind::Function,
290 1,
291 "def f(x):\n a()\n b()\n c()\n e()\n",
292 );
293 assert_eq!(generate_signature_variants(&[a, b]).len(), 1);
294 }
295
296 #[test]
297 fn signatureless_content_falls_back_to_a_bounded_prefix() {
298 let f = frag(
301 FragmentKind::Class,
302 1,
303 "class C:\n x = 1\n y = 2\n z = 3\n w = 4\n",
304 );
305 let stub = stub_of(&f);
306 assert!(
307 stub.lines().count() <= 2,
308 "fallback took too much: {stub:?}"
309 );
310 assert!(stub.starts_with("class C:"));
311 }
312}