1mod text;
21
22use crate::level_graph::LevelGraph;
23use crate::nodepath::{node_path, split_path};
24use cel::{Context, Program, Value};
25use code_ranker_plugin_api::{attrs::AttrValue, node::EXTERNAL, node::Node};
26use serde::Deserialize;
27use std::collections::{BTreeMap, HashMap, HashSet};
28use std::sync::Arc;
29use text::{references, render_message, replace_word};
30
31const DEFAULT_GROUP: &str = "LNT";
33
34#[derive(Debug, Clone, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct CheckDef {
38 pub when: String,
40 pub message: String,
44 #[serde(default)]
47 pub group: Option<String>,
48 #[serde(default)]
50 pub why: Option<String>,
51 #[serde(default)]
52 pub fix: Option<String>,
53 #[serde(default)]
55 pub title: Option<String>,
56}
57
58pub struct CompiledCheck {
61 pub id: String,
62 pub def: CheckDef,
63 program: Program,
64 uses: Uses,
65}
66
67#[derive(Default, Clone, Copy)]
70struct Uses {
71 deps: bool,
72 rdeps: bool,
73 files: bool,
74 siblings: bool,
75}
76
77#[derive(Debug, Clone)]
80pub struct CheckCompileError {
81 pub id: String,
82 pub message: String,
83}
84
85impl std::fmt::Display for CheckCompileError {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 write!(
88 f,
89 "check `{}`: invalid `when` predicate: {}",
90 self.id, self.message
91 )
92 }
93}
94
95impl std::error::Error for CheckCompileError {}
96
97#[derive(Debug, Clone)]
99pub struct CheckHit {
100 pub id: String,
101 pub message: String,
102 pub group: String,
103 pub why: Option<String>,
104 pub fix: Option<String>,
105 pub title: Option<String>,
106}
107
108pub fn compile(
112 id: &str,
113 def: &CheckDef,
114 defs: &BTreeMap<String, String>,
115) -> Result<CompiledCheck, CheckCompileError> {
116 let when = expand_defs(id, &def.when, defs)?;
117 let program = Program::compile(&when).map_err(|e| CheckCompileError {
118 id: id.to_string(),
119 message: e.to_string(),
120 })?;
121 let uses = Uses {
122 deps: references(&when, "deps"),
123 rdeps: references(&when, "rdeps"),
124 files: references(&when, "files"),
125 siblings: references(&when, "siblings"),
126 };
127 Ok(CompiledCheck {
128 id: id.to_string(),
129 def: def.clone(),
130 program,
131 uses,
132 })
133}
134
135impl CompiledCheck {
136 pub fn eval(&self, node: &Node, graph: &GraphView) -> Option<CheckHit> {
142 let mut ctx = Context::default();
148 crate::registry::register_math(&mut ctx);
149 graph.register_agg(&mut ctx);
153 register_graph_fns(&mut ctx, graph, &node.id);
154 bind_node(&mut ctx, node);
155 self.bind_collections(&mut ctx, node, graph);
156 match self.program.execute(&ctx) {
157 Ok(Value::Bool(true)) => Some(CheckHit {
158 id: self.id.clone(),
159 message: render_message(&self.def.message, node),
160 group: self
161 .def
162 .group
163 .clone()
164 .unwrap_or_else(|| DEFAULT_GROUP.to_string()),
165 why: self.def.why.as_deref().map(|s| render_message(s, node)),
168 fix: self.def.fix.as_deref().map(|s| render_message(s, node)),
169 title: self.def.title.clone(),
170 }),
171 _ => None,
172 }
173 }
174
175 fn bind_collections(&self, ctx: &mut Context, node: &Node, graph: &GraphView) {
181 if self.uses.deps {
182 let _ = ctx.add_variable("deps", graph.deps(&node.id));
183 }
184 if self.uses.rdeps {
185 let _ = ctx.add_variable("rdeps", graph.rdeps(&node.id));
186 }
187 if self.uses.files {
188 let _ = ctx.add_variable("files", graph.files_vec());
189 }
190 if self.uses.siblings {
191 let _ = ctx.add_variable("siblings", graph.siblings(&node_path(node)));
192 }
193 }
194}
195
196fn bind_node(ctx: &mut Context, node: &Node) {
199 for (key, value) in node.attrs.iter() {
200 match value {
201 AttrValue::Int(i) => {
202 let _ = ctx.add_variable(key.as_str(), *i);
203 }
204 AttrValue::Float(f) => {
205 let _ = ctx.add_variable(key.as_str(), *f);
206 }
207 AttrValue::Bool(b) => {
208 let _ = ctx.add_variable(key.as_str(), *b);
209 }
210 AttrValue::Str(s) => {
211 let _ = ctx.add_variable(key.as_str(), s.clone());
212 }
213 }
214 }
215 let path = node_path(node);
219 let parts = split_path(&path);
220 let _ = ctx.add_variable("path", path);
221 let _ = ctx.add_variable("name", parts.name);
222 let _ = ctx.add_variable("stem", parts.stem);
223 let _ = ctx.add_variable("ext", parts.ext);
224 let _ = ctx.add_variable("dir", parts.dir);
225}
226
227fn register_graph_fns(ctx: &mut Context, graph: &GraphView, node_id: &str) {
232 let out = graph.deps(node_id);
233 ctx.add_function("depends_on", move |s: Arc<String>| -> bool {
234 out.iter().any(|d| d.contains(s.as_str()))
235 });
236 let inc = graph.rdeps(node_id);
237 ctx.add_function("depended_on_by", move |s: Arc<String>| -> bool {
238 inc.iter().any(|d| d.contains(s.as_str()))
239 });
240 let files = graph.files_set_arc();
241 ctx.add_function("file_exists", move |p: Arc<String>| -> bool {
242 files.contains(p.as_str())
243 });
244}
245
246#[derive(Default)]
252pub struct GraphView {
253 out: HashMap<String, Vec<String>>,
254 inc: HashMap<String, Vec<String>>,
255 files: Arc<Vec<String>>,
256 files_set: Arc<HashSet<String>>,
257 by_dir: HashMap<String, Vec<String>>,
258 pops: Arc<crate::registry::Populations>,
262 agg_cache: AggCache,
266}
267
268type AggCache = Arc<std::sync::Mutex<HashMap<(String, String, String), f64>>>;
270
271impl GraphView {
272 pub fn build(level: &LevelGraph) -> Self {
275 let mut label: HashMap<String, String> = HashMap::new();
276 let mut files: Vec<String> = Vec::new();
277 let mut by_dir: HashMap<String, Vec<String>> = HashMap::new();
278 let mut rows: Vec<BTreeMap<String, f64>> = Vec::new();
279 let mut metric_keys: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
280 for n in &level.nodes {
281 let l = label_of(n);
282 label.insert(n.id.clone(), l.clone());
283 if n.kind != EXTERNAL {
284 files.push(l.clone());
285 by_dir.entry(split_path(&l).dir).or_default().push(l);
286 let row = numeric_attrs(n);
287 metric_keys.extend(row.keys().cloned());
288 rows.push(row);
289 }
290 }
291 sort_dedup(&mut files);
292 for v in by_dir.values_mut() {
293 sort_dedup(v);
294 }
295 let files_set: HashSet<String> = files.iter().cloned().collect();
296
297 let keys: Vec<String> = metric_keys.into_iter().collect();
301 let omit_at: BTreeMap<String, f64> = keys
302 .iter()
303 .map(|k| {
304 let floor = level
305 .node_attributes
306 .get(k)
307 .map(|s| s.omit_at)
308 .unwrap_or(0.0);
309 (k.clone(), floor)
310 })
311 .collect();
312 let pops = crate::registry::Populations::build(&rows, &keys, &omit_at);
313
314 let mut out: HashMap<String, Vec<String>> = HashMap::new();
315 let mut inc: HashMap<String, Vec<String>> = HashMap::new();
316 let resolve = |id: &str| label.get(id).cloned().unwrap_or_else(|| id.to_string());
317 for e in &level.edges {
318 out.entry(e.source.clone())
319 .or_default()
320 .push(resolve(&e.target));
321 inc.entry(e.target.clone())
322 .or_default()
323 .push(resolve(&e.source));
324 }
325 for v in out.values_mut() {
326 sort_dedup(v);
327 }
328 for v in inc.values_mut() {
329 sort_dedup(v);
330 }
331
332 GraphView {
333 out,
334 inc,
335 files: Arc::new(files),
336 files_set: Arc::new(files_set),
337 by_dir,
338 pops: Arc::new(pops),
339 agg_cache: Arc::new(std::sync::Mutex::new(HashMap::new())),
340 }
341 }
342
343 fn register_agg(&self, ctx: &mut Context) {
346 let pops = self.pops.clone();
347 let cache = self.agg_cache.clone();
348 ctx.add_function(
349 "agg",
350 move |key: Arc<String>, reducer: Arc<String>, population: Arc<String>| -> f64 {
351 let k = (
352 key.as_str().to_string(),
353 reducer.as_str().to_string(),
354 population.as_str().to_string(),
355 );
356 if let Some(v) = cache.lock().unwrap().get(&k) {
357 return *v;
358 }
359 let v = pops.reduce_for(&key, &reducer, &population);
360 cache.lock().unwrap().insert(k, v);
361 v
362 },
363 );
364 }
365
366 fn deps(&self, id: &str) -> Vec<String> {
367 self.out.get(id).cloned().unwrap_or_default()
368 }
369
370 fn rdeps(&self, id: &str) -> Vec<String> {
371 self.inc.get(id).cloned().unwrap_or_default()
372 }
373
374 fn files_vec(&self) -> Vec<String> {
375 (*self.files).clone()
376 }
377
378 fn files_set_arc(&self) -> Arc<HashSet<String>> {
379 self.files_set.clone()
380 }
381
382 fn siblings(&self, path: &str) -> Vec<String> {
384 let dir = split_path(path).dir;
385 self.by_dir
386 .get(&dir)
387 .map(|v| v.iter().filter(|f| f.as_str() != path).cloned().collect())
388 .unwrap_or_default()
389 }
390}
391
392fn numeric_attrs(node: &Node) -> BTreeMap<String, f64> {
394 let mut m = BTreeMap::new();
395 for (k, v) in node.attrs.iter() {
396 match v {
397 AttrValue::Int(i) => {
398 m.insert(k.clone(), *i as f64);
399 }
400 AttrValue::Float(f) => {
401 m.insert(k.clone(), *f);
402 }
403 _ => {}
404 }
405 }
406 m
407}
408
409fn sort_dedup(v: &mut Vec<String>) {
410 v.sort();
411 v.dedup();
412}
413
414fn label_of(node: &Node) -> String {
420 if node.kind == EXTERNAL || node.id.starts_with("ext:") {
421 return node.id.clone();
422 }
423 node_path(node)
424}
425
426fn expand_defs(
432 id: &str,
433 expr: &str,
434 defs: &BTreeMap<String, String>,
435) -> Result<String, CheckCompileError> {
436 let mut out = expr.to_string();
437 for _ in 0..=defs.len() {
440 let mut changed = false;
441 for (name, body) in defs {
442 if references(&out, name) {
443 out = replace_word(&out, name, &format!("({body})"));
444 changed = true;
445 }
446 }
447 if !changed {
448 return Ok(out);
449 }
450 }
451 Err(CheckCompileError {
452 id: id.to_string(),
453 message: "`[rules.defs]` helpers reference each other in a cycle".to_string(),
454 })
455}
456
457#[cfg(test)]
458#[path = "checks_test.rs"]
459mod tests;