1use oxc_ast::ast::*;
2use oxc_span::Span;
3
4use crate::ignore_directive::has_similarity_ignore_directive;
5use crate::parser::parse_and_convert_to_tree;
6use crate::tsed::{calculate_tsed, TSEDOptions};
7
8type CrossFileSimilarityResult = Vec<(String, SimilarityResult, String)>;
9
10#[derive(Debug, Clone)]
11pub struct SimilarityResult {
12 pub func1: FunctionDefinition,
13 pub func2: FunctionDefinition,
14 pub similarity: f64,
15 pub impact: u32, }
17
18impl SimilarityResult {
19 pub fn new(func1: FunctionDefinition, func2: FunctionDefinition, similarity: f64) -> Self {
20 let impact = func1.line_count().min(func2.line_count());
22 SimilarityResult { func1, func2, similarity, impact }
23 }
24}
25
26#[derive(Debug, Clone)]
27pub struct FunctionDefinition {
28 pub name: String,
29 pub function_type: FunctionType,
30 pub parameters: Vec<String>,
31 pub body_span: Span,
32 pub start_line: u32,
33 pub end_line: u32,
34 pub class_name: Option<String>,
35 pub parent_function: Option<String>,
36 pub node_count: Option<u32>,
37 pub has_ignore_directive: bool,
38}
39
40impl FunctionDefinition {
41 pub fn line_count(&self) -> u32 {
42 self.end_line - self.start_line + 1
43 }
44
45 pub fn is_parent_child_relationship(&self, other: &FunctionDefinition) -> bool {
47 let other_inside_self = self.start_line <= other.start_line
49 && self.end_line >= other.end_line
50 && self.body_span.start < other.body_span.start
51 && self.body_span.end > other.body_span.end;
52
53 let self_inside_other = other.start_line <= self.start_line
55 && other.end_line >= self.end_line
56 && other.body_span.start < self.body_span.start
57 && other.body_span.end > self.body_span.end;
58
59 other_inside_self || self_inside_other
60 }
61}
62
63#[derive(Debug, Clone, PartialEq)]
64pub enum FunctionType {
65 Function,
66 Method,
67 Arrow,
68 Constructor,
69}
70
71pub fn extract_functions(
73 filename: &str,
74 source_text: &str,
75) -> Result<Vec<FunctionDefinition>, String> {
76 use oxc_allocator::Allocator;
77 use oxc_parser::Parser;
78 use oxc_span::SourceType;
79
80 let allocator = Allocator::default();
81 let source_type = SourceType::from_path(filename).unwrap_or(SourceType::tsx());
82 let ret = Parser::new(&allocator, source_text, source_type).parse();
83
84 if !ret.errors.is_empty() {
85 let error_messages: Vec<String> =
87 ret.errors.iter().map(|e| e.message.to_string()).collect();
88 return Err(format!("Parse errors: {}", error_messages.join(", ")));
89 }
90
91 let mut functions = Vec::new();
92 let mut context = ExtractionContext {
93 functions: &mut functions,
94 source_text,
95 class_name: None,
96 parent_function: None,
97 };
98
99 extract_from_program(&ret.program, &mut context);
100 Ok(functions)
101}
102
103struct ExtractionContext<'a> {
104 functions: &'a mut Vec<FunctionDefinition>,
105 source_text: &'a str,
106 class_name: Option<String>,
107 parent_function: Option<String>,
108}
109
110fn extract_from_program(program: &Program, ctx: &mut ExtractionContext) {
111 for stmt in &program.body {
112 extract_from_statement(stmt, ctx);
113 }
114}
115
116fn extract_from_statement(stmt: &Statement, ctx: &mut ExtractionContext) {
117 match stmt {
118 Statement::FunctionDeclaration(func) => {
119 if let Some(name) = &func.id {
120 let func_name = name.name.to_string();
121 let params = extract_parameters(&func.params);
122 let start_line = get_line_number(func.span.start, ctx.source_text);
123 ctx.functions.push(FunctionDefinition {
124 name: func_name.clone(),
125 function_type: FunctionType::Function,
126 parameters: params,
127 body_span: func.span,
128 start_line,
129 end_line: get_line_number(func.span.end, ctx.source_text),
130 class_name: None,
131 parent_function: ctx.parent_function.clone(),
132 node_count: count_function_nodes(func.span, ctx.source_text),
133 has_ignore_directive: has_similarity_ignore_directive(
134 ctx.source_text,
135 start_line as usize,
136 ),
137 });
138
139 if let Some(body) = &func.body {
141 let saved_parent = ctx.parent_function.clone();
142 ctx.parent_function = Some(func_name);
143 extract_from_function_body(body, ctx);
144 ctx.parent_function = saved_parent;
145 }
146 }
147 }
148 Statement::ClassDeclaration(class) => {
149 let class_name = class.id.as_ref().map(|id| id.name.to_string());
150 let saved_class_name = ctx.class_name.clone();
151 ctx.class_name = class_name.clone();
152
153 for element in &class.body.body {
154 if let ClassElement::MethodDefinition(method) = element {
155 let method_name = match &method.key {
156 PropertyKey::StaticIdentifier(ident) => ident.name.to_string(),
157 PropertyKey::PrivateIdentifier(ident) => format!("#{}", ident.name),
158 _ => "anonymous".to_string(),
159 };
160
161 let params = extract_parameters(&method.value.params);
162 let function_type = if method.kind == MethodDefinitionKind::Constructor {
163 FunctionType::Constructor
164 } else {
165 FunctionType::Method
166 };
167
168 let method_full_name = if let Some(ref class) = class_name {
169 format!("{class}.{method_name}")
170 } else {
171 method_name.clone()
172 };
173 let start_line = get_line_number(method.span.start, ctx.source_text);
174
175 ctx.functions.push(FunctionDefinition {
176 name: method_name.clone(),
177 function_type,
178 parameters: params,
179 body_span: method.span,
180 start_line,
181 end_line: get_line_number(method.span.end, ctx.source_text),
182 class_name: class_name.clone(),
183 parent_function: ctx.parent_function.clone(),
184 node_count: count_function_nodes(method.span, ctx.source_text),
185 has_ignore_directive: has_similarity_ignore_directive(
186 ctx.source_text,
187 start_line as usize,
188 ),
189 });
190
191 if let Some(body) = &method.value.body {
193 let saved_parent = ctx.parent_function.clone();
194 ctx.parent_function = Some(method_full_name);
195 extract_from_function_body(body, ctx);
196 ctx.parent_function = saved_parent;
197 }
198 }
199 }
200
201 ctx.class_name = saved_class_name;
202 }
203 Statement::VariableDeclaration(var_decl) => {
204 for decl in &var_decl.declarations {
205 if let Some(Expression::ArrowFunctionExpression(arrow)) = &decl.init {
206 if let BindingPattern::BindingIdentifier(ident) = &decl.id {
207 let params = extract_parameters(&arrow.params);
208 let arrow_name = ident.name.to_string();
209 let start_line = get_line_number(arrow.span.start, ctx.source_text);
210 ctx.functions.push(FunctionDefinition {
211 name: arrow_name.clone(),
212 function_type: FunctionType::Arrow,
213 parameters: params,
214 body_span: arrow.span,
215 start_line,
216 end_line: get_line_number(arrow.span.end, ctx.source_text),
217 class_name: None,
218 parent_function: ctx.parent_function.clone(),
219 node_count: count_function_nodes(arrow.span, ctx.source_text),
220 has_ignore_directive: has_similarity_ignore_directive(
221 ctx.source_text,
222 start_line as usize,
223 ),
224 });
225
226 if !arrow.expression {
228 let saved_parent = ctx.parent_function.clone();
229 ctx.parent_function = Some(arrow_name);
230 extract_from_function_body(&arrow.body, ctx);
231 ctx.parent_function = saved_parent;
232 }
233 }
234 }
235 }
236 }
237 Statement::ExportNamedDeclaration(export) => {
238 if let Some(decl) = &export.declaration {
239 extract_from_declaration(decl, ctx);
240 }
241 }
242 Statement::ExportDefaultDeclaration(export) => {
243 if let ExportDefaultDeclarationKind::FunctionDeclaration(func) = &export.declaration {
244 let name = func
245 .id
246 .as_ref()
247 .map(|id| id.name.to_string())
248 .unwrap_or_else(|| "default".to_string());
249 let params = extract_parameters(&func.params);
250 let func_name = name.clone();
251 let start_line = get_line_number(func.span.start, ctx.source_text);
252 ctx.functions.push(FunctionDefinition {
253 name: func_name.clone(),
254 function_type: FunctionType::Function,
255 parameters: params,
256 body_span: func.span,
257 start_line,
258 end_line: get_line_number(func.span.end, ctx.source_text),
259 class_name: None,
260 parent_function: ctx.parent_function.clone(),
261 node_count: count_function_nodes(func.span, ctx.source_text),
262 has_ignore_directive: has_similarity_ignore_directive(
263 ctx.source_text,
264 start_line as usize,
265 ),
266 });
267
268 if let Some(body) = &func.body {
270 let saved_parent = ctx.parent_function.clone();
271 ctx.parent_function = Some(func_name);
272 extract_from_function_body(body, ctx);
273 ctx.parent_function = saved_parent;
274 }
275 }
276 }
277 _ => {}
278 }
279}
280
281fn extract_from_declaration(decl: &Declaration, ctx: &mut ExtractionContext) {
282 match decl {
283 Declaration::FunctionDeclaration(func) => {
284 if let Some(name) = &func.id {
285 let func_name = name.name.to_string();
286 let params = extract_parameters(&func.params);
287 let start_line = get_line_number(func.span.start, ctx.source_text);
288 ctx.functions.push(FunctionDefinition {
289 name: func_name.clone(),
290 function_type: FunctionType::Function,
291 parameters: params,
292 body_span: func.span,
293 start_line,
294 end_line: get_line_number(func.span.end, ctx.source_text),
295 class_name: None,
296 parent_function: ctx.parent_function.clone(),
297 node_count: count_function_nodes(func.span, ctx.source_text),
298 has_ignore_directive: has_similarity_ignore_directive(
299 ctx.source_text,
300 start_line as usize,
301 ),
302 });
303
304 if let Some(body) = &func.body {
306 let saved_parent = ctx.parent_function.clone();
307 ctx.parent_function = Some(func_name);
308 extract_from_function_body(body, ctx);
309 ctx.parent_function = saved_parent;
310 }
311 }
312 }
313 Declaration::ClassDeclaration(class) => {
314 let class_name = class.id.as_ref().map(|id| id.name.to_string());
315 let saved_class_name = ctx.class_name.clone();
316 ctx.class_name = class_name.clone();
317
318 for element in &class.body.body {
319 if let ClassElement::MethodDefinition(method) = element {
320 let method_name = match &method.key {
321 PropertyKey::StaticIdentifier(ident) => ident.name.to_string(),
322 PropertyKey::PrivateIdentifier(ident) => format!("#{}", ident.name),
323 _ => "anonymous".to_string(),
324 };
325
326 let params = extract_parameters(&method.value.params);
327 let function_type = if method.kind == MethodDefinitionKind::Constructor {
328 FunctionType::Constructor
329 } else {
330 FunctionType::Method
331 };
332
333 let method_full_name = if let Some(ref class) = class_name {
334 format!("{class}.{method_name}")
335 } else {
336 method_name.clone()
337 };
338 let start_line = get_line_number(method.span.start, ctx.source_text);
339
340 ctx.functions.push(FunctionDefinition {
341 name: method_name.clone(),
342 function_type,
343 parameters: params,
344 body_span: method.span,
345 start_line,
346 end_line: get_line_number(method.span.end, ctx.source_text),
347 class_name: class_name.clone(),
348 parent_function: ctx.parent_function.clone(),
349 node_count: count_function_nodes(method.span, ctx.source_text),
350 has_ignore_directive: has_similarity_ignore_directive(
351 ctx.source_text,
352 start_line as usize,
353 ),
354 });
355
356 if let Some(body) = &method.value.body {
358 let saved_parent = ctx.parent_function.clone();
359 ctx.parent_function = Some(method_full_name);
360 extract_from_function_body(body, ctx);
361 ctx.parent_function = saved_parent;
362 }
363 }
364 }
365
366 ctx.class_name = saved_class_name;
367 }
368 Declaration::VariableDeclaration(var) => {
369 for decl in &var.declarations {
370 if let Some(Expression::ArrowFunctionExpression(arrow)) = &decl.init {
371 if let BindingPattern::BindingIdentifier(ident) = &decl.id {
372 let params = extract_parameters(&arrow.params);
373 let arrow_name = ident.name.to_string();
374 let start_line = get_line_number(arrow.span.start, ctx.source_text);
375 ctx.functions.push(FunctionDefinition {
376 name: arrow_name.clone(),
377 function_type: FunctionType::Arrow,
378 parameters: params,
379 body_span: arrow.span,
380 start_line,
381 end_line: get_line_number(arrow.span.end, ctx.source_text),
382 class_name: None,
383 parent_function: ctx.parent_function.clone(),
384 node_count: count_function_nodes(arrow.span, ctx.source_text),
385 has_ignore_directive: has_similarity_ignore_directive(
386 ctx.source_text,
387 start_line as usize,
388 ),
389 });
390
391 if !arrow.expression {
393 let saved_parent = ctx.parent_function.clone();
394 ctx.parent_function = Some(arrow_name);
395 extract_from_function_body(&arrow.body, ctx);
396 ctx.parent_function = saved_parent;
397 }
398 }
399 }
400 }
401 }
402 _ => {}
403 }
404}
405
406fn extract_parameters(params: &oxc_ast::ast::FormalParameters) -> Vec<String> {
407 params
408 .items
409 .iter()
410 .filter_map(|param| match ¶m.pattern {
411 BindingPattern::BindingIdentifier(ident) => Some(ident.name.to_string()),
412 _ => None,
413 })
414 .collect()
415}
416
417fn extract_from_function_body(body: &FunctionBody, ctx: &mut ExtractionContext) {
418 for stmt in &body.statements {
419 extract_from_statement(stmt, ctx);
420 }
421}
422
423fn get_line_number(offset: u32, source_text: &str) -> u32 {
424 let mut line = 1;
425 let mut current_offset = 0;
426
427 for ch in source_text.chars() {
428 if current_offset >= offset as usize {
429 break;
430 }
431 if ch == '\n' {
432 line += 1;
433 }
434 current_offset += ch.len_utf8();
435 }
436
437 line
438}
439
440pub fn compare_functions(
442 func1: &FunctionDefinition,
443 func2: &FunctionDefinition,
444 source1: &str,
445 source2: &str,
446 options: &TSEDOptions,
447) -> Result<f64, String> {
448 let body1 = extract_body_text(func1, source1);
450 let body2 = extract_body_text(func2, source2);
451
452 let tree1 = parse_and_convert_to_tree("func1.ts", &body1)?;
454 let tree2 = parse_and_convert_to_tree("func2.ts", &body2)?;
455
456 let mut similarity = calculate_tsed(&tree1, &tree2, options);
457
458 if options.size_penalty {
460 let avg_lines = (func1.line_count() + func2.line_count()) as f64 / 2.0;
461 if avg_lines < 10.0 {
462 let penalty = avg_lines / 10.0;
464 similarity *= penalty;
465 }
466 }
467
468 Ok(similarity)
469}
470
471fn extract_body_text(func: &FunctionDefinition, source: &str) -> String {
472 let start = func.body_span.start as usize;
473 let end = func.body_span.end as usize;
474 source[start..end].to_string()
475}
476
477fn count_function_nodes(body_span: Span, source_text: &str) -> Option<u32> {
479 let start = body_span.start as usize;
480 let end = body_span.end as usize;
481 if start >= end || end > source_text.len() {
482 return None;
483 }
484
485 let body_text = &source_text[start..end];
486
487 match parse_and_convert_to_tree("temp.ts", body_text) {
490 Ok(tree) => Some(tree.get_subtree_size() as u32),
491 Err(_) => {
492 let wrapped = if body_text.starts_with("constructor") {
495 format!("class C {{ {body_text} }}")
496 } else if body_text.contains("(") && body_text.contains(")") && body_text.contains("{")
497 {
498 if body_text.trim().starts_with(|c: char| c.is_alphabetic() || c == '_' || c == '#')
500 {
501 format!("class C {{ {body_text} }}")
503 } else {
504 format!("const x = {body_text}")
506 }
507 } else {
508 body_text.to_string()
510 };
511
512 match parse_and_convert_to_tree("temp.ts", &wrapped) {
513 Ok(tree) => {
514 let base_nodes = if wrapped.starts_with("class C") {
516 3 } else if wrapped.starts_with("const x") {
518 2 } else {
520 0
521 };
522 Some((tree.get_subtree_size().saturating_sub(base_nodes)) as u32)
523 }
524 Err(_) => {
525 let node_count =
528 body_text.matches(['{', '}', '(', ')', ';']).count() as u32 + 1;
529 Some(node_count.max(1))
530 }
531 }
532 }
533 }
534}
535
536pub fn find_similar_functions_in_file(
538 filename: &str,
539 source_text: &str,
540 threshold: f64,
541 options: &TSEDOptions,
542) -> Result<Vec<SimilarityResult>, String> {
543 let mut functions = extract_functions(filename, source_text)?;
544 functions.retain(|function| !function.has_ignore_directive);
545 let mut similar_pairs = Vec::new();
546
547 for i in 0..functions.len() {
549 for j in (i + 1)..functions.len() {
550 if let Some(min_tokens) = options.min_tokens {
552 let tokens_i = functions[i].node_count.unwrap_or(0);
554 let tokens_j = functions[j].node_count.unwrap_or(0);
555 if tokens_i < min_tokens || tokens_j < min_tokens {
556 continue;
557 }
558 } else {
559 if functions[i].line_count() < options.min_lines
561 || functions[j].line_count() < options.min_lines
562 {
563 continue;
564 }
565 }
566
567 if functions[i].is_parent_child_relationship(&functions[j]) {
569 continue;
570 }
571
572 let similarity =
573 compare_functions(&functions[i], &functions[j], source_text, source_text, options)?;
574
575 if similarity >= threshold {
576 similar_pairs.push(SimilarityResult::new(
577 functions[i].clone(),
578 functions[j].clone(),
579 similarity,
580 ));
581 }
582 }
583 }
584
585 similar_pairs.sort_by(|a, b| {
587 b.impact
588 .cmp(&a.impact)
589 .then(b.similarity.partial_cmp(&a.similarity).unwrap_or(std::cmp::Ordering::Equal))
590 });
591
592 Ok(similar_pairs)
593}
594
595pub fn find_similar_functions_across_files(
597 files: &[(String, String)], threshold: f64,
599 options: &TSEDOptions,
600) -> Result<CrossFileSimilarityResult, String> {
601 let mut all_functions = Vec::new();
602
603 for (filename, source) in files {
605 let mut functions = extract_functions(filename, source)?;
606 functions.retain(|function| !function.has_ignore_directive);
607 for func in functions {
608 all_functions.push((filename.clone(), source.clone(), func));
609 }
610 }
611
612 let mut similar_pairs = Vec::new();
613
614 for i in 0..all_functions.len() {
616 for j in (i + 1)..all_functions.len() {
617 let (first_file, source1, func1) = &all_functions[i];
618 let (second_file, source2, func2) = &all_functions[j];
619
620 if first_file == second_file {
622 continue;
623 }
624
625 if let Some(min_tokens) = options.min_tokens {
627 let tokens1 = func1.node_count.unwrap_or(0);
629 let tokens2 = func2.node_count.unwrap_or(0);
630 if tokens1 < min_tokens || tokens2 < min_tokens {
631 continue;
632 }
633 } else {
634 if func1.line_count() < options.min_lines || func2.line_count() < options.min_lines
636 {
637 continue;
638 }
639 }
640
641 if func1.is_parent_child_relationship(func2) {
643 continue;
644 }
645
646 let similarity = compare_functions(func1, func2, source1, source2, options)?;
647
648 if similarity >= threshold {
649 similar_pairs.push((
650 first_file.clone(),
651 SimilarityResult::new(func1.clone(), func2.clone(), similarity),
652 second_file.clone(),
653 ));
654 }
655 }
656 }
657
658 similar_pairs.sort_by(|a, b| {
660 b.1.impact
661 .cmp(&a.1.impact)
662 .then(b.1.similarity.partial_cmp(&a.1.similarity).unwrap_or(std::cmp::Ordering::Equal))
663 });
664
665 Ok(similar_pairs)
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn test_extract_functions() {
674 let code = r"
675 function add(a: number, b: number): number {
676 return a + b;
677 }
678
679 const multiply = (x: number, y: number) => x * y;
680
681 class Calculator {
682 constructor(private initial: number) {}
683
684 add(value: number): number {
685 return this.initial + value;
686 }
687
688 subtract(value: number): number {
689 return this.initial - value;
690 }
691 }
692
693 export function divide(a: number, b: number): number {
694 return a / b;
695 }
696 ";
697
698 let functions = extract_functions("test.ts", code).unwrap();
699
700 assert_eq!(functions.len(), 6);
701
702 let names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();
704 assert!(names.contains(&"add"));
705 assert!(names.contains(&"multiply"));
706 assert!(names.contains(&"constructor"));
707 assert!(names.contains(&"subtract"));
708 assert!(names.contains(&"divide"));
709
710 let add_func =
712 functions.iter().find(|f| f.name == "add" && f.class_name.is_none()).unwrap();
713 assert_eq!(add_func.function_type, FunctionType::Function);
714 assert_eq!(add_func.parameters, vec!["a", "b"]);
715
716 let multiply_func = functions.iter().find(|f| f.name == "multiply").unwrap();
717 assert_eq!(multiply_func.function_type, FunctionType::Arrow);
718
719 let constructor = functions.iter().find(|f| f.name == "constructor").unwrap();
720 assert_eq!(constructor.function_type, FunctionType::Constructor);
721 assert_eq!(constructor.class_name, Some("Calculator".to_string()));
722
723 for func in &functions {
725 assert!(
726 func.node_count.is_some(),
727 "Function {} should have node_count populated",
728 func.name
729 );
730 assert!(
732 func.node_count.unwrap() > 0,
733 "Function {} should have positive node_count",
734 func.name
735 );
736 }
737 }
738
739 #[test]
740 fn test_node_count_calculation() {
741 let code = r#"
742 function simple() {
743 return 42;
744 }
745
746 function complex(a: number, b: number): number {
747 if (a > b) {
748 return a - b;
749 } else {
750 return a + b;
751 }
752 }
753 "#;
754
755 let functions = extract_functions("test.ts", code).unwrap();
756
757 let simple = functions.iter().find(|f| f.name == "simple").unwrap();
758 let complex = functions.iter().find(|f| f.name == "complex").unwrap();
759
760 println!("Simple function node count: {:?}", simple.node_count);
761 println!("Complex function node count: {:?}", complex.node_count);
762
763 assert!(simple.node_count.is_some());
765 assert!(complex.node_count.is_some());
766 assert!(simple.node_count.unwrap() < complex.node_count.unwrap());
767 }
768
769 #[test]
770 fn test_find_similar_functions_in_file() {
771 let code = r"
772 function calculateSum(a: number, b: number): number {
773 return a + b;
774 }
775
776 function addNumbers(x: number, y: number): number {
777 return x + y;
778 }
779
780 function multiply(a: number, b: number): number {
781 return a * b;
782 }
783
784 function computeSum(first: number, second: number): number {
785 return first + second;
786 }
787 ";
788
789 let mut options = TSEDOptions::default();
790 options.apted_options.rename_cost = 0.3; options.size_penalty = false; options.min_lines = 1; let similar_pairs = find_similar_functions_in_file("test.ts", code, 0.7, &options).unwrap();
795
796 assert!(
798 similar_pairs.len() >= 2,
799 "Expected at least 2 similar pairs, found {}",
800 similar_pairs.len()
801 );
802
803 let sum_pairs = similar_pairs
807 .iter()
808 .filter(|result| {
809 (result.func1.name.contains("Sum") || result.func2.name.contains("Sum"))
810 || (result.func1.name == "addNumbers" || result.func2.name == "addNumbers")
811 })
812 .count();
813 assert!(sum_pairs >= 3, "Expected at least 3 pairs involving sum functions");
814 }
815
816 #[test]
817 fn test_find_similar_functions_across_files() {
818 let file1 = (
819 "file1.ts".to_string(),
820 r#"
821 export function processUser(user: User): void {
822 validateUser(user);
823 saveUser(user);
824 notifyUser(user);
825 }
826
827 function validateUser(user: User): boolean {
828 return user.name.length > 0 && user.email.includes('@');
829 }
830 "#
831 .to_string(),
832 );
833
834 let file2 = (
835 "file2.ts".to_string(),
836 r#"
837 export function handleUser(u: User): void {
838 checkUser(u);
839 storeUser(u);
840 alertUser(u);
841 }
842
843 function checkUser(u: User): boolean {
844 return u.name.length > 0 && u.email.includes('@');
845 }
846 "#
847 .to_string(),
848 );
849
850 let mut options = TSEDOptions::default();
851 options.apted_options.rename_cost = 0.3;
852 options.size_penalty = false; options.min_lines = 1; let similar_pairs =
856 find_similar_functions_across_files(&[file1, file2], 0.7, &options).unwrap();
857
858 assert!(similar_pairs.len() >= 2);
860
861 let process_handle = similar_pairs.iter().find(|(_, result, _)| {
863 (result.func1.name == "processUser" && result.func2.name == "handleUser")
864 || (result.func1.name == "handleUser" && result.func2.name == "processUser")
865 });
866 assert!(process_handle.is_some());
867
868 let validate_check = similar_pairs.iter().find(|(_, result, _)| {
869 (result.func1.name == "validateUser" && result.func2.name == "checkUser")
870 || (result.func1.name == "checkUser" && result.func2.name == "validateUser")
871 });
872 assert!(validate_check.is_some());
873 }
874
875 #[test]
876 fn test_extract_functions_marks_similarity_ignore_directives() {
877 let code = r#"
878function keepMe() {
879 return 1;
880}
881
882// similarity-ignore
883function ignoreMe() {
884 return 2;
885}
886
887/**
888 * Keep this duplicated export local for now.
889 */
890// similarity-ignore
891export function ignoredExport() {
892 return 3;
893}
894"#;
895
896 let functions = extract_functions("test.ts", code).unwrap();
897
898 let keep = functions.iter().find(|f| f.name == "keepMe").unwrap();
899 assert!(!keep.has_ignore_directive);
900
901 let ignored = functions.iter().find(|f| f.name == "ignoreMe").unwrap();
902 assert!(ignored.has_ignore_directive);
903
904 let ignored_export = functions.iter().find(|f| f.name == "ignoredExport").unwrap();
905 assert!(ignored_export.has_ignore_directive);
906 }
907}