2025-06-17 01:17:48 +02:00
|
|
|
|
pub mod c;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
pub mod cpp;
|
2025-06-17 01:17:48 +02:00
|
|
|
|
mod go;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
mod java;
|
|
|
|
|
|
pub mod javascript;
|
2025-06-17 01:17:48 +02:00
|
|
|
|
mod php;
|
|
|
|
|
|
mod python;
|
2025-06-24 18:53:31 +02:00
|
|
|
|
mod ruby;
|
2025-06-24 20:27:06 +02:00
|
|
|
|
pub mod rust;
|
|
|
|
|
|
pub mod typescript;
|
2025-06-17 01:17:48 +02:00
|
|
|
|
|
2025-06-24 20:27:06 +02:00
|
|
|
|
use console::style;
|
|
|
|
|
|
use once_cell::sync::Lazy;
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
2025-06-17 01:17:48 +02:00
|
|
|
|
use std::collections::HashMap;
|
2025-06-23 20:27:16 +02:00
|
|
|
|
use std::fmt;
|
2025-06-17 16:46:45 +02:00
|
|
|
|
use std::str::FromStr;
|
2025-06-17 01:17:48 +02:00
|
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
2025-06-24 20:27:06 +02:00
|
|
|
|
pub enum Severity {
|
|
|
|
|
|
High,
|
|
|
|
|
|
Medium,
|
|
|
|
|
|
Low,
|
|
|
|
|
|
}
|
2025-06-23 20:27:16 +02:00
|
|
|
|
|
2026-02-25 04:02:11 -05:00
|
|
|
|
impl Severity {
|
|
|
|
|
|
/// Bracketed, colored, fixed-width tag for aligned console output.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Returns e.g. `"[HIGH] "` or `"[MEDIUM]"` — always 8 visible characters
|
|
|
|
|
|
/// so the column after the tag lines up regardless of severity.
|
|
|
|
|
|
pub fn colored_tag(self) -> String {
|
|
|
|
|
|
// Visible widths: "[HIGH]" = 6, "[MEDIUM]" = 8, "[LOW]" = 5.
|
|
|
|
|
|
// Pad the *whole* tag to 8 visible chars (the longest, "[MEDIUM]").
|
|
|
|
|
|
let (label, styled_fn): (&str, fn(&str) -> String) = match self {
|
|
|
|
|
|
Severity::High => ("HIGH", |s| style(s).red().bold().to_string()),
|
|
|
|
|
|
Severity::Medium => ("MEDIUM", |s| style(s).yellow().bold().to_string()),
|
|
|
|
|
|
Severity::Low => ("LOW", |s| style(s).cyan().bold().to_string()),
|
|
|
|
|
|
};
|
|
|
|
|
|
let bracket_len = label.len() + 2; // "[" + label + "]"
|
|
|
|
|
|
let pad = 8usize.saturating_sub(bracket_len);
|
|
|
|
|
|
format!("[{}]{:pad$}", styled_fn(label), "", pad = pad)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
|
impl fmt::Display for Severity {
|
2025-06-24 20:27:06 +02:00
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2026-02-25 04:02:11 -05:00
|
|
|
|
let styled = match *self {
|
2025-06-24 20:27:06 +02:00
|
|
|
|
Severity::High => style("HIGH").red().bold().to_string(),
|
|
|
|
|
|
Severity::Medium => style("MEDIUM").yellow().bold().to_string(),
|
|
|
|
|
|
Severity::Low => style("LOW").cyan().bold().to_string(),
|
|
|
|
|
|
};
|
2026-02-25 04:02:11 -05:00
|
|
|
|
f.write_str(&styled)
|
2025-06-24 20:27:06 +02:00
|
|
|
|
}
|
2025-06-17 01:17:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
|
impl Severity {
|
2025-06-24 20:27:06 +02:00
|
|
|
|
/// Textual value stored in SQLite.
|
|
|
|
|
|
pub fn as_db_str(self) -> &'static str {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
Severity::High => "HIGH",
|
|
|
|
|
|
Severity::Medium => "MEDIUM",
|
|
|
|
|
|
Severity::Low => "LOW",
|
|
|
|
|
|
}
|
2025-06-23 20:27:16 +02:00
|
|
|
|
}
|
2025-06-17 01:17:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 20:27:06 +02:00
|
|
|
|
impl FromStr for Severity {
|
|
|
|
|
|
// TODO: FIX
|
|
|
|
|
|
type Err = ();
|
2025-06-17 16:46:45 +02:00
|
|
|
|
|
2025-06-24 20:27:06 +02:00
|
|
|
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
|
match input.to_lowercase().as_str() {
|
|
|
|
|
|
"medium" => Ok(Severity::Medium),
|
|
|
|
|
|
"high" => Ok(Severity::High),
|
|
|
|
|
|
_ => Ok(Severity::Low),
|
|
|
|
|
|
}
|
2025-06-17 16:46:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
|
/// One AST pattern with a tree-sitter query and meta-data.
|
2025-06-25 00:49:29 +02:00
|
|
|
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
2025-06-23 20:27:16 +02:00
|
|
|
|
pub struct Pattern {
|
2025-06-24 20:27:06 +02:00
|
|
|
|
/// Unique identifier (snake-case preferred).
|
|
|
|
|
|
pub id: &'static str,
|
|
|
|
|
|
/// Human-readable explanation.
|
|
|
|
|
|
pub description: &'static str,
|
|
|
|
|
|
/// tree-sitter query string.
|
|
|
|
|
|
pub query: &'static str,
|
|
|
|
|
|
/// Rough severity bucket.
|
|
|
|
|
|
pub severity: Severity,
|
2025-06-23 20:27:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 01:17:48 +02:00
|
|
|
|
/// Global, lazily-initialised registry: lang-name → pattern slice
|
|
|
|
|
|
static REGISTRY: Lazy<HashMap<&'static str, &'static [Pattern]>> = Lazy::new(|| {
|
2025-06-24 20:27:06 +02:00
|
|
|
|
let mut m = HashMap::new();
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Rust ----
|
|
|
|
|
|
m.insert("rust", rust::PATTERNS);
|
|
|
|
|
|
|
|
|
|
|
|
// ---- TypeScript ----
|
|
|
|
|
|
m.insert("typescript", typescript::PATTERNS);
|
|
|
|
|
|
m.insert("ts", typescript::PATTERNS);
|
|
|
|
|
|
m.insert("tsx", typescript::PATTERNS);
|
|
|
|
|
|
|
|
|
|
|
|
// ---- JavaScript ----
|
|
|
|
|
|
m.insert("javascript", javascript::PATTERNS);
|
|
|
|
|
|
m.insert("js", javascript::PATTERNS);
|
|
|
|
|
|
|
|
|
|
|
|
// ---- C & C++ ----
|
|
|
|
|
|
m.insert("c", c::PATTERNS);
|
|
|
|
|
|
m.insert("cpp", cpp::PATTERNS);
|
|
|
|
|
|
m.insert("c++", cpp::PATTERNS);
|
|
|
|
|
|
|
2025-06-28 17:36:14 +02:00
|
|
|
|
// ---- Other patterns in the folder ----
|
2025-06-24 20:27:06 +02:00
|
|
|
|
m.insert("java", java::PATTERNS);
|
|
|
|
|
|
m.insert("go", go::PATTERNS);
|
|
|
|
|
|
m.insert("php", php::PATTERNS);
|
|
|
|
|
|
m.insert("python", python::PATTERNS);
|
|
|
|
|
|
m.insert("py", python::PATTERNS);
|
|
|
|
|
|
m.insert("ruby", ruby::PATTERNS);
|
|
|
|
|
|
m.insert("rb", ruby::PATTERNS);
|
|
|
|
|
|
|
2025-06-28 17:36:14 +02:00
|
|
|
|
tracing::debug!("AST-pattern registry initialised ({} patterns)", m.len());
|
2025-06-24 20:27:06 +02:00
|
|
|
|
|
|
|
|
|
|
m
|
2025-06-17 01:17:48 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/// Return all patterns for the requested language (case-insensitive).
|
|
|
|
|
|
///
|
2025-06-28 17:36:14 +02:00
|
|
|
|
/// Unknown patterns yield an **empty** `Vec`.
|
2025-06-17 01:17:48 +02:00
|
|
|
|
pub fn load(lang: &str) -> Vec<Pattern> {
|
2025-06-24 20:27:06 +02:00
|
|
|
|
let key = lang.to_ascii_lowercase();
|
|
|
|
|
|
REGISTRY.get(key.as_str()).copied().unwrap_or(&[]).to_vec()
|
|
|
|
|
|
}
|
2025-06-24 23:57:27 +02:00
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn severity_as_db_str_roundtrip() {
|
2025-06-25 00:49:29 +02:00
|
|
|
|
for &s in &[Severity::High, Severity::Medium, Severity::Low] {
|
|
|
|
|
|
let db = s.as_db_str();
|
|
|
|
|
|
assert!(matches!(db, "HIGH" | "MEDIUM" | "LOW"));
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(db.parse::<Severity>().unwrap(), s);
|
|
|
|
|
|
assert_eq!(db.to_lowercase().parse::<Severity>().unwrap(), s);
|
|
|
|
|
|
}
|
2025-06-24 23:57:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn severity_display_contains_uppercase_name() {
|
2025-06-25 00:49:29 +02:00
|
|
|
|
assert!(Severity::High.to_string().contains("HIGH"));
|
|
|
|
|
|
assert!(Severity::Medium.to_string().contains("MEDIUM"));
|
|
|
|
|
|
assert!(Severity::Low.to_string().contains("LOW"));
|
2025-06-24 23:57:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn load_returns_correct_pattern_slices() {
|
2025-06-25 00:49:29 +02:00
|
|
|
|
let rust = load("rust");
|
|
|
|
|
|
assert!(!rust.is_empty(), "Rust patterns should be loaded");
|
|
|
|
|
|
|
|
|
|
|
|
let ts = load("typescript");
|
|
|
|
|
|
let tsx = load("tsx");
|
|
|
|
|
|
assert_eq!(ts, tsx, "alias ‘tsx’ must map to TypeScript patterns");
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(load("RUST"), rust);
|
|
|
|
|
|
|
|
|
|
|
|
assert!(load("brainfuck").is_empty());
|
2025-06-24 23:57:27 +02:00
|
|
|
|
}
|