1use std::path::Path;
4
5use fallow_config::ResolvedConfig;
6use fallow_types::discover::DiscoveredFile;
7use fallow_types::extract::{FlagUse, FlagUseKind, ModuleInfo, ParseResult};
8use fallow_types::results::{FeatureFlag, FlagConfidence, FlagKind};
9use rustc_hash::FxHashMap;
10
11#[derive(Debug, Clone)]
13pub struct FeatureFlagsAnalysis {
14 pub flags: Vec<FeatureFlag>,
15 pub files_scanned: usize,
16}
17
18#[must_use]
20pub fn analyze_feature_flags(config: &ResolvedConfig) -> FeatureFlagsAnalysis {
21 let files = crate::discover_files_with_plugin_scopes(config);
22 let flags = collect_flags_for_files(config, &files);
23 FeatureFlagsAnalysis {
24 flags,
25 files_scanned: files.len(),
26 }
27}
28
29#[must_use]
31pub fn builtin_env_prefixes() -> &'static [&'static str] {
32 fallow_core::extract::flags::builtin_env_prefixes()
33}
34
35#[must_use]
37pub fn builtin_sdk_providers() -> Vec<&'static str> {
38 fallow_core::extract::flags::builtin_sdk_providers()
39}
40
41fn collect_flags_for_files(config: &ResolvedConfig, files: &[DiscoveredFile]) -> Vec<FeatureFlag> {
42 let cache_store = if config.no_cache {
43 None
44 } else {
45 fallow_core::cache::CacheStore::load(
46 &config.cache_dir,
47 config.cache_config_hash,
48 fallow_core::resolve_cache_max_size_bytes(config),
49 )
50 };
51 let parse_result = fallow_core::extract::parse_all_files(files, cache_store.as_ref(), false);
52
53 let mut flags = collect_flags_from_parse_result(config, files, &parse_result);
54 correlate_flags_with_dead_code(&mut flags, config, &parse_result);
55 flags
56}
57
58fn correlate_flags_with_dead_code(
59 flags: &mut [FeatureFlag],
60 config: &ResolvedConfig,
61 parse_result: &ParseResult,
62) {
63 #[expect(
64 deprecated,
65 reason = "fallow-engine is the typed migration boundary over the internal core backend"
66 )]
67 if let Ok(analysis_output) =
68 fallow_core::analyze_with_parse_result(config, &parse_result.modules)
69 {
70 #[expect(
71 deprecated,
72 reason = "fallow-engine is the typed migration boundary over the internal core backend"
73 )]
74 fallow_core::analyze::feature_flags::correlate_with_dead_code(
75 flags,
76 &analysis_output.results,
77 );
78 }
79}
80
81fn collect_flags_from_parse_result(
82 config: &ResolvedConfig,
83 files: &[DiscoveredFile],
84 parse_result: &ParseResult,
85) -> Vec<FeatureFlag> {
86 let file_paths: FxHashMap<_, _> = files.iter().map(|file| (file.id, &file.path)).collect();
87
88 let extra_sdk: Vec<(String, usize, String)> = config
89 .flags
90 .sdk_patterns
91 .iter()
92 .map(|pattern| {
93 (
94 pattern.function.clone(),
95 pattern.name_arg,
96 pattern.provider.clone().unwrap_or_default(),
97 )
98 })
99 .collect();
100 let has_custom_config = !extra_sdk.is_empty()
101 || !config.flags.env_prefixes.is_empty()
102 || config.flags.config_object_heuristics;
103
104 let mut flags = Vec::new();
105 for module in &parse_result.modules {
106 let Some(path) = file_paths.get(&module.file_id) else {
107 continue;
108 };
109
110 collect_builtin_flags(&mut flags, module, path);
111 if has_custom_config {
112 collect_custom_flags(&mut flags, config, module, path, &extra_sdk);
113 }
114 }
115 flags
116}
117
118fn collect_builtin_flags(flags: &mut Vec<FeatureFlag>, module: &ModuleInfo, path: &Path) {
119 let file_suppressed = fallow_core::suppress::is_file_suppressed(
120 &module.suppressions,
121 fallow_core::suppress::IssueKind::FeatureFlag,
122 );
123 for flag_use in &module.flag_uses {
124 if file_suppressed
125 || fallow_core::suppress::is_suppressed(
126 &module.suppressions,
127 flag_use.line,
128 fallow_core::suppress::IssueKind::FeatureFlag,
129 )
130 {
131 continue;
132 }
133 flags.push(flag_use_to_feature_flag(flag_use, module, path));
134 }
135}
136
137fn collect_custom_flags(
138 flags: &mut Vec<FeatureFlag>,
139 config: &ResolvedConfig,
140 module: &ModuleInfo,
141 path: &Path,
142 extra_sdk: &[(String, usize, String)],
143) {
144 let Ok(source) = std::fs::read_to_string(path) else {
145 return;
146 };
147
148 let custom_flags = fallow_core::extract::flags::extract_flags_from_source(
149 &source,
150 path,
151 extra_sdk,
152 &config.flags.env_prefixes,
153 config.flags.config_object_heuristics,
154 );
155 for flag_use in &custom_flags {
156 let already_found = module.flag_uses.iter().any(|existing| {
157 existing.line == flag_use.line && existing.flag_name == flag_use.flag_name
158 });
159 if !already_found
160 && !fallow_core::suppress::is_suppressed(
161 &module.suppressions,
162 flag_use.line,
163 fallow_core::suppress::IssueKind::FeatureFlag,
164 )
165 {
166 flags.push(flag_use_to_feature_flag(flag_use, module, path));
167 }
168 }
169}
170
171fn flag_use_to_feature_flag(flag_use: &FlagUse, module: &ModuleInfo, path: &Path) -> FeatureFlag {
172 let (kind, confidence) = match flag_use.kind {
173 FlagUseKind::EnvVar => (FlagKind::EnvironmentVariable, FlagConfidence::High),
174 FlagUseKind::SdkCall => (FlagKind::SdkCall, FlagConfidence::High),
175 FlagUseKind::ConfigObject => (FlagKind::ConfigObject, FlagConfidence::Low),
176 };
177
178 let (guard_line_start, guard_line_end) = if let (Some(start), Some(end)) =
179 (flag_use.guard_span_start, flag_use.guard_span_end)
180 && !module.line_offsets.is_empty()
181 {
182 let (start_line, _) =
183 fallow_types::extract::byte_offset_to_line_col(&module.line_offsets, start);
184 let (end_line, _) =
185 fallow_types::extract::byte_offset_to_line_col(&module.line_offsets, end);
186 (Some(start_line), Some(end_line))
187 } else {
188 (None, None)
189 };
190
191 FeatureFlag {
192 path: path.to_path_buf(),
193 flag_name: flag_use.flag_name.clone(),
194 kind,
195 confidence,
196 line: flag_use.line,
197 col: flag_use.col,
198 guard_span_start: flag_use.guard_span_start,
199 guard_span_end: flag_use.guard_span_end,
200 sdk_name: flag_use.sdk_name.clone(),
201 guard_line_start,
202 guard_line_end,
203 guarded_dead_exports: Vec::new(),
204 }
205}