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