1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use rayon::prelude::*;
6
7use crate::config::ConflicConfig;
8use crate::extract::Extractor;
9use crate::model::{ConfigAssertion, ParseDiagnostic, ScanResult};
10use crate::parse;
11use crate::planning::{
12 FileScanPlan, build_plan_for_path, discover_files, group_discovered_files,
13 impacted_extractor_indices, normalize_changed_files, normalize_content_overrides,
14 prepare_extractors, prepare_scan,
15};
16use crate::solve;
17
18#[derive(Debug)]
19pub struct DoctorFileInfo {
20 pub path: PathBuf,
21 pub filename: String,
22 pub matched_extractors: Vec<String>,
23 pub assertions: Vec<ConfigAssertion>,
24 pub parse_diagnostics: Vec<ParseDiagnostic>,
25}
26
27#[derive(Debug)]
28pub struct DoctorReport {
29 pub root: PathBuf,
30 pub discovered_files: HashMap<String, Vec<PathBuf>>,
31 pub file_details: Vec<DoctorFileInfo>,
32 pub scan_result: ScanResult,
33 pub extractor_count: usize,
34 pub extractor_names: Vec<(String, String)>,
35}
36
37#[derive(Debug)]
38pub(crate) struct ScanPipeline {
39 discovered_files: HashMap<String, Vec<PathBuf>>,
40 extractor_count: usize,
41 extractor_names: Vec<(String, String)>,
42 file_outcomes: Vec<ScannedFile>,
43 initial_diagnostics: Vec<ParseDiagnostic>,
44}
45
46#[derive(Debug, Clone)]
47pub(crate) struct ScannedFile {
48 pub(crate) filename: String,
49 pub(crate) path: PathBuf,
50 pub(crate) normalized_path: PathBuf,
51 pub(crate) matched_extractors: Vec<String>,
52 pub(crate) assertions: Vec<ConfigAssertion>,
53 pub(crate) parse_diagnostics: Vec<ParseDiagnostic>,
54}
55
56impl ScanPipeline {
57 pub(crate) fn full_scan_result(&self, root: &Path, config: &ConflicConfig) -> ScanResult {
58 let assertions = self
59 .file_outcomes
60 .iter()
61 .flat_map(|outcome| outcome.assertions.iter().cloned())
62 .collect();
63
64 let mut parse_diagnostics = self.initial_diagnostics.clone();
65 parse_diagnostics.extend(
66 self.file_outcomes
67 .iter()
68 .flat_map(|outcome| outcome.parse_diagnostics.iter().cloned()),
69 );
70
71 build_scan_result(root, config, assertions, parse_diagnostics)
72 }
73
74 pub(crate) fn into_doctor_report(self, root: &Path, config: &ConflicConfig) -> DoctorReport {
75 let scan_result = self.full_scan_result(root, config);
76 let file_details = self.doctor_file_details();
77
78 DoctorReport {
79 root: root.to_path_buf(),
80 discovered_files: self.discovered_files,
81 file_details,
82 scan_result,
83 extractor_count: self.extractor_count,
84 extractor_names: self.extractor_names,
85 }
86 }
87
88 fn doctor_file_details(&self) -> Vec<DoctorFileInfo> {
89 self.file_outcomes
90 .iter()
91 .map(|outcome| DoctorFileInfo {
92 path: outcome.path.clone(),
93 filename: outcome.filename.clone(),
94 matched_extractors: outcome.matched_extractors.clone(),
95 assertions: outcome.assertions.clone(),
96 parse_diagnostics: outcome.parse_diagnostics.clone(),
97 })
98 .collect()
99 }
100}
101
102pub(crate) fn run_scan_pipeline(
103 root: &Path,
104 config: &ConflicConfig,
105 content_overrides: Option<&HashMap<PathBuf, String>>,
106) -> ScanPipeline {
107 let preparation = prepare_scan(root, config);
108 let normalized_overrides =
109 content_overrides.map(|overrides| Arc::new(normalize_content_overrides(root, overrides)));
110 let mut file_outcomes = process_file_plans(
111 root,
112 &preparation.file_plans,
113 &preparation.extractors,
114 normalized_overrides.as_ref(),
115 );
116 file_outcomes.sort_by(|left, right| left.path.cmp(&right.path));
117
118 ScanPipeline {
119 discovered_files: preparation.discovered_files,
120 extractor_count: preparation.extractor_count,
121 extractor_names: preparation.extractor_names,
122 file_outcomes,
123 initial_diagnostics: preparation.initial_diagnostics,
124 }
125}
126
127pub(crate) fn run_diff_scan_pipeline(
128 root: &Path,
129 config: &ConflicConfig,
130 changed_files: &[PathBuf],
131) -> ScanPipeline {
132 let changed_normalized = normalize_changed_files(root, changed_files);
133 let preparation = prepare_extractors(config);
134 let normalized_config_path =
135 crate::pathing::normalize_for_workspace(root, &config.resolved_config_path(root));
136 let config_changed = changed_files
137 .iter()
138 .map(|path| crate::pathing::normalize_for_workspace(root, path))
139 .any(|path| path == normalized_config_path);
140
141 let mut impacted_concepts = HashSet::new();
142 let mut changed_plans = Vec::new();
143
144 for path in &changed_normalized {
145 if let Some(plan) = build_plan_for_path(root, path, &preparation.extractors, None) {
146 impacted_concepts.extend(plan.concept_ids.iter().cloned());
147 if path.exists() {
148 changed_plans.push(plan);
149 }
150 }
151 }
152
153 if config_changed {
154 impacted_concepts.extend(
155 preparation
156 .extractors
157 .iter()
158 .flat_map(|extractor| extractor.concept_ids()),
159 );
160 }
161
162 let mut file_outcomes = process_file_plans(root, &changed_plans, &preparation.extractors, None);
163 impacted_concepts.extend(file_outcomes.iter().flat_map(|outcome| {
164 outcome
165 .assertions
166 .iter()
167 .map(|assertion| assertion.concept.id.clone())
168 }));
169
170 let mut scanned_plans = changed_plans.clone();
171 if !impacted_concepts.is_empty() {
172 let impacted_extractors =
173 impacted_extractor_indices(&preparation.extractors, &impacted_concepts);
174 if !impacted_extractors.is_empty() {
175 let discovered_files = discover_files(root, config);
176 let mut peer_plans = Vec::new();
177
178 for paths in discovered_files.values() {
179 for path in paths {
180 let normalized_path = crate::pathing::normalize_for_workspace(root, path);
181 if changed_normalized.contains(&normalized_path) {
182 continue;
183 }
184
185 if let Some(plan) = build_plan_for_path(
186 root,
187 path,
188 &preparation.extractors,
189 Some(&impacted_extractors),
190 ) {
191 peer_plans.push(plan);
192 }
193 }
194 }
195
196 file_outcomes.extend(process_file_plans(
197 root,
198 &peer_plans,
199 &preparation.extractors,
200 None,
201 ));
202 scanned_plans.extend(peer_plans);
203 }
204 }
205
206 file_outcomes.sort_by(|left, right| left.path.cmp(&right.path));
207
208 ScanPipeline {
209 discovered_files: group_discovered_files(&scanned_plans),
210 extractor_count: preparation.extractor_count,
211 extractor_names: preparation.extractor_names,
212 file_outcomes,
213 initial_diagnostics: preparation.initial_diagnostics,
214 }
215}
216
217pub(crate) fn process_file_plans(
218 root: &Path,
219 plans: &[FileScanPlan],
220 extractors: &[Box<dyn Extractor>],
221 content_overrides: Option<&Arc<HashMap<PathBuf, String>>>,
222) -> Vec<ScannedFile> {
223 plans
224 .par_iter()
225 .map(|plan| process_file_plan(root, plan, extractors, content_overrides))
226 .collect()
227}
228
229fn process_file_plan(
230 root: &Path,
231 plan: &FileScanPlan,
232 extractors: &[Box<dyn Extractor>],
233 content_overrides: Option<&Arc<HashMap<PathBuf, String>>>,
234) -> ScannedFile {
235 let matched_extractors: Vec<String> = plan
236 .extractor_indices
237 .iter()
238 .map(|index| extractors[*index].id().to_string())
239 .collect();
240
241 if plan.extractor_indices.is_empty() {
242 return ScannedFile {
243 filename: plan.filename.clone(),
244 path: plan.path.clone(),
245 normalized_path: plan.normalized_path.clone(),
246 matched_extractors,
247 assertions: Vec::new(),
248 parse_diagnostics: Vec::new(),
249 };
250 }
251
252 let parsed = match content_overrides {
253 Some(overrides) => parse::parse_file_with_shared_context(
254 &plan.path,
255 root,
256 overrides.get(&plan.normalized_path).cloned(),
257 Arc::clone(overrides),
258 ),
259 None => parse::parse_file(&plan.path, root),
260 };
261
262 match parsed {
263 Ok(parsed) => {
264 let mut assertions = Vec::new();
265 for index in &plan.extractor_indices {
266 assertions.extend(extractors[*index].extract(&parsed));
267 }
268
269 ScannedFile {
270 filename: plan.filename.clone(),
271 path: plan.path.clone(),
272 normalized_path: plan.normalized_path.clone(),
273 matched_extractors,
274 assertions,
275 parse_diagnostics: parsed.take_parse_diagnostics(),
276 }
277 }
278 Err(error) => ScannedFile {
279 filename: plan.filename.clone(),
280 path: plan.path.clone(),
281 normalized_path: plan.normalized_path.clone(),
282 matched_extractors,
283 assertions: Vec::new(),
284 parse_diagnostics: vec![error],
285 },
286 }
287}
288
289fn build_scan_result(
290 root: &Path,
291 config: &ConflicConfig,
292 assertions: Vec<ConfigAssertion>,
293 parse_diagnostics: Vec<ParseDiagnostic>,
294) -> ScanResult {
295 let solvers = build_solver_registry(config);
296 let mut concept_results =
297 solve::compare_assertions_with_solvers(root, assertions, config, &solvers);
298
299 crate::policy::evaluate_policies(&mut concept_results, config);
300 crate::graph::evaluate_concept_rules(&mut concept_results, config);
301
302 ScanResult {
303 concept_results,
304 parse_diagnostics,
305 }
306}
307
308fn build_solver_registry(config: &ConflicConfig) -> solve::SolverRegistry {
309 let mut registry = solve::SolverRegistry::new();
310 for custom in &config.custom_extractor {
311 if let Some(ref solver_name) = custom.solver
312 && let Some(solver) = solve::registry::solver_from_name(solver_name)
313 {
314 registry.register(custom.concept.clone(), solver);
315 }
316 }
317 registry
318}