fallow_core/plugins/
trace_provenance.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ExportCredits {
19 Skip,
22 Collect { include_entry_exports: bool },
26}
27
28impl ExportCredits {
29 #[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#[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
73fn 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
135fn 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
151fn 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}