Skip to main content

code_moniker_check/check/
workspace.rs

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}