fallow-core 3.5.1

Internal detector backend for fallow-engine and fallow-api
Documentation
use super::*;

/// Credit member accesses produced by static-factory call bindings on the
/// originating class export.
pub(super) fn propagate_factory_call_accesses(
    graph: &ModuleGraph,
    resolved_modules: &[ResolvedModule],
    indexes: &MemberPassIndexes<'_>,
    accessed_members: &mut FxHashMap<ExportKey, FxHashSet<String>>,
) {
    for resolved in resolved_modules {
        let local_to_export_keys = indexes.local_keys(resolved.file_id);
        for access in factory_call_member_accesses(resolved) {
            let Some(seed_keys) = local_to_export_keys.get(access.callee_object.as_str()) else {
                continue;
            };
            for seed_key in seed_keys {
                for origin in
                    walk_re_export_origins(graph, seed_key.file_id, seed_key.export_name.as_str())
                {
                    let Some(origin_module) = indexes.module_by_id.get(&origin.file_id) else {
                        continue;
                    };
                    let matches_factory = origin_module.exports.iter().any(|export| {
                        export.name.matches_str(origin.export_name.as_str())
                            && export.members.iter().any(|member| {
                                member.is_instance_returning_static
                                    && member.kind == MemberKind::ClassMethod
                                    && member.name == access.callee_method
                            })
                    });
                    if !matches_factory {
                        continue;
                    }
                    accessed_members
                        .entry(origin)
                        .or_default()
                        .insert(access.member.clone());
                }
            }
        }
    }
}

pub(super) struct FactoryReturnCreditContext<'a, 'ctx> {
    pub(super) graph: &'ctx ModuleGraph,
    pub(super) indexes: &'ctx MemberPassIndexes<'a>,
    pub(super) accessed_members: &'ctx mut FxHashMap<ExportKey, FxHashSet<String>>,
}

pub(super) fn credit_factory_return_class_member(
    context: &mut FactoryReturnCreditContext<'_, '_>,
    factory_origin_file_id: FileId,
    class_local_name: &str,
    member: &str,
) {
    for class_origin in factory_return_class_origins(
        context.graph,
        context.indexes,
        factory_origin_file_id,
        class_local_name,
    ) {
        context
            .accessed_members
            .entry(class_origin)
            .or_default()
            .insert(member.to_string());
    }
}

/// Credit member accesses produced by cross-module free-function factory
/// bindings (`const x = importedFactory(); x.member`) onto the class the factory
/// returns. Each link in the resolution chain is also an over-credit guard, and
/// a wrong credit is a silent false-negative, so every link must hold:
///
///   1. the fact's callee resolves through the consumer's imports/exports to an
///      export key (`local_to_export_keys`);
///   2. that key walks (re-export aware) to an origin module that actually
///      declares an `exported_factory_returns` entry for the export, i.e. an
///      internal exported factory proven to return a single class;
///   3. the entry's `class_local_name` resolves through THAT factory module's own
///      imports/exports to a class export;
///   4. the resolved export is a class with members.
///
/// See issue #1441 (Part A).
pub(super) fn propagate_factory_fn_accesses(
    graph: &ModuleGraph,
    resolved_modules: &[ResolvedModule],
    indexes: &MemberPassIndexes<'_>,
    accessed_members: &mut FxHashMap<ExportKey, FxHashSet<String>>,
) {
    for resolved in resolved_modules {
        let local_to_export_keys = indexes.local_keys(resolved.file_id);
        for access in factory_fn_member_accesses(resolved) {
            let Some(seed_keys) = local_to_export_keys.get(access.callee_name.as_str()) else {
                continue;
            };
            let classes = seed_keys
                .iter()
                .flat_map(|seed_key| factory_return_classes_for_callee(graph, indexes, seed_key));
            for class_origin in classes {
                accessed_members
                    .entry(class_origin)
                    .or_default()
                    .insert(access.member.clone());
            }
        }
    }
}

/// Credit member accesses produced by cross-module OBJECT-LITERAL factory bindings
/// (`const ui = importedFactory(); ui.orders.member`) onto the class the factory's
/// returned object literal exposes at that property path. Mirrors
/// `propagate_factory_fn_accesses` with an extra property-path lookup dimension;
/// every link (callee resolves to an export, the export's factory module declares
/// an object shape with that path, the path's class resolves to a class-with-members
/// export) is an over-credit gate, so a wrong or unresolvable chain is a silent
/// false-negative, never a false positive. See issue #1858.
pub(super) fn propagate_factory_return_object_accesses(
    graph: &ModuleGraph,
    resolved_modules: &[ResolvedModule],
    indexes: &MemberPassIndexes<'_>,
    accessed_members: &mut FxHashMap<ExportKey, FxHashSet<String>>,
) {
    for resolved in resolved_modules {
        let local_to_export_keys = indexes.local_keys(resolved.file_id);
        for access in factory_return_object_property_accesses(resolved) {
            let Some(seed_keys) = local_to_export_keys.get(access.callee_name.as_str()) else {
                continue;
            };
            let classes = seed_keys.iter().flat_map(|seed_key| {
                factory_return_object_property_classes(
                    graph,
                    indexes,
                    seed_key,
                    access.property_path.as_str(),
                )
            });
            let mut context = FactoryReturnCreditContext {
                graph,
                indexes,
                accessed_members,
            };
            for (factory_origin_file_id, class_local_name) in classes {
                credit_factory_return_class_member(
                    &mut context,
                    factory_origin_file_id,
                    &class_local_name,
                    access.member.as_str(),
                );
            }
        }
    }
}

/// The `(factory-module, class-local-name)` pairs a callee's exported object-literal
/// factory exposes at `property_path`, resolved across re-export barrels. Empty for
/// any callee that is not an internal exported object-factory declaring that path.
fn factory_return_object_property_classes(
    graph: &ModuleGraph,
    indexes: &MemberPassIndexes<'_>,
    seed_key: &ExportKey,
    property_path: &str,
) -> Vec<(FileId, String)> {
    let mut out = Vec::new();
    for factory_origin in
        walk_re_export_origins(graph, seed_key.file_id, seed_key.export_name.as_str())
    {
        let Some(factory_module) = indexes.module_by_id.get(&factory_origin.file_id) else {
            continue;
        };
        let Some(shape) = factory_module
            .exported_factory_return_object_shapes
            .iter()
            .find(|shape| shape.export_name.as_str() == factory_origin.export_name.as_str())
        else {
            continue;
        };
        for property in &shape.properties {
            if property.property_path == property_path {
                out.push((factory_origin.file_id, property.class_local_name.clone()));
            }
        }
    }
    out
}

/// The class exports a proven exported factory returns, resolved from the factory's
/// own module. Shared by the member-credit and whole-object-suppress passes: each
/// link is an over-credit gate, so a callee that is not a proven factory yields
/// nothing.
fn factory_return_class_origins(
    graph: &ModuleGraph,
    indexes: &MemberPassIndexes<'_>,
    factory_origin_file_id: FileId,
    class_local_name: &str,
) -> Vec<ExportKey> {
    let factory_local_keys = indexes.local_keys(factory_origin_file_id);
    let Some(class_seed_keys) = factory_local_keys.get(class_local_name) else {
        return Vec::new();
    };
    class_seed_keys
        .iter()
        .flat_map(|class_seed| export_key_with_origins(graph, class_seed))
        .filter(|class_origin| {
            indexes
                .module_by_id
                .get(&class_origin.file_id)
                .is_some_and(|class_module| {
                    export_is_class_with_members(class_module, class_origin.export_name.as_str())
                })
        })
        .collect()
}

/// The classes a callee's proven exported factory returns, resolved across modules.
/// Empty for any callee that is not an internal exported factory with a strict,
/// value-proven return: each link of the chain is an over-credit gate.
fn factory_return_classes_for_callee(
    graph: &ModuleGraph,
    indexes: &MemberPassIndexes<'_>,
    seed_key: &ExportKey,
) -> Vec<ExportKey> {
    let mut origins = Vec::new();
    for factory_origin in
        walk_re_export_origins(graph, seed_key.file_id, seed_key.export_name.as_str())
    {
        let Some(factory_module) = indexes.module_by_id.get(&factory_origin.file_id) else {
            continue;
        };
        let Some(factory_return) =
            factory_module
                .exported_factory_returns
                .iter()
                .find(|factory_return| {
                    factory_origin.export_name.as_str() == factory_return.export_name.as_str()
                })
        else {
            continue;
        };
        origins.extend(factory_return_class_origins(
            graph,
            indexes,
            factory_origin.file_id,
            factory_return.class_local_name.as_str(),
        ));
    }
    origins
}

/// Suppress a class whose factory-returned instance is consumed opaquely
/// (`const { a, ...rest } = importedFactory()`, a computed destructure key).
///
/// The consumer can read any property, so no set of member accesses describes what
/// is used. Crediting only the keys the pattern names would leave every other live
/// member reported as dead. Mark the export wholly used instead; the member scan
/// then skips it, exactly as it does for a class whose instance escapes.
pub(super) fn propagate_factory_fn_whole_object_uses(
    graph: &ModuleGraph,
    resolved_modules: &[ResolvedModule],
    indexes: &MemberPassIndexes<'_>,
    whole_object_used_exports: &mut FxHashSet<ExportKey>,
) {
    for resolved in resolved_modules {
        let local_to_export_keys = indexes.local_keys(resolved.file_id);
        for fact in factory_fn_whole_objects(resolved) {
            let Some(seed_keys) = local_to_export_keys.get(fact.callee_name.as_str()) else {
                continue;
            };
            let classes = seed_keys
                .iter()
                .flat_map(|seed_key| factory_return_classes_for_callee(graph, indexes, seed_key));
            whole_object_used_exports.extend(classes);
        }
    }
}