1use std::path::Path;
42
43use oxc_allocator::Allocator;
44#[allow(clippy::wildcard_imports, reason = "many AST types used")]
45use oxc_ast::ast::*;
46use oxc_ast_visit::{Visit, walk};
47use oxc_parser::Parser;
48use oxc_semantic::ScopeFlags;
49use oxc_span::{SourceType, Span};
50use rustc_hash::FxHashMap;
51
52#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct InventoryEntry {
64 pub name: String,
66 pub line: u32,
68 pub start_column: u32,
70 pub end_line: u32,
72 pub end_column: u32,
74 pub source_hash: String,
82 pub is_callback: bool,
87}
88
89struct ColCache {
93 line_idx: usize,
94 byte_end: usize,
95 utf16_units: usize,
96}
97
98struct ResolvedName {
100 name: String,
101 is_callback: bool,
103}
104
105impl ResolvedName {
106 const fn named(name: String) -> Self {
107 Self {
108 name,
109 is_callback: false,
110 }
111 }
112}
113
114struct InventoryVisitor<'a> {
116 source: &'a str,
117 line_offsets: &'a [u32],
118 entries: Vec<InventoryEntry>,
119 col_cache: ColCache,
120 pending_name: Option<String>,
122 pending_callee_name: Option<String>,
126 anonymous_counter: u32,
128}
129
130impl<'a> InventoryVisitor<'a> {
131 const fn new(source: &'a str, line_offsets: &'a [u32]) -> Self {
132 Self {
133 source,
134 line_offsets,
135 entries: Vec::new(),
136 col_cache: ColCache {
137 line_idx: usize::MAX,
138 byte_end: 0,
139 utf16_units: 0,
140 },
141 pending_name: None,
142 pending_callee_name: None,
143 anonymous_counter: 0,
144 }
145 }
146
147 fn resolve_name(&mut self, explicit: Option<&str>) -> ResolvedName {
158 let n = self.anonymous_counter;
159 self.anonymous_counter += 1;
160 if let Some(pending) = self.pending_name.take() {
161 return ResolvedName::named(pending);
162 }
163 if let Some(name) = explicit {
164 return ResolvedName::named(name.to_owned());
165 }
166 if let Some(callee) = self.pending_callee_name.take() {
167 return ResolvedName {
168 name: callee,
169 is_callback: true,
170 };
171 }
172 ResolvedName::named(format!("(anonymous_{n})"))
173 }
174
175 fn record(&mut self, resolved: ResolvedName, span: Span) {
176 let (line, start_column) = self.line_col_utf16(span.start);
177 let (end_line, end_column) = self.line_col_utf16(span.end);
178 let source_hash = self
179 .source
180 .get(span.start as usize..span.end as usize)
181 .map_or_else(
182 || fallow_cov_protocol::source_hash_for(b""),
183 |slice| fallow_cov_protocol::source_hash_for(slice.as_bytes()),
184 );
185 self.entries.push(InventoryEntry {
186 name: resolved.name,
187 line,
188 start_column,
189 end_line,
190 end_column,
191 source_hash,
192 is_callback: resolved.is_callback,
193 });
194 }
195
196 fn line_col_utf16(&mut self, byte_offset: u32) -> (u32, u32) {
211 let line_idx = match self.line_offsets.binary_search(&byte_offset) {
212 Ok(idx) => idx,
213 Err(idx) => idx.saturating_sub(1),
214 };
215 let line = line_idx as u32 + 1;
216 let line_start = self.line_offsets[line_idx] as usize;
217 let mut end = byte_offset as usize;
218 while end > line_start && !self.source.is_char_boundary(end) {
219 end -= 1;
220 }
221 let from_cache = if self.col_cache.line_idx == line_idx {
225 if end >= self.col_cache.byte_end {
226 self.utf16_len(self.col_cache.byte_end, end)
227 .map(|gap| self.col_cache.utf16_units + gap)
228 } else {
229 self.utf16_len(end, self.col_cache.byte_end)
230 .map(|gap| self.col_cache.utf16_units - gap)
231 }
232 } else {
233 None
234 };
235 let col_utf16 = from_cache.unwrap_or_else(|| self.utf16_len(line_start, end).unwrap_or(0));
236 self.col_cache = ColCache {
237 line_idx,
238 byte_end: end,
239 utf16_units: col_utf16,
240 };
241 (line, col_utf16 as u32 + 1)
242 }
243
244 fn utf16_len(&self, start: usize, end: usize) -> Option<usize> {
247 self.source
248 .get(start..end)
249 .map(|slice| slice.encode_utf16().count())
250 }
251}
252
253impl<'ast> Visit<'ast> for InventoryVisitor<'_> {
254 fn visit_function(&mut self, func: &Function<'ast>, flags: ScopeFlags) {
255 if func.body.is_none() {
256 walk::walk_function(self, func, flags);
257 return;
258 }
259 let resolved = self.resolve_name(func.id.as_ref().map(|id| id.name.as_str()));
260 self.record(resolved, func.span);
261 walk::walk_function(self, func, flags);
262 }
263
264 fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'ast>) {
265 let resolved = self.resolve_name(None);
266 self.record(resolved, arrow.span);
267 walk::walk_arrow_function_expression(self, arrow);
268 }
269
270 fn visit_method_definition(&mut self, method: &MethodDefinition<'ast>) {
271 if let Some(name) = method.key.static_name() {
272 self.pending_name = Some(accessor_label(
273 matches!(method.kind, MethodDefinitionKind::Get),
274 matches!(method.kind, MethodDefinitionKind::Set),
275 &name,
276 ));
277 }
278 walk::walk_method_definition(self, method);
279 self.pending_name = None;
280 }
281
282 fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'ast>) {
283 if let Some(id) = decl.id.get_binding_identifier()
284 && decl.init.as_ref().is_some_and(|init| {
285 matches!(
286 init,
287 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
288 )
289 })
290 {
291 self.pending_name = Some(id.name.to_string());
292 }
293 walk::walk_variable_declarator(self, decl);
294 self.pending_name = None;
295 }
296
297 fn visit_object_property(&mut self, prop: &ObjectProperty<'ast>) {
304 let is_method_like =
305 prop.method || matches!(prop.kind, PropertyKind::Get | PropertyKind::Set);
306 let is_function_valued = matches!(
307 prop.value,
308 Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_)
309 );
310 self.pending_name = if is_method_like || is_function_valued {
311 prop.key.static_name().map(|name| {
312 accessor_label(
313 matches!(prop.kind, PropertyKind::Get),
314 matches!(prop.kind, PropertyKind::Set),
315 &name,
316 )
317 })
318 } else {
319 None
320 };
321 walk::walk_object_property(self, prop);
322 self.pending_name = None;
323 }
324
325 fn visit_assignment_expression(&mut self, expr: &AssignmentExpression<'ast>) {
330 if matches!(expr.operator, AssignmentOperator::Assign)
331 && matches!(
332 expr.right,
333 Expression::FunctionExpression(_)
334 | Expression::ArrowFunctionExpression(_)
335 | Expression::ClassExpression(_)
336 )
337 {
338 self.pending_name = assignment_target_name(&expr.left);
339 }
340 walk::walk_assignment_expression(self, expr);
341 self.pending_name = None;
342 }
343
344 fn visit_export_default_declaration(&mut self, decl: &ExportDefaultDeclaration<'ast>) {
347 let anonymous = match &decl.declaration {
348 ExportDefaultDeclarationKind::FunctionDeclaration(func) => func.id.is_none(),
349 ExportDefaultDeclarationKind::ArrowFunctionExpression(_) => true,
350 _ => false,
351 };
352 if anonymous {
353 self.pending_name = Some("default".to_owned());
354 }
355 walk::walk_export_default_declaration(self, decl);
356 self.pending_name = None;
357 }
358
359 fn visit_call_expression(&mut self, call: &CallExpression<'ast>) {
369 self.visit_expression(&call.callee);
370 let name = callee_name(&call.callee);
371 for argument in &call.arguments {
372 self.pending_callee_name.clone_from(&name);
373 self.visit_argument(argument);
374 }
375 self.pending_callee_name = None;
376 }
377
378 fn visit_new_expression(&mut self, new_expr: &NewExpression<'ast>) {
379 self.visit_expression(&new_expr.callee);
380 let name = callee_name(&new_expr.callee);
381 for argument in &new_expr.arguments {
382 self.pending_callee_name.clone_from(&name);
383 self.visit_argument(argument);
384 }
385 self.pending_callee_name = None;
386 }
387}
388
389fn accessor_label(is_getter: bool, is_setter: bool, name: &str) -> String {
393 if is_getter {
394 return format!("get {name}");
395 }
396 if is_setter {
397 return format!("set {name}");
398 }
399 name.to_owned()
400}
401
402fn assignment_target_name(target: &AssignmentTarget<'_>) -> Option<String> {
406 match target {
407 AssignmentTarget::StaticMemberExpression(member) => Some(member.property.name.to_string()),
408 AssignmentTarget::ComputedMemberExpression(member) => match &member.expression {
409 Expression::StringLiteral(lit) => Some(lit.value.to_string()),
410 _ => None,
411 },
412 _ => None,
413 }
414}
415
416fn callee_name(callee: &Expression<'_>) -> Option<String> {
422 match callee {
423 Expression::Identifier(ident) => Some(ident.name.to_string()),
424 Expression::StaticMemberExpression(member) => Some(member.property.name.to_string()),
425 Expression::ComputedMemberExpression(member) => match &member.expression {
426 Expression::StringLiteral(lit) => Some(lit.value.to_string()),
427 _ => None,
428 },
429 Expression::ParenthesizedExpression(paren) => callee_name(&paren.expression),
433 _ => None,
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub struct InventoryComplexity {
447 pub cyclomatic: u16,
449 pub cognitive: u16,
451}
452
453#[must_use]
463pub fn walk_source(path: &Path, source: &str) -> Vec<InventoryEntry> {
464 walk_source_with_complexity(path, source).0
465}
466
467#[must_use]
479pub fn walk_source_with_complexity(
480 path: &Path,
481 source: &str,
482) -> (Vec<InventoryEntry>, FxHashMap<String, InventoryComplexity>) {
483 let source_type = SourceType::from_path(path).unwrap_or_default();
484 let line_offsets = fallow_types::extract::compute_line_offsets(source);
485
486 let primary = walk_one_parse(source, source_type, &line_offsets);
487 if primary.0.is_empty() && !source_type.is_jsx() {
488 let jsx_type = if source_type.is_typescript() {
489 SourceType::tsx()
490 } else {
491 SourceType::jsx()
492 };
493 let retry = walk_one_parse(source, jsx_type, &line_offsets);
494 if !retry.0.is_empty() {
495 return retry;
496 }
497 }
498
499 primary
500}
501
502fn walk_one_parse(
505 source: &str,
506 source_type: SourceType,
507 line_offsets: &[u32],
508) -> (Vec<InventoryEntry>, FxHashMap<String, InventoryComplexity>) {
509 let allocator = Allocator::default();
510 let parser_return = Parser::new(&allocator, source, source_type).parse();
511
512 let mut visitor = InventoryVisitor::new(source, line_offsets);
513 visitor.visit_program(&parser_return.program);
514
515 let complexity =
516 crate::complexity::compute_complexity(&parser_return.program, source, line_offsets);
517 let metrics: FxHashMap<String, InventoryComplexity> = complexity
518 .into_iter()
519 .filter_map(|fc| {
520 fc.source_hash.map(|hash| {
521 (
522 hash,
523 InventoryComplexity {
524 cyclomatic: fc.cyclomatic,
525 cognitive: fc.cognitive,
526 },
527 )
528 })
529 })
530 .collect();
531
532 (visitor.entries, metrics)
533}
534
535#[cfg(all(test, not(miri)))]
536mod tests {
537 use super::*;
538 use std::path::PathBuf;
539
540 fn walk(source: &str) -> Vec<InventoryEntry> {
541 walk_source(&PathBuf::from("test.ts"), source)
542 }
543
544 #[test]
545 fn named_function_declaration_uses_its_own_name() {
546 let entries = walk("function foo() { return 1; }");
547 assert_eq!(entries.len(), 1);
548 assert_eq!(entries[0].name, "foo");
549 assert_eq!(entries[0].line, 1);
550 }
551
552 #[test]
553 fn const_arrow_captures_binding_name() {
554 let entries = walk("const bar = () => 42;");
555 assert_eq!(entries.len(), 1);
556 assert_eq!(entries[0].name, "bar");
557 }
558
559 #[test]
560 fn const_function_expression_captures_binding_name_not_fn_id() {
561 let entries = walk("const outer = function inner() { return 1; };");
562 assert_eq!(entries.len(), 1);
563 assert_eq!(entries[0].name, "outer");
564 }
565
566 #[test]
567 fn class_methods_use_method_names() {
568 let entries = walk(
569 r"
570 class Foo {
571 bar() { return 1; }
572 baz() { return 2; }
573 }",
574 );
575 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
576 assert_eq!(names, vec!["bar", "baz"]);
577 }
578
579 #[test]
580 fn callback_argument_takes_the_callee_name() {
581 let entries = walk("setTimeout(() => { console.log('hi'); }, 10);");
584 assert_eq!(entries.len(), 1);
585 assert_eq!(entries[0].name, "setTimeout");
586 }
587
588 #[test]
589 fn member_callee_names_each_callback_in_source_order() {
590 let entries = walk(
591 r"
592 [1, 2, 3].map(() => 1);
593 [4, 5, 6].filter(() => true);
594 ",
595 );
596 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
597 assert_eq!(names, vec!["map", "filter"]);
598 }
599
600 #[test]
601 fn named_function_still_advances_counter_matching_instrumenter() {
602 let entries = walk(
606 r"
607 function named() { return 1; }
608 [1].map(() => 2);
609 ",
610 );
611 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
612 assert_eq!(names, vec!["named", "map"]);
613 }
614
615 #[test]
616 fn plain_identifier_callee_names_the_callback() {
617 let entries = walk("useMemo(() => compute());");
618 assert_eq!(entries[0].name, "useMemo");
619 }
620
621 #[test]
622 fn new_expression_callee_names_the_callback() {
623 let entries = walk("new Promise((resolve) => resolve(1));");
624 assert_eq!(entries[0].name, "Promise");
625 }
626
627 #[test]
628 fn callback_after_a_string_argument_is_named_from_the_callee() {
629 let entries = walk(r#"el.addEventListener("click", () => handle());"#);
632 assert_eq!(entries[0].name, "addEventListener");
633 }
634
635 #[test]
636 fn computed_string_key_callee_is_named() {
637 let entries = walk(r#"obj["handler"](() => run());"#);
638 assert_eq!(entries[0].name, "handler");
639 }
640
641 #[test]
642 fn chained_call_does_not_leak_the_earlier_callee_onto_the_later_callback() {
643 let entries = walk("p.then(() => a).catch(() => b);");
647 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
648 assert_eq!(names, vec!["then", "catch"]);
649 }
650
651 #[test]
652 fn nested_callbacks_each_take_their_own_callee() {
653 let entries = walk("outer(() => inner(() => 1));");
654 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
655 assert_eq!(names, vec!["outer", "inner"]);
656 }
657
658 #[test]
659 fn binding_name_wins_over_callee() {
660 let entries = walk("const handler = () => run();");
663 assert_eq!(entries[0].name, "handler");
664 }
665
666 #[test]
667 fn named_function_expression_argument_keeps_its_own_id() {
668 let entries = walk("run(function inner() { return 1; });");
669 assert_eq!(entries[0].name, "inner");
670 }
671
672 #[test]
673 fn iife_callee_stays_anonymous() {
674 let entries = walk("(function () { return 1; })();");
676 assert_eq!(entries[0].name, "(anonymous_0)");
677 }
678
679 #[test]
680 fn computed_non_string_callee_stays_anonymous() {
681 let entries = walk("handlers[index](() => run());");
682 assert_eq!(entries[0].name, "(anonymous_0)");
683 }
684
685 #[test]
686 fn parenthesized_callee_unwraps_to_the_inner_name() {
687 assert_eq!(walk("(foo)(() => run());")[0].name, "foo");
688 assert_eq!(walk("(a.b)(() => run());")[0].name, "b");
689 }
690
691 #[test]
692 fn anonymous_after_named_chain_uses_next_counter_value() {
693 let entries = walk(
694 r"
695 function a() {}
696 function b() {}
697 function c() {}
698 const d = () => 4;
699 ",
700 );
701 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
702 assert_eq!(names, vec!["a", "b", "c", "d"]);
703 }
704
705 #[test]
706 fn typescript_overload_signatures_dont_emit_or_advance_counter() {
707 let entries = walk(
708 r"
709 function foo(): number;
710 function foo(s: string): string;
711 function foo(s?: string): number | string { return s ? s : 1; }
712 [1].map(() => 2);
713 ",
714 );
715 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
716 assert_eq!(names, vec!["foo", "map"]);
717 }
718
719 #[test]
720 fn export_default_named_function_keeps_explicit_name() {
721 let entries = walk("export default function foo() { return 1; }");
722 assert_eq!(entries.len(), 1);
723 assert_eq!(entries[0].name, "foo");
724 }
725
726 #[test]
727 fn export_default_anonymous_function_is_named_default() {
728 let entries = walk("export default function() { return 1; }");
729 assert_eq!(entries.len(), 1);
730 assert_eq!(entries[0].name, "default");
731 }
732
733 #[test]
734 fn export_default_anonymous_arrow_is_named_default() {
735 let entries = walk("export default () => 1;");
736 assert_eq!(entries.len(), 1);
737 assert_eq!(entries[0].name, "default");
738 }
739
740 #[test]
741 fn nested_function_numbered_after_parent_in_traversal_order() {
742 let entries = walk(
743 r"
744 function outer() {
745 return function() { return 1; };
746 }",
747 );
748 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
749 assert_eq!(names, vec!["outer", "(anonymous_1)"]);
750 }
751
752 #[test]
753 fn line_number_is_one_based_from_source_start() {
754 let entries = walk("\n\nfunction atLineThree() {}");
755 assert_eq!(entries.len(), 1);
756 assert_eq!(entries[0].line, 3);
757 }
758
759 #[test]
760 fn short_jsx_in_js_file_retries_with_jsx_parser() {
761 let entries = walk_source(&PathBuf::from("component.js"), "const A = () => <div />;");
762 assert_eq!(entries.len(), 1);
763 assert_eq!(entries[0].name, "A");
764 assert_eq!(entries[0].line, 1);
765 }
766
767 #[test]
768 fn object_method_shorthand_uses_the_key_name() {
769 let entries = walk("const obj = { run() { return 1; } };");
770 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
771 assert_eq!(names, vec!["run"]);
772 }
773
774 #[test]
775 fn object_accessors_carry_the_get_and_set_label() {
776 let entries = walk(
777 r"
778 const obj = {
779 get closed() { return true; },
780 set closed(value) { this.value = value; },
781 };",
782 );
783 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
784 assert_eq!(names, vec!["get closed", "set closed"]);
785 }
786
787 #[test]
788 fn function_valued_object_property_uses_the_key_name() {
789 let entries = walk("const client = { execute: async (sql) => sql.length };");
790 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
791 assert_eq!(names, vec!["execute"]);
792 }
793
794 #[test]
795 fn object_property_holding_a_call_leaves_the_callback_to_its_callee() {
796 let entries = walk("const column = { references: memo(() => other.id) };");
799 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
800 assert_eq!(names, vec!["memo"]);
801 }
802
803 #[test]
804 fn class_accessors_carry_the_get_and_set_label() {
805 let entries = walk(
806 r"
807 class Client {
808 get closed() { return true; }
809 set closed(value) { this.value = value; }
810 }",
811 );
812 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
813 assert_eq!(names, vec!["get closed", "set closed"]);
814 }
815
816 #[test]
817 fn member_assignment_target_names_the_assigned_function() {
818 let entries = walk(
819 r#"
820 client.execute = async (sql) => sql;
821 client["rollback"] = function () { return 1; };
822 "#,
823 );
824 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
825 assert_eq!(names, vec!["execute", "rollback"]);
826 }
827
828 #[test]
829 fn only_callee_named_functions_are_flagged_as_callbacks() {
830 let entries = walk(
831 r"
832 function declared() { return 1; }
833 const bound = () => 2;
834 const obj = { run() { return 3; } };
835 [1].map(() => 4);
836 (function () { return 5; })();
837 ",
838 );
839 let flags: Vec<_> = entries
840 .iter()
841 .map(|e| (e.name.as_str(), e.is_callback))
842 .collect();
843 assert_eq!(
844 flags,
845 vec![
846 ("declared", false),
847 ("bound", false),
848 ("run", false),
849 ("map", true),
850 ("(anonymous_4)", false),
851 ]
852 );
853 }
854
855 #[test]
856 fn drizzle_style_schema_names_every_callback_after_its_callee() {
857 let entries = walk(
861 r#"
862 export const users = sqliteTable("users", {
863 id: text("id").primaryKey(),
864 orgId: text("org_id").references(() => orgs.id),
865 }, (table) => [index("users_org_idx").on(table.orgId)]);
866 "#,
867 );
868 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
869 assert_eq!(names, vec!["references", "sqliteTable"]);
870 assert!(entries.iter().all(|entry| entry.is_callback));
871 }
872
873 #[test]
874 fn class_property_arrow_uses_anonymous_counter() {
875 let entries = walk(
876 r"
877 class Foo {
878 bar = () => 1;
879 }",
880 );
881 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
882 assert_eq!(names, vec!["(anonymous_0)"]);
883 }
884
885 #[test]
886 fn records_one_indexed_utf16_columns() {
887 let entries = walk("function foo() { return 1; }");
888 assert_eq!(entries.len(), 1);
889 assert_eq!(entries[0].start_column, 1);
890 assert_eq!(entries[0].end_line, 1);
891 assert!(entries[0].end_column > entries[0].start_column);
892 }
893
894 #[test]
895 fn utf16_column_counts_code_units_not_bytes() {
896 let entries = walk("const e = \"\u{1F600}\"; const f = () => 1;");
897 let f = entries.iter().find(|e| e.name == "f").expect("f present");
898 let byte_prefix_len = "const e = \"\u{1F600}\"; const f = ".len() as u32;
899 assert!(f.start_column < byte_prefix_len + 1);
900 }
901
902 #[test]
903 fn utf16_columns_stay_exact_across_a_long_single_line() {
904 use std::fmt::Write as _;
909 let mut src = String::new();
910 for i in 0..40 {
911 let _ = write!(src, "function f{i}() {{ return \"\u{1F600}\"; }} ");
912 }
913 src.push_str("function outer() { const inner = () => \"\u{1F600}\"; return inner; }");
914 src.push_str("\nconst tail = () => 1;");
915 let entries = walk(&src);
916 let col = |byte: usize| src[..byte].encode_utf16().count() as u32 + 1;
917
918 for i in [0_usize, 17, 39] {
919 let body = format!("function f{i}() {{ return \"\u{1F600}\"; }}");
920 let start = src.find(&body).expect("function text present");
921 let entry = entries
922 .iter()
923 .find(|e| e.name == format!("f{i}"))
924 .expect("entry present");
925 assert_eq!(entry.line, 1);
926 assert_eq!(entry.start_column, col(start));
927 assert_eq!(entry.end_line, 1);
928 assert_eq!(entry.end_column, col(start + body.len()));
929 }
930
931 let inner_start = src
932 .find("() => \"\u{1F600}\"")
933 .expect("inner arrow present");
934 let inner = entries
935 .iter()
936 .find(|e| e.name == "inner")
937 .expect("inner present");
938 assert_eq!(inner.line, 1);
939 assert_eq!(inner.start_column, col(inner_start));
940
941 let tail = entries
942 .iter()
943 .find(|e| e.name == "tail")
944 .expect("tail present");
945 let line2_start = src.find('\n').expect("newline present") + 1;
946 let tail_start = src.rfind("() => 1").expect("tail arrow present");
947 assert_eq!(tail.line, 2);
948 assert_eq!(
949 tail.start_column,
950 src[line2_start..tail_start].encode_utf16().count() as u32 + 1
951 );
952 }
953
954 #[test]
955 fn same_line_distinct_named_functions_have_distinct_positions() {
956 let entries = walk("function a() {} function b() {}");
957 let a = entries.iter().find(|e| e.name == "a").expect("a present");
958 let b = entries.iter().find(|e| e.name == "b").expect("b present");
959 assert_eq!(a.line, b.line, "both on line 1");
960 assert_ne!(
961 a.start_column, b.start_column,
962 "same-line functions are column-disambiguated"
963 );
964 }
965
966 #[test]
967 fn same_line_anonymous_functions_stay_distinct_via_counter() {
968 let entries = walk("const xs = [() => 1, () => 2];");
969 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
970 assert_eq!(names, vec!["(anonymous_0)", "(anonymous_1)"]);
971 assert_eq!(entries[0].line, entries[1].line, "both on line 1");
972 assert_ne!(
973 entries[0].name, entries[1].name,
974 "counter keeps them distinct"
975 );
976 }
977
978 #[test]
979 fn source_hash_is_the_content_digest_of_the_function_span() {
980 let src = "function foo() { return 1; }";
981 let entries = walk(src);
982 assert_eq!(entries.len(), 1);
983 assert_eq!(
984 entries[0].source_hash,
985 fallow_cov_protocol::source_hash_for(src.as_bytes())
986 );
987 assert_eq!(entries[0].source_hash.len(), 16);
988 assert!(
989 entries[0]
990 .source_hash
991 .chars()
992 .all(|c| c.is_ascii_hexdigit())
993 );
994 }
995
996 #[test]
997 fn source_hash_survives_line_moves_and_tracks_body_edits() {
998 let original = walk("function foo() { return 1; }");
999 let moved = walk("\n\nfunction foo() { return 1; }");
1000 assert_eq!(
1001 original[0].source_hash, moved[0].source_hash,
1002 "a moved-but-unedited function must keep its source_hash"
1003 );
1004 let edited = walk("function foo() { return 2; }");
1005 assert_ne!(
1006 original[0].source_hash, edited[0].source_hash,
1007 "an edited body must change the source_hash"
1008 );
1009 }
1010}