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.
19#[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            // Resolve guard span byte offsets to line numbers
40            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/// Correlate feature flags with dead code findings.
57///
58/// For each flag that guards a code span, check if any dead code findings
59/// (unused exports) fall within that span. Populates `guarded_dead_exports`
60/// on each flag.
61#[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        // Find unused exports in the same file within the guard span
77        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        // Also check unused type exports
88        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
100/// Convert an extraction-level `FlagUse` to a result-level `FeatureFlag`.
101fn 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}