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