Skip to main content

conflic/
pipeline.rs

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}