use rustc_hash::FxHashMap;
use fallow_types::extract::ModuleInfo;
use crate::discover::FileId;
use crate::graph::ModuleGraph;
use crate::resolve::{ResolvedModule, ResolvedReExport};
use crate::results::MixedClientServerBarrel;
use crate::suppress::{IssueKind, SuppressionContext};
use super::server_only::is_server_only_module;
use super::{LineOffsetsMap, byte_offset_to_line_col};
const USE_CLIENT: &str = "use client";
#[must_use]
pub fn find_mixed_client_server_barrels(
graph: &ModuleGraph,
modules: &[ModuleInfo],
resolved_modules: &[ResolvedModule],
declared_deps: &rustc_hash::FxHashSet<String>,
suppressions: &SuppressionContext<'_>,
line_offsets_by_file: &LineOffsetsMap<'_>,
) -> Vec<MixedClientServerBarrel> {
if !crate::analyze::predicates::project_uses_rsc_directives(declared_deps) {
return Vec::new();
}
let modules_by_id: FxHashMap<FileId, &ModuleInfo> =
modules.iter().map(|m| (m.file_id, m)).collect();
let path_by_id: FxHashMap<FileId, &std::path::Path> = graph
.modules
.iter()
.map(|module| (module.file_id, module.path.as_path()))
.collect();
let mut findings = Vec::new();
for resolved in resolved_modules {
if let Some(finding) = mixed_barrel_finding(
resolved,
&modules_by_id,
&path_by_id,
suppressions,
line_offsets_by_file,
) {
findings.push(finding);
}
}
findings
}
fn mixed_barrel_finding(
resolved: &ResolvedModule,
modules_by_id: &FxHashMap<FileId, &ModuleInfo>,
path_by_id: &FxHashMap<FileId, &std::path::Path>,
suppressions: &SuppressionContext<'_>,
line_offsets_by_file: &LineOffsetsMap<'_>,
) -> Option<MixedClientServerBarrel> {
if resolved.re_exports.is_empty() {
return None;
}
let MixedOrigins { client, server } = classify_mixed_barrel_origins(resolved, modules_by_id)?;
let barrel_id = resolved.file_id;
let path = path_by_id.get(&barrel_id)?;
let anchor_span = client.span_start.min(server.span_start);
let (line, col) = byte_offset_to_line_col(line_offsets_by_file, barrel_id, anchor_span);
if suppressions.is_suppressed(barrel_id, line, IssueKind::MixedClientServerBarrel) {
return None;
}
Some(MixedClientServerBarrel {
path: path.to_path_buf(),
client_origin: client.source.to_string(),
server_origin: server.source.to_string(),
line,
col,
})
}
fn classify_mixed_barrel_origins<'a>(
resolved: &'a ResolvedModule,
modules_by_id: &FxHashMap<FileId, &ModuleInfo>,
) -> Option<MixedOrigins<'a>> {
let mut client: Option<OffendingOrigin<'_>> = None;
let mut server: Option<OffendingOrigin<'_>> = None;
for re in &resolved.re_exports {
classify_re_export_origin(re, modules_by_id, &mut client, &mut server);
}
Some(MixedOrigins {
client: client?,
server: server?,
})
}
fn classify_re_export_origin<'a>(
re: &'a ResolvedReExport,
modules_by_id: &FxHashMap<FileId, &ModuleInfo>,
client: &mut Option<OffendingOrigin<'a>>,
server: &mut Option<OffendingOrigin<'a>>,
) {
if re.info.is_type_only {
return;
}
let Some(origin_id) = re.target.internal_file_id() else {
return;
};
let Some(origin) = modules_by_id.get(&origin_id) else {
return;
};
let span_start = re.info.span.start;
if client.is_none() && origin.directives.iter().any(|d| d == USE_CLIENT) {
*client = Some(OffendingOrigin {
source: re.info.source.as_str(),
span_start,
});
} else if server.is_none() && is_server_only_module(origin) {
*server = Some(OffendingOrigin {
source: re.info.source.as_str(),
span_start,
});
}
}
struct MixedOrigins<'a> {
client: OffendingOrigin<'a>,
server: OffendingOrigin<'a>,
}
struct OffendingOrigin<'a> {
source: &'a str,
span_start: u32,
}