mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
235 lines
8.3 KiB
Rust
235 lines
8.3 KiB
Rust
|
|
// ─── PredicateKind ───────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/// Classification of what an if-condition tests.
|
||
|
|
///
|
||
|
|
/// Determined by heuristic analysis of the raw condition text.
|
||
|
|
/// Classification is conservative: prefer [`Unknown`](PredicateKind::Unknown)
|
||
|
|
/// over a wrong guess.
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
|
|
pub enum PredicateKind {
|
||
|
|
/// `x.is_none()`, `x == null`, `x == nil`, `x is None`
|
||
|
|
NullCheck,
|
||
|
|
/// `x.is_empty()`, `x.len() == 0`, `x == ""`
|
||
|
|
EmptyCheck,
|
||
|
|
/// `x.is_err()`, `x.is_ok()`, `err != nil`
|
||
|
|
ErrorCheck,
|
||
|
|
/// Call to a validation/guard function: `validate(x)`, `is_safe(x)`
|
||
|
|
ValidationCall,
|
||
|
|
/// Call to a sanitizer function: `sanitize(x)`, `escape(x)`
|
||
|
|
SanitizerCall,
|
||
|
|
/// Comparison operators: `x == 5`, `x > threshold`
|
||
|
|
Comparison,
|
||
|
|
/// Generic boolean test — cannot classify further.
|
||
|
|
Unknown,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Classify a raw condition text into a [`PredicateKind`].
|
||
|
|
///
|
||
|
|
/// # Rules
|
||
|
|
///
|
||
|
|
/// - Empty/None text → [`Unknown`](PredicateKind::Unknown).
|
||
|
|
/// - `ValidationCall` / `SanitizerCall` require a `(` in the text **and** a
|
||
|
|
/// matching callee token. This avoids misclassifying comparisons like
|
||
|
|
/// `x_valid == true`.
|
||
|
|
/// - Prefers [`Unknown`](PredicateKind::Unknown) over false positives.
|
||
|
|
pub fn classify_condition(text: &str) -> PredicateKind {
|
||
|
|
if text.is_empty() {
|
||
|
|
return PredicateKind::Unknown;
|
||
|
|
}
|
||
|
|
|
||
|
|
let lower = text.to_ascii_lowercase();
|
||
|
|
|
||
|
|
// ── Error checks (before null checks: `err != nil` is an error check,
|
||
|
|
// not a null check, even though it contains `!= nil`) ──────────────
|
||
|
|
if lower.contains("is_err")
|
||
|
|
|| lower.contains("is_ok")
|
||
|
|
|| lower.contains("err != nil")
|
||
|
|
|| lower.contains("err == nil")
|
||
|
|
|| lower.contains("error != nil")
|
||
|
|
|| lower.contains("error == nil")
|
||
|
|
{
|
||
|
|
return PredicateKind::ErrorCheck;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Null checks ──────────────────────────────────────────────────────
|
||
|
|
if lower.contains("is_none")
|
||
|
|
|| lower.contains("is_some")
|
||
|
|
|| lower.contains("== none")
|
||
|
|
|| lower.contains("!= none")
|
||
|
|
|| lower.contains("is none")
|
||
|
|
|| lower.contains("is not none")
|
||
|
|
|| lower.contains("== null")
|
||
|
|
|| lower.contains("!= null")
|
||
|
|
|| lower.contains("=== null")
|
||
|
|
|| lower.contains("!== null")
|
||
|
|
|| lower.contains("== nil")
|
||
|
|
|| lower.contains("!= nil")
|
||
|
|
{
|
||
|
|
return PredicateKind::NullCheck;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Empty checks ─────────────────────────────────────────────────────
|
||
|
|
if lower.contains("is_empty")
|
||
|
|
|| lower.contains(".len() == 0")
|
||
|
|
|| lower.contains(".len() != 0")
|
||
|
|
|| lower.contains(".length == 0")
|
||
|
|
|| lower.contains(".length === 0")
|
||
|
|
|| lower.contains(".length != 0")
|
||
|
|
|| lower.contains(".length !== 0")
|
||
|
|
|| lower.contains("== \"\"")
|
||
|
|
|| lower.contains("== ''")
|
||
|
|
{
|
||
|
|
return PredicateKind::EmptyCheck;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Call-based kinds (require `(` to be present) ─────────────────────
|
||
|
|
if lower.contains('(') {
|
||
|
|
// Extract a rough callee token: everything before the first `(`
|
||
|
|
// that looks like an identifier (letters, digits, underscores, dots).
|
||
|
|
let callee_part = lower.split('(').next().unwrap_or("");
|
||
|
|
// Take the last segment (after `.` or `::`) as the bare name.
|
||
|
|
let bare = callee_part
|
||
|
|
.rsplit(['.', ':'])
|
||
|
|
.next()
|
||
|
|
.unwrap_or(callee_part)
|
||
|
|
.trim();
|
||
|
|
|
||
|
|
// Validation
|
||
|
|
if bare.contains("valid")
|
||
|
|
|| bare.contains("check")
|
||
|
|
|| bare.contains("verify")
|
||
|
|
|| bare.starts_with("is_safe")
|
||
|
|
|| bare.starts_with("is_authorized")
|
||
|
|
|| bare.starts_with("is_authenticated")
|
||
|
|
{
|
||
|
|
return PredicateKind::ValidationCall;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sanitizer
|
||
|
|
if bare.contains("sanitiz") || bare.contains("escape") || bare.contains("encode") {
|
||
|
|
return PredicateKind::SanitizerCall;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Comparison operators ─────────────────────────────────────────────
|
||
|
|
if lower.contains("==")
|
||
|
|
|| lower.contains("!=")
|
||
|
|
|| lower.contains(">=")
|
||
|
|
|| lower.contains("<=")
|
||
|
|
|| lower.contains(" > ")
|
||
|
|
|| lower.contains(" < ")
|
||
|
|
{
|
||
|
|
return PredicateKind::Comparison;
|
||
|
|
}
|
||
|
|
|
||
|
|
PredicateKind::Unknown
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
// ── classify_condition ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_empty_is_unknown() {
|
||
|
|
assert_eq!(classify_condition(""), PredicateKind::Unknown);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_null_checks() {
|
||
|
|
assert_eq!(classify_condition("x.is_none()"), PredicateKind::NullCheck);
|
||
|
|
assert_eq!(classify_condition("x == null"), PredicateKind::NullCheck);
|
||
|
|
assert_eq!(classify_condition("x != nil"), PredicateKind::NullCheck);
|
||
|
|
assert_eq!(classify_condition("x is None"), PredicateKind::NullCheck);
|
||
|
|
assert_eq!(classify_condition("x === null"), PredicateKind::NullCheck);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_error_checks() {
|
||
|
|
assert_eq!(classify_condition("x.is_err()"), PredicateKind::ErrorCheck);
|
||
|
|
assert_eq!(classify_condition("err != nil"), PredicateKind::ErrorCheck);
|
||
|
|
assert_eq!(classify_condition("x.is_ok()"), PredicateKind::ErrorCheck);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_empty_checks() {
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("x.is_empty()"),
|
||
|
|
PredicateKind::EmptyCheck
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("x.len() == 0"),
|
||
|
|
PredicateKind::EmptyCheck
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("x.length === 0"),
|
||
|
|
PredicateKind::EmptyCheck
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_validation_call() {
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("validate(x)"),
|
||
|
|
PredicateKind::ValidationCall
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("is_safe(input)"),
|
||
|
|
PredicateKind::ValidationCall
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("check_auth(req)"),
|
||
|
|
PredicateKind::ValidationCall
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("input.verify(sig)"),
|
||
|
|
PredicateKind::ValidationCall
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_validation_requires_paren() {
|
||
|
|
// `x_valid == true` should NOT be ValidationCall — no `(` call syntax.
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("x_valid == true"),
|
||
|
|
PredicateKind::Comparison
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("is_valid && ready"),
|
||
|
|
PredicateKind::Unknown
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_sanitizer_call() {
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("sanitize(x)"),
|
||
|
|
PredicateKind::SanitizerCall
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("html_escape(s)"),
|
||
|
|
PredicateKind::SanitizerCall
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
classify_condition("url_encode(path)"),
|
||
|
|
PredicateKind::SanitizerCall
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_comparison() {
|
||
|
|
assert_eq!(classify_condition("x == 5"), PredicateKind::Comparison);
|
||
|
|
assert_eq!(classify_condition("x != y"), PredicateKind::Comparison);
|
||
|
|
assert_eq!(classify_condition("a >= b"), PredicateKind::Comparison);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_unknown_fallback() {
|
||
|
|
assert_eq!(classify_condition("flag"), PredicateKind::Unknown);
|
||
|
|
assert_eq!(classify_condition("a && b"), PredicateKind::Unknown);
|
||
|
|
}
|
||
|
|
}
|