Skip to main content

fallow_core/plugins/
trace_provenance.rs

1//! What the plugin run knows about files, dependencies and exports beyond
2//! the module graph, collected once per analysis for the trace output.
3
4use std::path::Path;
5
6use fallow_types::extract::ExportName;
7use fallow_types::trace::{PluginExportCredit, TraceProvenance};
8
9use super::{
10    AggregatedPluginResult, CONFIG_ONLY_EXPORT_NAMES, CompiledPathRule, PluginUsedExportRule,
11    federation_trace_provenance, push_tooling_trace_credits,
12};
13use crate::discover::DiscoveredFile;
14use crate::extract::ModuleInfo;
15
16/// Which plugin export credits the trace provenance records.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ExportCredits {
19    /// Record no export credit. A run without a retained module graph gives
20    /// no trace, so it skips the match of each rule against each module.
21    Skip,
22    /// Record the plugin `used_exports` credits and, when
23    /// `include_entry_exports` is on, the config-file export credits, the
24    /// same as in the unused-export analysis.
25    Collect { include_entry_exports: bool },
26}
27
28impl ExportCredits {
29    /// The credits of a run: a trace needs the retained module graph.
30    #[must_use]
31    pub const fn for_run(retain_graph: bool, config: &fallow_config::ResolvedConfig) -> Self {
32        if retain_graph {
33            Self::Collect {
34                include_entry_exports: config.include_entry_exports,
35            }
36        } else {
37            Self::Skip
38        }
39    }
40}
41
42/// Collect the trace provenance of one analysis, with the seeds of the
43/// peer-dependency closures.
44#[must_use]
45pub fn build_trace_provenance(
46    root: &Path,
47    files: &[DiscoveredFile],
48    workspaces: &[fallow_config::WorkspaceInfo],
49    modules: &[ModuleInfo],
50    plugin_result: &AggregatedPluginResult,
51    export_credits: ExportCredits,
52) -> TraceProvenance {
53    let mut provenance =
54        federation_trace_provenance(root, files, &plugin_result.federation_sources, modules);
55    push_tooling_trace_credits(&mut provenance, root, workspaces, plugin_result);
56    push_config_export_provenance(&mut provenance, root, files, modules, plugin_result);
57    if let ExportCredits::Collect {
58        include_entry_exports,
59    } = export_credits
60    {
61        push_plugin_export_credits(
62            &mut provenance,
63            root,
64            files,
65            modules,
66            plugin_result,
67            include_entry_exports,
68        );
69    }
70    provenance.with_peer_seeds(crate::analyze::peer_closure_seeds(plugin_result))
71}
72
73/// Record each export that a plugin rule marks as used.
74///
75/// The rules are the plugin `used_exports` rules and, under
76/// `includeEntryExports`, the config-file export credits. A rule matches the
77/// export names that it lists, or every export for `*`.
78fn push_plugin_export_credits(
79    provenance: &mut TraceProvenance,
80    root: &Path,
81    files: &[DiscoveredFile],
82    modules: &[ModuleInfo],
83    plugin_result: &AggregatedPluginResult,
84    include_entry_exports: bool,
85) {
86    let config_credits: &[PluginUsedExportRule] = if include_entry_exports {
87        &plugin_result.config_export_credits
88    } else {
89        &[]
90    };
91    let rules: Vec<(CompiledPathRule, &PluginUsedExportRule)> = plugin_result
92        .used_exports
93        .iter()
94        .chain(config_credits)
95        .filter_map(|rule| {
96            let compiled = CompiledPathRule::for_used_export_rule(
97                &rule.rule.path,
98                "plugin export credit pattern",
99            )?;
100            Some((compiled, rule))
101        })
102        .collect();
103    if rules.is_empty() {
104        return;
105    }
106    for module in modules {
107        let Some(file) = files.get(module.file_id.0 as usize) else {
108            continue;
109        };
110        let relative = file.path.strip_prefix(root).unwrap_or(&file.path);
111        let relative_str = relative.to_string_lossy().replace('\\', "/");
112        let mut credits = Vec::new();
113        for (compiled, rule) in &rules {
114            if !compiled.matches(&relative_str) {
115                continue;
116            }
117            for name in export_names(module) {
118                if rule.rule.exports.iter().any(|e| e == "*" || e == name) {
119                    credits.push((
120                        name.to_owned(),
121                        PluginExportCredit {
122                            plugin: rule.plugin_name.clone(),
123                            pattern: rule.rule.path.pattern.clone(),
124                        },
125                    ));
126                }
127            }
128        }
129        if !credits.is_empty() {
130            provenance.push_export_credits(relative.to_path_buf(), credits);
131        }
132    }
133}
134
135/// The names that a module exports: its own exports and its named
136/// re-exports. The graph gives a named re-export its own export, so a plugin
137/// rule credits it in the analysis too.
138fn export_names(module: &ModuleInfo) -> impl Iterator<Item = &str> {
139    let own = module.exports.iter().map(|export| match &export.name {
140        ExportName::Named(name) => name.as_str(),
141        ExportName::Default => "default",
142    });
143    let re_exported = module
144        .re_exports
145        .iter()
146        .map(|re_export| re_export.exported_name.as_str())
147        .filter(|name| *name != "*");
148    own.chain(re_exported)
149}
150
151/// Record each export that a plugin `used_exports` rule names for its file
152/// and that is in [`CONFIG_ONLY_EXPORT_NAMES`]. Only modules that declare such
153/// a name pay for the glob match.
154fn push_config_export_provenance(
155    provenance: &mut TraceProvenance,
156    root: &Path,
157    files: &[DiscoveredFile],
158    modules: &[ModuleInfo],
159    plugin_result: &AggregatedPluginResult,
160) {
161    let rules: Vec<(CompiledPathRule, Vec<&str>)> = plugin_result
162        .used_exports
163        .iter()
164        .filter_map(|rule| {
165            let names: Vec<&str> = rule
166                .rule
167                .exports
168                .iter()
169                .map(String::as_str)
170                .filter(|name| CONFIG_ONLY_EXPORT_NAMES.contains(name))
171                .collect();
172            if names.is_empty() {
173                return None;
174            }
175            let compiled =
176                CompiledPathRule::for_used_export_rule(&rule.rule.path, "used_exports pattern")?;
177            Some((compiled, names))
178        })
179        .collect();
180    if rules.is_empty() {
181        return;
182    }
183    for module in modules {
184        let config_names: Vec<String> = module
185            .exports
186            .iter()
187            .filter_map(|export| match &export.name {
188                ExportName::Named(name)
189                    if !export.is_type_only
190                        && CONFIG_ONLY_EXPORT_NAMES.contains(&name.as_str()) =>
191                {
192                    Some(name.clone())
193                }
194                _ => None,
195            })
196            .collect();
197        if config_names.is_empty() {
198            continue;
199        }
200        let Some(file) = files.get(module.file_id.0 as usize) else {
201            continue;
202        };
203        let relative = file.path.strip_prefix(root).unwrap_or(&file.path);
204        let relative_str = relative.to_string_lossy().replace('\\', "/");
205        for name in config_names {
206            if rules
207                .iter()
208                .any(|(rule, names)| names.contains(&name.as_str()) && rule.matches(&relative_str))
209            {
210                provenance.push_config_export(relative.to_path_buf(), name);
211            }
212        }
213    }
214}