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