Skip to main content

perl_test_generators/
variable.rs

1//! Generators for Perl variable names.
2//!
3//! Covers sigils (`$`, `@`, `%`), special variables (`$_`, `@_`, `$1`–`$9`),
4//! and package-qualified names (`$Foo::bar`).
5
6use proptest::prelude::*;
7
8/// Valid ASCII identifier characters for variable name body (first char).
9static IDENT_START: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
10
11/// Valid ASCII identifier characters for subsequent positions.
12static IDENT_CONT: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
13
14/// Generate a single Perl identifier (without sigil).
15fn identifier() -> impl Strategy<Value = String> {
16    prop_oneof![
17        // Common short names
18        Just("self".to_string()),
19        Just("class".to_string()),
20        // Random identifier 1–12 chars
21        (
22            prop::sample::select(IDENT_START),
23            prop::collection::vec(prop::sample::select(IDENT_CONT), 0..=11_usize),
24        )
25            .prop_map(|(first, rest)| {
26                let mut s = String::new();
27                s.push(first as char);
28                for b in rest {
29                    s.push(b as char);
30                }
31                s
32            }),
33    ]
34}
35
36/// Generate a Perl package path (`Foo`, `Foo::Bar`, ...).
37fn package_path() -> impl Strategy<Value = String> {
38    prop::collection::vec(identifier(), 1..=4_usize).prop_map(|segments| segments.join("::"))
39}
40
41/// Generate a full Perl variable name including sigil.
42///
43/// Covers scalar (`$x`), array (`@x`), hash (`%x`), special variables
44/// (`$_`, `@_`, `$1`–`$9`), and optionally package-qualified names
45/// (`$Foo::Bar::baz`).
46pub fn variable() -> impl Strategy<Value = String> {
47    prop_oneof![
48        // Special variables
49        Just("$_".to_string()),
50        Just("@_".to_string()),
51        Just("%ENV".to_string()),
52        Just("@ARGV".to_string()),
53        Just("$0".to_string()),
54        (1u32..=9).prop_map(|n| format!("${}", n)),
55        // Simple sigiled variable
56        (prop_oneof![Just('$'), Just('@'), Just('%')], identifier())
57            .prop_map(|(sigil, name)| format!("{}{}", sigil, name)),
58        // Package-qualified
59        (prop_oneof![Just('$'), Just('@'), Just('%')], package_path(), identifier())
60            .prop_map(|(sigil, pkg, name)| format!("{}{}::{}", sigil, pkg, name)),
61    ]
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    proptest! {
69        #[test]
70        fn variable_starts_with_sigil(v in variable()) {
71            prop_assert!(v.starts_with('$') || v.starts_with('@') || v.starts_with('%'));
72        }
73
74        #[test]
75        fn variable_body_is_valid(v in variable()) {
76            let body = &v[1..];
77            prop_assert!(!body.is_empty(), "variable body must not be empty: {}", v);
78        }
79
80        #[test]
81        fn identifier_is_ascii(id in identifier()) {
82            prop_assert!(id.is_ascii(), "identifier must be ASCII: {}", id);
83        }
84
85        #[test]
86        fn package_path_has_no_empty_segments(pkg in package_path()) {
87            for segment in pkg.split("::") {
88                prop_assert!(!segment.is_empty(), "empty package segment in {pkg}");
89            }
90        }
91
92        #[test]
93        fn variable_is_ascii(v in variable()) {
94            prop_assert!(v.is_ascii(), "variable must be ASCII: {v}");
95        }
96
97        #[test]
98        fn variable_len_is_at_least_two(v in variable()) {
99            // sigil + at least one body character
100            prop_assert!(v.len() >= 2, "variable too short: {v}");
101        }
102
103        #[test]
104        fn identifier_starts_with_letter_or_underscore(id in identifier()) {
105            let first = id.chars().next();
106            match first {
107                Some(ch) => prop_assert!(
108                    ch.is_ascii_alphabetic() || ch == '_',
109                    "identifier must start with letter or '_': {id}"
110                ),
111                None => prop_assert!(false, "identifier must not be empty"),
112            }
113        }
114
115        #[test]
116        fn identifier_is_non_empty(id in identifier()) {
117            prop_assert!(!id.is_empty(), "identifier must be non-empty");
118        }
119
120        #[test]
121        fn identifier_body_chars_are_word_chars(id in identifier()) {
122            let mut chars = id.chars();
123            // Skip first character (allowed: alpha or underscore)
124            let _ = chars.next();
125            for ch in chars {
126                prop_assert!(
127                    ch.is_ascii_alphanumeric() || ch == '_',
128                    "invalid identifier body char '{ch}' in {id}"
129                );
130            }
131        }
132
133        #[test]
134        fn package_path_segment_count_bounded(pkg in package_path()) {
135            let count = pkg.split("::").count();
136            prop_assert!(count >= 1, "package path must have at least one segment: {pkg}");
137            prop_assert!(count <= 4, "package path must have at most 4 segments: {pkg}");
138        }
139
140        #[test]
141        fn package_qualified_variable_contains_double_colon(v in variable()) {
142            // Variables of the form $Foo::Bar::name contain "::" — when they do,
143            // the prefix before the last "::" is the package, which must be non-empty.
144            if let Some(last_sep) = v.rfind("::") {
145                let sigil_and_pkg = &v[..last_sep];
146                // sigil is at index 0, package part starts at index 1
147                let pkg = &sigil_and_pkg[1..];
148                prop_assert!(!pkg.is_empty(), "package prefix must be non-empty in {v}");
149            }
150        }
151
152        #[test]
153        fn numeric_special_variables_in_range(v in variable()) {
154            // $1–$9 are capture variables; all numeric special vars from the
155            // generator must be in the range 0–9.
156            if let Some(body) = v.strip_prefix('$') {
157                if let Ok(n) = body.parse::<u32>() {
158                    prop_assert!(n <= 9, "numeric capture variable out of range: {v}");
159                }
160            }
161        }
162
163        #[test]
164        fn special_at_underscore_sigil_is_at(v in variable()) {
165            // @_ is a known special variable produced by the generator
166            if v == "@_" {
167                prop_assert!(v.starts_with('@'));
168            }
169        }
170
171        #[test]
172        fn package_path_is_ascii(pkg in package_path()) {
173            prop_assert!(pkg.is_ascii(), "package path must be ASCII: {pkg}");
174        }
175    }
176}