use std::path::Path;
use rustc_hash::{FxHashMap, FxHashSet};
use fallow_types::extract::ModuleInfo;
use crate::discover::FileId;
use crate::graph::ModuleGraph;
use crate::results::UnusedComponentProp;
use super::{LineOffsetsMap, byte_offset_to_line_col};
#[must_use]
pub fn find_unused_component_props(
graph: &ModuleGraph,
modules: &[ModuleInfo],
declared_deps: &FxHashSet<String>,
line_offsets_by_file: &LineOffsetsMap<'_>,
) -> Vec<UnusedComponentProp> {
let gated = declared_deps.contains("vue")
|| declared_deps.contains("@vue/runtime-core")
|| declared_deps.contains("nuxt");
if !gated {
return Vec::new();
}
let modules_by_id: FxHashMap<FileId, &ModuleInfo> =
modules.iter().map(|m| (m.file_id, m)).collect();
let mut findings = Vec::new();
for node in &graph.modules {
if !node.is_reachable() {
continue;
}
if !is_vue_file(&node.path) {
continue;
}
let Some(module) = modules_by_id.get(&node.file_id) else {
continue;
};
if module.component_props.is_empty() {
continue;
}
if module.has_unharvestable_props
|| module.has_props_attrs_fallthrough
|| module.has_define_expose
|| module.has_define_model
{
continue;
}
let component_name = component_name_for(&node.path);
for prop in &module.component_props {
if prop.used_in_script || prop.used_in_template {
continue;
}
let (line, col) =
byte_offset_to_line_col(line_offsets_by_file, node.file_id, prop.span_start);
findings.push(UnusedComponentProp {
path: node.path.clone(),
component_name: component_name.clone(),
prop_name: prop.name.clone(),
line,
col,
});
}
}
findings.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.prop_name.cmp(&b.prop_name))
});
findings
}
fn is_vue_file(path: &Path) -> bool {
path.extension().and_then(|e| e.to_str()) == Some("vue")
}
fn component_name_for(path: &Path) -> String {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string()
}
fn is_react_file(path: &Path) -> bool {
matches!(
path.extension().and_then(|e| e.to_str()),
Some("jsx" | "tsx")
)
}
#[derive(Debug, Default)]
pub struct ReactPropScan {
pub findings: Vec<UnusedComponentProp>,
pub components_scanned: usize,
}
#[must_use]
pub fn find_unused_react_props(
graph: &ModuleGraph,
modules: &[ModuleInfo],
declared_deps: &FxHashSet<String>,
line_offsets_by_file: &LineOffsetsMap<'_>,
) -> ReactPropScan {
let gated = declared_deps.contains("react")
|| declared_deps.contains("react-dom")
|| declared_deps.contains("next")
|| declared_deps.contains("preact");
if !gated {
return ReactPropScan::default();
}
let modules_by_id: FxHashMap<FileId, &ModuleInfo> =
modules.iter().map(|m| (m.file_id, m)).collect();
let mut scan = ReactPropScan::default();
for node in &graph.modules {
if !node.is_reachable() {
continue;
}
if !is_react_file(&node.path) {
continue;
}
let Some(module) = modules_by_id.get(&node.file_id) else {
continue;
};
if module.component_functions.is_empty() {
continue;
}
scan.components_scanned += module.component_functions.len();
if module.react_props.is_empty() {
continue;
}
let abstained: FxHashSet<&str> = module
.component_functions
.iter()
.filter(|c| c.has_unharvestable_props || c.is_exported)
.map(|c| c.name.as_str())
.collect();
for prop in &module.react_props {
if prop.used_in_script {
continue;
}
if abstained.contains(prop.component.as_str()) {
continue;
}
let (line, col) =
byte_offset_to_line_col(line_offsets_by_file, node.file_id, prop.span_start);
scan.findings.push(UnusedComponentProp {
path: node.path.clone(),
component_name: prop.component.clone(),
prop_name: prop.name.clone(),
line,
col,
});
}
}
scan.findings.sort_by(|a, b| {
a.path
.cmp(&b.path)
.then(a.line.cmp(&b.line))
.then(a.component_name.cmp(&b.component_name))
.then(a.prop_name.cmp(&b.prop_name))
});
scan
}