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