1use std::time::Instant;
2
3use fallow_config::{DetectionMode, DuplicatesConfig};
4use fallow_engine::{project_config::ProjectConfig, session::AnalysisSession};
5use fallow_output::{
6 DUPES_PROGRAMMATIC_SCHEMA_VERSION, DupesNextStepsInput, DupesOutput, DupesOutputInput,
7 build_dupes_next_steps, build_dupes_output, dupes_meta,
8};
9use fallow_types::output_format::OutputFormat;
10use rustc_hash::FxHashSet;
11
12use crate::{
13 DupesReportPayload, DuplicationGroup, DuplicationMode, DuplicationOptions,
14 DuplicationProgrammaticOutput, ProgrammaticError,
15 analysis_context::{
16 ProgrammaticAnalysisContext, changed_files_for_run,
17 resolve_programmatic_analysis_context_deferred_workspace, workspace_roots_for_session,
18 },
19 duplication_filters::{apply_top, filter_by_diff, filter_by_workspaces},
20 next_steps::{setup_pointer_applicable, suggestions_enabled},
21};
22
23use super::{ProgrammaticResult, root_envelope_mode};
24
25pub fn run_duplication(
32 options: &DuplicationOptions,
33) -> ProgrammaticResult<DuplicationProgrammaticOutput> {
34 let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
35 resolved.install(|| run_duplication_inner(options, &resolved))
36}
37
38fn run_duplication_inner(
39 options: &DuplicationOptions,
40 resolved: &ProgrammaticAnalysisContext,
41) -> ProgrammaticResult<DuplicationProgrammaticOutput> {
42 let start = Instant::now();
43 resolved.ensure_not_cancelled("config load and file discovery")?;
44 let session = load_duplication_session(options, resolved)?;
45 run_duplication_with_session(options, resolved, &session, None, start)
46}
47
48pub(super) fn run_duplication_with_session(
49 options: &DuplicationOptions,
50 resolved: &ProgrammaticAnalysisContext,
51 session: &AnalysisSession,
52 changed_files: Option<&FxHashSet<std::path::PathBuf>>,
53 start: Instant,
54) -> ProgrammaticResult<DuplicationProgrammaticOutput> {
55 resolved.ensure_not_cancelled("duplication detection")?;
56 let dupes_config = build_dupes_config(options, &session.config().duplicates);
57 let resolved_changed_files = if changed_files.is_some() {
58 None
59 } else {
60 changed_files_for_run(resolved)?
61 };
62 let cache_dir = (!resolved.no_cache).then_some(session.config().cache_dir.as_path());
63 let report = if let Some(changed_files) = changed_files.or(resolved_changed_files.as_ref()) {
64 let changed_files = changed_files.iter().cloned().collect::<Vec<_>>();
65 session
66 .find_duplicates_touching_files_with_defaults(&dupes_config, &changed_files, cache_dir)
67 .report
68 } else {
69 session
70 .find_duplicates_with_defaults(&dupes_config, cache_dir)
71 .report
72 };
73
74 resolved.ensure_not_cancelled("the duplication report")?;
77 run_duplication_report_with_session(options, resolved, session, report, start)
78}
79
80pub(super) fn run_duplication_report_with_session(
81 options: &DuplicationOptions,
82 resolved: &ProgrammaticAnalysisContext,
83 session: &AnalysisSession,
84 mut report: fallow_engine::duplicates::DuplicationReport,
85 start: Instant,
86) -> ProgrammaticResult<DuplicationProgrammaticOutput> {
87 let dupes_config = build_dupes_config(options, &session.config().duplicates);
88 if let Some(diff) = resolved.diff.as_ref() {
89 filter_by_diff(&mut report, diff, session.root());
90 }
91 let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
92 if let Some(workspace_roots) = workspace_roots.as_ref() {
93 filter_by_workspaces(&mut report, workspace_roots, session.root());
94 }
95 if let Some(top) = options.top {
96 apply_top(&mut report, top, session.root());
97 }
98
99 let root = session.root();
100 let payload = DupesReportPayload::from_report_with_fragments(
101 &report,
102 options.include_fragments.unwrap_or(true),
103 );
104 let clone_fingerprints = payload
105 .clone_groups
106 .iter()
107 .map(|group| group.fingerprint.as_str())
108 .collect::<Vec<_>>();
109 let next_steps = build_dupes_next_steps(DupesNextStepsInput {
110 suggestions_enabled: suggestions_enabled(),
111 clone_fingerprints: &clone_fingerprints,
112 offer_setup: setup_pointer_applicable(root),
113 impact_digest: None,
114 audit_changed: fallow_engine::churn::is_git_repo(root),
115 });
116 let output: DupesOutput<DupesReportPayload, DuplicationGroup> =
117 build_dupes_output(DupesOutputInput {
118 gate_outcomes: None,
119 schema_version: DUPES_PROGRAMMATIC_SCHEMA_VERSION,
120 version: env!("CARGO_PKG_VERSION").to_string(),
121 elapsed: start.elapsed(),
122 report: payload,
123 clone_groups_shown: report.clone_groups_shown(),
124 clone_groups_omitted: report.clone_groups_omitted(),
125 clone_families_shown: report.clone_families_shown(),
126 clone_families_omitted: report.clone_families_omitted(),
127 grouped_by: None,
128 total_issues: None,
129 groups: None,
130 baseline_staleness: None,
133 meta: resolved.explain_enabled().then(dupes_meta),
134 workspace_diagnostics: session.workspace_diagnostics().to_vec(),
135 next_steps,
136 });
137 Ok(DuplicationProgrammaticOutput {
138 output,
139 root: session.root().to_path_buf(),
140 threshold: dupes_config.threshold,
141 envelope_mode: root_envelope_mode(),
142 telemetry_analysis_run_id: None,
143 })
144}
145
146pub(super) fn load_duplication_session(
147 options: &DuplicationOptions,
148 resolved: &ProgrammaticAnalysisContext,
149) -> ProgrammaticResult<AnalysisSession> {
150 let project_config = fallow_engine::project_config::config_for_project_with_load_options(
151 &resolved.root,
152 resolved.config_path.as_deref(),
153 fallow_config::ConfigLoadOptions {
154 allow_remote_extends: resolved.allow_remote_extends(),
155 },
156 )
157 .map_err(|err| {
158 ProgrammaticError::new(format!("failed to load config: {err}"), 2)
159 .with_code("FALLOW_CONFIG_LOAD_FAILED")
160 .with_context("analysis.configPath")
161 })?;
162 let project_config = configure_project_for_duplication(project_config, options, resolved);
163 Ok(super::dead_code::attach_cancellation(
164 AnalysisSession::from_config(project_config),
165 resolved,
166 ))
167}
168
169fn configure_project_for_duplication(
170 mut project_config: ProjectConfig,
171 options: &DuplicationOptions,
172 resolved: &ProgrammaticAnalysisContext,
173) -> ProjectConfig {
174 let production = resolved
175 .production_override
176 .unwrap_or(project_config.config.production);
177 project_config.config.production = production;
178 project_config.config.output = OutputFormat::Json;
179 project_config.config.threads = resolved.threads;
180 project_config.config.no_cache = resolved.no_cache;
181 project_config.config.duplicates =
182 build_dupes_config(options, &project_config.config.duplicates);
183 project_config
184}
185
186pub(super) fn build_dupes_config(
187 options: &DuplicationOptions,
188 config: &DuplicatesConfig,
189) -> DuplicatesConfig {
190 DuplicatesConfig {
191 enabled: true,
192 mode: options.mode.map_or(config.mode, duplication_mode_to_config),
193 near: options.near.unwrap_or(config.near),
194 min_tokens: options.min_tokens.unwrap_or(config.min_tokens),
195 min_lines: options.min_lines.unwrap_or(config.min_lines),
196 min_occurrences: options.min_occurrences.unwrap_or(config.min_occurrences),
197 threshold: options.threshold.unwrap_or(config.threshold),
198 ignore: config.ignore.clone(),
199 ignored_clones: config.ignored_clones.clone(),
200 ignore_defaults: config.ignore_defaults,
201 skip_local: options.skip_local.unwrap_or(config.skip_local),
202 cross_language: options.cross_language.unwrap_or(config.cross_language),
203 ignore_imports: options.ignore_imports.unwrap_or(config.ignore_imports),
204 normalization: config.normalization.clone(),
205 min_corpus_size_for_shingle_filter: config.min_corpus_size_for_shingle_filter,
206 min_corpus_size_for_token_cache: config.min_corpus_size_for_token_cache,
207 }
208}
209
210const fn duplication_mode_to_config(mode: DuplicationMode) -> DetectionMode {
211 match mode {
212 DuplicationMode::Strict => DetectionMode::Strict,
213 DuplicationMode::Mild => DetectionMode::Mild,
214 DuplicationMode::Weak => DetectionMode::Weak,
215 DuplicationMode::Semantic => DetectionMode::Semantic,
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn duplication_options_override_near_and_preserve_reviewed_clones() {
225 let config = DuplicatesConfig {
226 near: true,
227 ignored_clones: vec!["dup:12345678:2".to_string()],
228 ..DuplicatesConfig::default()
229 };
230 let options = DuplicationOptions {
231 near: Some(false),
232 ..DuplicationOptions::default()
233 };
234
235 let merged = build_dupes_config(&options, &config);
236
237 assert!(!merged.near);
238 assert_eq!(merged.ignored_clones, config.ignored_clones);
239 }
240}