Skip to main content

fallow_engine/
flags.rs

1//! Feature flag analysis owned by the engine boundary.
2
3use std::path::Path;
4
5use fallow_config::ResolvedConfig;
6use fallow_types::discover::DiscoveredFile;
7use fallow_types::extract::{FlagUse, FlagUseKind, ModuleInfo, ParseResult};
8use fallow_types::results::{FeatureFlag, FlagConfidence, FlagKind};
9use rustc_hash::FxHashMap;
10
11/// Typed result from running feature flag analysis.
12#[derive(Debug, Clone)]
13pub struct FeatureFlagsAnalysis {
14    pub flags: Vec<FeatureFlag>,
15    pub files_scanned: usize,
16}
17
18/// Run feature flag analysis for a resolved project config.
19#[must_use]
20pub fn analyze_feature_flags(config: &ResolvedConfig) -> FeatureFlagsAnalysis {
21    let files = crate::discover_files_with_plugin_scopes(config);
22    let flags = collect_flags_for_files(config, &files);
23    FeatureFlagsAnalysis {
24        flags,
25        files_scanned: files.len(),
26    }
27}
28
29/// Built-in environment variable prefixes treated as feature flags.
30#[must_use]
31pub fn builtin_env_prefixes() -> &'static [&'static str] {
32    fallow_core::extract::flags::builtin_env_prefixes()
33}
34
35/// Distinct built-in SDK provider labels, in declaration order.
36#[must_use]
37pub fn builtin_sdk_providers() -> Vec<&'static str> {
38    fallow_core::extract::flags::builtin_sdk_providers()
39}
40
41fn collect_flags_for_files(config: &ResolvedConfig, files: &[DiscoveredFile]) -> Vec<FeatureFlag> {
42    let cache_store = if config.no_cache {
43        None
44    } else {
45        fallow_core::cache::CacheStore::load(
46            &config.cache_dir,
47            config.cache_config_hash,
48            fallow_core::resolve_cache_max_size_bytes(config),
49        )
50    };
51    let parse_result = fallow_core::extract::parse_all_files(files, cache_store.as_ref(), false);
52
53    let mut flags = collect_flags_from_parse_result(config, files, &parse_result);
54    correlate_flags_with_dead_code(&mut flags, config, &parse_result);
55    flags
56}
57
58fn correlate_flags_with_dead_code(
59    flags: &mut [FeatureFlag],
60    config: &ResolvedConfig,
61    parse_result: &ParseResult,
62) {
63    #[expect(
64        deprecated,
65        reason = "fallow-engine is the typed migration boundary over the internal core backend"
66    )]
67    if let Ok(analysis_output) =
68        fallow_core::analyze_with_parse_result(config, &parse_result.modules)
69    {
70        #[expect(
71            deprecated,
72            reason = "fallow-engine is the typed migration boundary over the internal core backend"
73        )]
74        fallow_core::analyze::feature_flags::correlate_with_dead_code(
75            flags,
76            &analysis_output.results,
77        );
78    }
79}
80
81fn collect_flags_from_parse_result(
82    config: &ResolvedConfig,
83    files: &[DiscoveredFile],
84    parse_result: &ParseResult,
85) -> Vec<FeatureFlag> {
86    let file_paths: FxHashMap<_, _> = files.iter().map(|file| (file.id, &file.path)).collect();
87
88    let extra_sdk: Vec<(String, usize, String)> = config
89        .flags
90        .sdk_patterns
91        .iter()
92        .map(|pattern| {
93            (
94                pattern.function.clone(),
95                pattern.name_arg,
96                pattern.provider.clone().unwrap_or_default(),
97            )
98        })
99        .collect();
100    let has_custom_config = !extra_sdk.is_empty()
101        || !config.flags.env_prefixes.is_empty()
102        || config.flags.config_object_heuristics;
103
104    let mut flags = Vec::new();
105    for module in &parse_result.modules {
106        let Some(path) = file_paths.get(&module.file_id) else {
107            continue;
108        };
109
110        collect_builtin_flags(&mut flags, module, path);
111        if has_custom_config {
112            collect_custom_flags(&mut flags, config, module, path, &extra_sdk);
113        }
114    }
115    flags
116}
117
118fn collect_builtin_flags(flags: &mut Vec<FeatureFlag>, module: &ModuleInfo, path: &Path) {
119    let file_suppressed = fallow_core::suppress::is_file_suppressed(
120        &module.suppressions,
121        fallow_core::suppress::IssueKind::FeatureFlag,
122    );
123    for flag_use in &module.flag_uses {
124        if file_suppressed
125            || fallow_core::suppress::is_suppressed(
126                &module.suppressions,
127                flag_use.line,
128                fallow_core::suppress::IssueKind::FeatureFlag,
129            )
130        {
131            continue;
132        }
133        flags.push(flag_use_to_feature_flag(flag_use, module, path));
134    }
135}
136
137fn collect_custom_flags(
138    flags: &mut Vec<FeatureFlag>,
139    config: &ResolvedConfig,
140    module: &ModuleInfo,
141    path: &Path,
142    extra_sdk: &[(String, usize, String)],
143) {
144    let Ok(source) = std::fs::read_to_string(path) else {
145        return;
146    };
147
148    let custom_flags = fallow_core::extract::flags::extract_flags_from_source(
149        &source,
150        path,
151        extra_sdk,
152        &config.flags.env_prefixes,
153        config.flags.config_object_heuristics,
154    );
155    for flag_use in &custom_flags {
156        let already_found = module.flag_uses.iter().any(|existing| {
157            existing.line == flag_use.line && existing.flag_name == flag_use.flag_name
158        });
159        if !already_found
160            && !fallow_core::suppress::is_suppressed(
161                &module.suppressions,
162                flag_use.line,
163                fallow_core::suppress::IssueKind::FeatureFlag,
164            )
165        {
166            flags.push(flag_use_to_feature_flag(flag_use, module, path));
167        }
168    }
169}
170
171fn flag_use_to_feature_flag(flag_use: &FlagUse, module: &ModuleInfo, path: &Path) -> FeatureFlag {
172    let (kind, confidence) = match flag_use.kind {
173        FlagUseKind::EnvVar => (FlagKind::EnvironmentVariable, FlagConfidence::High),
174        FlagUseKind::SdkCall => (FlagKind::SdkCall, FlagConfidence::High),
175        FlagUseKind::ConfigObject => (FlagKind::ConfigObject, FlagConfidence::Low),
176    };
177
178    let (guard_line_start, guard_line_end) = if let (Some(start), Some(end)) =
179        (flag_use.guard_span_start, flag_use.guard_span_end)
180        && !module.line_offsets.is_empty()
181    {
182        let (start_line, _) =
183            fallow_types::extract::byte_offset_to_line_col(&module.line_offsets, start);
184        let (end_line, _) =
185            fallow_types::extract::byte_offset_to_line_col(&module.line_offsets, end);
186        (Some(start_line), Some(end_line))
187    } else {
188        (None, None)
189    };
190
191    FeatureFlag {
192        path: path.to_path_buf(),
193        flag_name: flag_use.flag_name.clone(),
194        kind,
195        confidence,
196        line: flag_use.line,
197        col: flag_use.col,
198        guard_span_start: flag_use.guard_span_start,
199        guard_span_end: flag_use.guard_span_end,
200        sdk_name: flag_use.sdk_name.clone(),
201        guard_line_start,
202        guard_line_end,
203        guarded_dead_exports: Vec::new(),
204    }
205}