fallow_api/runtime/
feature_flags.rs1use std::time::Instant;
2
3use fallow_engine::{project_config::ProjectConfig, session::AnalysisSession};
4use fallow_output::{
5 FEATURE_FLAGS_SCHEMA_VERSION, FeatureFlagsOutputInput, build_feature_flags_output,
6 feature_flags_meta,
7};
8use fallow_types::output_format::OutputFormat;
9use fallow_types::results::FeatureFlag;
10
11use crate::{
12 FeatureFlagsOptions, FeatureFlagsProgrammaticOutput, ProgrammaticError,
13 analysis_context::{
14 ProgrammaticAnalysisContext, changed_files_for_run,
15 resolve_programmatic_analysis_context_deferred_workspace, workspace_roots_for_session,
16 },
17};
18
19use super::ProgrammaticResult;
20
21pub fn run_feature_flags(
31 options: &FeatureFlagsOptions,
32) -> ProgrammaticResult<FeatureFlagsProgrammaticOutput> {
33 let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
34 resolved.install(|| run_feature_flags_inner(options, &resolved))
35}
36
37fn run_feature_flags_inner(
38 options: &FeatureFlagsOptions,
39 resolved: &ProgrammaticAnalysisContext,
40) -> ProgrammaticResult<FeatureFlagsProgrammaticOutput> {
41 let start = Instant::now();
42 resolved.ensure_not_cancelled("config load and file discovery")?;
43 let session = load_feature_flags_session(resolved)?;
44 let analysis =
45 fallow_engine::flags::analyze_feature_flags_with_session(&session).map_err(|err| {
46 super::dead_code::map_engine_error(
47 &err,
48 "feature-flag analysis failed",
49 "FALLOW_FEATURE_FLAGS_FAILED",
50 "feature-flags",
51 )
52 })?;
53 if analysis.files_scanned == 0 {
54 return Err(ProgrammaticError::new("no files discovered", 2)
55 .with_code("FALLOW_NO_FILES_DISCOVERED")
56 .with_context("feature-flags"));
57 }
58
59 let mut flags = analysis.flags;
60 apply_feature_flags_scope(&mut flags, resolved, &session)?;
61 sort_and_limit_feature_flags(&mut flags, options.top);
62
63 let output = build_feature_flags_output(FeatureFlagsOutputInput {
64 schema_version: FEATURE_FLAGS_SCHEMA_VERSION,
65 version: env!("CARGO_PKG_VERSION").to_string(),
66 elapsed: start.elapsed(),
67 flags: &flags,
68 root: session.root(),
69 workspace_diagnostics: session.current_workspace_diagnostics(),
73 request_outcomes: resolved.request_outcomes(),
77 meta: resolved.explain_enabled().then(feature_flags_meta),
78 });
79
80 Ok(FeatureFlagsProgrammaticOutput {
81 output,
82 telemetry_analysis_run_id: None,
83 })
84}
85
86fn load_feature_flags_session(
87 resolved: &ProgrammaticAnalysisContext,
88) -> ProgrammaticResult<AnalysisSession> {
89 let project_config = fallow_engine::project_config::config_for_project_with_load_options(
90 &resolved.root,
91 resolved.config_path.as_deref(),
92 fallow_config::ConfigLoadOptions {
93 allow_remote_extends: resolved.allow_remote_extends(),
94 },
95 )
96 .map_err(|err| {
97 ProgrammaticError::new(format!("failed to load config: {err}"), 2)
98 .with_code("FALLOW_CONFIG_LOAD_FAILED")
99 .with_context("analysis.configPath")
100 })?;
101 Ok(super::dead_code::attach_cancellation(
102 AnalysisSession::from_config(configure_project_for_feature_flags(
103 project_config,
104 resolved,
105 )),
106 resolved,
107 ))
108}
109
110fn configure_project_for_feature_flags(
111 mut project_config: ProjectConfig,
112 resolved: &ProgrammaticAnalysisContext,
113) -> ProjectConfig {
114 project_config.config.output = OutputFormat::Json;
115 project_config.config.no_cache = resolved.no_cache;
116 project_config.config.threads = resolved.threads;
117 project_config.config.production = resolved
118 .production_override
119 .unwrap_or(project_config.config.production);
120 project_config
121}
122
123fn apply_feature_flags_scope(
124 flags: &mut Vec<FeatureFlag>,
125 resolved: &ProgrammaticAnalysisContext,
126 session: &AnalysisSession,
127) -> ProgrammaticResult<()> {
128 let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
129 if let Some(workspace_roots) = workspace_roots.as_ref() {
130 flags.retain(|flag| {
131 workspace_roots
132 .iter()
133 .any(|root| flag.path.starts_with(root))
134 });
135 }
136 if let Some(changed_files) = changed_files_for_run(resolved)? {
137 resolved
138 .measure_changed_since_scope(session.files().iter().map(|file| file.path.as_path()));
139 flags.retain(|flag| changed_files.contains(&flag.path));
140 }
141 if let Some(diff) = resolved.diff.as_ref() {
142 flags.retain(|flag| {
143 diff.key_for(&flag.path, session.root())
144 .is_none_or(|rel| diff.touches_file(&rel))
145 });
146 }
147 Ok(())
148}
149
150fn sort_and_limit_feature_flags(flags: &mut Vec<FeatureFlag>, top: Option<usize>) {
151 flags.sort_by(|a, b| {
152 a.path
153 .cmp(&b.path)
154 .then(a.line.cmp(&b.line))
155 .then(a.flag_name.cmp(&b.flag_name))
156 });
157
158 if let Some(top) = top {
159 flags.truncate(top);
160 }
161}