fallow_engine/
list_inventory.rs1use fallow_config::{ResolvedConfig, WorkspaceInfo};
4
5use crate::{
6 discover::{DiscoveredFile, EntryPoint},
7 plugins::AggregatedPluginResult,
8 session::AnalysisSession,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ListInventoryError {
14 Plugins(String),
17}
18
19impl ListInventoryError {
20 #[must_use]
22 pub fn message(&self) -> &str {
23 match self {
24 Self::Plugins(message) => message,
25 }
26 }
27}
28
29#[derive(Debug, Clone)]
31pub struct ListingInventory {
32 pub plugins: AggregatedPluginResult,
34 pub entry_points: Option<Vec<EntryPoint>>,
37}
38
39pub fn collect_listing_inventory(
57 session: &AnalysisSession,
58 with_entry_points: bool,
59) -> Result<ListingInventory, ListInventoryError> {
60 let prelude = crate::core_backend::prepare_dead_code_backend_prelude(
61 session.config(),
62 session.discovery(),
63 )
64 .map_err(|err| ListInventoryError::Plugins(err.message().to_owned()))?;
65 let entry_points = with_entry_points.then(|| {
66 crate::core_backend::discover_dead_code_entry_points(&prelude)
67 .all()
68 .to_vec()
69 });
70 let plugins = AggregatedPluginResult::from(prelude.plugin_result());
71 prelude.finish();
72 Ok(ListingInventory {
73 plugins,
74 entry_points,
75 })
76}
77
78#[must_use]
80pub fn collect_entry_points(
81 config: &ResolvedConfig,
82 discovered: &[DiscoveredFile],
83 workspaces: &[WorkspaceInfo],
84 plugin_result: Option<&AggregatedPluginResult>,
85) -> Vec<EntryPoint> {
86 let mut entries = crate::discover::discover_entry_points(config, discovered);
87 for workspace in workspaces {
88 entries.extend(crate::discover::discover_workspace_entry_points(
89 &workspace.root,
90 config,
91 discovered,
92 ));
93 }
94 if let Some(plugin_result) = plugin_result {
95 entries.extend(crate::discover::discover_plugin_entry_points(
96 plugin_result,
97 config,
98 discovered,
99 ));
100 }
101 entries
102}
103
104#[cfg(test)]
105mod tests {
106 use std::path::Path;
107
108 use fallow_config::{FallowConfig, WorkspaceInfo};
109 use fallow_types::output_format::OutputFormat;
110
111 use super::*;
112 use crate::discover::{EntryPointSource, FileId};
113
114 #[test]
115 fn entry_points_include_root_and_workspace_entries() {
116 let temp = tempfile::tempdir().expect("tempdir");
117 let root = temp.path();
118 let config = FallowConfig::default().resolve(
119 root.to_path_buf(),
120 OutputFormat::Json,
121 1,
122 false,
123 true,
124 None,
125 );
126 let workspace = WorkspaceInfo {
127 root: root.join("packages/web"),
128 name: "web".to_owned(),
129 is_internal_dependency: false,
130 };
131 let discovered = vec![
132 DiscoveredFile {
133 id: FileId(0),
134 path: root.join("src/main.ts"),
135 size_bytes: 0,
136 },
137 DiscoveredFile {
138 id: FileId(1),
139 path: root.join("packages/web/src/index.ts"),
140 size_bytes: 0,
141 },
142 ];
143
144 let entries = collect_entry_points(&config, &discovered, &[workspace], None);
145
146 assert!(
147 entries
148 .iter()
149 .any(|entry| entry.path.ends_with("src/main.ts"))
150 );
151 assert!(
152 entries
153 .iter()
154 .any(|entry| entry.path.ends_with("packages/web/src/index.ts"))
155 );
156 }
157
158 fn session_at(root: &Path) -> AnalysisSession {
159 let config = FallowConfig::default().resolve(
160 root.to_path_buf(),
161 OutputFormat::Json,
162 1,
163 true,
164 true,
165 None,
166 );
167 AnalysisSession::from_resolved_config(config).expect("session")
168 }
169
170 #[test]
171 fn active_plugins_ignores_missing_package_manifests() {
172 let temp = tempfile::tempdir().expect("tempdir");
173 let session = session_at(temp.path());
174 let inventory =
175 collect_listing_inventory(&session, true).expect("missing package should not fail");
176
177 assert!(inventory.plugins.active_plugins().is_empty());
178 let plugins_only =
179 collect_listing_inventory(&session, false).expect("missing package should not fail");
180 assert!(
181 plugins_only.entry_points.is_none(),
182 "a plugins-only listing skips entry-point discovery"
183 );
184 }
185
186 #[test]
187 fn entry_points_accept_plugin_result() {
188 let temp = tempfile::tempdir().expect("tempdir");
189 std::fs::write(
190 temp.path().join("package.json"),
191 r#"{"dependencies":{"next":"15.0.0"}}"#,
192 )
193 .expect("package manifest");
194 for file in ["src/app/dashboard/page.tsx", "src/helpers/format.ts"] {
195 let path = temp.path().join(file);
196 std::fs::create_dir_all(path.parent().expect("parent")).expect("dirs");
197 std::fs::write(path, "export const x = 1;\n").expect("source");
198 }
199 let session = session_at(temp.path());
200 let config = session.config();
201 let discovered = session.files();
202 let inventory = collect_listing_inventory(&session, true).expect("Next.js plugins load");
203 assert!(
204 inventory
205 .entry_points
206 .as_deref()
207 .unwrap_or_default()
208 .iter()
209 .any(|entry| entry.path.ends_with("src/app/dashboard/page.tsx")),
210 "the listing inventory carries the analysis entry points"
211 );
212 let plugin_result = inventory.plugins;
213
214 let entries = collect_entry_points(config, discovered, &[], None);
215
216 assert!(
217 entries
218 .iter()
219 .all(|entry| !matches!(entry.source, EntryPointSource::Plugin { .. }))
220 );
221
222 let entries = collect_entry_points(config, discovered, &[], Some(&plugin_result));
223 let plugin_entries: Vec<_> = entries
224 .iter()
225 .filter_map(|entry| match &entry.source {
226 EntryPointSource::Plugin { name } => Some((
227 entry.path.strip_prefix(&config.root).expect("under root"),
228 name.as_str(),
229 )),
230 _ => None,
231 })
232 .collect();
233 assert_eq!(
234 plugin_entries,
235 vec![(Path::new("src/app/dashboard/page.tsx"), "nextjs")]
236 );
237 }
238}