diff --git a/array-parameterized-test/src/lib.rs b/array-parameterized-test/src/lib.rs index 7bb1fd52..9e62e1aa 100644 --- a/array-parameterized-test/src/lib.rs +++ b/array-parameterized-test/src/lib.rs @@ -1,4 +1,4 @@ -use proc_macro2::{Group, TokenStream}; +use proc_macro2::{Group, TokenStream, TokenTree}; use quote::{ToTokens, format_ident, quote}; use syn::{Expr, Ident, ItemConst, ItemFn, Token, parse, parse_macro_input}; @@ -122,11 +122,45 @@ pub fn generate_tests(input: proc_macro::TokenStream) -> proc_macro::TokenStream .step_by(2) .enumerate() .flat_map(|(i, value)| { - let suffix = value - .to_string() + let suffix = string_value(&value) .escape_default() - .collect::() - .replace(|c: char| !c.is_ascii_alphanumeric(), "_"); + .map(|c| match c { + ' ' => "_space_".to_owned(), + '!' => "_exclamation_mark_".to_owned(), + '"' => "_double_quote_".to_owned(), + '#' => "_hash_sign_".to_owned(), + '$' => "_dollar_sign_".to_owned(), + '%' => "_percent_sign_".to_owned(), + '&' => "_ampersand_".to_owned(), + '\'' => "_single_quote_".to_owned(), + '(' => "_opening_parenthesis_".to_owned(), + ')' => "_closing_parenthesis_".to_owned(), + '*' => "_asterisk_".to_owned(), + '+' => "_plus_sign_".to_owned(), + ',' => "_comma_".to_owned(), + '-' => "_hyphen_".to_owned(), + '.' => "_period_".to_owned(), + '/' => "_slash_".to_owned(), + ':' => "_colon_".to_owned(), + ';' => "_semicolon_".to_owned(), + '<' => "_less_than_sign_".to_owned(), + '=' => "_equals_sign_".to_owned(), + '>' => "_greater_than_sign_".to_owned(), + '?' => "_question_mark_".to_owned(), + '@' => "_at_sign_".to_owned(), + '[' => "_opening_square_bracket_".to_owned(), + '\\' => "_backslash_".to_owned(), + ']' => "_closing_square_bracket_".to_owned(), + '^' => "_caret_".to_owned(), + '_' => "_underscore_".to_owned(), + '`' => "_backtick_".to_owned(), + '{' => "_opening_curly_brace_".to_owned(), + '|' => "_vertical_bar_".to_owned(), + '}' => "_closing_curly_brace_".to_owned(), + '~' => "_tilde_".to_owned(), + _ => c.to_string(), + }) + .collect::(); let test_name = format_ident!("_{suffix}"); @@ -142,3 +176,19 @@ pub fn generate_tests(input: proc_macro::TokenStream) -> proc_macro::TokenStream tokens.into() } + +fn string_value(tree: &TokenTree) -> String { + match tree { + TokenTree::Literal(literal) => { + let s = literal.to_string(); + if s.starts_with('"') && s.ends_with('"') { + s.trim_matches('"').to_owned() + } else if s.starts_with('\'') && s.ends_with('\'') { + s.trim_matches('\'').to_owned() + } else { + s + } + } + TokenTree::Group(_) | TokenTree::Ident(_) | TokenTree::Punct(_) => tree.to_string(), + } +} diff --git a/src/tests.rs b/src/tests.rs index b8f241c5..78c46ac1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -413,3 +413,202 @@ mod unicase { assert!(unicase::eq("\u{03c1}", "\u{03f1}")); } } + +mod case_insensitive_regex { + use array_parameterized_test::{parameterized_test, test_parameter}; + + use super::super::case_insensitive_regex; + + #[test_parameter] + const POSIX_CHARACTER_CLASSES: [&str; 15] = [ + "alnum", "alpha", "blank", "cntrl", "digit", "graph", "lower", "print", "punct", "space", + "upper", "xdigit", "d", "s", "w", + ]; + + #[test_parameter] + const IDENTITY_ESCAPE_CHARS: [char; 62] = [ + ' ', '!', '"', '#', '%', '&', '\'', ',', '-', ':', ';', '<', '=', '>', '@', '_', '`', '~', + 'A', 'C', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'Q', 'R', 'T', 'U', 'V', + 'X', 'Y', 'Z', 'a', 'c', 'e', 'g', 'h', 'i', 'j', 'l', 'm', 'o', 'q', 'x', 'y', 'z', '1', + '2', '3', '4', '5', '6', '7', '8', '9', + ]; + + #[test_parameter] + const SYNTAX_ESCAPE_CHARS: [char; 15] = [ + '$', '(', ')', '*', '+', '.', '/', '?', '[', '\\', ']', '^', '{', '|', '}', + ]; + + // From + #[test_parameter] + const UNICODE_STRING_PROPERTY_NAMES: [&str; 7] = [ + "Basic_Emoji", + "Emoji_Keycap_Sequence", + "RGI_Emoji_Modifier_Sequence", + "RGI_Emoji_Flag_Sequence", + "RGI_Emoji_Tag_Sequence", + "RGI_Emoji_ZWJ_Sequence", + "RGI_Emoji", + ]; + + #[test] + fn should_be_case_insensitive() { + let regex = case_insensitive_regex("A").unwrap(); + + assert!(regex.find("a").is_some()); + } + + #[test] + fn should_support_unicode() { + let regex = case_insensitive_regex("\\u{61}").unwrap(); + + assert!(regex.find("a").is_some()); + + let regex = case_insensitive_regex("[\u{1F604}]").unwrap(); + + assert!(regex.find("\u{1F604}").is_some()); + } + + #[test] + fn should_support_i_modifier() { + let regex = case_insensitive_regex("(?-i:A)").unwrap(); + + assert!(regex.find("A").is_some()); + assert!(regex.find("a").is_none()); + } + + #[test] + fn should_support_m_modifier() { + let regex = case_insensitive_regex("A\n(?m:^)B").unwrap(); + + assert!(regex.find("A\nB").is_some()); + } + + #[test] + fn should_support_s_modifier() { + let regex = case_insensitive_regex("A(?s:.)B").unwrap(); + + assert!(regex.find("A\nB").is_some()); + } + + #[parameterized_test(POSIX_CHARACTER_CLASSES)] + fn does_not_support_named_character_classes(class: &str) { + let err = case_insensitive_regex(&format!("[[:{class}:]]")).unwrap_err(); + + assert_eq!("Invalid atom character", err.to_string()); + } + + #[test] + fn should_not_support_quantifiers_on_lookahead_assertions() { + let err = case_insensitive_regex("(?=a)?b").unwrap_err(); + + assert_eq!("Quantifier not allowed here", err.to_string()); + } + + #[test] + fn should_not_support_octal_escapes() { + let err = case_insensitive_regex("\\01").unwrap_err(); + + assert_eq!("Invalid character escape", err.to_string()); + } + + #[test] + fn should_not_support_character_ranges_with_character_class_boundaries() { + let err = case_insensitive_regex("[\\s-9]").unwrap_err(); + + assert_eq!("Invalid character range", err.to_string()); + } + + #[parameterized_test(IDENTITY_ESCAPE_CHARS)] + fn should_not_support_identity_escapes(c: char) { + let err = case_insensitive_regex(&format!("\\{c}")).unwrap_err(); + + assert_eq!("Invalid character escape", err.to_string()); + } + + #[parameterized_test(SYNTAX_ESCAPE_CHARS)] + fn should_support_syntax_character_escapes(c: char) { + let regex = case_insensitive_regex(&format!("\\{c}")).unwrap(); + + assert!(regex.find(&c.to_string()).is_some()); + } + + #[test] + fn should_support_null_escape() { + let regex = case_insensitive_regex("\\0").unwrap(); + + assert!(regex.find("\0").is_some()); + } + + #[test] + fn should_support_control_character_escapes() { + let regex = case_insensitive_regex("\\cJ").unwrap(); + + assert!(regex.find("\n").is_some()); + } + + #[test] + fn should_not_support_digit_or_underscore_control_character_escapes_in_character_classes() { + let err = case_insensitive_regex("[\\c0]").unwrap_err(); + + assert_eq!("Invalid character escape", err.to_string()); + + let err = case_insensitive_regex("[\\c_]").unwrap_err(); + + assert_eq!("Invalid character escape", err.to_string()); + } + + #[test] + fn should_not_support_backslash_k_when_there_are_no_capturing_groups() { + let err = case_insensitive_regex("\\k").unwrap_err(); + + assert_eq!( + "Backreference to invalid named capture group: name", + err.to_string() + ); + } + + #[test] + fn should_not_support_unescaped_unambiguous_brackets() { + let err = case_insensitive_regex("]").unwrap_err(); + + assert_eq!("Invalid atom character", err.to_string()); + + let err = case_insensitive_regex("{").unwrap_err(); + + assert_eq!("Invalid atom character", err.to_string()); + + let err = case_insensitive_regex("}").unwrap_err(); + + assert_eq!("Invalid atom character", err.to_string()); + } + + #[parameterized_test(UNICODE_STRING_PROPERTY_NAMES)] + fn does_not_support_unicode_property_escapes_for_strings(value: &str) { + let err = case_insensitive_regex(&format!("\\p{{{value}}}")).unwrap_err(); + + assert_eq!("Invalid property name", err.to_string()); + } + + #[test] + fn does_not_support_character_class_intersection() { + let regex = case_insensitive_regex("[\\p{Script_Extensions=Greek}&&\\p{Letter}]").unwrap(); + + assert!(regex.find("\u{03C0}").is_some()); + assert!(regex.find("A").is_some()); + assert!(regex.find("\u{1018A}").is_some()); + } + + #[test] + fn does_not_support_character_class_subtraction() { + let err = case_insensitive_regex("[\\p{Decimal_Number}--\\d]").unwrap_err(); + + assert_eq!("Invalid character range", err.to_string()); + } + + #[test] + fn does_not_support_q_escapes_in_character_classes() { + let err = case_insensitive_regex("^[\\q{\u{1F1FA}}]$").unwrap_err(); + + assert_eq!("Invalid character escape", err.to_string()); + } +}