code_moniker_check/check/
workspace.rs1use 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}