1use std::collections::BTreeSet;
4use std::path::Path;
5
6use fallow_types::similar_code::{
7 ExtractedSimilarCodeFunction, SIMILAR_CODE_EXTRACTION_SEMANTICS_VERSION, SimilarCodeExtraction,
8 SimilarCodeExtractionLimits, SimilarCodeExtractionSkip, SimilarCodeExtractionSkipReason,
9 SimilarCodeFunctionKind, SimilarCodeFunctionLocation, SimilarCodeSideEffectHint,
10 SimilarCodeSourceDigest,
11};
12use oxc_allocator::Allocator;
13use oxc_ast::ast::{
14 ArrowFunctionExpression, AssignmentExpression, AwaitExpression, CallExpression, Class,
15 ClassElement, ComputedMemberExpression, Declaration, ExportDefaultDeclarationKind, Function,
16 FunctionBody, ImportExpression, JSXElement, JSXFragment, MethodDefinitionKind, NewExpression,
17 ObjectExpression, ObjectPropertyKind, PrivateFieldExpression, Program, PropertyKind, Statement,
18 StaticMemberExpression, TaggedTemplateExpression, ThrowStatement, UnaryExpression,
19 UpdateExpression, VariableDeclaration, YieldExpression,
20};
21use oxc_ast_visit::{Visit, walk};
22use oxc_parser::Parser;
23use oxc_semantic::ScopeFlags;
24use oxc_span::{GetSpan, SourceType, Span};
25use rustc_hash::FxHashMap;
26use sha2::{Digest, Sha256};
27
28#[derive(Debug, Clone, Copy)]
29struct ReviewMetadata {
30 param_count: u32,
31 is_async: bool,
32 is_generator: bool,
33 has_await: bool,
34 has_throw: bool,
35 side_effect_hint: SimilarCodeSideEffectHint,
36}
37
38impl ReviewMetadata {
39 fn for_function(function: &Function<'_>, syntax_reliable: bool) -> Self {
40 Self::from_body(
41 function.body.as_deref(),
42 function.params.items.len(),
43 function.params.rest.is_some(),
44 function.r#async,
45 function.generator,
46 syntax_reliable,
47 )
48 }
49
50 fn for_arrow(arrow: &ArrowFunctionExpression<'_>, syntax_reliable: bool) -> Self {
51 Self::from_body(
52 Some(arrow.body.as_ref()),
53 arrow.params.items.len(),
54 arrow.params.rest.is_some(),
55 arrow.r#async,
56 false,
57 syntax_reliable,
58 )
59 }
60
61 fn from_body(
62 body: Option<&FunctionBody<'_>>,
63 fixed_params: usize,
64 has_rest: bool,
65 is_async: bool,
66 is_generator: bool,
67 syntax_reliable: bool,
68 ) -> Self {
69 let mut visitor = ReviewMetadataVisitor::default();
70 if let Some(body) = body {
71 visitor.visit_function_body(body);
72 }
73 let param_count = fixed_params.saturating_add(usize::from(has_rest));
74 let side_effect_hint = if !syntax_reliable {
75 SimilarCodeSideEffectHint::Unknown
76 } else if visitor.may_have_side_effects {
77 SimilarCodeSideEffectHint::MayHaveSideEffects
78 } else {
79 SimilarCodeSideEffectHint::PureLooking
80 };
81 Self {
82 param_count: u32::try_from(param_count).unwrap_or(u32::MAX),
83 is_async,
84 is_generator,
85 has_await: visitor.has_await,
86 has_throw: visitor.has_throw,
87 side_effect_hint,
88 }
89 }
90}
91
92#[derive(Default)]
93struct ReviewMetadataVisitor {
94 has_await: bool,
95 has_throw: bool,
96 may_have_side_effects: bool,
97}
98
99impl<'ast> Visit<'ast> for ReviewMetadataVisitor {
100 fn visit_function(&mut self, _function: &Function<'ast>, _flags: ScopeFlags) {}
101
102 fn visit_arrow_function_expression(&mut self, _arrow: &ArrowFunctionExpression<'ast>) {}
103
104 fn visit_call_expression(&mut self, expression: &CallExpression<'ast>) {
105 self.may_have_side_effects = true;
106 walk::walk_call_expression(self, expression);
107 }
108
109 fn visit_new_expression(&mut self, expression: &NewExpression<'ast>) {
110 self.may_have_side_effects = true;
111 walk::walk_new_expression(self, expression);
112 }
113
114 fn visit_assignment_expression(&mut self, expression: &AssignmentExpression<'ast>) {
115 self.may_have_side_effects = true;
116 walk::walk_assignment_expression(self, expression);
117 }
118
119 fn visit_update_expression(&mut self, expression: &UpdateExpression<'ast>) {
120 self.may_have_side_effects = true;
121 walk::walk_update_expression(self, expression);
122 }
123
124 fn visit_unary_expression(&mut self, expression: &UnaryExpression<'ast>) {
125 self.may_have_side_effects |= expression.operator.is_delete();
126 walk::walk_unary_expression(self, expression);
127 }
128
129 fn visit_await_expression(&mut self, expression: &AwaitExpression<'ast>) {
130 self.has_await = true;
131 self.may_have_side_effects = true;
132 walk::walk_await_expression(self, expression);
133 }
134
135 fn visit_throw_statement(&mut self, statement: &ThrowStatement<'ast>) {
136 self.has_throw = true;
137 self.may_have_side_effects = true;
138 walk::walk_throw_statement(self, statement);
139 }
140
141 fn visit_yield_expression(&mut self, expression: &YieldExpression<'ast>) {
142 self.may_have_side_effects = true;
143 walk::walk_yield_expression(self, expression);
144 }
145
146 fn visit_import_expression(&mut self, expression: &ImportExpression<'ast>) {
147 self.may_have_side_effects = true;
148 walk::walk_import_expression(self, expression);
149 }
150
151 fn visit_tagged_template_expression(&mut self, expression: &TaggedTemplateExpression<'ast>) {
152 self.may_have_side_effects = true;
153 walk::walk_tagged_template_expression(self, expression);
154 }
155
156 fn visit_computed_member_expression(&mut self, expression: &ComputedMemberExpression<'ast>) {
157 self.may_have_side_effects = true;
158 walk::walk_computed_member_expression(self, expression);
159 }
160
161 fn visit_static_member_expression(&mut self, expression: &StaticMemberExpression<'ast>) {
162 self.may_have_side_effects = true;
163 walk::walk_static_member_expression(self, expression);
164 }
165
166 fn visit_private_field_expression(&mut self, expression: &PrivateFieldExpression<'ast>) {
167 self.may_have_side_effects = true;
168 walk::walk_private_field_expression(self, expression);
169 }
170
171 fn visit_jsx_element(&mut self, element: &JSXElement<'ast>) {
172 self.may_have_side_effects = true;
173 walk::walk_jsx_element(self, element);
174 }
175
176 fn visit_jsx_fragment(&mut self, fragment: &JSXFragment<'ast>) {
177 self.may_have_side_effects = true;
178 walk::walk_jsx_fragment(self, fragment);
179 }
180}
181
182struct ExtractionBuilder<'a> {
183 file: String,
184 source: &'a str,
185 line_offsets: Vec<u32>,
186 limits: SimilarCodeExtractionLimits,
187 functions: Vec<ExtractedSimilarCodeFunction>,
188 source_bytes: usize,
189 syntax_reliable: bool,
190 classified_spans: BTreeSet<(u32, u32)>,
191 skips: FxHashMap<SimilarCodeExtractionSkipReason, usize>,
192}
193
194impl<'a> ExtractionBuilder<'a> {
195 fn new(
196 file: String,
197 source: &'a str,
198 limits: SimilarCodeExtractionLimits,
199 syntax_reliable: bool,
200 ) -> Self {
201 Self {
202 file,
203 source,
204 line_offsets: fallow_types::extract::compute_line_offsets(source),
205 limits,
206 functions: Vec::new(),
207 source_bytes: 0,
208 syntax_reliable,
209 classified_spans: BTreeSet::new(),
210 skips: FxHashMap::default(),
211 }
212 }
213
214 fn collect_program(&mut self, program: &Program<'_>) {
215 for statement in &program.body {
216 match statement {
217 Statement::FunctionDeclaration(function) => {
218 self.collect_function_declaration(function, None);
219 }
220 Statement::VariableDeclaration(declaration) => {
221 self.collect_variable_declaration(declaration);
222 }
223 Statement::ClassDeclaration(class) => {
224 self.collect_class_declaration(class, None);
225 }
226 Statement::ExportNamedDeclaration(export) => {
227 if let Some(declaration) = &export.declaration {
228 self.collect_declaration(declaration);
229 }
230 }
231 Statement::ExportDefaultDeclaration(export) => {
232 self.collect_default_export(&export.declaration);
233 }
234 _ => {}
235 }
236 }
237 }
238
239 fn collect_declaration(&mut self, declaration: &Declaration<'_>) {
240 match declaration {
241 Declaration::FunctionDeclaration(function) => {
242 self.collect_function_declaration(function, None);
243 }
244 Declaration::VariableDeclaration(declaration) => {
245 self.collect_variable_declaration(declaration);
246 }
247 Declaration::ClassDeclaration(class) => {
248 self.collect_class_declaration(class, None);
249 }
250 _ => {}
251 }
252 }
253
254 fn collect_default_export(&mut self, declaration: &ExportDefaultDeclarationKind<'_>) {
255 match declaration {
256 ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
257 self.collect_function_declaration(function, Some("default"));
258 }
259 ExportDefaultDeclarationKind::FunctionExpression(function) => {
260 self.collect_function_expression(function, "default");
261 }
262 ExportDefaultDeclarationKind::ArrowFunctionExpression(arrow) => {
263 self.collect_arrow_function(arrow, "default");
264 }
265 ExportDefaultDeclarationKind::ClassDeclaration(class)
266 | ExportDefaultDeclarationKind::ClassExpression(class) => {
267 self.collect_class_declaration(class, Some("default"));
268 }
269 ExportDefaultDeclarationKind::ObjectExpression(object) => {
270 self.collect_object_methods(object, "default");
271 }
272 _ => {}
273 }
274 }
275
276 fn collect_variable_declaration(&mut self, declaration: &VariableDeclaration<'_>) {
277 for declarator in &declaration.declarations {
278 let Some(initializer) = &declarator.init else {
279 continue;
280 };
281 let is_supported_container = matches!(
282 initializer,
283 oxc_ast::ast::Expression::FunctionExpression(_)
284 | oxc_ast::ast::Expression::ArrowFunctionExpression(_)
285 | oxc_ast::ast::Expression::ClassExpression(_)
286 | oxc_ast::ast::Expression::ObjectExpression(_)
287 );
288 if !is_supported_container {
289 continue;
290 }
291
292 let Some(binding) = declarator.id.get_binding_identifier() else {
293 if matches!(
294 initializer,
295 oxc_ast::ast::Expression::FunctionExpression(_)
296 | oxc_ast::ast::Expression::ArrowFunctionExpression(_)
297 ) {
298 self.classified_spans
299 .insert((initializer.span().start, initializer.span().end));
300 self.record_skip(SimilarCodeExtractionSkipReason::UnsupportedFunctionForm, 1);
301 }
302 continue;
303 };
304 match initializer {
305 oxc_ast::ast::Expression::FunctionExpression(function) => {
306 self.collect_function_expression(function, binding.name.as_str());
307 }
308 oxc_ast::ast::Expression::ArrowFunctionExpression(arrow) => {
309 self.collect_arrow_function(arrow, binding.name.as_str());
310 }
311 oxc_ast::ast::Expression::ClassExpression(class) => {
312 self.collect_class(class, binding.name.as_str());
313 }
314 oxc_ast::ast::Expression::ObjectExpression(object) => {
315 self.collect_object_methods(object, binding.name.as_str());
316 }
317 _ => {}
318 }
319 }
320 }
321
322 fn collect_class_declaration(&mut self, class: &Class<'_>, fallback: Option<&str>) {
323 let Some(name) = class
324 .id
325 .as_ref()
326 .map(|identifier| identifier.name.as_str())
327 .or(fallback)
328 else {
329 return;
330 };
331 self.collect_class(class, name);
332 }
333
334 fn collect_class(&mut self, class: &Class<'_>, class_name: &str) {
335 for element in &class.body.body {
336 let ClassElement::MethodDefinition(method) = element else {
337 continue;
338 };
339 self.classified_spans
340 .insert((method.value.span.start, method.value.span.end));
341 if method.value.body.is_none() {
342 self.record_skip(SimilarCodeExtractionSkipReason::DeclarationWithoutBody, 1);
343 continue;
344 }
345 if method.kind != MethodDefinitionKind::Method || method.computed {
346 self.record_skip(SimilarCodeExtractionSkipReason::UnsupportedFunctionForm, 1);
347 continue;
348 }
349 let Some(method_name) = method.key.static_name() else {
350 self.record_skip(SimilarCodeExtractionSkipReason::UnsupportedFunctionForm, 1);
351 continue;
352 };
353 self.retain_function(
354 &format!("{class_name}.{method_name}"),
355 SimilarCodeFunctionKind::ClassMethod,
356 method.span,
357 ReviewMetadata::for_function(&method.value, self.syntax_reliable),
358 );
359 }
360 }
361
362 fn collect_object_methods(&mut self, object: &ObjectExpression<'_>, binding_name: &str) {
363 for property in &object.properties {
364 let ObjectPropertyKind::ObjectProperty(property) = property else {
365 continue;
366 };
367 if !property.method {
368 continue;
369 }
370 let oxc_ast::ast::Expression::FunctionExpression(function) = &property.value else {
371 continue;
372 };
373 self.classified_spans
374 .insert((function.span.start, function.span.end));
375 if function.body.is_none() {
376 self.record_skip(SimilarCodeExtractionSkipReason::DeclarationWithoutBody, 1);
377 continue;
378 }
379 if property.kind != PropertyKind::Init || property.computed {
380 self.record_skip(SimilarCodeExtractionSkipReason::UnsupportedFunctionForm, 1);
381 continue;
382 }
383 let Some(method_name) = property.key.static_name() else {
384 self.record_skip(SimilarCodeExtractionSkipReason::UnsupportedFunctionForm, 1);
385 continue;
386 };
387 self.retain_function(
388 &format!("{binding_name}.{method_name}"),
389 SimilarCodeFunctionKind::ObjectMethod,
390 property.span,
391 ReviewMetadata::for_function(function, self.syntax_reliable),
392 );
393 }
394 }
395
396 fn collect_function_declaration(&mut self, function: &Function<'_>, fallback: Option<&str>) {
397 self.classified_spans
398 .insert((function.span.start, function.span.end));
399 let Some(_body) = &function.body else {
400 self.record_skip(SimilarCodeExtractionSkipReason::DeclarationWithoutBody, 1);
401 return;
402 };
403 let Some(name) = function
404 .id
405 .as_ref()
406 .map(|identifier| identifier.name.as_str())
407 .or(fallback)
408 else {
409 self.record_skip(SimilarCodeExtractionSkipReason::UnsupportedFunctionForm, 1);
410 return;
411 };
412 self.retain_function(
413 name,
414 SimilarCodeFunctionKind::FunctionDeclaration,
415 function.span,
416 ReviewMetadata::for_function(function, self.syntax_reliable),
417 );
418 }
419
420 fn collect_function_expression(&mut self, function: &Function<'_>, name: &str) {
421 self.classified_spans
422 .insert((function.span.start, function.span.end));
423 if function.body.is_none() {
424 self.record_skip(SimilarCodeExtractionSkipReason::DeclarationWithoutBody, 1);
425 return;
426 }
427 self.retain_function(
428 name,
429 SimilarCodeFunctionKind::FunctionExpression,
430 function.span,
431 ReviewMetadata::for_function(function, self.syntax_reliable),
432 );
433 }
434
435 fn collect_arrow_function(&mut self, arrow: &ArrowFunctionExpression<'_>, name: &str) {
436 self.classified_spans
437 .insert((arrow.span.start, arrow.span.end));
438 self.retain_function(
439 name,
440 SimilarCodeFunctionKind::ArrowFunction,
441 arrow.span,
442 ReviewMetadata::for_arrow(arrow, self.syntax_reliable),
443 );
444 }
445
446 fn retain_function(
447 &mut self,
448 name: &str,
449 kind: SimilarCodeFunctionKind,
450 span: Span,
451 metadata: ReviewMetadata,
452 ) {
453 let start = span.start as usize;
454 let end = span.end as usize;
455 let Some(source) = self.source.get(start..end) else {
456 self.record_skip(SimilarCodeExtractionSkipReason::InvalidSourceSpan, 1);
457 return;
458 };
459 if source.len() > self.limits.max_source_bytes_per_function {
460 self.record_skip(
461 SimilarCodeExtractionSkipReason::SourceBytesPerFunctionLimit,
462 1,
463 );
464 return;
465 }
466 if self.functions.len() >= self.limits.max_functions {
467 self.record_skip(SimilarCodeExtractionSkipReason::FunctionLimit, 1);
468 return;
469 }
470 if self.source_bytes.saturating_add(source.len()) > self.limits.max_total_source_bytes {
471 self.record_skip(SimilarCodeExtractionSkipReason::TotalSourceBytesLimit, 1);
472 return;
473 }
474 let Some((start_line, start_column_utf8)) =
475 utf8_line_col(self.source, &self.line_offsets, span.start)
476 else {
477 self.record_skip(SimilarCodeExtractionSkipReason::InvalidSourceSpan, 1);
478 return;
479 };
480 let Some((end_line, end_column_utf8)) =
481 utf8_line_col(self.source, &self.line_offsets, span.end)
482 else {
483 self.record_skip(SimilarCodeExtractionSkipReason::InvalidSourceSpan, 1);
484 return;
485 };
486
487 let source_sha256 = SimilarCodeSourceDigest::new(Sha256::digest(source.as_bytes()).into());
488 self.source_bytes = self.source_bytes.saturating_add(source.len());
489 self.functions.push(ExtractedSimilarCodeFunction {
490 name: name.to_string(),
491 kind,
492 location: SimilarCodeFunctionLocation {
493 file: self.file.clone(),
494 start_byte: span.start,
495 end_byte: span.end,
496 start_line,
497 start_column_utf8,
498 end_line,
499 end_column_utf8,
500 },
501 source_sha256,
502 source: source.to_string(),
503 param_count: metadata.param_count,
504 is_async: metadata.is_async,
505 is_generator: metadata.is_generator,
506 has_await: metadata.has_await,
507 has_throw: metadata.has_throw,
508 side_effect_hint: metadata.side_effect_hint,
509 });
510 }
511
512 fn record_skip(&mut self, reason: SimilarCodeExtractionSkipReason, count: usize) {
513 let value = self.skips.entry(reason).or_default();
514 *value = value.saturating_add(count);
515 }
516
517 fn finish(mut self, program: &Program<'_>) -> SimilarCodeExtraction {
518 let mut visitor = UnsupportedFunctionVisitor::new(&self.classified_spans);
519 visitor.visit_program(program);
520 for (reason, count) in visitor.skips {
521 self.record_skip(reason, count);
522 }
523 build_result(self.functions, self.source_bytes, self.skips)
524 }
525}
526
527struct UnsupportedFunctionVisitor<'a> {
528 classified_spans: &'a BTreeSet<(u32, u32)>,
529 function_depth: usize,
530 skips: FxHashMap<SimilarCodeExtractionSkipReason, usize>,
531}
532
533impl<'a> UnsupportedFunctionVisitor<'a> {
534 fn new(classified_spans: &'a BTreeSet<(u32, u32)>) -> Self {
535 Self {
536 classified_spans,
537 function_depth: 0,
538 skips: FxHashMap::default(),
539 }
540 }
541
542 fn record_unclassified(&mut self, span: Span, has_body: bool) {
543 if self.classified_spans.contains(&(span.start, span.end)) {
544 return;
545 }
546 let reason = if !has_body {
547 SimilarCodeExtractionSkipReason::DeclarationWithoutBody
548 } else if self.function_depth > 0 {
549 SimilarCodeExtractionSkipReason::NestedFunction
550 } else {
551 SimilarCodeExtractionSkipReason::UnsupportedFunctionForm
552 };
553 let value = self.skips.entry(reason).or_default();
554 *value = value.saturating_add(1);
555 }
556}
557
558impl<'ast> Visit<'ast> for UnsupportedFunctionVisitor<'_> {
559 fn visit_function(&mut self, function: &Function<'ast>, flags: ScopeFlags) {
560 self.record_unclassified(function.span, function.body.is_some());
561 self.function_depth = self.function_depth.saturating_add(1);
562 walk::walk_function(self, function, flags);
563 self.function_depth = self.function_depth.saturating_sub(1);
564 }
565
566 fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'ast>) {
567 self.record_unclassified(arrow.span, true);
568 self.function_depth = self.function_depth.saturating_add(1);
569 walk::walk_arrow_function_expression(self, arrow);
570 self.function_depth = self.function_depth.saturating_sub(1);
571 }
572}
573
574#[must_use]
584pub fn extract_similar_code_functions(
585 path: &Path,
586 source: &str,
587 limits: SimilarCodeExtractionLimits,
588) -> SimilarCodeExtraction {
589 let Some(file) = path.to_str() else {
590 return single_skip_result(SimilarCodeExtractionSkipReason::NonUtf8Path);
591 };
592 let file = file.replace('\\', "/");
593 if is_declaration_file(&file) {
594 return single_skip_result(SimilarCodeExtractionSkipReason::DeclarationFile);
595 }
596 let Some(source_type) = supported_source_type(path) else {
597 return single_skip_result(SimilarCodeExtractionSkipReason::UnsupportedFileType);
598 };
599
600 let source = crate::strip_bom(source);
601 if is_generated_source(&file, source) {
602 return single_skip_result(SimilarCodeExtractionSkipReason::GeneratedSource);
603 }
604
605 let allocator = Allocator::default();
606 let parsed = Parser::new(&allocator, source, source_type).parse();
607 let syntax_reliable = parsed.errors.is_empty();
608 let mut builder = ExtractionBuilder::new(file, source, limits, syntax_reliable);
609 builder.record_skip(
610 SimilarCodeExtractionSkipReason::SyntaxDiagnostic,
611 parsed.errors.len(),
612 );
613 builder.collect_program(&parsed.program);
614 builder.finish(&parsed.program)
615}
616
617fn supported_source_type(path: &Path) -> Option<SourceType> {
618 let extension = path.extension()?.to_str()?.to_ascii_lowercase();
619 matches!(
620 extension.as_str(),
621 "js" | "jsx" | "mjs" | "cjs" | "ts" | "tsx" | "mts" | "cts"
622 )
623 .then(|| SourceType::from_path(path).unwrap_or_default())
624}
625
626fn is_declaration_file(file: &str) -> bool {
627 let file = file.to_ascii_lowercase();
628 file.ends_with(".d.ts") || file.ends_with(".d.mts") || file.ends_with(".d.cts")
629}
630
631fn is_generated_source(file: &str, source: &str) -> bool {
632 let lower_file = file.to_ascii_lowercase();
633 let generated_path = lower_file.split('/').any(|part| {
634 matches!(part, "generated" | "__generated__")
635 || part.contains(".generated.")
636 || part.contains(".gen.")
637 });
638 if generated_path {
639 return true;
640 }
641
642 let header = source
643 .chars()
644 .take(2_048)
645 .collect::<String>()
646 .to_ascii_lowercase();
647 header.contains("@generated")
648 || (header.contains("do not edit")
649 && (header.contains("code generated") || header.contains("automatically generated")))
650}
651
652fn utf8_line_col(source: &str, line_offsets: &[u32], byte_offset: u32) -> Option<(u32, u32)> {
653 let line_index = match line_offsets.binary_search(&byte_offset) {
654 Ok(index) => index,
655 Err(index) => index.saturating_sub(1),
656 };
657 let line_start = *line_offsets.get(line_index)? as usize;
658 let column = source
659 .get(line_start..byte_offset as usize)?
660 .chars()
661 .count();
662 Some((
663 u32::try_from(line_index).ok()?.saturating_add(1),
664 u32::try_from(column).ok()?,
665 ))
666}
667
668fn single_skip_result(reason: SimilarCodeExtractionSkipReason) -> SimilarCodeExtraction {
669 build_result(Vec::new(), 0, FxHashMap::from_iter([(reason, 1)]))
670}
671
672fn build_result(
673 functions: Vec<ExtractedSimilarCodeFunction>,
674 source_bytes: usize,
675 skips: FxHashMap<SimilarCodeExtractionSkipReason, usize>,
676) -> SimilarCodeExtraction {
677 let mut skipped = skips
678 .into_iter()
679 .filter(|(_, count)| *count > 0)
680 .map(|(reason, count)| SimilarCodeExtractionSkip { reason, count })
681 .collect::<Vec<_>>();
682 skipped.sort_by_key(|skip| skip.reason);
683 SimilarCodeExtraction {
684 extraction_semantics_version: SIMILAR_CODE_EXTRACTION_SEMANTICS_VERSION,
685 functions,
686 source_bytes,
687 skipped,
688 }
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 fn extract(file: &str, source: &str) -> SimilarCodeExtraction {
696 extract_similar_code_functions(
697 Path::new(file),
698 source,
699 SimilarCodeExtractionLimits::default(),
700 )
701 }
702
703 fn skip_count(
704 result: &SimilarCodeExtraction,
705 reason: SimilarCodeExtractionSkipReason,
706 ) -> usize {
707 result
708 .skipped
709 .iter()
710 .find(|skip| skip.reason == reason)
711 .map_or(0, |skip| skip.count)
712 }
713
714 #[test]
715 fn extracts_supported_top_level_forms_in_source_order() {
716 let source = r"
717function declared(value: number) { return value + 1; }
718const expression = function named(value: number) { return value + 2; };
719export const arrow = (value: number) => value + 3;
720export default function fallback(value: number) { return value + 4; }
721";
722 let result = extract("src/forms.ts", source);
723
724 assert_eq!(
725 result
726 .functions
727 .iter()
728 .map(|function| (function.name.as_str(), function.kind))
729 .collect::<Vec<_>>(),
730 vec![
731 ("declared", SimilarCodeFunctionKind::FunctionDeclaration),
732 ("expression", SimilarCodeFunctionKind::FunctionExpression),
733 ("arrow", SimilarCodeFunctionKind::ArrowFunction),
734 ("fallback", SimilarCodeFunctionKind::FunctionDeclaration),
735 ]
736 );
737 assert!(result.skipped.is_empty());
738 assert_eq!(
739 result.source_bytes,
740 result
741 .functions
742 .iter()
743 .map(|function| function.source.len())
744 .sum::<usize>()
745 );
746 }
747
748 #[test]
749 fn records_exact_utf8_span_source_and_full_sha256() {
750 let source = "const label = '🙂'; const naïve = (waarde: string) => {\n return `${label}:${waarde}`;\n};\n";
751 let result = extract("src/utf8.ts", source);
752 let function = &result.functions[0];
753 let expected_start = source.find("(waarde").unwrap();
754 let expected_end = source.rfind("};\n").map_or(source.len(), |index| index + 1);
755 let expected_source = &source[expected_start..expected_end];
756
757 assert_eq!(function.name, "naïve");
758 assert_eq!(function.location.start_byte as usize, expected_start);
759 assert_eq!(
760 function.location.start_column_utf8 as usize,
761 source[..expected_start].chars().count()
762 );
763 assert_eq!(function.source, expected_source);
764 assert_eq!(
765 function.source_sha256,
766 SimilarCodeSourceDigest::new(Sha256::digest(expected_source.as_bytes()).into())
767 );
768 }
769
770 #[test]
771 fn extracts_named_methods_and_skips_nested_or_unsupported_methods() {
772 let source = r#"
773function outer() {
774 const nested = () => 1;
775 return nested();
776}
777class Service {
778 constructor() {}
779 get value() { return 1; }
780 set value(next) {}
781 ["computed"]() {}
782 method(value) { return value + 2; }
783 static helper() { return 3; }
784}
785const object = {
786 get value() { return 1; },
787 ["computed"]() {},
788 method(value) { return value + 3; },
789};
790"#;
791 let result = extract("src/scopes.ts", source);
792
793 assert_eq!(
794 result
795 .functions
796 .iter()
797 .map(|function| (function.name.as_str(), function.kind))
798 .collect::<Vec<_>>(),
799 vec![
800 ("outer", SimilarCodeFunctionKind::FunctionDeclaration),
801 ("Service.method", SimilarCodeFunctionKind::ClassMethod),
802 ("Service.helper", SimilarCodeFunctionKind::ClassMethod),
803 ("object.method", SimilarCodeFunctionKind::ObjectMethod),
804 ]
805 );
806 assert_eq!(
807 skip_count(&result, SimilarCodeExtractionSkipReason::NestedFunction),
808 1
809 );
810 assert_eq!(
811 skip_count(
812 &result,
813 SimilarCodeExtractionSkipReason::UnsupportedFunctionForm
814 ),
815 6
816 );
817 }
818
819 #[test]
820 fn records_conservative_review_metadata_without_nested_leakage() {
821 let source = r#"
822async function load(input, ...rest) {
823 await fetch(input);
824 if (rest.length > 0) throw new Error("unexpected");
825}
826function* generate(value) { yield value; }
827function pure(left, right) { return left + right; }
828function wrapper() {
829 const nested = async () => { await fetch("nested"); throw new Error("nested"); };
830 return 1;
831}
832"#;
833 let result = extract("src/metadata.ts", source);
834 let find = |name: &str| {
835 result
836 .functions
837 .iter()
838 .find(|function| function.name == name)
839 .unwrap()
840 };
841
842 let load = find("load");
843 assert_eq!(load.param_count, 2);
844 assert!(load.is_async);
845 assert!(!load.is_generator);
846 assert!(load.has_await);
847 assert!(load.has_throw);
848 assert_eq!(
849 load.side_effect_hint,
850 SimilarCodeSideEffectHint::MayHaveSideEffects
851 );
852
853 let generate = find("generate");
854 assert!(generate.is_generator);
855 assert_eq!(
856 generate.side_effect_hint,
857 SimilarCodeSideEffectHint::MayHaveSideEffects
858 );
859
860 let pure = find("pure");
861 assert_eq!(pure.param_count, 2);
862 assert_eq!(
863 pure.side_effect_hint,
864 SimilarCodeSideEffectHint::PureLooking
865 );
866
867 let wrapper = find("wrapper");
868 assert!(!wrapper.has_await);
869 assert!(!wrapper.has_throw);
870 assert_eq!(
871 wrapper.side_effect_hint,
872 SimilarCodeSideEffectHint::PureLooking
873 );
874 }
875
876 #[test]
877 fn parser_recovery_marks_review_metadata_unknown() {
878 let result = extract(
879 "src/recovered.ts",
880 "function recovered(value) { return value; }\nreturn;",
881 );
882
883 assert_eq!(result.functions.len(), 1);
884 assert_eq!(
885 result.functions[0].side_effect_hint,
886 SimilarCodeSideEffectHint::Unknown
887 );
888 assert!(skip_count(&result, SimilarCodeExtractionSkipReason::SyntaxDiagnostic) > 0);
889 }
890
891 #[test]
892 fn generated_and_declaration_sources_are_excluded_before_parsing() {
893 let generated = extract(
894 "src/__generated__/client.ts",
895 "export const call = () => 1;",
896 );
897 assert_eq!(
898 generated.skipped,
899 vec![SimilarCodeExtractionSkip {
900 reason: SimilarCodeExtractionSkipReason::GeneratedSource,
901 count: 1,
902 }]
903 );
904
905 let declaration = extract("src/api.d.ts", "export declare function call(): void;");
906 assert_eq!(
907 declaration.skipped,
908 vec![SimilarCodeExtractionSkip {
909 reason: SimilarCodeExtractionSkipReason::DeclarationFile,
910 count: 1,
911 }]
912 );
913 }
914
915 #[test]
916 fn source_payload_limits_fail_closed_with_typed_counts() {
917 let source = "const first = () => 'this payload is definitely too large';\nconst second = () => 2;\nconst third = () => 3;\n";
918 let result = extract_similar_code_functions(
919 Path::new("src/limits.ts"),
920 source,
921 SimilarCodeExtractionLimits {
922 max_functions: 1,
923 max_source_bytes_per_function: 22,
924 max_total_source_bytes: 22,
925 },
926 );
927
928 assert_eq!(result.functions.len(), 1);
929 assert_eq!(
930 skip_count(
931 &result,
932 SimilarCodeExtractionSkipReason::SourceBytesPerFunctionLimit
933 ),
934 1
935 );
936 assert_eq!(
937 skip_count(&result, SimilarCodeExtractionSkipReason::FunctionLimit),
938 1
939 );
940 assert!(result.source_bytes <= 22);
941 }
942}