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