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