1use std::{path::Path, sync::Arc};
4
5use fallow_config::ResolvedConfig;
6use fallow_types::discover::DiscoveredFile;
7use fallow_types::extract::{FlagUse, FlagUseKind, ModuleInfo};
8use fallow_types::results::{AnalysisResults, FeatureFlag, FlagConfidence, FlagKind};
9use rustc_hash::FxHashMap;
10
11use crate::session::AnalysisSession;
12use crate::suppress::{IssueKind, is_file_suppressed, is_suppressed};
13
14#[derive(Debug, Clone)]
16pub struct FeatureFlagsAnalysis {
17 pub flags: Vec<FeatureFlag>,
19 pub files_scanned: usize,
21}
22
23pub fn analyze_feature_flags_with_session(
32 session: &AnalysisSession,
33) -> crate::EngineResult<FeatureFlagsAnalysis> {
34 let modules = session.shared_parsed_modules_cancellable(false, "the feature-flag scan")?;
35 let flags = collect_flags_for_modules(session, session.files(), &modules)?;
36 Ok(FeatureFlagsAnalysis {
37 flags,
38 files_scanned: session.files().len(),
39 })
40}
41
42#[must_use]
48pub fn analyze_feature_flags_with_session_and_results(
49 session: &AnalysisSession,
50 results: &AnalysisResults,
51) -> FeatureFlagsAnalysis {
52 let modules = session.shared_parsed_modules(false);
53 let mut flags = collect_flags_from_modules(session.config(), session.files(), &modules);
54 correlate_with_dead_code(&mut flags, results);
55 FeatureFlagsAnalysis {
56 flags,
57 files_scanned: session.files().len(),
58 }
59}
60
61#[must_use]
63pub fn builtin_env_prefixes() -> &'static [&'static str] {
64 crate::feature_flags::builtin_env_prefixes()
65}
66
67#[must_use]
69pub fn builtin_sdk_providers() -> Vec<&'static str> {
70 crate::feature_flags::builtin_sdk_providers()
71}
72
73fn collect_flags_for_modules(
74 session: &AnalysisSession,
75 files: &[DiscoveredFile],
76 modules: &Arc<[ModuleInfo]>,
77) -> crate::EngineResult<Vec<FeatureFlag>> {
78 let mut flags = collect_flags_from_modules(session.config(), files, modules);
79 correlate_flags_with_dead_code(&mut flags, session, modules)?;
80 Ok(flags)
81}
82
83fn correlate_flags_with_dead_code(
84 flags: &mut [FeatureFlag],
85 session: &AnalysisSession,
86 modules: &Arc<[ModuleInfo]>,
87) -> crate::EngineResult<()> {
88 match session.analyze_dead_code_with_shared_modules(Arc::clone(modules)) {
89 Ok(analysis_output) => correlate_with_dead_code(flags, &analysis_output.results),
90 Err(err) if err.is_cancelled() => return Err(err),
94 Err(_) => {}
95 }
96 Ok(())
97}
98
99fn correlate_with_dead_code(flags: &mut [FeatureFlag], results: &AnalysisResults) {
100 if results.unused_exports.is_empty() && results.unused_types.is_empty() {
101 return;
102 }
103
104 for flag in flags.iter_mut() {
105 let (Some(guard_start), Some(guard_end)) = (flag.guard_line_start, flag.guard_line_end)
106 else {
107 continue;
108 };
109
110 for export in &results.unused_exports {
111 if export.export.path == flag.path
112 && export.export.line >= guard_start
113 && export.export.line <= guard_end
114 {
115 flag.guarded_dead_exports
116 .push(export.export.export_name.clone());
117 }
118 }
119
120 for export in &results.unused_types {
121 if export.export.path == flag.path
122 && export.export.line >= guard_start
123 && export.export.line <= guard_end
124 {
125 flag.guarded_dead_exports
126 .push(export.export.export_name.clone());
127 }
128 }
129 }
130}
131
132fn collect_flags_from_modules(
133 config: &ResolvedConfig,
134 files: &[DiscoveredFile],
135 modules: &[ModuleInfo],
136) -> Vec<FeatureFlag> {
137 let file_paths: FxHashMap<_, _> = files.iter().map(|file| (file.id, &file.path)).collect();
138
139 let extra_sdk: Vec<(String, usize, String)> = config
140 .flags
141 .sdk_patterns
142 .iter()
143 .map(|pattern| {
144 (
145 pattern.function.clone(),
146 pattern.name_arg,
147 pattern.provider.clone().unwrap_or_default(),
148 )
149 })
150 .collect();
151 let has_custom_config = !extra_sdk.is_empty()
152 || !config.flags.env_prefixes.is_empty()
153 || config.flags.config_object_heuristics;
154
155 let mut flags = Vec::new();
156 for module in modules {
157 let Some(path) = file_paths.get(&module.file_id) else {
158 continue;
159 };
160
161 collect_builtin_flags(&mut flags, module, path);
162 if has_custom_config {
163 collect_custom_flags(&mut flags, config, module, path, &extra_sdk);
164 }
165 }
166 flags
167}
168
169fn collect_builtin_flags(flags: &mut Vec<FeatureFlag>, module: &ModuleInfo, path: &Path) {
170 let file_suppressed = is_file_suppressed(&module.suppressions, IssueKind::FeatureFlag);
171 for flag_use in &module.flag_uses {
172 if file_suppressed
173 || is_suppressed(&module.suppressions, flag_use.line, IssueKind::FeatureFlag)
174 {
175 continue;
176 }
177 flags.push(flag_use_to_feature_flag(flag_use, module, path));
178 }
179}
180
181fn collect_custom_flags(
182 flags: &mut Vec<FeatureFlag>,
183 config: &ResolvedConfig,
184 module: &ModuleInfo,
185 path: &Path,
186 extra_sdk: &[(String, usize, String)],
187) {
188 let Ok(source) = std::fs::read_to_string(path) else {
189 return;
190 };
191
192 let custom_flags = crate::feature_flags::extract_flags_from_source(
193 &source,
194 path,
195 extra_sdk,
196 &config.flags.env_prefixes,
197 config.flags.config_object_heuristics,
198 );
199 for flag_use in &custom_flags {
200 let already_found = module.flag_uses.iter().any(|existing| {
201 existing.line == flag_use.line && existing.flag_name == flag_use.flag_name
202 });
203 if !already_found
204 && !is_suppressed(&module.suppressions, flag_use.line, IssueKind::FeatureFlag)
205 {
206 flags.push(flag_use_to_feature_flag(flag_use, module, path));
207 }
208 }
209}
210
211fn flag_use_to_feature_flag(flag_use: &FlagUse, module: &ModuleInfo, path: &Path) -> FeatureFlag {
212 let (kind, confidence) = match flag_use.kind {
213 FlagUseKind::EnvVar => (FlagKind::EnvironmentVariable, FlagConfidence::High),
214 FlagUseKind::SdkCall => (FlagKind::SdkCall, FlagConfidence::High),
215 FlagUseKind::ConfigObject => (FlagKind::ConfigObject, FlagConfidence::Low),
216 };
217
218 let (guard_line_start, guard_line_end) = if let (Some(start), Some(end)) =
219 (flag_use.guard_span_start, flag_use.guard_span_end)
220 && !module.line_offsets.is_empty()
221 {
222 let (start_line, _) =
223 fallow_types::extract::byte_offset_to_line_col(&module.line_offsets, start);
224 let (end_line, _) =
225 fallow_types::extract::byte_offset_to_line_col(&module.line_offsets, end);
226 (Some(start_line), Some(end_line))
227 } else {
228 (None, None)
229 };
230
231 FeatureFlag {
232 path: path.to_path_buf(),
233 flag_name: flag_use.flag_name.clone(),
234 kind,
235 confidence,
236 line: flag_use.line,
237 col: flag_use.col,
238 guard_span_start: flag_use.guard_span_start,
239 guard_span_end: flag_use.guard_span_end,
240 sdk_name: flag_use.sdk_name.clone(),
241 guard_line_start,
242 guard_line_end,
243 guarded_dead_exports: Vec::new(),
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn session_runner_uses_session_discovery_instead_of_rediscovering() {
253 let project = tempfile::tempdir().expect("temp dir");
254 let root = project.path();
255 std::fs::create_dir(root.join("src")).expect("src dir");
256 std::fs::write(
257 root.join("package.json"),
258 r#"{"name":"flags-session","main":"src/index.ts"}"#,
259 )
260 .expect("package json");
261 std::fs::write(
262 root.join("src/index.ts"),
263 "if (process.env.FEATURE_EXISTING) {}\n",
264 )
265 .expect("initial source");
266
267 let session = AnalysisSession::load(root, None).expect("session loads");
268
269 std::fs::write(
270 root.join("src/late.ts"),
271 "if (process.env.FEATURE_LATE) {}\n",
272 )
273 .expect("late source");
274
275 let session_flags =
276 analyze_feature_flags_with_session(&session).expect("session flag scan");
277 let session_names: Vec<_> = session_flags
278 .flags
279 .iter()
280 .map(|flag| flag.flag_name.as_str())
281 .collect();
282 assert_eq!(session_names, vec!["FEATURE_EXISTING"]);
283
284 let second_session_flags =
285 analyze_feature_flags_with_session(&session).expect("second session flag scan");
286 let second_session_names: Vec<_> = second_session_flags
287 .flags
288 .iter()
289 .map(|flag| flag.flag_name.as_str())
290 .collect();
291 assert_eq!(second_session_names, vec!["FEATURE_EXISTING"]);
292 }
293}