Skip to main content

fallow_core/analyze/
feature_flags.rs

1//! Feature flag collection and cross-reference with dead code findings.
2//!
3//! Collects per-file flag uses from parsed modules and builds
4//! project-level `FeatureFlag` results. Optionally correlates with
5//! dead code findings to identify flags guarding unused code.
6
7use 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/// Collect feature flag uses from all parsed modules into `FeatureFlag` results.
15///
16/// Maps extraction-level `FlagUse` (per-file, no path) to result-level
17/// `FeatureFlag` (with full path, confidence). Resolves guard span byte
18/// offsets to line numbers using per-file line offset tables.
19pub 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            // Resolve guard span byte offsets to line numbers
36            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
52/// Correlate feature flags with dead code findings.
53///
54/// For each flag that guards a code span, check if any dead code findings
55/// (unused exports) fall within that span. Populates `guarded_dead_exports`
56/// on each flag.
57pub 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        // Find unused exports in the same file within the guard span
69        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        // Also check unused type exports
76        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
84/// Convert an extraction-level `FlagUse` to a result-level `FeatureFlag`.
85fn 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}