use std::path::Path;
use fallow_config::ResolvedConfig;
use fallow_types::discover::DiscoveredFile;
use fallow_types::extract::{FlagUse, FlagUseKind, ModuleInfo};
use fallow_types::results::{AnalysisResults, FeatureFlag, FlagConfidence, FlagKind};
use rustc_hash::FxHashMap;
use crate::session::AnalysisSession;
use crate::suppress::{IssueKind, is_file_suppressed, is_suppressed};
#[derive(Debug, Clone)]
pub struct FeatureFlagsAnalysis {
pub flags: Vec<FeatureFlag>,
pub files_scanned: usize,
}
#[must_use]
pub fn analyze_feature_flags_with_session(session: &AnalysisSession) -> FeatureFlagsAnalysis {
let parsed = session.parsed_parts(false);
let flags = collect_flags_for_modules(session, &parsed.files, &parsed.modules);
FeatureFlagsAnalysis {
flags,
files_scanned: parsed.files.len(),
}
}
#[must_use]
pub fn builtin_env_prefixes() -> &'static [&'static str] {
crate::feature_flags::builtin_env_prefixes()
}
#[must_use]
pub fn builtin_sdk_providers() -> Vec<&'static str> {
crate::feature_flags::builtin_sdk_providers()
}
fn collect_flags_for_modules(
session: &AnalysisSession,
files: &[DiscoveredFile],
modules: &[ModuleInfo],
) -> Vec<FeatureFlag> {
let mut flags = collect_flags_from_modules(session.config(), files, modules);
correlate_flags_with_dead_code(&mut flags, session, modules);
flags
}
fn correlate_flags_with_dead_code(
flags: &mut [FeatureFlag],
session: &AnalysisSession,
modules: &[ModuleInfo],
) {
if let Ok(analysis_output) = session.analyze_dead_code_with_parsed_modules(modules) {
correlate_with_dead_code(flags, &analysis_output.results);
}
}
fn correlate_with_dead_code(flags: &mut [FeatureFlag], results: &AnalysisResults) {
if results.unused_exports.is_empty() && results.unused_types.is_empty() {
return;
}
for flag in flags.iter_mut() {
let (Some(guard_start), Some(guard_end)) = (flag.guard_line_start, flag.guard_line_end)
else {
continue;
};
for export in &results.unused_exports {
if export.export.path == flag.path
&& export.export.line >= guard_start
&& export.export.line <= guard_end
{
flag.guarded_dead_exports
.push(export.export.export_name.clone());
}
}
for export in &results.unused_types {
if export.export.path == flag.path
&& export.export.line >= guard_start
&& export.export.line <= guard_end
{
flag.guarded_dead_exports
.push(export.export.export_name.clone());
}
}
}
}
fn collect_flags_from_modules(
config: &ResolvedConfig,
files: &[DiscoveredFile],
modules: &[ModuleInfo],
) -> Vec<FeatureFlag> {
let file_paths: FxHashMap<_, _> = files.iter().map(|file| (file.id, &file.path)).collect();
let extra_sdk: Vec<(String, usize, String)> = config
.flags
.sdk_patterns
.iter()
.map(|pattern| {
(
pattern.function.clone(),
pattern.name_arg,
pattern.provider.clone().unwrap_or_default(),
)
})
.collect();
let has_custom_config = !extra_sdk.is_empty()
|| !config.flags.env_prefixes.is_empty()
|| config.flags.config_object_heuristics;
let mut flags = Vec::new();
for module in modules {
let Some(path) = file_paths.get(&module.file_id) else {
continue;
};
collect_builtin_flags(&mut flags, module, path);
if has_custom_config {
collect_custom_flags(&mut flags, config, module, path, &extra_sdk);
}
}
flags
}
fn collect_builtin_flags(flags: &mut Vec<FeatureFlag>, module: &ModuleInfo, path: &Path) {
let file_suppressed = is_file_suppressed(&module.suppressions, IssueKind::FeatureFlag);
for flag_use in &module.flag_uses {
if file_suppressed
|| is_suppressed(&module.suppressions, flag_use.line, IssueKind::FeatureFlag)
{
continue;
}
flags.push(flag_use_to_feature_flag(flag_use, module, path));
}
}
fn collect_custom_flags(
flags: &mut Vec<FeatureFlag>,
config: &ResolvedConfig,
module: &ModuleInfo,
path: &Path,
extra_sdk: &[(String, usize, String)],
) {
let Ok(source) = std::fs::read_to_string(path) else {
return;
};
let custom_flags = crate::feature_flags::extract_flags_from_source(
&source,
path,
extra_sdk,
&config.flags.env_prefixes,
config.flags.config_object_heuristics,
);
for flag_use in &custom_flags {
let already_found = module.flag_uses.iter().any(|existing| {
existing.line == flag_use.line && existing.flag_name == flag_use.flag_name
});
if !already_found
&& !is_suppressed(&module.suppressions, flag_use.line, IssueKind::FeatureFlag)
{
flags.push(flag_use_to_feature_flag(flag_use, module, path));
}
}
}
fn flag_use_to_feature_flag(flag_use: &FlagUse, module: &ModuleInfo, path: &Path) -> FeatureFlag {
let (kind, confidence) = match flag_use.kind {
FlagUseKind::EnvVar => (FlagKind::EnvironmentVariable, FlagConfidence::High),
FlagUseKind::SdkCall => (FlagKind::SdkCall, FlagConfidence::High),
FlagUseKind::ConfigObject => (FlagKind::ConfigObject, FlagConfidence::Low),
};
let (guard_line_start, guard_line_end) = if let (Some(start), Some(end)) =
(flag_use.guard_span_start, flag_use.guard_span_end)
&& !module.line_offsets.is_empty()
{
let (start_line, _) =
fallow_types::extract::byte_offset_to_line_col(&module.line_offsets, start);
let (end_line, _) =
fallow_types::extract::byte_offset_to_line_col(&module.line_offsets, end);
(Some(start_line), Some(end_line))
} else {
(None, None)
};
FeatureFlag {
path: path.to_path_buf(),
flag_name: flag_use.flag_name.clone(),
kind,
confidence,
line: flag_use.line,
col: flag_use.col,
guard_span_start: flag_use.guard_span_start,
guard_span_end: flag_use.guard_span_end,
sdk_name: flag_use.sdk_name.clone(),
guard_line_start,
guard_line_end,
guarded_dead_exports: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_runner_uses_session_discovery_instead_of_rediscovering() {
let project = tempfile::tempdir().expect("temp dir");
let root = project.path();
std::fs::create_dir(root.join("src")).expect("src dir");
std::fs::write(
root.join("package.json"),
r#"{"name":"flags-session","main":"src/index.ts"}"#,
)
.expect("package json");
std::fs::write(
root.join("src/index.ts"),
"if (process.env.FEATURE_EXISTING) {}\n",
)
.expect("initial source");
let session = AnalysisSession::load(root, None).expect("session loads");
std::fs::write(
root.join("src/late.ts"),
"if (process.env.FEATURE_LATE) {}\n",
)
.expect("late source");
let session_flags = analyze_feature_flags_with_session(&session);
let session_names: Vec<_> = session_flags
.flags
.iter()
.map(|flag| flag.flag_name.as_str())
.collect();
assert_eq!(session_names, vec!["FEATURE_EXISTING"]);
let second_session_flags = analyze_feature_flags_with_session(&session);
let second_session_names: Vec<_> = second_session_flags
.flags
.iter()
.map(|flag| flag.flag_name.as_str())
.collect();
assert_eq!(second_session_names, vec!["FEATURE_EXISTING"]);
}
}