Add some tests for regex syntax support

Some functionality is only supported because the regex is built with the
Unicode flag instead of the UnicodeSets flag, but that's done for
backwards compatibility as the latter disallows some syntax that may be
in use.
This commit is contained in:
Oliver Hamlet
2026-01-18 21:33:23 +00:00
parent 9c59d87f23
commit 55510729ca
2 changed files with 254 additions and 5 deletions
+55 -5
View File
@@ -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::<String>()
.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::<String>();
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(),
}
}
+199
View File
@@ -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 <https://262.ecma-international.org/16.0/index.html#table-binary-unicode-properties-of-strings>
#[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<name>").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());
}
}