Skip to main content

code_moniker_check/check/
workspace.rs

1use std::collections::hash_map::Entry;
2use std::path::PathBuf;
3
4use code_moniker_core::lang::Lang;
5use rustc_hash::FxHashMap;
6
7use crate::check;
8use code_moniker_workspace::environment::{
9	self, IdentityResolver, IndexedSourceMaterial, ResourceCache,
10};
11use code_moniker_workspace::snapshot::{CodeIndex, LinkageSnapshot, ResourceGeneration, SymbolId};
12
13use crate::{RuleSetRequest, RuleSeverity};
14
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct WorkspaceCheckRunnerOptions {
17	pub rules: PathBuf,
18	pub profile: Option<String>,
19	pub scheme: String,
20}
21
22impl WorkspaceCheckRunnerOptions {
23	pub fn new(rules: PathBuf, profile: Option<String>, scheme: impl Into<String>) -> Self {
24		Self {
25			rules,
26			profile,
27			scheme: scheme.into(),
28		}
29	}
30}
31
32pub struct WorkspaceCheckRunner {
33	options: WorkspaceCheckRunnerOptions,
34	cache: ResourceCache,
35}
36
37impl WorkspaceCheckRunner {
38	pub fn new(options: WorkspaceCheckRunnerOptions, cache: ResourceCache) -> Self {
39		Self { options, cache }
40	}
41
42	pub fn run_check(
43		&mut self,
44		index: &CodeIndex,
45		_linkage: &LinkageSnapshot,
46	) -> anyhow::Result<WorkspaceRuleDiagnostics> {
47		let material = environment::cached_index_material(&self.cache, index.generation)
48			.ok_or_else(|| anyhow::anyhow!("code index material is unavailable"))?;
49		let generation = environment::next_resource_generation(&self.cache);
50		let diagnostics = collect_diagnostics(&material, &self.options)?;
51		Ok(WorkspaceRuleDiagnostics::with_diagnostics(
52			generation,
53			index.generation,
54			diagnostics,
55		))
56	}
57}
58
59fn collect_diagnostics(
60	material: &IndexedSourceMaterial,
61	options: &WorkspaceCheckRunnerOptions,
62) -> anyhow::Result<Vec<WorkspaceRuleDiagnostic>> {
63	let cfg = load_config(options)?;
64	let excludes = check::UriExclusionMatcher::new(&cfg.exclude.uris);
65	let identity = IdentityResolver::new(options.scheme.clone());
66	let symbol_by_identity = material
67		.symbols()
68		.map(|(id, moniker)| (identity.moniker_uri(moniker), id))
69		.collect::<std::collections::BTreeMap<_, _>>();
70	let mut compiled: FxHashMap<Lang, check::CompiledRules> = FxHashMap::default();
71	let mut diagnostics = Vec::new();
72	for file in material
73		.files
74		.iter()
75		.filter(|file| !excludes.matches_path(&file.path))
76	{
77		let rules = match compiled.entry(file.lang) {
78			Entry::Occupied(entry) => entry.into_mut(),
79			Entry::Vacant(entry) => entry.insert(
80				check::compile_rules(&cfg, file.lang, &options.scheme)
81					.map_err(|err| anyhow::anyhow!(err.to_string()))?,
82			),
83		};
84		let raw =
85			check::evaluate_compiled(&file.graph, &file.source, file.lang, &options.scheme, rules);
86		let violations = check::apply_suppressions(&file.graph, &file.source, raw);
87		diagnostics.extend(
88			violations
89				.into_iter()
90				.map(|violation| diagnostic_from_violation(violation, &symbol_by_identity)),
91		);
92	}
93	Ok(diagnostics)
94}
95
96fn load_config(options: &WorkspaceCheckRunnerOptions) -> anyhow::Result<check::Config> {
97	RuleSetRequest::with_rules(options.rules.clone(), options.scheme.clone())
98		.with_profile(options.profile.clone())
99		.load_config()
100}
101
102fn diagnostic_from_violation(
103	violation: check::Violation,
104	symbol_by_identity: &std::collections::BTreeMap<String, SymbolId>,
105) -> WorkspaceRuleDiagnostic {
106	WorkspaceRuleDiagnostic::new(
107		violation.rule_id,
108		violation.severity,
109		symbol_by_identity.get(&violation.moniker).cloned(),
110		violation.message,
111	)
112}
113
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub struct WorkspaceRuleDiagnostics {
116	pub generation: ResourceGeneration,
117	pub index_generation: ResourceGeneration,
118	pub errors: usize,
119	pub warnings: usize,
120	pub diagnostics: Vec<WorkspaceRuleDiagnostic>,
121}
122
123impl WorkspaceRuleDiagnostics {
124	pub fn with_diagnostics(
125		generation: ResourceGeneration,
126		index_generation: ResourceGeneration,
127		diagnostics: Vec<WorkspaceRuleDiagnostic>,
128	) -> Self {
129		let errors = diagnostics
130			.iter()
131			.filter(|diagnostic| diagnostic.severity.is_error())
132			.count();
133		let warnings = diagnostics
134			.iter()
135			.filter(|diagnostic| diagnostic.severity.is_warn())
136			.count();
137		Self {
138			generation,
139			index_generation,
140			errors,
141			warnings,
142			diagnostics,
143		}
144	}
145}
146
147#[derive(Clone, Debug, Eq, PartialEq)]
148pub struct WorkspaceRuleDiagnostic {
149	pub rule_id: String,
150	pub severity: RuleSeverity,
151	pub symbol: Option<SymbolId>,
152	pub message: String,
153}
154
155impl WorkspaceRuleDiagnostic {
156	pub fn new(
157		rule_id: impl Into<String>,
158		severity: RuleSeverity,
159		symbol: Option<SymbolId>,
160		message: impl Into<String>,
161	) -> Self {
162		Self {
163			rule_id: rule_id.into(),
164			severity,
165			symbol,
166			message: message.into(),
167		}
168	}
169}