fallow_core/analyze/
feature_flags.rs1use std::path::PathBuf;
8
9use fallow_types::extract::{FlagUse, FlagUseKind, ModuleInfo, byte_offset_to_line_col};
10use fallow_types::results::{AnalysisResults, FeatureFlag, FlagConfidence, FlagKind};
11
12use crate::graph::ModuleGraph;
13
14pub fn collect_feature_flags(modules: &[ModuleInfo], graph: &ModuleGraph) -> Vec<FeatureFlag> {
20 let mut flags = Vec::new();
21
22 for module in modules {
23 if module.flag_uses.is_empty() {
24 continue;
25 }
26
27 let idx = module.file_id.0 as usize;
28 let Some(node) = graph.modules.get(idx) else {
29 continue;
30 };
31
32 for flag_use in &module.flag_uses {
33 let mut flag = flag_use_to_feature_flag(flag_use, node.path.clone());
34
35 if let (Some(start), Some(end)) = (flag_use.guard_span_start, flag_use.guard_span_end)
37 && !module.line_offsets.is_empty()
38 {
39 let (start_line, _) = byte_offset_to_line_col(&module.line_offsets, start);
40 let (end_line, _) = byte_offset_to_line_col(&module.line_offsets, end);
41 flag.guard_line_start = Some(start_line);
42 flag.guard_line_end = Some(end_line);
43 }
44
45 flags.push(flag);
46 }
47 }
48
49 flags
50}
51
52pub fn correlate_with_dead_code(flags: &mut [FeatureFlag], results: &AnalysisResults) {
58 if results.unused_exports.is_empty() && results.unused_types.is_empty() {
59 return;
60 }
61
62 for flag in flags.iter_mut() {
63 let (Some(guard_start), Some(guard_end)) = (flag.guard_line_start, flag.guard_line_end)
64 else {
65 continue;
66 };
67
68 for export in &results.unused_exports {
70 if export.path == flag.path && export.line >= guard_start && export.line <= guard_end {
71 flag.guarded_dead_exports.push(export.export_name.clone());
72 }
73 }
74
75 for export in &results.unused_types {
77 if export.path == flag.path && export.line >= guard_start && export.line <= guard_end {
78 flag.guarded_dead_exports.push(export.export_name.clone());
79 }
80 }
81 }
82}
83
84fn flag_use_to_feature_flag(flag_use: &FlagUse, path: PathBuf) -> FeatureFlag {
86 let (kind, confidence) = match flag_use.kind {
87 FlagUseKind::EnvVar => (FlagKind::EnvironmentVariable, FlagConfidence::High),
88 FlagUseKind::SdkCall => (FlagKind::SdkCall, FlagConfidence::High),
89 FlagUseKind::ConfigObject => (FlagKind::ConfigObject, FlagConfidence::Low),
90 };
91
92 FeatureFlag {
93 path,
94 flag_name: flag_use.flag_name.clone(),
95 kind,
96 confidence,
97 line: flag_use.line,
98 col: flag_use.col,
99 guard_span_start: flag_use.guard_span_start,
100 guard_span_end: flag_use.guard_span_end,
101 sdk_name: flag_use.sdk_name.clone(),
102 guard_line_start: None,
103 guard_line_end: None,
104 guarded_dead_exports: Vec::new(),
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn flag_use_to_feature_flag_env_var() {
114 let flag_use = FlagUse {
115 flag_name: "FEATURE_X".to_string(),
116 kind: FlagUseKind::EnvVar,
117 line: 10,
118 col: 4,
119 guard_span_start: Some(100),
120 guard_span_end: Some(200),
121 sdk_name: None,
122 };
123
124 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/config.ts"));
125 assert_eq!(result.flag_name, "FEATURE_X");
126 assert_eq!(result.kind, FlagKind::EnvironmentVariable);
127 assert_eq!(result.confidence, FlagConfidence::High);
128 assert_eq!(result.line, 10);
129 assert!(result.guard_span_start.is_some());
130 }
131
132 #[test]
133 fn flag_use_to_feature_flag_sdk_call() {
134 let flag_use = FlagUse {
135 flag_name: "new-checkout".to_string(),
136 kind: FlagUseKind::SdkCall,
137 line: 5,
138 col: 0,
139 guard_span_start: None,
140 guard_span_end: None,
141 sdk_name: Some("LaunchDarkly".to_string()),
142 };
143
144 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/hooks.ts"));
145 assert_eq!(result.kind, FlagKind::SdkCall);
146 assert_eq!(result.confidence, FlagConfidence::High);
147 assert_eq!(result.sdk_name.as_deref(), Some("LaunchDarkly"));
148 }
149
150 #[test]
151 fn flag_use_to_feature_flag_config_object() {
152 let flag_use = FlagUse {
153 flag_name: "features.newCheckout".to_string(),
154 kind: FlagUseKind::ConfigObject,
155 line: 42,
156 col: 8,
157 guard_span_start: None,
158 guard_span_end: None,
159 sdk_name: None,
160 };
161
162 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/app.ts"));
163 assert_eq!(result.kind, FlagKind::ConfigObject);
164 assert_eq!(result.confidence, FlagConfidence::Low);
165 }
166}