Skip to main content

fallow_engine/
trace_usage_impl.rs

1//! Per-specifier usage of a dependency (`fallow trace --dependency`).
2//!
3//! The walk is syntactic. It reads the import bindings, call sites and
4//! binding references that extraction keeps on each `ModuleInfo`, and it
5//! follows one hop from a project wrapper to the files that import the
6//! wrapper. Each use that it cannot resolve to a call gets a named reason, so
7//! the counts never hide a gap.
8
9use std::fmt::Write as _;
10use std::path::{Path, PathBuf};
11
12use fallow_types::discover::FileId;
13use fallow_types::extract::{
14    ExportName, ImportBindingReference, ImportBindingReferenceKind, ImportedName, ModuleInfo,
15    byte_offset_to_line_col,
16};
17use fallow_types::trace_usage::{
18    ClosureFile, ConsumerClosure, DependencyUsage, DependencyUsageQuery,
19    DependencyUsageSchemaVersion, FileLevelUnresolved, SpecifierUnresolved, SpecifierUsage,
20    UsageConfidence, UsageSite, UsageSiteKind, UsageSitePage, UsageWrapper, WrapperShape,
21};
22use rustc_hash::{FxHashMap, FxHashSet};
23
24use super::trace_impl::relativize;
25use crate::graph::ModuleGraph;
26
27/// The name of a bare namespace use.
28const NAMESPACE_SPECIFIER: &str = "*";
29/// The name of a default import.
30const DEFAULT_SPECIFIER: &str = "default";
31/// The version prefix of a site cursor.
32const CURSOR_PREFIX: &str = "v1.";
33/// The separator between the fields of a decoded cursor.
34const CURSOR_SEPARATOR: char = '\0';
35const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
36const FNV_PRIME: u64 = 0x0100_0000_01b3;
37
38/// Why a usage query has no answer.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum UsageError {
41    /// The cursor does not decode, or it belongs to another query.
42    InvalidCursor,
43}
44
45impl std::fmt::Display for UsageError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::InvalidCursor => {
49                f.write_str("the cursor is not valid for this query; start again without --cursor")
50            }
51        }
52    }
53}
54
55impl std::error::Error for UsageError {}
56
57/// Compute the usage of `package_name` across `modules`.
58///
59/// `imported_by` is the root-relative importer list of the dependency trace.
60/// It seeds the closure and finds the files that import the package through
61/// a specifier that does not name it.
62///
63/// # Errors
64///
65/// Returns [`UsageError::InvalidCursor`] when the query carries a cursor that
66/// does not decode or that belongs to another query.
67pub fn dependency_usage(
68    graph: &ModuleGraph,
69    modules: &[ModuleInfo],
70    root: &Path,
71    package_name: &str,
72    imported_by: &[PathBuf],
73    query: &DependencyUsageQuery,
74) -> Result<DependencyUsage, UsageError> {
75    let ctx = UsageContext::new(graph, modules, root, package_name);
76    let mut scan = Scan::default();
77    for module in modules {
78        scan.scan_module(&ctx, module);
79    }
80    scan.follow_wrappers(&ctx);
81
82    let selected = selected_specifiers(&query.specifiers);
83    let mut unresolved = scan.file_level_unresolved();
84    unresolved.unattributed_file = imported_by
85        .iter()
86        .filter(|path| {
87            ctx.file_id(&path_key(path))
88                .is_none_or(|id| !scan.attributed_files.contains(&id))
89        })
90        .count();
91    let specifiers = scan.specifier_usages(selected.as_deref());
92    let sites = match &query.sites {
93        Some(request) => Some(site_page(
94            &scan.sites,
95            selected.as_deref(),
96            package_name,
97            request.limit,
98            request.cursor.as_deref(),
99        )?),
100        None => None,
101    };
102    let closure = query.closure_depth.map(|depth| {
103        let seeds = closure_seeds(&ctx, &scan, imported_by, selected.as_deref());
104        consumer_closure(graph, root, &seeds, depth)
105    });
106    Ok(DependencyUsage {
107        schema_version: DependencyUsageSchemaVersion::V1,
108        confidence: UsageConfidence::Syntactic,
109        specifiers,
110        unresolved,
111        sites,
112        closure,
113    })
114}
115
116/// The sorted, deduplicated specifier filter, or `None` for all names.
117fn selected_specifiers(specifiers: &[String]) -> Option<Vec<String>> {
118    if specifiers.is_empty() {
119        return None;
120    }
121    let mut selected = specifiers.to_vec();
122    selected.sort_unstable();
123    selected.dedup();
124    Some(selected)
125}
126
127fn path_key(path: &Path) -> String {
128    path.to_string_lossy().replace('\\', "/")
129}
130
131struct UsageContext<'a> {
132    graph: &'a ModuleGraph,
133    root: &'a Path,
134    package_name: &'a str,
135    modules_by_id: FxHashMap<FileId, &'a ModuleInfo>,
136    files_by_path: FxHashMap<String, FileId>,
137}
138
139impl<'a> UsageContext<'a> {
140    fn new(
141        graph: &'a ModuleGraph,
142        modules: &'a [ModuleInfo],
143        root: &'a Path,
144        package_name: &'a str,
145    ) -> Self {
146        let modules_by_id = modules
147            .iter()
148            .map(|module| (module.file_id, module))
149            .collect();
150        let files_by_path = graph
151            .modules
152            .iter()
153            .map(|node| (relativize(&node.path, root), node.file_id))
154            .collect();
155        Self {
156            graph,
157            root,
158            package_name,
159            modules_by_id,
160            files_by_path,
161        }
162    }
163
164    fn file_id(&self, path: &str) -> Option<FileId> {
165        self.files_by_path.get(path).copied()
166    }
167
168    fn file_path(&self, id: FileId) -> Option<String> {
169        self.graph
170            .modules
171            .get(id.0 as usize)
172            .map(|node| relativize(&node.path, self.root))
173    }
174
175    /// Whether an import specifier names the package, after the same `npm:`
176    /// normalization that the resolver uses.
177    fn names_package(&self, source: &str) -> bool {
178        if let Some(rest) = source.strip_prefix("npm:") {
179            return fallow_graph::resolve::extract_package_name(
180                &fallow_graph::resolve::normalize_npm_specifier(rest),
181            ) == self.package_name;
182        }
183        source.starts_with(self.package_name)
184            && fallow_graph::resolve::extract_package_name(source) == self.package_name
185    }
186}
187
188/// A runtime import binding of the package in one file.
189#[derive(Clone)]
190enum BindingName {
191    /// A named or default import: the imported name.
192    Named(String),
193    /// A namespace import: the name comes from the first member.
194    Namespace,
195}
196
197impl BindingName {
198    /// The specifier and the remaining member of a use with `member_path`.
199    fn split(&self, member_path: &str) -> (String, Option<String>) {
200        match self {
201            Self::Named(name) => (name.clone(), non_empty(member_path)),
202            Self::Namespace => match member_path.split_once('.') {
203                Some((first, rest)) => (first.to_owned(), non_empty(rest)),
204                None if member_path.is_empty() => (NAMESPACE_SPECIFIER.to_owned(), None),
205                None => (member_path.to_owned(), None),
206            },
207        }
208    }
209}
210
211fn non_empty(value: &str) -> Option<String> {
212    (!value.is_empty()).then(|| value.to_owned())
213}
214
215/// What one file holds for one specifier.
216#[derive(Default)]
217struct FileSpecifier {
218    runtime_binding: bool,
219    type_binding: bool,
220}
221
222/// A wrapper found in the scan. The counts of a wrapper come from its sites.
223struct Wrapper {
224    file_id: FileId,
225    file: String,
226    export: String,
227    /// The local name of the declarator in its own module.
228    declared: String,
229    /// Whether this entry owns the uses of the declarator in its own module.
230    /// A declarator with more than one export name has one owner, so a use
231    /// counts once.
232    owns_module_uses: bool,
233    specifier: String,
234    shape: WrapperShape,
235    line: u32,
236}
237
238impl Wrapper {
239    fn via(&self) -> String {
240        format!("{}:{}", self.file, self.export)
241    }
242}
243
244/// The counts of one wrapper under one specifier.
245#[derive(Default)]
246struct WrapperCounts<'w> {
247    wrapper: Option<&'w Wrapper>,
248    calls: usize,
249    files: FxHashSet<FileId>,
250}
251
252/// One site with the file that holds it.
253struct RawSite {
254    file_id: FileId,
255    site: UsageSite,
256    /// A call wrapper definition is a real call of the package export, so it
257    /// counts in `call_site_count`.
258    counts_as_call: bool,
259}
260
261/// A site that goes through a wrapper.
262struct ViaSite<'a> {
263    kind: UsageSiteKind,
264    offset: u32,
265    local_name: &'a str,
266    member: Option<String>,
267}
268
269/// One use of a wrapper in a consumer module or in its own module.
270#[derive(Clone, Copy)]
271enum WrapperUse {
272    /// An admitted call of the wrapper binding.
273    Call,
274    /// A binding reference of this kind.
275    Reference(ImportBindingReferenceKind),
276}
277
278#[derive(Default)]
279struct Scan {
280    sites: Vec<RawSite>,
281    /// Per file and specifier: the bindings that count toward `file_count`.
282    file_specifiers: FxHashMap<(FileId, String), FileSpecifier>,
283    /// Files and specifiers with at least one site.
284    sited: FxHashSet<(FileId, String)>,
285    wrappers: Vec<Wrapper>,
286    /// Files with an import, re-export, dynamic import or `require` that
287    /// names the package.
288    attributed_files: FxHashSet<FileId>,
289}
290
291/// The fixed inputs of one module scan.
292struct ModuleScan<'m> {
293    module: &'m ModuleInfo,
294    file: String,
295    /// Runtime bindings of the package by import index.
296    bindings: FxHashMap<u32, BindingName>,
297    /// Import index by local binding name, for the runtime bindings.
298    locals: FxHashMap<&'m str, u32>,
299    /// The index of the first site of this module in `Scan::sites`.
300    first_site: usize,
301}
302
303impl ModuleScan<'_> {
304    fn position(&self, offset: u32) -> (u32, u32) {
305        byte_offset_to_line_col(&self.module.line_offsets, offset)
306    }
307}
308
309/// Fields of a site that the caller chooses.
310struct SiteSpec {
311    offset: u32,
312    specifier: Option<String>,
313    local_name: Option<String>,
314    member: Option<String>,
315    kind: UsageSiteKind,
316    via: Option<String>,
317    counts_as_call: bool,
318}
319
320impl Scan {
321    fn push_site(&mut self, scan: &ModuleScan<'_>, spec: SiteSpec) {
322        let (line, col) = scan.position(spec.offset);
323        if let Some(specifier) = &spec.specifier {
324            self.sited.insert((scan.module.file_id, specifier.clone()));
325        }
326        self.sites.push(RawSite {
327            file_id: scan.module.file_id,
328            counts_as_call: spec.counts_as_call,
329            site: UsageSite {
330                file: scan.file.clone(),
331                line,
332                col,
333                specifier: spec.specifier,
334                local_name: spec.local_name,
335                member: spec.member,
336                kind: spec.kind,
337                via: spec.via,
338            },
339        });
340    }
341
342    fn file_level_site(&mut self, scan: &ModuleScan<'_>, offset: u32, kind: UsageSiteKind) {
343        self.push_site(
344            scan,
345            SiteSpec {
346                offset,
347                specifier: None,
348                local_name: None,
349                member: None,
350                kind,
351                via: None,
352                counts_as_call: false,
353            },
354        );
355    }
356
357    fn note_binding(&mut self, file_id: FileId, specifier: &str, type_only: bool) {
358        let entry = self
359            .file_specifiers
360            .entry((file_id, specifier.to_owned()))
361            .or_default();
362        if type_only {
363            entry.type_binding = true;
364        } else {
365            entry.runtime_binding = true;
366        }
367    }
368
369    fn scan_module(&mut self, ctx: &UsageContext<'_>, module: &ModuleInfo) {
370        let Some(file) = ctx.file_path(module.file_id) else {
371            return;
372        };
373        let mut scan = ModuleScan {
374            module,
375            file,
376            bindings: FxHashMap::default(),
377            locals: FxHashMap::default(),
378            first_site: self.sites.len(),
379        };
380        self.scan_imports(ctx, &mut scan);
381        self.scan_calls(&scan);
382        let first_wrapper = self.wrappers.len();
383        self.scan_binding_references(&scan);
384        self.scan_module_wrapper_uses(&scan, first_wrapper);
385        self.scan_file_level_forms(ctx, &scan);
386        self.close_namespace_bindings(&scan);
387    }
388
389    fn scan_imports(&mut self, ctx: &UsageContext<'_>, scan: &mut ModuleScan<'_>) {
390        let file_id = scan.module.file_id;
391        for (index, import) in scan.module.imports.iter().enumerate() {
392            if !ctx.names_package(&import.source) {
393                continue;
394            }
395            self.attributed_files.insert(file_id);
396            let Ok(index) = u32::try_from(index) else {
397                break;
398            };
399            let name = match &import.imported_name {
400                ImportedName::SideEffect => {
401                    self.file_level_site(scan, import.span.start, UsageSiteKind::SideEffectImport);
402                    continue;
403                }
404                ImportedName::Named(name) => BindingName::Named(name.clone()),
405                ImportedName::Default => BindingName::Named(DEFAULT_SPECIFIER.to_owned()),
406                ImportedName::Namespace => BindingName::Namespace,
407            };
408            match &name {
409                BindingName::Named(specifier) => {
410                    let type_only = import.is_type_only
411                        || is_type_position_only(scan.module, &import.local_name);
412                    self.note_binding(file_id, specifier, type_only);
413                }
414                BindingName::Namespace
415                    if import.is_type_only
416                        || is_type_position_only(scan.module, &import.local_name) =>
417                {
418                    self.note_binding(file_id, NAMESPACE_SPECIFIER, true);
419                }
420                BindingName::Namespace => {}
421            }
422            if !import.is_type_only && !import.local_name.is_empty() {
423                scan.bindings.insert(index, name);
424                scan.locals.insert(import.local_name.as_str(), index);
425            }
426        }
427    }
428
429    /// The runtime binding of a local name.
430    fn binding_of<'s>(scan: &'s ModuleScan<'_>, local_name: &str) -> Option<&'s BindingName> {
431        scan.locals
432            .get(local_name)
433            .and_then(|index| scan.bindings.get(index))
434    }
435
436    fn scan_calls(&mut self, scan: &ModuleScan<'_>) {
437        let wrapper_calls = wrapper_initializer_calls(scan);
438        for call in scan.module.imported_call_sites.iter() {
439            let Some(binding) = Self::binding_of(scan, &call.local_name) else {
440                continue;
441            };
442            // The wrapper definition owns this offset (precedence rule).
443            if wrapper_calls.contains(&call.span_start) {
444                continue;
445            }
446            let (specifier, member) = binding.split(&call.member_path);
447            self.note_namespace_use(scan, binding, &specifier);
448            self.push_site(
449                scan,
450                SiteSpec {
451                    offset: call.span_start,
452                    specifier: Some(specifier),
453                    local_name: Some(call.local_name.clone()),
454                    member,
455                    kind: UsageSiteKind::Call,
456                    via: None,
457                    counts_as_call: false,
458                },
459            );
460        }
461    }
462
463    /// A namespace member use counts the file toward the member's
464    /// `file_count`.
465    fn note_namespace_use(&mut self, scan: &ModuleScan<'_>, binding: &BindingName, name: &str) {
466        if matches!(binding, BindingName::Namespace) {
467            self.note_binding(scan.module.file_id, name, false);
468        }
469    }
470
471    fn scan_binding_references(&mut self, scan: &ModuleScan<'_>) {
472        for reference in direct_references(scan.module) {
473            let Some(binding) = scan.bindings.get(&reference.import_index) else {
474                continue;
475            };
476            let Some(import) = scan.module.imports.get(reference.import_index as usize) else {
477                continue;
478            };
479            let (specifier, member) = binding.split(&reference.member_path);
480            let wrapper = wrapper_shape(reference, member.as_deref()).and_then(|shape| {
481                let exports = wrapper_exports(scan.module, reference);
482                (!exports.is_empty()).then_some((shape, exports))
483            });
484            let kind = match (reference.kind, &wrapper) {
485                (_, Some(_)) => UsageSiteKind::WrapperDefinition,
486                // A call site covers an initializer call that is no wrapper.
487                (ImportBindingReferenceKind::InitializerCall, None) => continue,
488                (ImportBindingReferenceKind::ValueAlias, None) => UsageSiteKind::ValueAlias,
489                (ImportBindingReferenceKind::JsxElement, None) => UsageSiteKind::JsxElement,
490                (ImportBindingReferenceKind::Other | ImportBindingReferenceKind::Call, None) => {
491                    UsageSiteKind::NonCallReference
492                }
493            };
494            let counts_as_call = matches!(&wrapper, Some((WrapperShape::Call, _)));
495            if let Some((shape, exports)) = wrapper {
496                let (line, _) = scan.position(reference.span_start);
497                let declared = reference.declared_name.as_deref().unwrap_or_default();
498                for (index, export) in exports.into_iter().enumerate() {
499                    self.wrappers.push(Wrapper {
500                        file_id: scan.module.file_id,
501                        file: scan.file.clone(),
502                        export,
503                        declared: declared.to_owned(),
504                        owns_module_uses: index == 0,
505                        specifier: specifier.clone(),
506                        shape,
507                        line,
508                    });
509                }
510            }
511            self.note_namespace_use(scan, binding, &specifier);
512            self.push_site(
513                scan,
514                SiteSpec {
515                    offset: reference.span_start,
516                    specifier: Some(specifier),
517                    local_name: Some(import.local_name.clone()),
518                    member,
519                    kind,
520                    via: None,
521                    counts_as_call,
522                },
523            );
524        }
525    }
526
527    /// Count the uses of each wrapper in the module that declares it.
528    fn scan_module_wrapper_uses(&mut self, scan: &ModuleScan<'_>, first_wrapper: usize) {
529        let wrappers = self.wrappers.split_off(first_wrapper);
530        for wrapper in wrappers.iter().filter(|wrapper| wrapper.owns_module_uses) {
531            let declared = wrapper.declared.clone();
532            let uses = scan
533                .module
534                .import_binding_references
535                .iter()
536                .filter(|reference| reference.through.as_deref() == Some(declared.as_str()));
537            for reference in uses {
538                let member = non_empty(&reference.member_path);
539                let Some(kind) = via_site_kind(
540                    wrapper.shape,
541                    WrapperUse::Reference(reference.kind),
542                    member.as_deref(),
543                    exports_declared_value(scan.module, reference),
544                    false,
545                ) else {
546                    continue;
547                };
548                self.push_via_site(
549                    scan,
550                    wrapper,
551                    ViaSite {
552                        kind,
553                        offset: reference.span_start,
554                        local_name: &declared,
555                        member,
556                    },
557                );
558            }
559        }
560        self.wrappers.extend(wrappers);
561    }
562
563    /// Push a site that goes through `wrapper`.
564    fn push_via_site(&mut self, scan: &ModuleScan<'_>, wrapper: &Wrapper, site: ViaSite<'_>) {
565        let ViaSite {
566            kind,
567            offset,
568            local_name,
569            member,
570        } = site;
571        // A wrapper of a whole namespace takes its specifier from the member.
572        let (specifier, member) = if wrapper.specifier == NAMESPACE_SPECIFIER {
573            BindingName::Namespace.split(member.as_deref().unwrap_or_default())
574        } else {
575            (wrapper.specifier.clone(), member)
576        };
577        self.push_site(
578            scan,
579            SiteSpec {
580                offset,
581                specifier: Some(specifier),
582                local_name: Some(local_name.to_owned()),
583                member,
584                kind,
585                via: Some(wrapper.via()),
586                counts_as_call: false,
587            },
588        );
589    }
590
591    fn scan_file_level_forms(&mut self, ctx: &UsageContext<'_>, scan: &ModuleScan<'_>) {
592        let file_id = scan.module.file_id;
593        for re_export in &scan.module.re_exports {
594            if !ctx.names_package(&re_export.source) {
595                continue;
596            }
597            self.attributed_files.insert(file_id);
598            if re_export.imported_name == NAMESPACE_SPECIFIER {
599                self.file_level_site(scan, re_export.span.start, UsageSiteKind::StarReExport);
600                continue;
601            }
602            self.note_binding(file_id, &re_export.imported_name, re_export.is_type_only);
603            if re_export.is_type_only {
604                continue;
605            }
606            self.push_site(
607                scan,
608                SiteSpec {
609                    offset: re_export.span.start,
610                    specifier: Some(re_export.imported_name.clone()),
611                    local_name: Some(re_export.imported_name.clone()),
612                    member: None,
613                    kind: UsageSiteKind::ReExport,
614                    via: None,
615                    counts_as_call: false,
616                },
617            );
618        }
619        for dynamic in &scan.module.dynamic_imports {
620            if dynamic.is_speculative || !ctx.names_package(&dynamic.source) {
621                continue;
622            }
623            self.attributed_files.insert(file_id);
624            self.file_level_site(scan, dynamic.span.start, UsageSiteKind::DynamicImport);
625        }
626        for require in &scan.module.require_calls {
627            if !ctx.names_package(&require.source) {
628                continue;
629            }
630            self.attributed_files.insert(file_id);
631            self.file_level_site(scan, require.span.start, UsageSiteKind::Require);
632        }
633    }
634
635    /// A runtime namespace binding with no member use is a bare binding of
636    /// `*`, so it counts toward `binding_without_site`.
637    fn close_namespace_bindings(&mut self, scan: &ModuleScan<'_>) {
638        let file_id = scan.module.file_id;
639        for (index, binding) in &scan.bindings {
640            if !matches!(binding, BindingName::Namespace) {
641                continue;
642            }
643            let Some(import) = scan.module.imports.get(*index as usize) else {
644                continue;
645            };
646            if is_type_position_only(scan.module, &import.local_name) {
647                continue;
648            }
649            let used = self
650                .sites
651                .get(scan.first_site..)
652                .unwrap_or_default()
653                .iter()
654                .any(|raw| raw.site.local_name.as_deref() == Some(import.local_name.as_str()));
655            if !used {
656                self.note_binding(file_id, NAMESPACE_SPECIFIER, false);
657            }
658        }
659    }
660
661    /// Follow one hop from each wrapper to the files that import it.
662    fn follow_wrappers(&mut self, ctx: &UsageContext<'_>) {
663        let wrappers = std::mem::take(&mut self.wrappers);
664        let star_owners = star_re_export_owners(&wrappers);
665        for (index, wrapper) in wrappers.iter().enumerate() {
666            for (consumer_id, local_name, prefix) in wrapper_consumers(ctx.graph, wrapper) {
667                let Some(consumer) = ctx.modules_by_id.get(&consumer_id) else {
668                    continue;
669                };
670                let Some(file) = ctx.file_path(consumer_id) else {
671                    continue;
672                };
673                let scan = ModuleScan {
674                    module: consumer,
675                    file,
676                    bindings: FxHashMap::default(),
677                    locals: FxHashMap::default(),
678                    first_site: self.sites.len(),
679                };
680                self.scan_consumer(&scan, wrapper, &local_name, prefix.as_deref());
681            }
682            self.scan_barrels(ctx, wrapper, star_owners.contains(&index));
683        }
684        self.wrappers = wrappers;
685    }
686
687    /// A project re-export of a wrapper is one `re_export` site through the
688    /// wrapper. The consumers of the barrel are not followed. An `export *`
689    /// statement gives one site for each declarator, so only the entry that
690    /// `owns_star` emits it.
691    fn scan_barrels(&mut self, ctx: &UsageContext<'_>, wrapper: &Wrapper, owns_star: bool) {
692        let mut importers: Vec<FileId> = ctx.graph.importers_of(wrapper.file_id).to_vec();
693        importers.sort_unstable_by_key(|id| id.0);
694        importers.dedup();
695        for importer in importers {
696            let (Some(node), Some(module), Some(file)) = (
697                ctx.graph.modules.get(importer.0 as usize),
698                ctx.modules_by_id.get(&importer),
699                ctx.file_path(importer),
700            ) else {
701                continue;
702            };
703            let scan = ModuleScan {
704                module,
705                file,
706                bindings: FxHashMap::default(),
707                locals: FxHashMap::default(),
708                first_site: self.sites.len(),
709            };
710            for re_export in &node.re_exports {
711                let names_wrapper = re_export.imported_name == wrapper.export
712                    || (re_export.imported_name == NAMESPACE_SPECIFIER && owns_star);
713                // The graph adds re-exports with an empty span for `export *`
714                // chains. Only a written re-export is a site.
715                let written = !(re_export.span.start == 0 && re_export.span.end == 0);
716                if re_export.source_file != wrapper.file_id
717                    || re_export.is_type_only
718                    || !names_wrapper
719                    || !written
720                {
721                    continue;
722                }
723                self.push_site(
724                    &scan,
725                    SiteSpec {
726                        offset: re_export.span.start,
727                        specifier: Some(wrapper.specifier.clone()),
728                        local_name: Some(wrapper.export.clone()),
729                        member: None,
730                        kind: UsageSiteKind::ReExport,
731                        via: Some(wrapper.via()),
732                        counts_as_call: false,
733                    },
734                );
735            }
736        }
737    }
738
739    fn scan_consumer(
740        &mut self,
741        scan: &ModuleScan<'_>,
742        wrapper: &Wrapper,
743        local_name: &str,
744        prefix: Option<&str>,
745    ) {
746        let indexes: FxHashSet<u32> = scan
747            .module
748            .imports
749            .iter()
750            .enumerate()
751            .filter(|(_, import)| import.local_name == local_name && !import.is_type_only)
752            .filter_map(|(index, _)| u32::try_from(index).ok())
753            .collect();
754        let references: Vec<(&ImportBindingReference, Option<String>)> =
755            direct_references(scan.module)
756                .filter(|reference| indexes.contains(&reference.import_index))
757                .filter_map(
758                    |reference| match match_member_prefix(&reference.member_path, prefix) {
759                        PrefixMatch::Member(member) => Some((reference, member)),
760                        PrefixMatch::Outside => None,
761                    },
762                )
763                .collect();
764        // The binding reference owns the offset of an initializer call that
765        // it reports itself.
766        let owned_calls: FxHashSet<u32> = references
767            .iter()
768            .filter(|(reference, member)| {
769                reference.kind == ImportBindingReferenceKind::InitializerCall
770                    && via_site_kind(
771                        wrapper.shape,
772                        WrapperUse::Reference(reference.kind),
773                        member.as_deref(),
774                        exports_declared_value(scan.module, reference),
775                        true,
776                    )
777                    .is_some()
778            })
779            .map(|(reference, _)| reference.span_start)
780            .collect();
781        for call in scan.module.imported_call_sites.iter() {
782            if call.local_name != local_name || owned_calls.contains(&call.span_start) {
783                continue;
784            }
785            let PrefixMatch::Member(member) = match_member_prefix(&call.member_path, prefix) else {
786                continue;
787            };
788            let Some(kind) = via_site_kind(
789                wrapper.shape,
790                WrapperUse::Call,
791                member.as_deref(),
792                false,
793                true,
794            ) else {
795                continue;
796            };
797            self.push_via_site(
798                scan,
799                wrapper,
800                ViaSite {
801                    kind,
802                    offset: call.span_start,
803                    local_name,
804                    member,
805                },
806            );
807        }
808        for (reference, member) in references {
809            let Some(kind) = via_site_kind(
810                wrapper.shape,
811                WrapperUse::Reference(reference.kind),
812                member.as_deref(),
813                exports_declared_value(scan.module, reference),
814                true,
815            ) else {
816                continue;
817            };
818            self.push_via_site(
819                scan,
820                wrapper,
821                ViaSite {
822                    kind,
823                    offset: reference.span_start,
824                    local_name,
825                    member,
826                },
827            );
828        }
829    }
830
831    /// The counts of each wrapper per specifier, built from the sites. A
832    /// wrapper has an entry under its own specifier and under each specifier
833    /// that a site through it carries. Thus a wrapper of a whole namespace
834    /// shows under each member name that its consumers use.
835    fn wrapper_counts(&self) -> FxHashMap<(&str, String), WrapperCounts<'_>> {
836        let by_via: FxHashMap<String, &Wrapper> = self
837            .wrappers
838            .iter()
839            .map(|wrapper| (wrapper.via(), wrapper))
840            .collect();
841        let mut counts: FxHashMap<(&str, String), WrapperCounts<'_>> = FxHashMap::default();
842        for wrapper in &self.wrappers {
843            counts
844                .entry((wrapper.specifier.as_str(), wrapper.via()))
845                .or_default()
846                .wrapper = Some(wrapper);
847        }
848        for raw in &self.sites {
849            let (Some(name), Some(via)) = (raw.site.specifier.as_deref(), &raw.site.via) else {
850                continue;
851            };
852            let entry = counts.entry((name, via.clone())).or_default();
853            entry.wrapper = by_via.get(via).copied();
854            if raw.site.kind == UsageSiteKind::Call {
855                entry.calls += 1;
856                entry.files.insert(raw.file_id);
857            }
858        }
859        counts
860    }
861
862    fn file_level_unresolved(&self) -> FileLevelUnresolved {
863        let mut unresolved = FileLevelUnresolved::default();
864        for raw in &self.sites {
865            match raw.site.kind {
866                UsageSiteKind::DynamicImport => unresolved.dynamic_import += 1,
867                UsageSiteKind::Require => unresolved.require += 1,
868                UsageSiteKind::SideEffectImport => unresolved.side_effect_import += 1,
869                UsageSiteKind::StarReExport => unresolved.star_re_export += 1,
870                _ => {}
871            }
872        }
873        unresolved
874    }
875
876    fn specifier_usages(&self, selected: Option<&[String]>) -> Vec<SpecifierUsage> {
877        let mut usages: FxHashMap<String, SpecifierUsage> = FxHashMap::default();
878        let entry = |usages: &mut FxHashMap<String, SpecifierUsage>, name: &str| {
879            if usages.contains_key(name) {
880                return;
881            }
882            usages.insert(name.to_owned(), empty_specifier_usage(name));
883        };
884        if let Some(selected) = selected {
885            for name in selected {
886                entry(&mut usages, name);
887            }
888        }
889        let wanted = |name: &str| selected.is_none_or(|names| names.iter().any(|n| n == name));
890        for ((file_id, name), state) in &self.file_specifiers {
891            if !wanted(name) {
892                continue;
893            }
894            entry(&mut usages, name);
895            let Some(usage) = usages.get_mut(name) else {
896                continue;
897            };
898            usage.file_count += 1;
899            if state.type_binding && !state.runtime_binding {
900                usage.type_only_file_count += 1;
901            }
902            if state.runtime_binding && !self.sited.contains(&(*file_id, name.clone())) {
903                usage.unresolved.binding_without_site += 1;
904            }
905        }
906        for raw in &self.sites {
907            let Some(name) = raw.site.specifier.as_deref() else {
908                continue;
909            };
910            if !wanted(name) {
911                continue;
912            }
913            entry(&mut usages, name);
914            if let Some(usage) = usages.get_mut(name) {
915                count_site(usage, raw);
916            }
917        }
918        for ((name, _), counts) in self.wrapper_counts() {
919            let (Some(usage), Some(wrapper)) = (usages.get_mut(name), counts.wrapper) else {
920                continue;
921            };
922            usage.wrappers.push(UsageWrapper {
923                file: wrapper.file.clone(),
924                export: wrapper.export.clone(),
925                shape: wrapper.shape,
926                line: wrapper.line,
927                consumer_file_count: counts.files.len(),
928                call_site_count: counts.calls,
929            });
930        }
931        let mut usages: Vec<SpecifierUsage> = usages.into_values().collect();
932        for usage in &mut usages {
933            usage
934                .wrappers
935                .sort_by(|a, b| a.file.cmp(&b.file).then_with(|| a.export.cmp(&b.export)));
936        }
937        usages.sort_by(|a, b| a.name.cmp(&b.name));
938        usages
939    }
940}
941
942/// Whether a value import binding is read only in type positions, for example
943/// `import { PayloadAction } from "pkg"` used as `action: PayloadAction<T>`.
944/// TypeScript erases such a binding, so it counts as a type-only binding. A
945/// binding with no reference at all (a template use in Vue, Svelte or Astro)
946/// stays a runtime binding.
947fn is_type_position_only(module: &ModuleInfo, local_name: &str) -> bool {
948    let has = |names: &[String]| names.iter().any(|name| name == local_name);
949    has(&module.type_referenced_import_bindings) && !has(&module.value_referenced_import_bindings)
950}
951
952fn empty_specifier_usage(name: &str) -> SpecifierUsage {
953    SpecifierUsage {
954        name: name.to_owned(),
955        file_count: 0,
956        type_only_file_count: 0,
957        call_site_count: 0,
958        unresolved: SpecifierUnresolved::default(),
959        wrappers: Vec::new(),
960    }
961}
962
963fn count_site(usage: &mut SpecifierUsage, raw: &RawSite) {
964    let site = &raw.site;
965    let unresolved = &mut usage.unresolved;
966    match site.kind {
967        UsageSiteKind::Call if site.via.is_none() => usage.call_site_count += 1,
968        UsageSiteKind::WrapperDefinition if raw.counts_as_call => usage.call_site_count += 1,
969        UsageSiteKind::ValueAlias => unresolved.value_alias += 1,
970        UsageSiteKind::NonCallReference => unresolved.non_call_reference += 1,
971        UsageSiteKind::JsxElement => unresolved.jsx_element += 1,
972        UsageSiteKind::ReExport => unresolved.re_export += 1,
973        UsageSiteKind::NestedWrapper => unresolved.nested_wrapper += 1,
974        _ => {}
975    }
976}
977
978/// The index of the wrapper entry that emits the site of an `export *`
979/// statement, one entry for each declarator. A default export is not part of
980/// `export *`, so the owner is the first entry with another export name.
981fn star_re_export_owners(wrappers: &[Wrapper]) -> FxHashSet<usize> {
982    let mut seen: FxHashSet<(FileId, &str)> = FxHashSet::default();
983    wrappers
984        .iter()
985        .enumerate()
986        .filter(|(_, wrapper)| wrapper.export != DEFAULT_SPECIFIER)
987        .filter(|(_, wrapper)| seen.insert((wrapper.file_id, wrapper.declared.as_str())))
988        .map(|(index, _)| index)
989        .collect()
990}
991
992/// The start offsets of the initializer calls that define a wrapper.
993fn wrapper_initializer_calls(scan: &ModuleScan<'_>) -> FxHashSet<u32> {
994    direct_references(scan.module)
995        .filter(|reference| {
996            reference.kind == ImportBindingReferenceKind::InitializerCall
997                && scan.bindings.contains_key(&reference.import_index)
998                && exports_declared_value(scan.module, reference)
999        })
1000        .map(|reference| reference.span_start)
1001        .collect()
1002}
1003
1004/// The wrapper shape that a reference can define. An alias wrapper is the
1005/// bare binding or one static namespace member.
1006const fn wrapper_shape(
1007    reference: &ImportBindingReference,
1008    member: Option<&str>,
1009) -> Option<WrapperShape> {
1010    match reference.kind {
1011        ImportBindingReferenceKind::InitializerCall => Some(WrapperShape::Call),
1012        ImportBindingReferenceKind::ValueAlias if member.is_none() => Some(WrapperShape::Alias),
1013        _ => None,
1014    }
1015}
1016
1017/// The references to an import binding itself, without the uses of a
1018/// wrapper in its own module.
1019fn direct_references(module: &ModuleInfo) -> impl Iterator<Item = &ImportBindingReference> {
1020    module
1021        .import_binding_references
1022        .iter()
1023        .filter(|reference| reference.through.is_none())
1024}
1025
1026/// The exported names of the top-level declarator that `reference`
1027/// initializes, when the module exports it as a value. One declarator can
1028/// have more than one export name: `export { x as a, x as b }`.
1029fn wrapper_exports(module: &ModuleInfo, reference: &ImportBindingReference) -> Vec<String> {
1030    let Some(declared) = reference.declared_name.as_deref() else {
1031        return Vec::new();
1032    };
1033    module
1034        .exports
1035        .iter()
1036        .filter(|export| {
1037            !export.is_type_only
1038                && match &export.local_name {
1039                    Some(local) => local == declared,
1040                    None => export.name.matches_str(declared),
1041                }
1042        })
1043        .map(|export| match &export.name {
1044            ExportName::Named(name) => name.clone(),
1045            ExportName::Default => DEFAULT_SPECIFIER.to_owned(),
1046        })
1047        .collect()
1048}
1049
1050fn exports_declared_value(module: &ModuleInfo, reference: &ImportBindingReference) -> bool {
1051    !wrapper_exports(module, reference).is_empty()
1052}
1053
1054/// The site kind of one use of a wrapper, or `None` when another record owns
1055/// the use.
1056///
1057/// `member` is the static member after the wrapper. A call wrapper returns a
1058/// value, so a use of a member of that value (`store.dispatch()`) is no call
1059/// of the package export. `has_call_entry` is true in a consumer module,
1060/// where an admitted call site records each call of the binding.
1061fn via_site_kind(
1062    shape: WrapperShape,
1063    wrapper_use: WrapperUse,
1064    member: Option<&str>,
1065    exported: bool,
1066    has_call_entry: bool,
1067) -> Option<UsageSiteKind> {
1068    let reads_result_member = shape == WrapperShape::Call && member.is_some();
1069    let call_kind = if reads_result_member {
1070        UsageSiteKind::NonCallReference
1071    } else {
1072        UsageSiteKind::Call
1073    };
1074    let kind = match wrapper_use {
1075        WrapperUse::Call | WrapperUse::Reference(ImportBindingReferenceKind::Call) => call_kind,
1076        WrapperUse::Reference(ImportBindingReferenceKind::InitializerCall) => {
1077            if exported && !reads_result_member {
1078                UsageSiteKind::NestedWrapper
1079            } else if has_call_entry {
1080                return None;
1081            } else {
1082                call_kind
1083            }
1084        }
1085        WrapperUse::Reference(ImportBindingReferenceKind::ValueAlias) => {
1086            if exported && !reads_result_member {
1087                UsageSiteKind::NestedWrapper
1088            } else {
1089                UsageSiteKind::ValueAlias
1090            }
1091        }
1092        WrapperUse::Reference(ImportBindingReferenceKind::JsxElement) => UsageSiteKind::JsxElement,
1093        WrapperUse::Reference(ImportBindingReferenceKind::Other) => UsageSiteKind::NonCallReference,
1094    };
1095    Some(kind)
1096}
1097
1098/// How a member path relates to a required namespace prefix.
1099enum PrefixMatch {
1100    /// The path does not start with the prefix.
1101    Outside,
1102    /// The path starts with the prefix, with this member after it.
1103    Member(Option<String>),
1104}
1105
1106/// Match a member path against the namespace prefix that names a wrapper.
1107fn match_member_prefix(member_path: &str, prefix: Option<&str>) -> PrefixMatch {
1108    let Some(prefix) = prefix else {
1109        return PrefixMatch::Member(non_empty(member_path));
1110    };
1111    if member_path == prefix {
1112        return PrefixMatch::Member(None);
1113    }
1114    member_path
1115        .strip_prefix(prefix)
1116        .and_then(|rest| rest.strip_prefix('.'))
1117        .map_or(PrefixMatch::Outside, |rest| {
1118            PrefixMatch::Member(non_empty(rest))
1119        })
1120}
1121
1122/// The files that import a wrapper, with the local binding and, for a
1123/// namespace import, the member that names the wrapper.
1124fn wrapper_consumers(
1125    graph: &ModuleGraph,
1126    wrapper: &Wrapper,
1127) -> Vec<(FileId, String, Option<String>)> {
1128    let mut consumers = Vec::new();
1129    let mut seen: FxHashSet<(FileId, &str)> = FxHashSet::default();
1130    for &importer in graph.importers_of(wrapper.file_id) {
1131        for (target, symbols) in graph.outgoing_symbol_edges(importer) {
1132            if target != wrapper.file_id {
1133                continue;
1134            }
1135            for symbol in symbols {
1136                if symbol.is_type_only {
1137                    continue;
1138                }
1139                let prefix = match &symbol.imported_name {
1140                    ImportedName::Named(name) if *name == wrapper.export => None,
1141                    ImportedName::Default if wrapper.export == DEFAULT_SPECIFIER => None,
1142                    ImportedName::Namespace => Some(wrapper.export.clone()),
1143                    _ => continue,
1144                };
1145                if seen.insert((importer, symbol.local_name.as_str())) {
1146                    consumers.push((importer, symbol.local_name.clone(), prefix));
1147                }
1148            }
1149        }
1150    }
1151    consumers
1152}
1153
1154/// The sort key of a site: `file`, `line`, `col`, kind, `specifier`, `via`,
1155/// `local_name`, `member`. Two sites at one position can differ only in the
1156/// last two fields, so the key holds every field to keep pages exact.
1157type SiteKey<'a> = (
1158    &'a str,
1159    u32,
1160    u32,
1161    UsageSiteKind,
1162    &'a str,
1163    &'a str,
1164    &'a str,
1165    &'a str,
1166);
1167
1168fn site_key(site: &UsageSite) -> SiteKey<'_> {
1169    (
1170        site.file.as_str(),
1171        site.line,
1172        site.col,
1173        site.kind,
1174        site.specifier.as_deref().unwrap_or(""),
1175        site.via.as_deref().unwrap_or(""),
1176        site.local_name.as_deref().unwrap_or(""),
1177        site.member.as_deref().unwrap_or(""),
1178    )
1179}
1180
1181/// A decoded cursor: the key of the last item of the previous page.
1182struct CursorKey {
1183    file: String,
1184    line: u32,
1185    col: u32,
1186    kind: UsageSiteKind,
1187    specifier: String,
1188    via: String,
1189    local_name: String,
1190    member: String,
1191}
1192
1193impl CursorKey {
1194    fn key(&self) -> SiteKey<'_> {
1195        (
1196            self.file.as_str(),
1197            self.line,
1198            self.col,
1199            self.kind,
1200            self.specifier.as_str(),
1201            self.via.as_str(),
1202            self.local_name.as_str(),
1203            self.member.as_str(),
1204        )
1205    }
1206}
1207
1208fn site_page(
1209    sites: &[RawSite],
1210    selected: Option<&[String]>,
1211    package_name: &str,
1212    limit: u16,
1213    cursor: Option<&str>,
1214) -> Result<UsageSitePage, UsageError> {
1215    let hash = query_hash(package_name, selected.unwrap_or(&[]));
1216    let mut matching: Vec<&UsageSite> = sites
1217        .iter()
1218        .map(|raw| &raw.site)
1219        .filter(|site| match selected {
1220            None => true,
1221            Some(names) => site
1222                .specifier
1223                .as_deref()
1224                .is_some_and(|name| names.iter().any(|n| n == name)),
1225        })
1226        .collect();
1227    matching.sort_by(|a, b| site_key(a).cmp(&site_key(b)));
1228    let total = matching.len();
1229    let start = match cursor {
1230        Some(token) => {
1231            let after = decode_cursor(token, hash)?;
1232            let after = after.key();
1233            matching.partition_point(|site| site_key(site) <= after)
1234        }
1235        None => 0,
1236    };
1237    let end = start.saturating_add(usize::from(limit)).min(total);
1238    let items: Vec<UsageSite> = matching
1239        .get(start..end)
1240        .unwrap_or_default()
1241        .iter()
1242        .map(|site| (*site).clone())
1243        .collect();
1244    let next_cursor = (end < total)
1245        .then(|| items.last().map(|last| encode_cursor(hash, last)))
1246        .flatten();
1247    Ok(UsageSitePage {
1248        items,
1249        total,
1250        limit,
1251        next_cursor,
1252    })
1253}
1254
1255/// A 64-bit FNV-1a hash of the package name and the sorted specifier filter.
1256fn query_hash(package_name: &str, selected: &[String]) -> u64 {
1257    let mut hash = FNV_OFFSET_BASIS;
1258    let mut feed = |bytes: &[u8]| {
1259        for byte in bytes {
1260            hash ^= u64::from(*byte);
1261            hash = hash.wrapping_mul(FNV_PRIME);
1262        }
1263    };
1264    feed(package_name.as_bytes());
1265    for name in selected {
1266        feed(&[0]);
1267        feed(name.as_bytes());
1268    }
1269    hash
1270}
1271
1272fn encode_cursor(hash: u64, site: &UsageSite) -> String {
1273    let payload = [
1274        format!("{hash:016x}"),
1275        site.file.clone(),
1276        site.line.to_string(),
1277        site.col.to_string(),
1278        site.kind.as_str().to_owned(),
1279        site.specifier.clone().unwrap_or_default(),
1280        site.via.clone().unwrap_or_default(),
1281        site.local_name.clone().unwrap_or_default(),
1282        site.member.clone().unwrap_or_default(),
1283    ]
1284    .join(&CURSOR_SEPARATOR.to_string());
1285    let mut token = String::with_capacity(CURSOR_PREFIX.len() + payload.len() * 2);
1286    token.push_str(CURSOR_PREFIX);
1287    for byte in payload.as_bytes() {
1288        let _ = write!(token, "{byte:02x}");
1289    }
1290    token
1291}
1292
1293fn decode_cursor(token: &str, expected_hash: u64) -> Result<CursorKey, UsageError> {
1294    let hex = token
1295        .strip_prefix(CURSOR_PREFIX)
1296        .ok_or(UsageError::InvalidCursor)?;
1297    if hex.len() % 2 != 0 {
1298        return Err(UsageError::InvalidCursor);
1299    }
1300    let bytes = (0..hex.len())
1301        .step_by(2)
1302        .map(|index| {
1303            hex.get(index..index + 2)
1304                .and_then(|pair| u8::from_str_radix(pair, 16).ok())
1305        })
1306        .collect::<Option<Vec<u8>>>()
1307        .ok_or(UsageError::InvalidCursor)?;
1308    let payload = String::from_utf8(bytes).map_err(|_| UsageError::InvalidCursor)?;
1309    let fields: Vec<&str> = payload.split(CURSOR_SEPARATOR).collect();
1310    let [
1311        hash,
1312        file,
1313        line,
1314        col,
1315        kind,
1316        specifier,
1317        via,
1318        local_name,
1319        member,
1320    ] = fields.as_slice()
1321    else {
1322        return Err(UsageError::InvalidCursor);
1323    };
1324    if u64::from_str_radix(hash, 16).ok() != Some(expected_hash) {
1325        return Err(UsageError::InvalidCursor);
1326    }
1327    Ok(CursorKey {
1328        file: (*file).to_owned(),
1329        line: line.parse().map_err(|_| UsageError::InvalidCursor)?,
1330        col: col.parse().map_err(|_| UsageError::InvalidCursor)?,
1331        kind: site_kind_from_str(kind).ok_or(UsageError::InvalidCursor)?,
1332        specifier: (*specifier).to_owned(),
1333        via: (*via).to_owned(),
1334        local_name: (*local_name).to_owned(),
1335        member: (*member).to_owned(),
1336    })
1337}
1338
1339fn site_kind_from_str(value: &str) -> Option<UsageSiteKind> {
1340    [
1341        UsageSiteKind::Call,
1342        UsageSiteKind::WrapperDefinition,
1343        UsageSiteKind::ValueAlias,
1344        UsageSiteKind::NonCallReference,
1345        UsageSiteKind::JsxElement,
1346        UsageSiteKind::ReExport,
1347        UsageSiteKind::NestedWrapper,
1348        UsageSiteKind::DynamicImport,
1349        UsageSiteKind::Require,
1350        UsageSiteKind::SideEffectImport,
1351        UsageSiteKind::StarReExport,
1352    ]
1353    .into_iter()
1354    .find(|kind| kind.as_str() == value)
1355}
1356
1357/// The seed files of the closure: the importers of the package, or with a
1358/// specifier filter, the files with a site of a selected name.
1359fn closure_seeds(
1360    ctx: &UsageContext<'_>,
1361    scan: &Scan,
1362    imported_by: &[PathBuf],
1363    selected: Option<&[String]>,
1364) -> FxHashSet<FileId> {
1365    match selected {
1366        None => imported_by
1367            .iter()
1368            .filter_map(|path| ctx.file_id(&path_key(path)))
1369            .collect(),
1370        Some(names) => scan
1371            .sites
1372            .iter()
1373            .filter(|raw| {
1374                raw.site
1375                    .specifier
1376                    .as_deref()
1377                    .is_some_and(|name| names.iter().any(|n| n == name))
1378            })
1379            .map(|raw| raw.file_id)
1380            .collect(),
1381    }
1382}
1383
1384/// Walk the importers of the seeds up to `depth` edges. The seeds are not
1385/// listed.
1386fn consumer_closure(
1387    graph: &ModuleGraph,
1388    root: &Path,
1389    seeds: &FxHashSet<FileId>,
1390    depth: u32,
1391) -> ConsumerClosure {
1392    let mut visited = seeds.clone();
1393    let mut frontier: Vec<FileId> = seeds.iter().copied().collect();
1394    frontier.sort_unstable_by_key(|id| id.0);
1395    let mut found: Vec<(FileId, u32)> = Vec::new();
1396    let mut reached = 0;
1397    for level in 1..=depth {
1398        let mut next = Vec::new();
1399        for file in &frontier {
1400            for &importer in graph.importers_of(*file) {
1401                if visited.insert(importer) {
1402                    next.push(importer);
1403                    found.push((importer, level));
1404                }
1405            }
1406        }
1407        frontier = next;
1408        if frontier.is_empty() {
1409            break;
1410        }
1411        reached = level;
1412    }
1413    let truncated = reached == depth
1414        && frontier.iter().any(|file| {
1415            graph
1416                .importers_of(*file)
1417                .iter()
1418                .any(|importer| !visited.contains(importer))
1419        });
1420    let mut files: Vec<ClosureFile> = found
1421        .into_iter()
1422        .filter_map(|(id, level)| {
1423            graph.modules.get(id.0 as usize).map(|node| ClosureFile {
1424                file: relativize(&node.path, root),
1425                depth: level,
1426            })
1427        })
1428        .collect();
1429    files.sort_by(|a, b| a.depth.cmp(&b.depth).then_with(|| a.file.cmp(&b.file)));
1430    ConsumerClosure {
1431        depth,
1432        file_count: files.len(),
1433        files,
1434        truncated,
1435    }
1436}
1437
1438#[cfg(test)]
1439#[path = "trace_usage_impl_tests.rs"]
1440mod tests;