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