1use crate::line_index::LineIndex;
24use cpd_core::models::Location;
25use oxc_allocator::Allocator;
26use oxc_ast::AstKind;
27use oxc_ast_visit::Visit;
28use oxc_parser::Parser;
29use oxc_span::GetSpan;
30
31#[derive(Debug, Clone, PartialEq)]
33pub struct RawFunction {
34 pub grammar: &'static str,
37 pub name: String,
38 pub start: Location,
39 pub end: Location,
40 pub head: Location,
44 pub kinds: Vec<u16>,
46}
47
48pub trait FunctionExtractor: Send + Sync {
50 fn grammar(&self) -> &'static str;
52 fn formats(&self) -> &'static [&'static str];
54 fn extract(&self, source: &str, format: &str) -> Vec<RawFunction>;
56}
57
58pub static EXTRACTORS: &[&dyn FunctionExtractor] = &[&OxcExtractor];
60
61pub fn extractor_for(format: &str) -> Option<&'static dyn FunctionExtractor> {
63 EXTRACTORS
64 .iter()
65 .copied()
66 .find(|e| e.formats().contains(&format))
67}
68
69pub fn supports_functions(format: &str) -> bool {
71 extractor_for(format).is_some()
72}
73
74pub fn supported_function_formats() -> Vec<&'static str> {
76 EXTRACTORS
77 .iter()
78 .flat_map(|e| e.formats().iter().copied())
79 .collect()
80}
81
82pub fn extract_functions(source: &str, format: &str) -> Vec<RawFunction> {
85 extract_with(extractor_for(format), source, format)
86}
87
88pub fn extract_with(
92 extractor: Option<&dyn FunctionExtractor>,
93 source: &str,
94 format: &str,
95) -> Vec<RawFunction> {
96 match extractor {
97 Some(extractor) if !source.is_empty() => extractor.extract(source, format),
98 _ => Vec::new(),
99 }
100}
101
102pub struct OxcExtractor;
104
105impl FunctionExtractor for OxcExtractor {
106 fn grammar(&self) -> &'static str {
107 "oxc"
108 }
109
110 fn formats(&self) -> &'static [&'static str] {
111 &["javascript", "typescript", "jsx", "tsx"]
112 }
113
114 fn extract(&self, source: &str, format: &str) -> Vec<RawFunction> {
115 extract_with_oxc(source, format)
116 }
117}
118
119fn extract_with_oxc(source: &str, format: &str) -> Vec<RawFunction> {
120 let allocator = Allocator::new();
121 let source_type = crate::javascript::source_type_for_format(format);
122 let parsed = Parser::new(&allocator, source, source_type).parse();
123 if parsed.fatal_error {
126 return Vec::new();
127 }
128 let line_index = LineIndex::new(source.as_bytes());
129 let mut extractor = Extractor {
130 frames: Vec::new(),
131 out: Vec::new(),
132 pending_name: None,
133 pending_head: None,
134 line_index: &line_index,
135 len: source.len(),
136 };
137 extractor.visit_program(&parsed.program);
138 extractor.out
139}
140
141struct Frame {
142 name: String,
143 head: u32,
144 start: u32,
145 end: u32,
146 kinds: Vec<u16>,
147}
148
149struct Extractor<'i> {
150 frames: Vec<Frame>,
151 out: Vec<RawFunction>,
152 pending_name: Option<String>,
155 pending_head: Option<(u32, u32)>,
159 line_index: &'i LineIndex,
160 len: usize,
161}
162
163impl Extractor<'_> {
164 fn open(&mut self, name: String, start: u32, end: u32) {
165 let head = self
166 .pending_head
167 .take()
168 .filter(|&(_, value)| value == start)
169 .map_or(start, |(head, _)| head);
170 self.frames.push(Frame {
171 name,
172 head,
173 start,
174 end,
175 kinds: Vec::new(),
176 });
177 }
178
179 fn close(&mut self) {
180 let Some(frame) = self.frames.pop() else {
181 return;
182 };
183 let start = (frame.start as usize).min(self.len);
184 let end = (frame.end as usize).min(self.len);
185 let head = (frame.head as usize).min(start);
186 self.out.push(RawFunction {
187 grammar: OxcExtractor.grammar(),
188 name: frame.name,
189 start: self.line_index.location(start),
190 end: self.line_index.location(end),
191 head: self.line_index.location(head),
192 kinds: frame.kinds,
193 });
194 }
195
196 fn name_head(&mut self, head: u32, value: Option<u32>) {
200 self.pending_head = match (&self.pending_name, value) {
201 (Some(_), Some(value)) => Some((head, value)),
202 _ => None,
203 };
204 }
205}
206
207impl<'a> Visit<'a> for Extractor<'_> {
208 fn enter_node(&mut self, kind: AstKind<'a>) {
209 match kind {
210 AstKind::VariableDeclarator(d) => {
211 self.pending_name = d.id.get_identifier_name().map(|n| n.to_string());
212 self.name_head(d.span.start, d.init.as_ref().map(|v| v.span().start));
213 }
214 AstKind::MethodDefinition(m) => {
215 self.pending_name = m.key.static_name().map(|n| n.into_owned());
216 self.name_head(m.key.span().start, Some(m.value.span.start));
217 }
218 AstKind::PropertyDefinition(p) => {
219 self.pending_name = p.key.static_name().map(|n| n.into_owned());
220 self.name_head(p.key.span().start, p.value.as_ref().map(|v| v.span().start));
221 }
222 AstKind::ObjectProperty(p) => {
223 self.pending_name = p.key.static_name().map(|n| n.into_owned());
224 self.name_head(p.key.span().start, Some(p.value.span().start));
225 }
226 AstKind::Function(f) => {
227 let name =
228 f.id.as_ref()
229 .map(|id| id.name.to_string())
230 .or_else(|| self.pending_name.take())
231 .unwrap_or_else(|| "<anonymous>".to_string());
232 let span = f.span;
233 self.open(name, span.start, span.end);
234 }
235 AstKind::ArrowFunctionExpression(a) => {
236 let name = self
237 .pending_name
238 .take()
239 .unwrap_or_else(|| "<arrow>".to_string());
240 let span = a.span;
241 self.open(name, span.start, span.end);
242 }
243 _ => {}
244 }
245 let ty = kind.ty() as u16;
246 for frame in &mut self.frames {
247 frame.kinds.push(ty);
248 }
249 }
250
251 fn leave_node(&mut self, kind: AstKind<'a>) {
252 match kind {
253 AstKind::Function(_) | AstKind::ArrowFunctionExpression(_) => self.close(),
254 AstKind::VariableDeclarator(_)
255 | AstKind::MethodDefinition(_)
256 | AstKind::PropertyDefinition(_)
257 | AstKind::ObjectProperty(_) => {
258 self.pending_name = None;
259 self.pending_head = None;
260 }
261 _ => {}
262 }
263 let _ = kind.span();
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 const SRC: &str = "export function total(items) {\n let sum = 0;\n for (const it of items) { sum += it.price; }\n return sum;\n}\nconst double = (x) => x * 2;\nclass Cart {\n add(item) { this.items.push(item); }\n}\nconst obj = { run() { return 1; }, cb: function () { return 2; } };\n";
272
273 #[test]
274 fn extracts_declarations_arrows_methods_and_properties_with_names() {
275 let fns = extract_functions(SRC, "javascript");
276 let names: Vec<&str> = fns.iter().map(|f| f.name.as_str()).collect();
277 assert_eq!(names, vec!["total", "double", "add", "run", "cb"]);
278 let total = &fns[0];
279 assert_eq!((total.start.line, total.end.line), (1, 5));
280 assert!(total.kinds.len() > 20, "{}", total.kinds.len());
281 assert_eq!(total.kinds[0], oxc_ast::AstType::Function as u16);
282 }
283
284 #[test]
285 fn nested_functions_are_emitted_separately_and_contribute_to_the_outer() {
286 let src = "function outer() {\n const inner = () => 1;\n return inner();\n}\n";
287 let fns = extract_functions(src, "typescript");
288 assert_eq!(fns.len(), 2);
289 assert_eq!(fns[0].name, "inner"); assert_eq!(fns[1].name, "outer");
291 assert!(fns[1].kinds.len() > fns[0].kinds.len());
292 }
293
294 #[test]
295 fn registry_dispatches_by_format_and_tags_the_grammar() {
296 assert_eq!(extractor_for("typescript").unwrap().grammar(), "oxc");
297 assert!(extractor_for("python").is_none());
298 let formats = supported_function_formats();
299 for f in ["javascript", "typescript", "jsx", "tsx"] {
300 assert!(formats.contains(&f), "{f}");
301 assert!(supports_functions(f));
302 }
303 let fns = extract_functions("const f = () => 1;", "jsx");
304 assert_eq!(fns.len(), 1);
305 assert_eq!(fns[0].grammar, "oxc");
306 }
307
308 #[test]
309 fn unsupported_or_empty_sources_yield_nothing() {
310 assert!(extract_functions("def f():\n pass\n", "python").is_empty());
311 assert!(extract_functions("", "javascript").is_empty());
312 }
313
314 #[test]
315 fn redeclared_functions_are_still_extracted() {
316 let src = "function f(a) { return a + 1; }\nfunction f(b) { return b + 1; }\n";
317 let fns = extract_functions(src, "javascript");
318 assert_eq!(
319 fns.len(),
320 2,
321 "a redeclaration diagnostic must not drop the file"
322 );
323 assert_eq!(fns[0].kinds, fns[1].kinds);
324 }
325
326 #[test]
327 fn renamed_copies_share_the_same_kind_sequence() {
328 let a = extract_functions("function a(x) { return x + 1; }", "javascript");
329 let b = extract_functions("function b(y) { return y + 1; }", "javascript");
330 assert_eq!(a[0].kinds, b[0].kinds);
331 }
332}