weavatrix-edit 0.1.3

Deterministic, Unicode-safe edit-plan validation and application for source refactoring
Documentation
use weavatrix_edit::{
    EditPlan, ErrorCode, FileEdit, PlanLimits, Position, Provenance, TextEdit, TextRange,
    validate_plan_path,
};

const SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

fn edit_with_provenance(provenance: &str) -> TextEdit {
    TextEdit::replace(
        TextRange::new(Position::new(1, 0), Position::new(1, 1)),
        "a",
        "b",
        provenance,
    )
}

fn file(path: impl Into<String>) -> FileEdit {
    FileEdit::new(
        path,
        SHA256,
        vec![edit_with_provenance(Provenance::EXACT_LSP)],
    )
}

fn plan_with_file(path: &str) -> EditPlan {
    EditPlan::new("rename_symbol", vec![file(path)])
}

fn validation_code(plan: &EditPlan) -> ErrorCode {
    plan.validate().unwrap_err().code()
}

#[test]
fn rejects_unsafe_and_nonportable_paths() {
    for path in [
        "",
        "/a.rs",
        "a\\b.rs",
        "C:/a.rs",
        "a//b.rs",
        "a/./b.rs",
        "a/../b.rs",
        ".GIT/config",
        "src/.git./hooks.rs",
        ".git /x",
        "a.rs:stream",
        "src./a.rs",
        "src/a.rs ",
        "NUL",
        "con.txt",
        "COM1.rs",
        "src/\0bad.rs",
        "src/\u{7f}bad.rs",
    ] {
        let error = validate_plan_path(path, 4_096).unwrap_err();
        assert_eq!(error.code(), ErrorCode::InvalidPath, "{path:?}");
    }

    for path in [
        "src/lib.rs",
        "\u{65e5}\u{672c}\u{8a9e}/\u{1f600}.rs",
        "foo..bar/a.rs",
    ] {
        assert!(validate_plan_path(path, 4_096).is_ok(), "{path:?}");
    }
}

#[test]
fn rejects_exact_and_portable_path_aliases() {
    let exact = EditPlan::new("rename_symbol", vec![file("src/a.rs"), file("src/a.rs")]);
    assert_eq!(validation_code(&exact), ErrorCode::InvalidPlan);

    let case_alias = EditPlan::new(
        "rename_symbol",
        vec![file("Src/Foo.rs"), file("src/foo.RS")],
    );
    assert_eq!(validation_code(&case_alias), ErrorCode::InvalidPlan);
}

#[test]
fn default_file_and_per_file_edit_limits_match_the_js_contract() {
    let at_file_limit = EditPlan::new(
        "rename_symbol",
        (0..500)
            .map(|index| file(format!("src/{index}.rs")))
            .collect(),
    );
    assert!(at_file_limit.validate().is_ok());

    let over_file_limit = EditPlan::new(
        "rename_symbol",
        (0..501)
            .map(|index| file(format!("src/{index}.rs")))
            .collect(),
    );
    assert_eq!(validation_code(&over_file_limit), ErrorCode::PlanTooLarge);

    let at_edit_limit = EditPlan::new(
        "rename_symbol",
        vec![FileEdit::new(
            "src/a.rs",
            SHA256,
            (0..2_000)
                .map(|_| edit_with_provenance(Provenance::EXACT_LSP))
                .collect(),
        )],
    );
    assert!(at_edit_limit.validate().is_ok());

    let over_edit_limit = EditPlan::new(
        "rename_symbol",
        vec![FileEdit::new(
            "src/a.rs",
            SHA256,
            (0..2_001)
                .map(|_| edit_with_provenance(Provenance::EXACT_LSP))
                .collect(),
        )],
    );
    assert_eq!(validation_code(&over_edit_limit), ErrorCode::PlanTooLarge);
}

#[test]
fn enforces_global_edit_text_and_path_budgets() {
    let two_files = EditPlan::new("rename_symbol", vec![file("src/a.rs"), file("src/b.rs")]);
    let one_total_edit = PlanLimits {
        max_total_edits: 1,
        ..PlanLimits::default()
    };
    assert_eq!(
        two_files.validate_with(one_total_edit).unwrap_err().code(),
        ErrorCode::PlanTooLarge
    );

    let plan = plan_with_file("src/a.rs");
    let exact_text_budget = PlanLimits {
        max_total_text_bytes: 2,
        ..PlanLimits::default()
    };
    assert!(plan.validate_with(exact_text_budget).is_ok());
    let short_text_budget = PlanLimits {
        max_total_text_bytes: 1,
        ..PlanLimits::default()
    };
    assert_eq!(
        plan.validate_with(short_text_budget).unwrap_err().code(),
        ErrorCode::PlanTooLarge
    );

    assert!(validate_plan_path("a.rs", 4).is_ok());
    assert_eq!(
        validate_plan_path("a.rs", 3).unwrap_err().code(),
        ErrorCode::InvalidPath
    );
}