pub mod rules;
pub use crate::definitions::Diagnostic;
use crate::definitions::Severity;
use crate::definitions::analysis::{Analysis, WorkflowFacts};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Level {
Deny,
Warn,
}
impl Level {
pub fn as_str(self) -> &'static str {
match self {
Level::Deny => "deny",
Level::Warn => "warn",
}
}
fn severity(self) -> Severity {
match self {
Level::Deny => Severity::Error,
Level::Warn => Severity::Warning,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Group {
Correctness,
Perf,
Duplication,
Style,
}
impl Group {
pub fn as_str(self) -> &'static str {
match self {
Group::Correctness => "correctness",
Group::Perf => "perf",
Group::Duplication => "duplication",
Group::Style => "style",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scope {
Workflow,
Set,
}
impl Scope {
pub fn as_str(self) -> &'static str {
match self {
Scope::Workflow => "workflow",
Scope::Set => "set",
}
}
}
pub trait Rule: Send + Sync {
fn id(&self) -> &'static str;
fn group(&self) -> Group;
fn level(&self) -> Level;
fn scope(&self) -> Scope;
fn needs_config(&self) -> bool {
false
}
fn summary(&self) -> &'static str;
fn explain(&self) -> &'static str;
fn check(&self, cx: &Analysis<'_>, out: &mut Vec<Diagnostic>);
}
pub fn registry() -> &'static [&'static dyn Rule] {
rules::ALL
}
pub fn find(id: &str) -> Option<&'static dyn Rule> {
registry().iter().copied().find(|r| r.id() == id)
}
impl Diagnostic {
pub fn on_workflow(
rule: &dyn Rule,
cx: &Analysis<'_>,
wf: &WorkflowFacts,
path: Option<&str>,
message: impl Into<String>,
) -> Self {
Self {
severity: rule.level().severity(),
check: rule.id(),
entity: format!("workflow '{}'", wf.name),
file: Some(wf.origin.clone()),
path: path.map(str::to_string),
line: path.and_then(|p| cx.locate(&wf.origin, p)),
message: message.into(),
remedy: None,
}
}
pub fn at(
rule: &dyn Rule,
cx: &Analysis<'_>,
entity: impl Into<String>,
origin: &str,
path: Option<&str>,
message: impl Into<String>,
) -> Self {
Self {
severity: rule.level().severity(),
check: rule.id(),
entity: entity.into(),
file: Some(origin.to_string()),
path: path.map(str::to_string),
line: path.and_then(|p| cx.locate(origin, p)),
message: message.into(),
remedy: None,
}
}
}
#[derive(Debug, Default)]
pub struct Report {
pub diagnostics: Vec<Diagnostic>,
pub skipped: Vec<&'static str>,
}
pub fn run(cx: &Analysis<'_>) -> Report {
let mut report = Report::default();
for rule in registry() {
if rule.needs_config() && cx.config.is_none() {
report.skipped.push(rule.id());
continue;
}
rule.check(cx, &mut report.diagnostics);
}
report.diagnostics.sort_by(|a, b| {
(&a.file, &a.path, a.check, &a.message).cmp(&(&b.file, &b.path, b.check, &b.message))
});
report
}
pub fn list_table() -> String {
let mut out =
String::from("rule level scope summary\n");
for rule in registry() {
out.push_str(&format!(
"{:<43} {:<6} {:<9} {}\n",
rule.id(),
rule.level().as_str(),
rule.scope().as_str(),
rule.summary()
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_are_unique_and_named_by_group() {
let mut seen = std::collections::BTreeSet::new();
for rule in registry() {
assert!(seen.insert(rule.id()), "duplicate rule id {}", rule.id());
let (group, name) = rule.id().split_once('.').expect("group.name");
assert_eq!(group, rule.group().as_str(), "{}", rule.id());
assert!(
name.chars().all(|c| c.is_ascii_lowercase() || c == '_'),
"{}: snake_case",
rule.id()
);
assert!(
!rule.summary().is_empty() && !rule.explain().is_empty(),
"{}",
rule.id()
);
assert!(
rule.explain().contains("Proof") && rule.explain().contains("Silent"),
"{}: explain() must state its proof and when it is silent",
rule.id()
);
}
}
}