detect 0.3.0

Expression-based file search combining name, content, metadata, and structured data predicates
Documentation
use detect::parser::test_utils::RawTestExpr;
use detect::parser::*;

// ==============================================================================
// Basic Syntax Tests - Predicates, Values, Quotes
// ==============================================================================

#[test]
fn test_simple_predicate() {
    let result = RawParser::parse_raw_expr("name == foo").unwrap();
    let expected = RawTestExpr::string_predicate("name", "==", "foo");
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_quoted_values() {
    let result = RawParser::parse_raw_expr(r#"filename == "my file.txt""#).unwrap();
    let expected = RawTestExpr::quoted_predicate("filename", "==", "my file.txt");
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_single_quoted_values() {
    let result = RawParser::parse_raw_expr("filename == 'my file.txt'").unwrap();
    let expected = RawTestExpr::quoted_predicate("filename", "==", "my file.txt");
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_escape_sequences() {
    // Test double quote escapes
    let result = RawParser::parse_raw_expr(r#"name == "file\"with\"quotes""#).unwrap();
    let expected = RawTestExpr::quoted_predicate("name", "==", r#"file\"with\"quotes"#);
    assert_eq!(result.to_test_expr(), expected);

    // Test various escape sequences
    let result = RawParser::parse_raw_expr(r#"content == "line1\nline2\ttab\\backslash""#).unwrap();
    let expected =
        RawTestExpr::quoted_predicate("content", "==", r#"line1\nline2\ttab\\backslash"#);
    assert_eq!(result.to_test_expr(), expected);

    // Test single quote escapes
    let result = RawParser::parse_raw_expr(r"name == 'file\'with\'quotes'").unwrap();
    let expected = RawTestExpr::quoted_predicate("name", "==", r"file\'with\'quotes");
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_set_values() {
    let result = RawParser::parse_raw_expr("ext in [rs, js, ts]").unwrap();
    // With new parser, sets are raw tokens - spaces preserved
    let expected = RawTestExpr::string_predicate("ext", "in", "[rs, js, ts]");
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_mixed_set() {
    let result = RawParser::parse_raw_expr(r#"name in [README, "my file", config]"#).unwrap();
    let expected = RawTestExpr::string_predicate("name", "in", r#"[README, "my file", config]"#);
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_set_with_quotes_and_escapes() {
    let result = RawParser::parse_raw_expr(r#"name in ["file\"1", 'file\'2', plain]"#).unwrap();
    let expected = RawTestExpr::string_predicate("name", "in", r#"["file\"1", 'file\'2', plain]"#);
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_empty_set() {
    let result = RawParser::parse_raw_expr("ext in []").unwrap();
    let expected = RawTestExpr::string_predicate("ext", "in", "[]");
    assert_eq!(result.to_test_expr(), expected);
}

// ==============================================================================
// Boolean Logic Tests - AND, OR, NOT, Precedence
// ==============================================================================

#[test]
fn test_boolean_logic() {
    let result = RawParser::parse_raw_expr("name == foo AND size > 1000").unwrap();
    let expected = RawTestExpr::and(
        RawTestExpr::string_predicate("name", "==", "foo"),
        RawTestExpr::string_predicate("size", ">", "1000"),
    );
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_or_logic() {
    let result = RawParser::parse_raw_expr("name == foo OR name == bar").unwrap();
    let expected = RawTestExpr::or(
        RawTestExpr::string_predicate("name", "==", "foo"),
        RawTestExpr::string_predicate("name", "==", "bar"),
    );
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_negation_variants() {
    // Test NOT keyword
    let result = RawParser::parse_raw_expr("NOT name == foo").unwrap();
    let expected = RawTestExpr::not(RawTestExpr::string_predicate("name", "==", "foo"));
    assert_eq!(result.to_test_expr(), expected);

    // Test ! symbol
    let result = RawParser::parse_raw_expr("! name == foo").unwrap();
    let expected = RawTestExpr::not(RawTestExpr::string_predicate("name", "==", "foo"));
    assert_eq!(result.to_test_expr(), expected);

    // Test escaped ! symbol
    let result = RawParser::parse_raw_expr("\\! name == foo").unwrap();
    let expected = RawTestExpr::not(RawTestExpr::string_predicate("name", "==", "foo"));
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_operator_precedence() {
    // AND should bind tighter than OR
    let result = RawParser::parse_raw_expr("a == b OR c == d AND e == f").unwrap();
    let expected = RawTestExpr::or(
        RawTestExpr::string_predicate("a", "==", "b"),
        RawTestExpr::and(
            RawTestExpr::string_predicate("c", "==", "d"),
            RawTestExpr::string_predicate("e", "==", "f"),
        ),
    );
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_parentheses() {
    let result = RawParser::parse_raw_expr("(a == b OR c == d) AND e == f").unwrap();
    let expected = RawTestExpr::and(
        RawTestExpr::or(
            RawTestExpr::string_predicate("a", "==", "b"),
            RawTestExpr::string_predicate("c", "==", "d"),
        ),
        RawTestExpr::string_predicate("e", "==", "f"),
    );
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_complex_expression() {
    let result =
        RawParser::parse_raw_expr(r#"(name == "test.rs" OR ext in [js, ts]) AND NOT size > 1mb"#)
            .unwrap();

    let expected = RawTestExpr::and(
        RawTestExpr::or(
            RawTestExpr::quoted_predicate("name", "==", "test.rs"),
            RawTestExpr::string_predicate("ext", "in", "[js, ts]"),
        ),
        RawTestExpr::not(RawTestExpr::string_predicate("size", ">", "1mb")),
    );
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_all_operators() {
    let test_cases = vec![
        ("name == foo", "=="),
        ("name != foo", "!="),
        ("name ~= foo", "~="),
        ("name > foo", ">"),
        ("name < foo", "<"),
        ("name >= foo", ">="),
        ("name <= foo", "<="),
        ("name contains foo", "contains"),
        ("name in [foo]", "in"),
    ];

    for (input, expected_op) in test_cases {
        let result = RawParser::parse_raw_expr(input).unwrap();
        match result.to_test_expr() {
            RawTestExpr::Predicate(pred) => {
                assert_eq!(pred.operator, expected_op, "Failed for input: {}", input);
            }
            _ => panic!("Expected predicate for input: {}", input),
        }
    }
}

#[test]
fn test_complex_selectors() {
    let result = RawParser::parse_raw_expr("name == test.rs").unwrap();
    let expected = RawTestExpr::string_predicate("name", "==", "test.rs");
    assert_eq!(result.to_test_expr(), expected);

    let result = RawParser::parse_raw_expr("meta.size > 1000").unwrap();
    let expected = RawTestExpr::string_predicate("meta.size", ">", "1000");
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_case_insensitive_keywords() {
    let result = RawParser::parse_raw_expr("name == foo AND name == bar").unwrap();
    let result_upper = RawParser::parse_raw_expr("name == foo AND name == bar").unwrap();
    assert_eq!(result.to_test_expr(), result_upper.to_test_expr());

    let result = RawParser::parse_raw_expr("NOT name == foo").unwrap();
    let result_lower = RawParser::parse_raw_expr("not name == foo").unwrap();
    assert_eq!(result.to_test_expr(), result_lower.to_test_expr());
}

#[test]
fn test_syntax_errors() {
    // Missing value
    let result = RawParser::parse_raw_expr("name ==");
    assert!(result.is_err());

    // Missing operator
    let result = RawParser::parse_raw_expr("name foo");
    assert!(result.is_err());

    // Unclosed parentheses
    let result = RawParser::parse_raw_expr("(name == foo");
    assert!(result.is_err());

    // Unclosed bracket - now parses as bare token (grammar is permissive)
    // This is valid: searches for files named "[foo"
    let result = RawParser::parse_raw_expr("name in [foo");
    assert!(
        result.is_ok(),
        "Permissive grammar allows [foo as bare token"
    );

    // Unclosed quote
    let result = RawParser::parse_raw_expr(r#"name == "unclosed"#);
    assert!(result.is_err());
}

#[test]
fn test_invalid_escape_sequences() {
    // Since we're a syntax-only parser, we preserve escape sequences without validating them
    // This previously "invalid" escape sequence is now just preserved as-is
    let result = RawParser::parse_raw_expr(r#"name == "invalid\x""#);
    let expected = RawTestExpr::quoted_predicate("name", "==", r"invalid\x");
    assert_eq!(result.unwrap().to_test_expr(), expected);

    // Unterminated string (this is actually a syntax error, not escape error)
    let result = RawParser::parse_raw_expr("name == \"unterminated");
    assert!(result.is_err());
}

// ==============================================================================
#[test]
fn test_whitespace_handling() {
    // Basic predicate whitespace tolerance
    let result1 = RawParser::parse_raw_expr("name==foo").unwrap();
    let result2 = RawParser::parse_raw_expr("name == foo").unwrap();
    let result3 = RawParser::parse_raw_expr("  name   ==   foo  ").unwrap();

    assert_eq!(result1.to_test_expr(), result2.to_test_expr());
    assert_eq!(result2.to_test_expr(), result3.to_test_expr());

    // Whitespace in sets (preserved as raw token)
    let result = RawParser::parse_raw_expr("ext in [ rs , js , ts ]").unwrap();
    let expected = RawTestExpr::string_predicate("ext", "in", "[ rs , js , ts ]");
    assert_eq!(result.to_test_expr(), expected);
}

#[test]
fn test_edge_cases() {
    // Empty string value
    let result = RawParser::parse_raw_expr(r#"name == """#).unwrap();
    let expected = RawTestExpr::quoted_predicate("name", "==", "");
    assert_eq!(result.to_test_expr(), expected);

    // Value with special characters
    let result = RawParser::parse_raw_expr("name == foo-bar_baz.txt").unwrap();
    let expected = RawTestExpr::string_predicate("name", "==", "foo-bar_baz.txt");
    assert_eq!(result.to_test_expr(), expected);

    // Selector with dots and underscores
    let result = RawParser::parse_raw_expr("path.name_with_underscores == foo").unwrap();
    let expected = RawTestExpr::string_predicate("path.name_with_underscores", "==", "foo");
    assert_eq!(result.to_test_expr(), expected);
}

// Bug: Reserved word substrings in bare values