use fallow_config::{ResolvedConfig, WorkspaceInfo};
use crate::{
discover::{DiscoveredFile, EntryPoint},
plugins::AggregatedPluginResult,
session::AnalysisSession,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListInventoryError {
Plugins(String),
}
impl ListInventoryError {
#[must_use]
pub fn message(&self) -> &str {
match self {
Self::Plugins(message) => message,
}
}
}
#[derive(Debug, Clone)]
pub struct ListingInventory {
pub plugins: AggregatedPluginResult,
pub entry_points: Option<Vec<EntryPoint>>,
}
pub fn collect_listing_inventory(
session: &AnalysisSession,
with_entry_points: bool,
) -> Result<ListingInventory, ListInventoryError> {
let prelude = crate::core_backend::prepare_dead_code_backend_prelude(
session.config(),
session.discovery(),
)
.map_err(|err| ListInventoryError::Plugins(err.message().to_owned()))?;
let entry_points = with_entry_points.then(|| {
crate::core_backend::discover_dead_code_entry_points(&prelude)
.all()
.to_vec()
});
let plugins = AggregatedPluginResult::from(prelude.plugin_result());
prelude.finish();
Ok(ListingInventory {
plugins,
entry_points,
})
}
#[must_use]
pub fn collect_entry_points(
config: &ResolvedConfig,
discovered: &[DiscoveredFile],
workspaces: &[WorkspaceInfo],
plugin_result: Option<&AggregatedPluginResult>,
) -> Vec<EntryPoint> {
let mut entries = crate::discover::discover_entry_points(config, discovered);
for workspace in workspaces {
entries.extend(crate::discover::discover_workspace_entry_points(
&workspace.root,
config,
discovered,
));
}
if let Some(plugin_result) = plugin_result {
entries.extend(crate::discover::discover_plugin_entry_points(
plugin_result,
config,
discovered,
));
}
entries
}
#[cfg(test)]
mod tests {
use std::path::Path;
use fallow_config::{FallowConfig, WorkspaceInfo};
use fallow_types::output_format::OutputFormat;
use super::*;
use crate::discover::{EntryPointSource, FileId};
#[test]
fn entry_points_include_root_and_workspace_entries() {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path();
let config = FallowConfig::default().resolve(
root.to_path_buf(),
OutputFormat::Json,
1,
false,
true,
None,
);
let workspace = WorkspaceInfo {
root: root.join("packages/web"),
name: "web".to_owned(),
is_internal_dependency: false,
};
let discovered = vec![
DiscoveredFile {
id: FileId(0),
path: root.join("src/main.ts"),
size_bytes: 0,
},
DiscoveredFile {
id: FileId(1),
path: root.join("packages/web/src/index.ts"),
size_bytes: 0,
},
];
let entries = collect_entry_points(&config, &discovered, &[workspace], None);
assert!(
entries
.iter()
.any(|entry| entry.path.ends_with("src/main.ts"))
);
assert!(
entries
.iter()
.any(|entry| entry.path.ends_with("packages/web/src/index.ts"))
);
}
fn session_at(root: &Path) -> AnalysisSession {
let config = FallowConfig::default().resolve(
root.to_path_buf(),
OutputFormat::Json,
1,
true,
true,
None,
);
AnalysisSession::from_resolved_config(config).expect("session")
}
#[test]
fn active_plugins_ignores_missing_package_manifests() {
let temp = tempfile::tempdir().expect("tempdir");
let session = session_at(temp.path());
let inventory =
collect_listing_inventory(&session, true).expect("missing package should not fail");
assert!(inventory.plugins.active_plugins().is_empty());
let plugins_only =
collect_listing_inventory(&session, false).expect("missing package should not fail");
assert!(
plugins_only.entry_points.is_none(),
"a plugins-only listing skips entry-point discovery"
);
}
#[test]
fn entry_points_accept_plugin_result() {
let temp = tempfile::tempdir().expect("tempdir");
std::fs::write(
temp.path().join("package.json"),
r#"{"dependencies":{"next":"15.0.0"}}"#,
)
.expect("package manifest");
for file in ["src/app/dashboard/page.tsx", "src/helpers/format.ts"] {
let path = temp.path().join(file);
std::fs::create_dir_all(path.parent().expect("parent")).expect("dirs");
std::fs::write(path, "export const x = 1;\n").expect("source");
}
let session = session_at(temp.path());
let config = session.config();
let discovered = session.files();
let inventory = collect_listing_inventory(&session, true).expect("Next.js plugins load");
assert!(
inventory
.entry_points
.as_deref()
.unwrap_or_default()
.iter()
.any(|entry| entry.path.ends_with("src/app/dashboard/page.tsx")),
"the listing inventory carries the analysis entry points"
);
let plugin_result = inventory.plugins;
let entries = collect_entry_points(config, discovered, &[], None);
assert!(
entries
.iter()
.all(|entry| !matches!(entry.source, EntryPointSource::Plugin { .. }))
);
let entries = collect_entry_points(config, discovered, &[], Some(&plugin_result));
let plugin_entries: Vec<_> = entries
.iter()
.filter_map(|entry| match &entry.source {
EntryPointSource::Plugin { name } => Some((
entry.path.strip_prefix(&config.root).expect("under root"),
name.as_str(),
)),
_ => None,
})
.collect();
assert_eq!(
plugin_entries,
vec![(Path::new("src/app/dashboard/page.tsx"), "nextjs")]
);
}
}