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
52use crate::js_text::JsText;
53
54#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct InventoryEntry {
66 pub name: String,
68 pub line: u32,
70 pub start_column: u32,
72 pub end_line: u32,
74 pub end_column: u32,
76 pub source_hash: Option<String>,
86 pub is_callback: bool,
91}
92
93#[derive(Clone, Copy)]
97struct ColCache {
98 line_idx: usize,
99 byte_end: usize,
100 utf16_units: usize,
101}
102
103impl ColCache {
104 const EMPTY: Self = Self {
105 line_idx: usize::MAX,
106 byte_end: 0,
107 utf16_units: 0,
108 };
109}
110
111#[cfg(test)]
112thread_local! {
113 static UTF16_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
115}
116
117#[cfg(test)]
120fn take_utf16_bytes() -> usize {
121 UTF16_BYTES.with(|encoded| encoded.replace(0))
122}
123
124struct OpenFunction {
126 entry: usize,
128 span: Span,
129 height: u32,
131}
132
133struct ResolvedName {
135 name: String,
136 is_callback: bool,
138}
139
140impl ResolvedName {
141 const fn named(name: String) -> Self {
142 Self {
143 name,
144 is_callback: false,
145 }
146 }
147}
148
149struct InventoryVisitor<'a> {
151 source: &'a str,
152 line_offsets: &'a [u32],
153 entries: Vec<InventoryEntry>,
154 start_cols: ColCache,
157 end_cols: ColCache,
158 open: Vec<OpenFunction>,
160 pending_name: Option<String>,
162 pending_callee_name: Option<String>,
166 anonymous_counter: u32,
168}
169
170impl<'a> InventoryVisitor<'a> {
171 const fn new(source: &'a str, line_offsets: &'a [u32]) -> Self {
172 Self {
173 source,
174 line_offsets,
175 entries: Vec::new(),
176 start_cols: ColCache::EMPTY,
177 end_cols: ColCache::EMPTY,
178 open: Vec::new(),
179 pending_name: None,
180 pending_callee_name: None,
181 anonymous_counter: 0,
182 }
183 }
184
185 fn resolve_name(&mut self, explicit: Option<&str>) -> ResolvedName {
196 let n = self.anonymous_counter;
197 self.anonymous_counter += 1;
198 if let Some(pending) = self.pending_name.take() {
199 return ResolvedName::named(pending);
200 }
201 if let Some(name) = explicit {
202 return ResolvedName::named(name.to_owned());
203 }
204 if let Some(callee) = self.pending_callee_name.take() {
205 return ResolvedName {
206 name: callee,
207 is_callback: true,
208 };
209 }
210 ResolvedName::named(format!("(anonymous_{n})"))
211 }
212
213 fn record(&mut self, resolved: ResolvedName, span: Span) {
214 let mut start_cols = self.start_cols;
215 let (line, start_column) = self.line_col_utf16(&mut start_cols, span.start);
216 self.start_cols = start_cols;
217 let mut end_cols = self.end_cols;
218 let (end_line, end_column) = self.line_col_utf16(&mut end_cols, span.end);
219 self.end_cols = end_cols;
220 self.open.push(OpenFunction {
221 entry: self.entries.len(),
222 span,
223 height: 0,
224 });
225 self.entries.push(InventoryEntry {
226 name: resolved.name,
227 line,
228 start_column,
229 end_line,
230 end_column,
231 source_hash: None,
232 is_callback: resolved.is_callback,
233 });
234 }
235
236 fn leave_function(&mut self) {
238 let Some(open) = self.open.pop() else {
239 return;
240 };
241 self.entries[open.entry].source_hash =
242 crate::function_hash::function_source_hash(self.source, open.span, open.height);
243 if let Some(parent) = self.open.last_mut() {
244 parent.height = parent.height.max(open.height + 1);
245 }
246 }
247
248 fn line_col_utf16(&self, cache: &mut ColCache, byte_offset: u32) -> (u32, u32) {
263 let line_idx = match self.line_offsets.binary_search(&byte_offset) {
264 Ok(idx) => idx,
265 Err(idx) => idx.saturating_sub(1),
266 };
267 let line = line_idx as u32 + 1;
268 let line_start = self.line_offsets[line_idx] as usize;
269 let mut end = byte_offset as usize;
270 while end > line_start && !self.source.is_char_boundary(end) {
271 end -= 1;
272 }
273 let from_cache = if cache.line_idx == line_idx {
277 if end >= cache.byte_end {
278 self.utf16_len(cache.byte_end, end)
279 .map(|gap| cache.utf16_units + gap)
280 } else {
281 self.utf16_len(end, cache.byte_end)
282 .map(|gap| cache.utf16_units - gap)
283 }
284 } else {
285 None
286 };
287 let col_utf16 = from_cache.unwrap_or_else(|| self.utf16_len(line_start, end).unwrap_or(0));
288 *cache = ColCache {
289 line_idx,
290 byte_end: end,
291 utf16_units: col_utf16,
292 };
293 (line, col_utf16 as u32 + 1)
294 }
295
296 fn utf16_len(&self, start: usize, end: usize) -> Option<usize> {
299 let slice = self.source.get(start..end)?;
300 #[cfg(test)]
301 UTF16_BYTES.with(|encoded| encoded.set(encoded.get() + slice.len()));
302 Some(slice.encode_utf16().count())
303 }
304}
305
306impl<'ast> Visit<'ast> for InventoryVisitor<'_> {
307 fn visit_function(&mut self, func: &Function<'ast>, flags: ScopeFlags) {
308 if func.body.is_none() {
309 walk::walk_function(self, func, flags);
310 return;
311 }
312 let resolved = self.resolve_name(func.id.as_ref().map(|id| id.name.as_str()));
313 self.record(resolved, func.span);
314 walk::walk_function(self, func, flags);
315 self.leave_function();
316 }
317
318 fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'ast>) {
319 let resolved = self.resolve_name(None);
320 self.record(resolved, arrow.span);
321 walk::walk_arrow_function_expression(self, arrow);
322 self.leave_function();
323 }
324
325 fn visit_method_definition(&mut self, method: &MethodDefinition<'ast>) {
326 if let Some(name) = method.key.static_name() {
327 self.pending_name = Some(accessor_label(
328 matches!(method.kind, MethodDefinitionKind::Get),
329 matches!(method.kind, MethodDefinitionKind::Set),
330 &name.text(),
331 ));
332 }
333 walk::walk_method_definition(self, method);
334 self.pending_name = None;
335 }
336
337 fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'ast>) {
338 if let Some(id) = decl.id.get_binding_identifier()
339 && decl.init.as_ref().is_some_and(|init| {
340 matches!(
341 init,
342 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
343 )
344 })
345 {
346 self.pending_name = Some(id.name.to_string());
347 }
348 walk::walk_variable_declarator(self, decl);
349 self.pending_name = None;
350 }
351
352 fn visit_object_property(&mut self, prop: &ObjectProperty<'ast>) {
359 let is_method_like =
360 prop.method || matches!(prop.kind, PropertyKind::Get | PropertyKind::Set);
361 let is_function_valued = matches!(
362 prop.value,
363 Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_)
364 );
365 self.pending_name = if is_method_like || is_function_valued {
366 prop.key.static_name().map(|name| {
367 accessor_label(
368 matches!(prop.kind, PropertyKind::Get),
369 matches!(prop.kind, PropertyKind::Set),
370 &name.text(),
371 )
372 })
373 } else {
374 None
375 };
376 walk::walk_object_property(self, prop);
377 self.pending_name = None;
378 }
379
380 fn visit_assignment_expression(&mut self, expr: &AssignmentExpression<'ast>) {
385 if matches!(expr.operator, AssignmentOperator::Assign)
386 && matches!(
387 expr.right,
388 Expression::FunctionExpression(_)
389 | Expression::ArrowFunctionExpression(_)
390 | Expression::ClassExpression(_)
391 )
392 {
393 self.pending_name = assignment_target_name(&expr.left);
394 }
395 walk::walk_assignment_expression(self, expr);
396 self.pending_name = None;
397 }
398
399 fn visit_export_default_declaration(&mut self, decl: &ExportDefaultDeclaration<'ast>) {
402 let anonymous = match &decl.declaration {
403 ExportDefaultDeclarationKind::FunctionDeclaration(func) => func.id.is_none(),
404 ExportDefaultDeclarationKind::ArrowFunctionExpression(_) => true,
405 _ => false,
406 };
407 if anonymous {
408 self.pending_name = Some("default".to_owned());
409 }
410 walk::walk_export_default_declaration(self, decl);
411 self.pending_name = None;
412 }
413
414 fn visit_call_expression(&mut self, call: &CallExpression<'ast>) {
424 self.visit_expression(&call.callee);
425 let name = callee_name(&call.callee);
426 for argument in &call.arguments {
427 self.pending_callee_name.clone_from(&name);
428 self.visit_argument(argument);
429 }
430 self.pending_callee_name = None;
431 }
432
433 fn visit_new_expression(&mut self, new_expr: &NewExpression<'ast>) {
434 self.visit_expression(&new_expr.callee);
435 let name = callee_name(&new_expr.callee);
436 for argument in &new_expr.arguments {
437 self.pending_callee_name.clone_from(&name);
438 self.visit_argument(argument);
439 }
440 self.pending_callee_name = None;
441 }
442}
443
444fn accessor_label(is_getter: bool, is_setter: bool, name: &str) -> String {
448 if is_getter {
449 return format!("get {name}");
450 }
451 if is_setter {
452 return format!("set {name}");
453 }
454 name.to_owned()
455}
456
457fn assignment_target_name(target: &AssignmentTarget<'_>) -> Option<String> {
461 match target {
462 AssignmentTarget::StaticMemberExpression(member) => Some(member.property.name.to_string()),
463 AssignmentTarget::ComputedMemberExpression(member) => match &member.expression {
464 Expression::StringLiteral(lit) => Some(lit.value.text().into_owned()),
465 _ => None,
466 },
467 _ => None,
468 }
469}
470
471fn callee_name(callee: &Expression<'_>) -> Option<String> {
477 match callee {
478 Expression::Identifier(ident) => Some(ident.name.to_string()),
479 Expression::StaticMemberExpression(member) => Some(member.property.name.to_string()),
480 Expression::ComputedMemberExpression(member) => match &member.expression {
481 Expression::StringLiteral(lit) => Some(lit.value.text().into_owned()),
482 _ => None,
483 },
484 Expression::ParenthesizedExpression(paren) => callee_name(&paren.expression),
488 _ => None,
489 }
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub struct InventoryComplexity {
502 pub cyclomatic: u16,
504 pub cognitive: u16,
506}
507
508#[must_use]
518pub fn walk_source(path: &Path, source: &str) -> Vec<InventoryEntry> {
519 walk_source_with_complexity(path, source).0
520}
521
522#[must_use]
534pub fn walk_source_with_complexity(
535 path: &Path,
536 source: &str,
537) -> (Vec<InventoryEntry>, FxHashMap<String, InventoryComplexity>) {
538 let source_type = SourceType::from_path(path).unwrap_or_default();
539 let line_offsets = fallow_types::extract::compute_line_offsets(source);
540
541 let primary = walk_one_parse(source, source_type, &line_offsets);
542 if primary.0.is_empty() && !source_type.is_jsx() {
543 let jsx_type = if source_type.is_typescript() {
544 SourceType::tsx()
545 } else {
546 SourceType::jsx()
547 };
548 let retry = walk_one_parse(source, jsx_type, &line_offsets);
549 if !retry.0.is_empty() {
550 return retry;
551 }
552 }
553
554 primary
555}
556
557fn walk_one_parse(
560 source: &str,
561 source_type: SourceType,
562 line_offsets: &[u32],
563) -> (Vec<InventoryEntry>, FxHashMap<String, InventoryComplexity>) {
564 crate::parse_bounded(source, || {
565 walk_one_parse_unbounded(source, source_type, line_offsets)
566 })
567 .unwrap_or_default()
568}
569
570fn walk_one_parse_unbounded(
571 source: &str,
572 source_type: SourceType,
573 line_offsets: &[u32],
574) -> (Vec<InventoryEntry>, FxHashMap<String, InventoryComplexity>) {
575 let allocator = Allocator::default();
576 let parser_return = Parser::new(&allocator, source, source_type).parse();
577
578 let mut visitor = InventoryVisitor::new(source, line_offsets);
579 visitor.visit_program(&parser_return.program);
580
581 let complexity =
582 crate::complexity::compute_complexity(&parser_return.program, source, line_offsets);
583 let metrics: FxHashMap<String, InventoryComplexity> = complexity
584 .into_iter()
585 .filter_map(|fc| {
586 fc.source_hash.map(|hash| {
587 (
588 hash,
589 InventoryComplexity {
590 cyclomatic: fc.cyclomatic,
591 cognitive: fc.cognitive,
592 },
593 )
594 })
595 })
596 .collect();
597
598 (visitor.entries, metrics)
599}
600
601#[cfg(all(test, not(miri)))]
602mod tests {
603 use super::*;
604 use std::path::PathBuf;
605
606 fn walk(source: &str) -> Vec<InventoryEntry> {
607 walk_source(&PathBuf::from("test.ts"), source)
608 }
609
610 #[test]
611 fn named_function_declaration_uses_its_own_name() {
612 let entries = walk("function foo() { return 1; }");
613 assert_eq!(entries.len(), 1);
614 assert_eq!(entries[0].name, "foo");
615 assert_eq!(entries[0].line, 1);
616 }
617
618 #[test]
619 fn const_arrow_captures_binding_name() {
620 let entries = walk("const bar = () => 42;");
621 assert_eq!(entries.len(), 1);
622 assert_eq!(entries[0].name, "bar");
623 }
624
625 #[test]
626 fn const_function_expression_captures_binding_name_not_fn_id() {
627 let entries = walk("const outer = function inner() { return 1; };");
628 assert_eq!(entries.len(), 1);
629 assert_eq!(entries[0].name, "outer");
630 }
631
632 #[test]
633 fn class_methods_use_method_names() {
634 let entries = walk(
635 r"
636 class Foo {
637 bar() { return 1; }
638 baz() { return 2; }
639 }",
640 );
641 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
642 assert_eq!(names, vec!["bar", "baz"]);
643 }
644
645 #[test]
646 fn callback_argument_takes_the_callee_name() {
647 let entries = walk("setTimeout(() => { console.log('hi'); }, 10);");
650 assert_eq!(entries.len(), 1);
651 assert_eq!(entries[0].name, "setTimeout");
652 }
653
654 #[test]
655 fn member_callee_names_each_callback_in_source_order() {
656 let entries = walk(
657 r"
658 [1, 2, 3].map(() => 1);
659 [4, 5, 6].filter(() => true);
660 ",
661 );
662 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
663 assert_eq!(names, vec!["map", "filter"]);
664 }
665
666 #[test]
667 fn named_function_still_advances_counter_matching_instrumenter() {
668 let entries = walk(
672 r"
673 function named() { return 1; }
674 [1].map(() => 2);
675 ",
676 );
677 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
678 assert_eq!(names, vec!["named", "map"]);
679 }
680
681 #[test]
682 fn plain_identifier_callee_names_the_callback() {
683 let entries = walk("useMemo(() => compute());");
684 assert_eq!(entries[0].name, "useMemo");
685 }
686
687 #[test]
688 fn new_expression_callee_names_the_callback() {
689 let entries = walk("new Promise((resolve) => resolve(1));");
690 assert_eq!(entries[0].name, "Promise");
691 }
692
693 #[test]
694 fn callback_after_a_string_argument_is_named_from_the_callee() {
695 let entries = walk(r#"el.addEventListener("click", () => handle());"#);
698 assert_eq!(entries[0].name, "addEventListener");
699 }
700
701 #[test]
702 fn computed_string_key_callee_is_named() {
703 let entries = walk(r#"obj["handler"](() => run());"#);
704 assert_eq!(entries[0].name, "handler");
705 }
706
707 #[test]
708 fn chained_call_does_not_leak_the_earlier_callee_onto_the_later_callback() {
709 let entries = walk("p.then(() => a).catch(() => b);");
713 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
714 assert_eq!(names, vec!["then", "catch"]);
715 }
716
717 #[test]
718 fn nested_callbacks_each_take_their_own_callee() {
719 let entries = walk("outer(() => inner(() => 1));");
720 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
721 assert_eq!(names, vec!["outer", "inner"]);
722 }
723
724 #[test]
725 fn binding_name_wins_over_callee() {
726 let entries = walk("const handler = () => run();");
729 assert_eq!(entries[0].name, "handler");
730 }
731
732 #[test]
733 fn named_function_expression_argument_keeps_its_own_id() {
734 let entries = walk("run(function inner() { return 1; });");
735 assert_eq!(entries[0].name, "inner");
736 }
737
738 #[test]
739 fn iife_callee_stays_anonymous() {
740 let entries = walk("(function () { return 1; })();");
742 assert_eq!(entries[0].name, "(anonymous_0)");
743 }
744
745 #[test]
746 fn computed_non_string_callee_stays_anonymous() {
747 let entries = walk("handlers[index](() => run());");
748 assert_eq!(entries[0].name, "(anonymous_0)");
749 }
750
751 #[test]
752 fn parenthesized_callee_unwraps_to_the_inner_name() {
753 assert_eq!(walk("(foo)(() => run());")[0].name, "foo");
754 assert_eq!(walk("(a.b)(() => run());")[0].name, "b");
755 }
756
757 #[test]
758 fn anonymous_after_named_chain_uses_next_counter_value() {
759 let entries = walk(
760 r"
761 function a() {}
762 function b() {}
763 function c() {}
764 const d = () => 4;
765 ",
766 );
767 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
768 assert_eq!(names, vec!["a", "b", "c", "d"]);
769 }
770
771 #[test]
772 fn typescript_overload_signatures_dont_emit_or_advance_counter() {
773 let entries = walk(
774 r"
775 function foo(): number;
776 function foo(s: string): string;
777 function foo(s?: string): number | string { return s ? s : 1; }
778 [1].map(() => 2);
779 ",
780 );
781 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
782 assert_eq!(names, vec!["foo", "map"]);
783 }
784
785 #[test]
786 fn export_default_named_function_keeps_explicit_name() {
787 let entries = walk("export default function foo() { return 1; }");
788 assert_eq!(entries.len(), 1);
789 assert_eq!(entries[0].name, "foo");
790 }
791
792 #[test]
793 fn export_default_anonymous_function_is_named_default() {
794 let entries = walk("export default function() { return 1; }");
795 assert_eq!(entries.len(), 1);
796 assert_eq!(entries[0].name, "default");
797 }
798
799 #[test]
800 fn export_default_anonymous_arrow_is_named_default() {
801 let entries = walk("export default () => 1;");
802 assert_eq!(entries.len(), 1);
803 assert_eq!(entries[0].name, "default");
804 }
805
806 #[test]
807 fn nested_function_numbered_after_parent_in_traversal_order() {
808 let entries = walk(
809 r"
810 function outer() {
811 return function() { return 1; };
812 }",
813 );
814 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
815 assert_eq!(names, vec!["outer", "(anonymous_1)"]);
816 }
817
818 #[test]
819 fn line_number_is_one_based_from_source_start() {
820 let entries = walk("\n\nfunction atLineThree() {}");
821 assert_eq!(entries.len(), 1);
822 assert_eq!(entries[0].line, 3);
823 }
824
825 #[test]
826 fn short_jsx_in_js_file_retries_with_jsx_parser() {
827 let entries = walk_source(&PathBuf::from("component.js"), "const A = () => <div />;");
828 assert_eq!(entries.len(), 1);
829 assert_eq!(entries[0].name, "A");
830 assert_eq!(entries[0].line, 1);
831 }
832
833 #[test]
834 fn object_method_shorthand_uses_the_key_name() {
835 let entries = walk("const obj = { run() { return 1; } };");
836 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
837 assert_eq!(names, vec!["run"]);
838 }
839
840 #[test]
841 fn object_accessors_carry_the_get_and_set_label() {
842 let entries = walk(
843 r"
844 const obj = {
845 get closed() { return true; },
846 set closed(value) { this.value = value; },
847 };",
848 );
849 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
850 assert_eq!(names, vec!["get closed", "set closed"]);
851 }
852
853 #[test]
854 fn function_valued_object_property_uses_the_key_name() {
855 let entries = walk("const client = { execute: async (sql) => sql.length };");
856 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
857 assert_eq!(names, vec!["execute"]);
858 }
859
860 #[test]
861 fn object_property_holding_a_call_leaves_the_callback_to_its_callee() {
862 let entries = walk("const column = { references: memo(() => other.id) };");
865 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
866 assert_eq!(names, vec!["memo"]);
867 }
868
869 #[test]
870 fn class_accessors_carry_the_get_and_set_label() {
871 let entries = walk(
872 r"
873 class Client {
874 get closed() { return true; }
875 set closed(value) { this.value = value; }
876 }",
877 );
878 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
879 assert_eq!(names, vec!["get closed", "set closed"]);
880 }
881
882 #[test]
883 fn member_assignment_target_names_the_assigned_function() {
884 let entries = walk(
885 r#"
886 client.execute = async (sql) => sql;
887 client["rollback"] = function () { return 1; };
888 "#,
889 );
890 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
891 assert_eq!(names, vec!["execute", "rollback"]);
892 }
893
894 #[test]
895 fn only_callee_named_functions_are_flagged_as_callbacks() {
896 let entries = walk(
897 r"
898 function declared() { return 1; }
899 const bound = () => 2;
900 const obj = { run() { return 3; } };
901 [1].map(() => 4);
902 (function () { return 5; })();
903 ",
904 );
905 let flags: Vec<_> = entries
906 .iter()
907 .map(|e| (e.name.as_str(), e.is_callback))
908 .collect();
909 assert_eq!(
910 flags,
911 vec![
912 ("declared", false),
913 ("bound", false),
914 ("run", false),
915 ("map", true),
916 ("(anonymous_4)", false),
917 ]
918 );
919 }
920
921 #[test]
922 fn drizzle_style_schema_names_every_callback_after_its_callee() {
923 let entries = walk(
927 r#"
928 export const users = sqliteTable("users", {
929 id: text("id").primaryKey(),
930 orgId: text("org_id").references(() => orgs.id),
931 }, (table) => [index("users_org_idx").on(table.orgId)]);
932 "#,
933 );
934 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
935 assert_eq!(names, vec!["references", "sqliteTable"]);
936 assert!(entries.iter().all(|entry| entry.is_callback));
937 }
938
939 #[test]
940 fn class_property_arrow_uses_anonymous_counter() {
941 let entries = walk(
942 r"
943 class Foo {
944 bar = () => 1;
945 }",
946 );
947 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
948 assert_eq!(names, vec!["(anonymous_0)"]);
949 }
950
951 #[test]
952 fn records_one_indexed_utf16_columns() {
953 let entries = walk("function foo() { return 1; }");
954 assert_eq!(entries.len(), 1);
955 assert_eq!(entries[0].start_column, 1);
956 assert_eq!(entries[0].end_line, 1);
957 assert!(entries[0].end_column > entries[0].start_column);
958 }
959
960 #[test]
961 fn utf16_column_counts_code_units_not_bytes() {
962 let entries = walk("const e = \"\u{1F600}\"; const f = () => 1;");
963 let f = entries.iter().find(|e| e.name == "f").expect("f present");
964 let byte_prefix_len = "const e = \"\u{1F600}\"; const f = ".len() as u32;
965 assert!(f.start_column < byte_prefix_len + 1);
966 }
967
968 #[test]
969 fn utf16_columns_stay_exact_across_a_long_single_line() {
970 use std::fmt::Write as _;
975 let mut src = String::new();
976 for i in 0..40 {
977 let _ = write!(src, "function f{i}() {{ return \"\u{1F600}\"; }} ");
978 }
979 src.push_str("function outer() { const inner = () => \"\u{1F600}\"; return inner; }");
980 src.push_str("\nconst tail = () => 1;");
981 let entries = walk(&src);
982 let col = |byte: usize| src[..byte].encode_utf16().count() as u32 + 1;
983
984 for i in [0_usize, 17, 39] {
985 let body = format!("function f{i}() {{ return \"\u{1F600}\"; }}");
986 let start = src.find(&body).expect("function text present");
987 let entry = entries
988 .iter()
989 .find(|e| e.name == format!("f{i}"))
990 .expect("entry present");
991 assert_eq!(entry.line, 1);
992 assert_eq!(entry.start_column, col(start));
993 assert_eq!(entry.end_line, 1);
994 assert_eq!(entry.end_column, col(start + body.len()));
995 }
996
997 let inner_start = src
998 .find("() => \"\u{1F600}\"")
999 .expect("inner arrow present");
1000 let inner = entries
1001 .iter()
1002 .find(|e| e.name == "inner")
1003 .expect("inner present");
1004 assert_eq!(inner.line, 1);
1005 assert_eq!(inner.start_column, col(inner_start));
1006
1007 let tail = entries
1008 .iter()
1009 .find(|e| e.name == "tail")
1010 .expect("tail present");
1011 let line2_start = src.find('\n').expect("newline present") + 1;
1012 let tail_start = src.rfind("() => 1").expect("tail arrow present");
1013 assert_eq!(tail.line, 2);
1014 assert_eq!(
1015 tail.start_column,
1016 src[line2_start..tail_start].encode_utf16().count() as u32 + 1
1017 );
1018 }
1019
1020 #[test]
1021 fn same_line_distinct_named_functions_have_distinct_positions() {
1022 let entries = walk("function a() {} function b() {}");
1023 let a = entries.iter().find(|e| e.name == "a").expect("a present");
1024 let b = entries.iter().find(|e| e.name == "b").expect("b present");
1025 assert_eq!(a.line, b.line, "both on line 1");
1026 assert_ne!(
1027 a.start_column, b.start_column,
1028 "same-line functions are column-disambiguated"
1029 );
1030 }
1031
1032 #[test]
1033 fn same_line_anonymous_functions_stay_distinct_via_counter() {
1034 let entries = walk("const xs = [() => 1, () => 2];");
1035 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
1036 assert_eq!(names, vec!["(anonymous_0)", "(anonymous_1)"]);
1037 assert_eq!(entries[0].line, entries[1].line, "both on line 1");
1038 assert_ne!(
1039 entries[0].name, entries[1].name,
1040 "counter keeps them distinct"
1041 );
1042 }
1043
1044 fn nested_arrow_source(levels: usize) -> String {
1046 format!("export const f = {}1;\n", "()=>".repeat(levels))
1047 }
1048
1049 #[test]
1050 fn deep_nesting_keeps_the_hash_join_with_complexity() {
1051 let levels = crate::function_hash::MAX_HASHED_FUNCTION_HEIGHT as usize + 3;
1052 let source = nested_arrow_source(levels);
1053 let (entries, complexity) = walk_source_with_complexity(&PathBuf::from("a.js"), &source);
1054 assert_eq!(entries.len(), levels);
1055 for (index, entry) in entries.iter().enumerate() {
1058 let height = levels - 1 - index;
1059 let kept = height <= crate::function_hash::MAX_HASHED_FUNCTION_HEIGHT as usize;
1060 assert_eq!(entry.source_hash.is_some(), kept, "height {height}");
1061 if let Some(hash) = &entry.source_hash {
1062 assert!(
1063 complexity.contains_key(hash),
1064 "height {height} has no metrics"
1065 );
1066 }
1067 }
1068 }
1069
1070 #[test]
1071 fn column_lookups_stay_linear_on_nested_functions_in_one_line() {
1072 const STACK: usize = 64 << 20;
1075 let (encoded, entries, source_len) = std::thread::Builder::new()
1076 .stack_size(STACK)
1077 .spawn(|| {
1078 crate::register_thread_stack(STACK);
1079 take_utf16_bytes();
1080 let source = nested_arrow_source(4_000);
1081 let entries = walk_source(&PathBuf::from("a.js"), &source);
1082 (take_utf16_bytes(), entries.len(), source.len())
1083 })
1084 .expect("probe thread should start")
1085 .join()
1086 .expect("probe thread should finish");
1087 assert_eq!(entries, 4_000);
1088 assert!(
1089 encoded <= 4 * source_len,
1090 "encoded {encoded} bytes for {source_len}"
1091 );
1092 }
1093
1094 #[test]
1095 fn source_hash_is_the_content_digest_of_the_function_span() {
1096 let src = "function foo() { return 1; }";
1097 let entries = walk(src);
1098 assert_eq!(entries.len(), 1);
1099 let hash = entries[0]
1100 .source_hash
1101 .as_deref()
1102 .expect("a shallow function keeps its hash");
1103 assert_eq!(hash, fallow_cov_protocol::source_hash_for(src.as_bytes()));
1104 assert_eq!(hash.len(), 16);
1105 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
1106 }
1107
1108 #[test]
1109 fn source_hash_survives_line_moves_and_tracks_body_edits() {
1110 let original = walk("function foo() { return 1; }");
1111 let moved = walk("\n\nfunction foo() { return 1; }");
1112 assert_eq!(
1113 original[0].source_hash, moved[0].source_hash,
1114 "a moved-but-unedited function must keep its source_hash"
1115 );
1116 let edited = walk("function foo() { return 2; }");
1117 assert_ne!(
1118 original[0].source_hash, edited[0].source_hash,
1119 "an edited body must change the source_hash"
1120 );
1121 }
1122}