perl-module 0.16.0

Perl module resolution, import analysis, and refactoring — unified facade
Documentation
use perl_module::boundary::{
    contains_standalone_module_token, find_standalone_module_token_ranges,
};
use perl_module::name::legacy_package_separator;
use proptest::prelude::*;

fn module_name_strategy() -> impl Strategy<Value = String> {
    proptest::collection::vec("[A-Za-z_][A-Za-z0-9_]{0,7}", 1..5)
        .prop_map(|segments| segments.join("::"))
}

fn boundary_padding_strategy() -> impl Strategy<Value = String> {
    let boundary_char = prop_oneof![
        Just(' '),
        Just('\t'),
        Just(';'),
        Just(','),
        Just('('),
        Just(')'),
        Just('['),
        Just(']'),
        Just('{'),
        Just('}'),
        Just('/'),
        Just('-'),
        Just('"'),
    ];

    proptest::collection::vec(boundary_char, 0..4).prop_map(|chars| chars.into_iter().collect())
}

fn nonempty_boundary_padding_strategy() -> impl Strategy<Value = String> {
    boundary_padding_strategy()
        .prop_filter("boundary padding must not be empty", |padding| !padding.is_empty())
}

proptest! {
    #[test]
    fn prop_finds_exact_range_for_direct_use_lines(module in module_name_strategy()) {
        let line = format!("use {module};");
        let ranges = find_standalone_module_token_ranges(&line, &module).collect::<Vec<_>>();

        prop_assert_eq!(ranges.len(), 1);
        prop_assert_eq!(ranges[0].start, 4);
        prop_assert_eq!(ranges[0].end, 4 + module.len());
        prop_assert!(contains_standalone_module_token(&line, &module));
    }

    #[test]
    fn prop_finds_exact_range_for_direct_use_lines_with_legacy_separators(module in module_name_strategy()) {
        let legacy_module = legacy_package_separator(&module).into_owned();
        let line = format!("use {legacy_module};");
        let ranges = find_standalone_module_token_ranges(&line, &legacy_module).collect::<Vec<_>>();

        prop_assert_eq!(ranges.len(), 1);
        prop_assert_eq!(ranges[0].start, 4);
        prop_assert_eq!(ranges[0].end, 4 + legacy_module.len());
        prop_assert!(contains_standalone_module_token(&line, &legacy_module));
    }

    #[test]
    fn prop_rejects_embedded_module_name_in_larger_identifier(module in module_name_strategy()) {
        let line = format!("use {module}Suffix;");

        let ranges = find_standalone_module_token_ranges(&line, &module).collect::<Vec<_>>();
        prop_assert!(ranges.is_empty());
        prop_assert!(!contains_standalone_module_token(&line, &module));
    }

    #[test]
    fn prop_repeated_standalone_matches_are_non_overlapping_and_complete(
        module in module_name_strategy(),
        prefix in boundary_padding_strategy(),
        infix in nonempty_boundary_padding_strategy(),
        suffix in boundary_padding_strategy(),
    ) {
        let line = format!("{prefix}{module}{infix}{module}{suffix}");
        let ranges = find_standalone_module_token_ranges(&line, &module).collect::<Vec<_>>();

        prop_assert_eq!(ranges.len(), 2);
        prop_assert!(contains_standalone_module_token(&line, &module));

        let first = ranges[0];
        let second = ranges[1];
        prop_assert_eq!(&line[first.start..first.end], module.as_str());
        prop_assert_eq!(&line[second.start..second.end], module.as_str());
        prop_assert!(first.end <= second.start);
    }

    #[test]
    fn prop_contains_agrees_with_range_presence(
        line in "(?s).{0,256}",
        module_name in "[A-Za-z_][A-Za-z0-9_:'\\:]{0,24}",
    ) {
        let ranges = find_standalone_module_token_ranges(&line, &module_name).collect::<Vec<_>>();
        prop_assert_eq!(contains_standalone_module_token(&line, &module_name), !ranges.is_empty());

        let mut prev_end = 0usize;
        for range in ranges {
            prop_assert!(range.start <= range.end);
            prop_assert!(range.end <= line.len());
            prop_assert!(line.is_char_boundary(range.start));
            prop_assert!(line.is_char_boundary(range.end));
            prop_assert!(range.start >= prev_end);
            prop_assert_eq!(&line[range.start..range.end], module_name.as_str());
            prev_end = range.end;
        }
    }
}