weavatrix-scan 0.4.0

Deterministic, safe repository scanner for code intelligence
Documentation
#[allow(dead_code)]
mod support;

use std::path::Path;
use support::Fixture;
use weavatrix_scan::{NamedFileTypes, ScanOptions, Scanner, WalkBuilder};

#[test]
fn multi_root_builder_preserves_native_roots_and_relative_paths() {
    let first = Fixture::new("weavatrix-p2-multi-first");
    let second = Fixture::new("weavatrix-p2-multi-second");
    first.write("first.rs", "fn first() {}\n");
    second.write("second.rs", "fn second() {}\n");

    let entries = WalkBuilder::new(&first.root)
        .add_root(&second.root)
        .sort_by_file_name()
        .build()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    let files = entries
        .iter()
        .filter(|entry| entry.is_file())
        .map(|entry| {
            (
                entry.path().parent().unwrap().to_path_buf(),
                entry.relative_path().to_path_buf(),
            )
        })
        .collect::<Vec<_>>();

    assert_eq!(
        files,
        [
            (first.root.clone(), "first.rs".into()),
            (second.root.clone(), "second.rs".into())
        ]
    );
}

#[test]
fn sorting_filtering_and_contents_first_match_builder_contracts() {
    let fixture = Fixture::new("weavatrix-p2-builder");
    fixture.write("b/file.rs", "fn b() {}\n");
    fixture.write("a/file.rs", "fn a() {}\n");
    fixture.write("skip/hidden.rs", "fn hidden() {}\n");

    let sorted = WalkBuilder::new(&fixture.root)
        .sort_by_name(|left, right| right.cmp(left))
        .filter_directories(|entry| entry.file_name() != "skip")
        .build()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    let depth_one = sorted
        .iter()
        .filter(|entry| entry.depth() == 1)
        .map(|entry| entry.file_name().to_string_lossy().into_owned())
        .collect::<Vec<_>>();
    assert_eq!(depth_one, ["b", "a"]);
    assert!(
        sorted
            .iter()
            .all(|entry| !entry.path().ends_with(Path::new("skip/hidden.rs")))
    );

    let contents_first = WalkBuilder::new(&fixture.root)
        .sort_by_file_name()
        .filter_directories(|entry| entry.file_name() != "skip")
        .contents_first(true)
        .build()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    let positions = |suffix: &str| {
        contents_first
            .iter()
            .position(|entry| entry.path().ends_with(suffix))
            .unwrap()
    };
    assert!(positions("a/file.rs") < positions("a"));
    assert!(positions("b/file.rs") < positions("b"));
    assert!(
        contents_first
            .last()
            .unwrap()
            .relative_path()
            .as_os_str()
            .is_empty()
    );
}

#[test]
fn named_file_types_combine_with_direct_extensions() {
    let fixture = Fixture::new("weavatrix-p2-types");
    fixture.write("lib.rs", "fn run() {}\n");
    fixture.write("view.tsx", "export const View = () => null;\n");
    fixture.write("main.go", "package main\n");
    fixture.write("README.md", "# docs\n");
    let types = NamedFileTypes::new()
        .with_type("rust", ["rs"])
        .with_type("web", ["ts", "tsx"])
        .select(["rust", "web"]);
    let report = Scanner::new(&fixture.root)
        .options(
            ScanOptions::default()
                .with_extensions(["md"])
                .with_file_types(types)
                .metadata_only(),
        )
        .scan()
        .unwrap();
    let files = report
        .files
        .iter()
        .map(|file| file.relative.as_str())
        .collect::<Vec<_>>();

    assert_eq!(files, ["README.md", "lib.rs", "view.tsx"]);
}

#[test]
fn custom_sort_can_group_directories_before_files() {
    let fixture = Fixture::new("weavatrix-p2-custom-sort");
    fixture.write("z.rs", "fn z() {}\n");
    fixture.write("a/file.rs", "fn a() {}\n");
    let entries = WalkBuilder::new(&fixture.root)
        .sort_by(|left, right| {
            let left_is_file = left.file_type().is_ok_and(|kind| kind.is_file());
            let right_is_file = right.file_type().is_ok_and(|kind| kind.is_file());
            left_is_file
                .cmp(&right_is_file)
                .then_with(|| left.file_name().cmp(&right.file_name()))
        })
        .build()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();

    assert_eq!(entries[1].file_name(), "a");
}