1use std::collections::hash_map::Entry;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use code_moniker_core::lang::Lang;
6use rustc_hash::FxHashMap;
7
8use crate::check;
9use code_moniker_workspace::environment::{
10 self, IdentityResolver, IndexedSourceMaterial, ResourceCache,
11};
12use code_moniker_workspace::snapshot::{
13 CodeIndex, LinkageSnapshot, ResourceGeneration, SymbolId, SymbolSet,
14};
15
16use crate::{RuleSetRequest, RuleSeverity};
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct WorkspaceCheckRunnerOptions {
20 pub rules: PathBuf,
21 pub profile: Option<String>,
22 pub scheme: String,
23}
24
25impl WorkspaceCheckRunnerOptions {
26 pub fn new(rules: PathBuf, profile: Option<String>, scheme: impl Into<String>) -> Self {
27 Self {
28 rules,
29 profile,
30 scheme: scheme.into(),
31 }
32 }
33}
34
35pub struct WorkspaceCheckRunner {
36 options: WorkspaceCheckRunnerOptions,
37 cache: ResourceCache,
38 state: Option<WorkspaceEvaluationState>,
39}
40
41impl WorkspaceCheckRunner {
42 pub fn new(options: WorkspaceCheckRunnerOptions, cache: ResourceCache) -> Self {
43 Self {
44 options,
45 cache,
46 state: None,
47 }
48 }
49
50 pub fn run_check(
51 &mut self,
52 index: &CodeIndex,
53 linkage: &LinkageSnapshot,
54 ) -> anyhow::Result<WorkspaceRuleDiagnostics> {
55 if linkage.index_generation != index.generation {
56 anyhow::bail!(
57 "linkage snapshot targets index generation {}, current index is {}",
58 linkage.index_generation.value(),
59 index.generation.value()
60 );
61 }
62 let material = environment::cached_index_material(&self.cache, index.generation)
63 .ok_or_else(|| anyhow::anyhow!("code index material is unavailable"))?;
64 let generation = environment::next_resource_generation(&self.cache);
65 let collected = collect_diagnostics(
66 &material,
67 index,
68 &self.options,
69 &self.cache,
70 linkage,
71 self.state.as_ref(),
72 )?;
73 self.state = Some(collected.state);
74 let mut diagnostics = WorkspaceRuleDiagnostics::with_diagnostics(
75 generation,
76 index.generation,
77 collected.diagnostics,
78 collected.reports,
79 );
80 diagnostics.evaluation = collected.metrics;
81 Ok(diagnostics)
82 }
83}
84
85struct WorkspaceEvaluationState {
86 index_generation: ResourceGeneration,
87 fingerprint: String,
88 inventory: Arc<code_moniker_workspace::snapshot::SymbolInventoryIndex>,
89 universe: SymbolSet,
90 evaluation: crate::check::workspace_eval::WorkspaceEvaluation,
91}
92
93struct CollectedDiagnostics {
94 diagnostics: Vec<WorkspaceRuleDiagnostic>,
95 reports: Vec<crate::check::eval::RuleReport>,
96 metrics: WorkspaceEvaluationMetrics,
97 state: WorkspaceEvaluationState,
98}
99
100fn collect_diagnostics(
101 material: &IndexedSourceMaterial,
102 index: &CodeIndex,
103 options: &WorkspaceCheckRunnerOptions,
104 cache: &ResourceCache,
105 linkage: &LinkageSnapshot,
106 previous: Option<&WorkspaceEvaluationState>,
107) -> anyhow::Result<CollectedDiagnostics> {
108 let cfg = load_config(options)?;
109 let excludes = check::UriExclusionMatcher::new(&cfg.exclude.uris);
110 let identity = IdentityResolver::new(options.scheme.clone());
111 let symbol_by_identity = material
112 .symbols()
113 .map(|(id, moniker)| (identity.moniker_uri(moniker), id))
114 .collect::<std::collections::BTreeMap<_, _>>();
115 let mut compiled: FxHashMap<Lang, check::CompiledRules> = FxHashMap::default();
116 let mut diagnostics = Vec::new();
117 for file in material
118 .files
119 .iter()
120 .filter(|file| !excludes.matches_path(&file.path))
121 {
122 let rules = match compiled.entry(file.lang) {
123 Entry::Occupied(entry) => entry.into_mut(),
124 Entry::Vacant(entry) => entry.insert(
125 check::compile_rules(&cfg, file.lang, &options.scheme)
126 .map_err(|err| anyhow::anyhow!(err.to_string()))?,
127 ),
128 };
129 let raw =
130 check::evaluate_compiled(&file.graph, &file.source, file.lang, &options.scheme, rules);
131 let violations = check::apply_suppressions(&file.graph, &file.source, raw);
132 diagnostics.extend(
133 violations
134 .into_iter()
135 .map(|violation| diagnostic_from_violation(violation, None, &symbol_by_identity)),
136 );
137 }
138 let workspace_rules =
139 crate::check::workspace_eval::compile_workspace_rules(&cfg, &options.scheme)?;
140 let included_symbols = included_workspace_symbols(index, &excludes);
141 let fingerprint = workspace_fingerprint(&workspace_rules, &cfg);
142 let (mut workspace_evaluation, metrics) =
143 evaluate_workspace_snapshot(WorkspaceSnapshotEvaluationInput {
144 index,
145 universe: &included_symbols,
146 compiled: &workspace_rules,
147 fingerprint: &fingerprint,
148 cache,
149 linkage,
150 previous,
151 });
152 let mut reports = std::mem::take(&mut workspace_evaluation.reports);
153 let mut workspace_by_source = std::collections::BTreeMap::<
154 usize,
155 Vec<crate::check::workspace_eval::WorkspaceSymbolViolation>,
156 >::new();
157 for violation in workspace_evaluation.violations.iter().cloned() {
158 workspace_by_source
159 .entry(violation.source.file())
160 .or_default()
161 .push(violation);
162 }
163 for (source, workspace_violations) in workspace_by_source {
164 let Some(file) = material.files.get(source) else {
165 continue;
166 };
167 let primary_symbols = workspace_violations
168 .iter()
169 .filter_map(|workspace_violation| {
170 Some((
171 (
172 workspace_violation.violation.rule_id.clone(),
173 workspace_violation.violation.moniker.clone(),
174 ),
175 workspace_violation.symbol?,
176 ))
177 })
178 .collect::<std::collections::BTreeMap<_, _>>();
179 let (suppressible, violations): (Vec<_>, Vec<_>) = workspace_violations
180 .into_iter()
181 .partition(|violation| violation.source_suppression);
182 let mut violations = violations
183 .into_iter()
184 .map(|violation| violation.violation)
185 .collect::<Vec<_>>();
186 violations.extend(check::apply_suppressions(
187 &file.graph,
188 &file.source,
189 suppressible
190 .into_iter()
191 .map(|violation| violation.violation)
192 .collect(),
193 ));
194 diagnostics.extend(violations.into_iter().map(|violation| {
195 let primary = primary_symbols
196 .get(&(violation.rule_id.clone(), violation.moniker.clone()))
197 .copied();
198 diagnostic_from_violation(violation, primary, &symbol_by_identity)
199 }));
200 }
201 realign_workspace_reports(&mut reports, &diagnostics);
202 Ok(CollectedDiagnostics {
203 diagnostics,
204 reports,
205 metrics,
206 state: WorkspaceEvaluationState {
207 index_generation: index.generation,
208 fingerprint,
209 inventory: Arc::clone(&index.inventory),
210 universe: included_symbols,
211 evaluation: workspace_evaluation,
212 },
213 })
214}
215
216fn realign_workspace_reports(
217 reports: &mut [crate::check::eval::RuleReport],
218 diagnostics: &[WorkspaceRuleDiagnostic],
219) {
220 for report in reports.iter_mut().filter(|report| report.verdict.is_some()) {
221 report.violations = diagnostics
222 .iter()
223 .filter(|diagnostic| diagnostic.rule_id == report.rule_id)
224 .count();
225 report.verdict = Some(if report.violations > 0 {
226 crate::check::eval::RuleVerdict::Fail
227 } else if report.inconclusive.unwrap_or_default() > 0 {
228 crate::check::eval::RuleVerdict::Inconclusive
229 } else {
230 crate::check::eval::RuleVerdict::Pass
231 });
232 }
233}
234
235fn included_workspace_symbols(
236 index: &CodeIndex,
237 excludes: &check::UriExclusionMatcher,
238) -> SymbolSet {
239 index
240 .inventory
241 .all_symbols()
242 .iter()
243 .filter(|ordinal| {
244 index.inventory.record(*ordinal).is_some_and(|record| {
245 !excludes.matches_path(Path::new(record.source_path.as_ref()))
246 })
247 })
248 .collect()
249}
250
251fn workspace_fingerprint(
252 compiled: &crate::check::workspace_eval::CompiledWorkspaceRules,
253 cfg: &check::Config,
254) -> String {
255 format!(
256 "{:?}|workspace={:?}|exclude={:?}",
257 compiled.specs(),
258 cfg.workspace,
259 cfg.exclude.uris
260 )
261}
262
263struct WorkspaceSnapshotEvaluationInput<'a> {
264 index: &'a CodeIndex,
265 universe: &'a SymbolSet,
266 compiled: &'a crate::check::workspace_eval::CompiledWorkspaceRules,
267 fingerprint: &'a str,
268 cache: &'a ResourceCache,
269 linkage: &'a LinkageSnapshot,
270 previous: Option<&'a WorkspaceEvaluationState>,
271}
272
273fn evaluate_workspace_snapshot(
274 input: WorkspaceSnapshotEvaluationInput<'_>,
275) -> (
276 crate::check::workspace_eval::WorkspaceEvaluation,
277 WorkspaceEvaluationMetrics,
278) {
279 let WorkspaceSnapshotEvaluationInput {
280 index,
281 universe,
282 compiled,
283 fingerprint,
284 cache,
285 linkage,
286 previous,
287 } = input;
288 if compiled.has_linkage_rules() {
289 return evaluate_workspace_full(index, linkage, universe, compiled);
290 }
291 let Some(previous) = previous.filter(|state| state.fingerprint == fingerprint) else {
292 return evaluate_workspace_full(index, linkage, universe, compiled);
293 };
294 if previous.index_generation == index.generation {
295 return (
296 previous.evaluation.clone(),
297 WorkspaceEvaluationMetrics {
298 mode: WorkspaceEvaluationMode::Incremental,
299 dirty_symbols: 0,
300 evaluated_symbols: 0,
301 affected_groups: 0,
302 },
303 );
304 }
305 let Some((diff_base, diff)) = environment::cached_index_diff(cache, index.generation) else {
306 return evaluate_workspace_full(index, linkage, universe, compiled);
307 };
308 if diff_base != previous.index_generation {
309 return evaluate_workspace_full(index, linkage, universe, compiled);
310 }
311 let incremental = crate::check::workspace_eval::evaluate_workspace_rules_incremental(
312 crate::check::workspace_eval::WorkspaceIncrementalInput {
313 previous_inventory: &previous.inventory,
314 current_inventory: &index.inventory,
315 previous_universe: &previous.universe,
316 current_universe: universe,
317 diff: &diff,
318 compiled,
319 previous: &previous.evaluation,
320 },
321 );
322 (
323 incremental.evaluation,
324 WorkspaceEvaluationMetrics {
325 mode: WorkspaceEvaluationMode::Incremental,
326 dirty_symbols: incremental.dirty_symbols,
327 evaluated_symbols: incremental.evaluated_symbols,
328 affected_groups: incremental.affected_groups,
329 },
330 )
331}
332
333fn evaluate_workspace_full(
334 index: &CodeIndex,
335 linkage: &LinkageSnapshot,
336 universe: &SymbolSet,
337 compiled: &crate::check::workspace_eval::CompiledWorkspaceRules,
338) -> (
339 crate::check::workspace_eval::WorkspaceEvaluation,
340 WorkspaceEvaluationMetrics,
341) {
342 let evaluation = crate::check::workspace_eval::evaluate_workspace_rules_linked_in_current(
343 index,
344 linkage,
345 universe,
346 compiled,
347 compiled.has_linkage_rules(),
348 );
349 let metrics = WorkspaceEvaluationMetrics {
350 mode: WorkspaceEvaluationMode::Full,
351 dirty_symbols: universe.len(),
352 evaluated_symbols: universe.len(),
353 affected_groups: evaluation.groups.len(),
354 };
355 (evaluation, metrics)
356}
357
358fn load_config(options: &WorkspaceCheckRunnerOptions) -> anyhow::Result<check::Config> {
359 RuleSetRequest::with_rules(options.rules.clone(), options.scheme.clone())
360 .with_profile(options.profile.clone())
361 .load_config()
362}
363
364fn diagnostic_from_violation(
365 violation: check::Violation,
366 primary: Option<SymbolId>,
367 symbol_by_identity: &std::collections::BTreeMap<String, SymbolId>,
368) -> WorkspaceRuleDiagnostic {
369 WorkspaceRuleDiagnostic::new(
370 violation.rule_id,
371 violation.severity,
372 primary.or_else(|| symbol_by_identity.get(&violation.moniker).copied()),
373 violation.message,
374 )
375}
376
377#[derive(Clone, Debug, Eq, PartialEq)]
378pub struct WorkspaceRuleDiagnostics {
379 pub generation: ResourceGeneration,
380 pub index_generation: ResourceGeneration,
381 pub evaluation: WorkspaceEvaluationMetrics,
382 pub reports: Vec<crate::check::eval::RuleReport>,
383 pub errors: usize,
384 pub warnings: usize,
385 pub diagnostics: Vec<WorkspaceRuleDiagnostic>,
386}
387
388impl WorkspaceRuleDiagnostics {
389 pub fn with_diagnostics(
390 generation: ResourceGeneration,
391 index_generation: ResourceGeneration,
392 diagnostics: Vec<WorkspaceRuleDiagnostic>,
393 reports: Vec<crate::check::eval::RuleReport>,
394 ) -> Self {
395 let errors = diagnostics
396 .iter()
397 .filter(|diagnostic| diagnostic.severity.is_error())
398 .count();
399 let warnings = diagnostics
400 .iter()
401 .filter(|diagnostic| diagnostic.severity.is_warn())
402 .count();
403 Self {
404 generation,
405 index_generation,
406 evaluation: WorkspaceEvaluationMetrics::full(),
407 reports,
408 errors,
409 warnings,
410 diagnostics,
411 }
412 }
413}
414
415#[derive(Clone, Copy, Debug, Eq, PartialEq)]
416pub enum WorkspaceEvaluationMode {
417 Full,
418 Incremental,
419}
420
421#[derive(Clone, Debug, Eq, PartialEq)]
422pub struct WorkspaceEvaluationMetrics {
423 pub mode: WorkspaceEvaluationMode,
424 pub dirty_symbols: usize,
425 pub evaluated_symbols: usize,
426 pub affected_groups: usize,
427}
428
429impl WorkspaceEvaluationMetrics {
430 fn full() -> Self {
431 Self {
432 mode: WorkspaceEvaluationMode::Full,
433 dirty_symbols: 0,
434 evaluated_symbols: 0,
435 affected_groups: 0,
436 }
437 }
438}
439
440#[derive(Clone, Debug, Eq, PartialEq)]
441pub struct WorkspaceRuleDiagnostic {
442 pub rule_id: String,
443 pub severity: RuleSeverity,
444 pub symbol: Option<SymbolId>,
445 pub message: String,
446}
447
448impl WorkspaceRuleDiagnostic {
449 pub fn new(
450 rule_id: impl Into<String>,
451 severity: RuleSeverity,
452 symbol: Option<SymbolId>,
453 message: impl Into<String>,
454 ) -> Self {
455 Self {
456 rule_id: rule_id.into(),
457 severity,
458 symbol,
459 message: message.into(),
460 }
461 }
462}