fallow_api/runtime/
feature_flags.rs1use std::time::Instant;
2
3use fallow_engine::{project_config::ProjectConfig, session::AnalysisSession};
4use fallow_output::{
5 CHECK_SCHEMA_VERSION, FeatureFlagsOutputInput, build_feature_flags_output, feature_flags_meta,
6 relative_to_diff_path,
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, root_envelope_mode};
20
21pub fn run_feature_flags(
28 options: &FeatureFlagsOptions,
29) -> ProgrammaticResult<FeatureFlagsProgrammaticOutput> {
30 let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
31 resolved.install(|| run_feature_flags_inner(options, &resolved))
32}
33
34fn run_feature_flags_inner(
35 options: &FeatureFlagsOptions,
36 resolved: &ProgrammaticAnalysisContext,
37) -> ProgrammaticResult<FeatureFlagsProgrammaticOutput> {
38 let start = Instant::now();
39 let session = load_feature_flags_session(resolved)?;
40 let analysis = fallow_engine::flags::analyze_feature_flags_with_session(&session);
41 if analysis.files_scanned == 0 {
42 return Err(ProgrammaticError::new("no files discovered", 2)
43 .with_code("FALLOW_NO_FILES_DISCOVERED")
44 .with_context("feature-flags"));
45 }
46
47 let mut flags = analysis.flags;
48 apply_feature_flags_scope(&mut flags, resolved, &session)?;
49 sort_and_limit_feature_flags(&mut flags, options.top);
50
51 let output = build_feature_flags_output(FeatureFlagsOutputInput {
52 schema_version: CHECK_SCHEMA_VERSION,
53 version: env!("CARGO_PKG_VERSION").to_string(),
54 elapsed: start.elapsed(),
55 flags: &flags,
56 root: session.root(),
57 meta: resolved.explain_enabled().then(feature_flags_meta),
58 });
59
60 Ok(FeatureFlagsProgrammaticOutput {
61 output,
62 envelope_mode: root_envelope_mode(),
63 telemetry_analysis_run_id: None,
64 })
65}
66
67fn load_feature_flags_session(
68 resolved: &ProgrammaticAnalysisContext,
69) -> ProgrammaticResult<AnalysisSession> {
70 let project_config = fallow_engine::project_config::config_for_project(
71 &resolved.root,
72 resolved.config_path.as_deref(),
73 )
74 .map_err(|err| {
75 ProgrammaticError::new(format!("failed to load config: {err}"), 2)
76 .with_code("FALLOW_CONFIG_LOAD_FAILED")
77 .with_context("analysis.configPath")
78 })?;
79 Ok(AnalysisSession::from_config(
80 configure_project_for_feature_flags(project_config, resolved),
81 ))
82}
83
84fn configure_project_for_feature_flags(
85 mut project_config: ProjectConfig,
86 resolved: &ProgrammaticAnalysisContext,
87) -> ProjectConfig {
88 project_config.config.output = OutputFormat::Json;
89 project_config.config.no_cache = resolved.no_cache;
90 project_config.config.threads = resolved.threads;
91 project_config.config.production = resolved
92 .production_override
93 .unwrap_or(project_config.config.production);
94 project_config
95}
96
97fn apply_feature_flags_scope(
98 flags: &mut Vec<FeatureFlag>,
99 resolved: &ProgrammaticAnalysisContext,
100 session: &AnalysisSession,
101) -> ProgrammaticResult<()> {
102 let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
103 if let Some(workspace_roots) = workspace_roots.as_ref() {
104 flags.retain(|flag| {
105 workspace_roots
106 .iter()
107 .any(|root| flag.path.starts_with(root))
108 });
109 }
110 if let Some(changed_files) = changed_files_for_run(resolved)? {
111 flags.retain(|flag| changed_files.contains(&flag.path));
112 }
113 if let Some(diff) = resolved.diff.as_ref() {
114 flags.retain(|flag| {
115 relative_to_diff_path(&flag.path, session.root())
116 .is_none_or(|rel| diff.touches_file(&rel))
117 });
118 }
119 Ok(())
120}
121
122fn sort_and_limit_feature_flags(flags: &mut Vec<FeatureFlag>, top: Option<usize>) {
123 flags.sort_by(|a, b| {
124 a.path
125 .cmp(&b.path)
126 .then(a.line.cmp(&b.line))
127 .then(a.flag_name.cmp(&b.flag_name))
128 });
129
130 if let Some(top) = top {
131 flags.truncate(top);
132 }
133}