mod text;
use crate::level_graph::LevelGraph;
use crate::nodepath::{node_path, split_path};
use cel::{Context, Program, Value};
use code_ranker_plugin_api::{attrs::AttrValue, node::EXTERNAL, node::Node};
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
use text::{references, render_message, replace_word};
const DEFAULT_GROUP: &str = "LNT";
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CheckDef {
pub when: String,
pub message: String,
#[serde(default)]
pub group: Option<String>,
#[serde(default)]
pub why: Option<String>,
#[serde(default)]
pub fix: Option<String>,
#[serde(default)]
pub title: Option<String>,
}
pub struct CompiledCheck {
pub id: String,
pub def: CheckDef,
program: Program,
uses: Uses,
}
#[derive(Default, Clone, Copy)]
struct Uses {
deps: bool,
rdeps: bool,
files: bool,
siblings: bool,
}
#[derive(Debug, Clone)]
pub struct CheckCompileError {
pub id: String,
pub message: String,
}
impl std::fmt::Display for CheckCompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"check `{}`: invalid `when` predicate: {}",
self.id, self.message
)
}
}
impl std::error::Error for CheckCompileError {}
#[derive(Debug, Clone)]
pub struct CheckHit {
pub id: String,
pub message: String,
pub group: String,
pub why: Option<String>,
pub fix: Option<String>,
pub title: Option<String>,
}
pub fn compile(
id: &str,
def: &CheckDef,
defs: &BTreeMap<String, String>,
) -> Result<CompiledCheck, CheckCompileError> {
let when = expand_defs(id, &def.when, defs)?;
let program = Program::compile(&when).map_err(|e| CheckCompileError {
id: id.to_string(),
message: e.to_string(),
})?;
let uses = Uses {
deps: references(&when, "deps"),
rdeps: references(&when, "rdeps"),
files: references(&when, "files"),
siblings: references(&when, "siblings"),
};
Ok(CompiledCheck {
id: id.to_string(),
def: def.clone(),
program,
uses,
})
}
impl CompiledCheck {
pub fn eval(&self, node: &Node, graph: &GraphView) -> Option<CheckHit> {
let mut ctx = Context::default();
crate::registry::register_math(&mut ctx);
graph.register_agg(&mut ctx);
register_graph_fns(&mut ctx, graph, &node.id);
bind_node(&mut ctx, node);
self.bind_collections(&mut ctx, node, graph);
match self.program.execute(&ctx) {
Ok(Value::Bool(true)) => Some(CheckHit {
id: self.id.clone(),
message: render_message(&self.def.message, node),
group: self
.def
.group
.clone()
.unwrap_or_else(|| DEFAULT_GROUP.to_string()),
why: self.def.why.as_deref().map(|s| render_message(s, node)),
fix: self.def.fix.as_deref().map(|s| render_message(s, node)),
title: self.def.title.clone(),
}),
_ => None,
}
}
fn bind_collections(&self, ctx: &mut Context, node: &Node, graph: &GraphView) {
if self.uses.deps {
let _ = ctx.add_variable("deps", graph.deps(&node.id));
}
if self.uses.rdeps {
let _ = ctx.add_variable("rdeps", graph.rdeps(&node.id));
}
if self.uses.files {
let _ = ctx.add_variable("files", graph.files_vec());
}
if self.uses.siblings {
let _ = ctx.add_variable("siblings", graph.siblings(&node_path(node)));
}
}
}
fn bind_node(ctx: &mut Context, node: &Node) {
for (key, value) in node.attrs.iter() {
match value {
AttrValue::Int(i) => {
let _ = ctx.add_variable(key.as_str(), *i);
}
AttrValue::Float(f) => {
let _ = ctx.add_variable(key.as_str(), *f);
}
AttrValue::Bool(b) => {
let _ = ctx.add_variable(key.as_str(), *b);
}
AttrValue::Str(s) => {
let _ = ctx.add_variable(key.as_str(), s.clone());
}
}
}
let path = node_path(node);
let parts = split_path(&path);
let _ = ctx.add_variable("path", path);
let _ = ctx.add_variable("name", parts.name);
let _ = ctx.add_variable("stem", parts.stem);
let _ = ctx.add_variable("ext", parts.ext);
let _ = ctx.add_variable("dir", parts.dir);
}
fn register_graph_fns(ctx: &mut Context, graph: &GraphView, node_id: &str) {
let out = graph.deps(node_id);
ctx.add_function("depends_on", move |s: Arc<String>| -> bool {
out.iter().any(|d| d.contains(s.as_str()))
});
let inc = graph.rdeps(node_id);
ctx.add_function("depended_on_by", move |s: Arc<String>| -> bool {
inc.iter().any(|d| d.contains(s.as_str()))
});
let files = graph.files_set_arc();
ctx.add_function("file_exists", move |p: Arc<String>| -> bool {
files.contains(p.as_str())
});
}
#[derive(Default)]
pub struct GraphView {
out: HashMap<String, Vec<String>>,
inc: HashMap<String, Vec<String>>,
files: Arc<Vec<String>>,
files_set: Arc<HashSet<String>>,
by_dir: HashMap<String, Vec<String>>,
pops: Arc<crate::registry::Populations>,
agg_cache: AggCache,
}
type AggCache = Arc<std::sync::Mutex<HashMap<(String, String, String), f64>>>;
impl GraphView {
pub fn build(level: &LevelGraph) -> Self {
let mut label: HashMap<String, String> = HashMap::new();
let mut files: Vec<String> = Vec::new();
let mut by_dir: HashMap<String, Vec<String>> = HashMap::new();
let mut rows: Vec<BTreeMap<String, f64>> = Vec::new();
let mut metric_keys: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for n in &level.nodes {
let l = label_of(n);
label.insert(n.id.clone(), l.clone());
if n.kind != EXTERNAL {
files.push(l.clone());
by_dir.entry(split_path(&l).dir).or_default().push(l);
let row = numeric_attrs(n);
metric_keys.extend(row.keys().cloned());
rows.push(row);
}
}
sort_dedup(&mut files);
for v in by_dir.values_mut() {
sort_dedup(v);
}
let files_set: HashSet<String> = files.iter().cloned().collect();
let keys: Vec<String> = metric_keys.into_iter().collect();
let omit_at: BTreeMap<String, f64> = keys
.iter()
.map(|k| {
let floor = level
.node_attributes
.get(k)
.map(|s| s.omit_at)
.unwrap_or(0.0);
(k.clone(), floor)
})
.collect();
let pops = crate::registry::Populations::build(&rows, &keys, &omit_at);
let mut out: HashMap<String, Vec<String>> = HashMap::new();
let mut inc: HashMap<String, Vec<String>> = HashMap::new();
let resolve = |id: &str| label.get(id).cloned().unwrap_or_else(|| id.to_string());
for e in &level.edges {
out.entry(e.source.clone())
.or_default()
.push(resolve(&e.target));
inc.entry(e.target.clone())
.or_default()
.push(resolve(&e.source));
}
for v in out.values_mut() {
sort_dedup(v);
}
for v in inc.values_mut() {
sort_dedup(v);
}
GraphView {
out,
inc,
files: Arc::new(files),
files_set: Arc::new(files_set),
by_dir,
pops: Arc::new(pops),
agg_cache: Arc::new(std::sync::Mutex::new(HashMap::new())),
}
}
fn register_agg(&self, ctx: &mut Context) {
let pops = self.pops.clone();
let cache = self.agg_cache.clone();
ctx.add_function(
"agg",
move |key: Arc<String>, reducer: Arc<String>, population: Arc<String>| -> f64 {
let k = (
key.as_str().to_string(),
reducer.as_str().to_string(),
population.as_str().to_string(),
);
if let Some(v) = cache.lock().unwrap().get(&k) {
return *v;
}
let v = pops.reduce_for(&key, &reducer, &population);
cache.lock().unwrap().insert(k, v);
v
},
);
}
fn deps(&self, id: &str) -> Vec<String> {
self.out.get(id).cloned().unwrap_or_default()
}
fn rdeps(&self, id: &str) -> Vec<String> {
self.inc.get(id).cloned().unwrap_or_default()
}
fn files_vec(&self) -> Vec<String> {
(*self.files).clone()
}
fn files_set_arc(&self) -> Arc<HashSet<String>> {
self.files_set.clone()
}
fn siblings(&self, path: &str) -> Vec<String> {
let dir = split_path(path).dir;
self.by_dir
.get(&dir)
.map(|v| v.iter().filter(|f| f.as_str() != path).cloned().collect())
.unwrap_or_default()
}
}
fn numeric_attrs(node: &Node) -> BTreeMap<String, f64> {
let mut m = BTreeMap::new();
for (k, v) in node.attrs.iter() {
match v {
AttrValue::Int(i) => {
m.insert(k.clone(), *i as f64);
}
AttrValue::Float(f) => {
m.insert(k.clone(), *f);
}
_ => {}
}
}
m
}
fn sort_dedup(v: &mut Vec<String>) {
v.sort();
v.dedup();
}
fn label_of(node: &Node) -> String {
if node.kind == EXTERNAL || node.id.starts_with("ext:") {
return node.id.clone();
}
node_path(node)
}
fn expand_defs(
id: &str,
expr: &str,
defs: &BTreeMap<String, String>,
) -> Result<String, CheckCompileError> {
let mut out = expr.to_string();
for _ in 0..=defs.len() {
let mut changed = false;
for (name, body) in defs {
if references(&out, name) {
out = replace_word(&out, name, &format!("({body})"));
changed = true;
}
}
if !changed {
return Ok(out);
}
}
Err(CheckCompileError {
id: id.to_string(),
message: "`[rules.defs]` helpers reference each other in a cycle".to_string(),
})
}
#[cfg(test)]
#[path = "checks_test.rs"]
mod tests;