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
14#[deprecated(
20 since = "2.76.0",
21 note = "fallow_core is internal; there is no programmatic equivalent today. Use the `fallow flags --format json` CLI output for feature-flag data. See docs/fallow-core-migration.md and ADR-008."
22)]
23pub fn collect_feature_flags(modules: &[ModuleInfo], graph: &ModuleGraph) -> Vec<FeatureFlag> {
24 let mut flags = Vec::new();
25
26 for module in modules {
27 if module.flag_uses.is_empty() {
28 continue;
29 }
30
31 let idx = module.file_id.0 as usize;
32 let Some(node) = graph.modules.get(idx) else {
33 continue;
34 };
35
36 for flag_use in &module.flag_uses {
37 let mut flag = flag_use_to_feature_flag(flag_use, node.path.clone());
38
39 if let (Some(start), Some(end)) = (flag_use.guard_span_start, flag_use.guard_span_end)
41 && !module.line_offsets.is_empty()
42 {
43 let (start_line, _) = byte_offset_to_line_col(&module.line_offsets, start);
44 let (end_line, _) = byte_offset_to_line_col(&module.line_offsets, end);
45 flag.guard_line_start = Some(start_line);
46 flag.guard_line_end = Some(end_line);
47 }
48
49 flags.push(flag);
50 }
51 }
52
53 flags
54}
55
56#[deprecated(
62 since = "2.76.0",
63 note = "fallow_core is internal; there is no programmatic equivalent today. Use the `fallow flags --format json` CLI output (the `guarded_dead_exports` field carries the same correlation). See docs/fallow-core-migration.md and ADR-008."
64)]
65pub fn 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 {
78 if export.export.path == flag.path
79 && export.export.line >= guard_start
80 && export.export.line <= guard_end
81 {
82 flag.guarded_dead_exports
83 .push(export.export.export_name.clone());
84 }
85 }
86
87 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 flag_use_to_feature_flag(flag_use: &FlagUse, path: PathBuf) -> FeatureFlag {
102 let (kind, confidence) = match flag_use.kind {
103 FlagUseKind::EnvVar => (FlagKind::EnvironmentVariable, FlagConfidence::High),
104 FlagUseKind::SdkCall => (FlagKind::SdkCall, FlagConfidence::High),
105 FlagUseKind::ConfigObject => (FlagKind::ConfigObject, FlagConfidence::Low),
106 };
107
108 FeatureFlag {
109 path,
110 flag_name: flag_use.flag_name.clone(),
111 kind,
112 confidence,
113 line: flag_use.line,
114 col: flag_use.col,
115 guard_span_start: flag_use.guard_span_start,
116 guard_span_end: flag_use.guard_span_end,
117 sdk_name: flag_use.sdk_name.clone(),
118 guard_line_start: None,
119 guard_line_end: None,
120 guarded_dead_exports: Vec::new(),
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn flag_use_to_feature_flag_env_var() {
130 let flag_use = FlagUse {
131 flag_name: "FEATURE_X".to_string(),
132 kind: FlagUseKind::EnvVar,
133 line: 10,
134 col: 4,
135 guard_span_start: Some(100),
136 guard_span_end: Some(200),
137 sdk_name: None,
138 };
139
140 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/config.ts"));
141 assert_eq!(result.flag_name, "FEATURE_X");
142 assert_eq!(result.kind, FlagKind::EnvironmentVariable);
143 assert_eq!(result.confidence, FlagConfidence::High);
144 assert_eq!(result.line, 10);
145 assert!(result.guard_span_start.is_some());
146 }
147
148 #[test]
149 fn flag_use_to_feature_flag_sdk_call() {
150 let flag_use = FlagUse {
151 flag_name: "new-checkout".to_string(),
152 kind: FlagUseKind::SdkCall,
153 line: 5,
154 col: 0,
155 guard_span_start: None,
156 guard_span_end: None,
157 sdk_name: Some("LaunchDarkly".to_string()),
158 };
159
160 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/hooks.ts"));
161 assert_eq!(result.kind, FlagKind::SdkCall);
162 assert_eq!(result.confidence, FlagConfidence::High);
163 assert_eq!(result.sdk_name.as_deref(), Some("LaunchDarkly"));
164 }
165
166 #[test]
167 fn flag_use_to_feature_flag_config_object() {
168 let flag_use = FlagUse {
169 flag_name: "features.newCheckout".to_string(),
170 kind: FlagUseKind::ConfigObject,
171 line: 42,
172 col: 8,
173 guard_span_start: None,
174 guard_span_end: None,
175 sdk_name: None,
176 };
177
178 let result = flag_use_to_feature_flag(&flag_use, PathBuf::from("src/app.ts"));
179 assert_eq!(result.kind, FlagKind::ConfigObject);
180 assert_eq!(result.confidence, FlagConfidence::Low);
181 }
182}