261023.1.1 · e422fc3027e36145174c0964cc4b24d0363388e9
Fix smart-case detection with inline case-insensitive flags
Inspect literal characters in the regex AST rather than the case-folded HIR. Otherwise an inline (?i) flag introduces uppercase alternatives and incorrectly makes other --and patterns case-sensitive.
Add unit and CLI regressions, including scoped flags and explicit case-sensitivity options.
Assisted-by: OpenAI Codex (investigation, implementation, tests and validation) Signed-off-by: Mohammad Abunemeh <mabunemeh@protonmail.com>
reading declarations
Changes to src/regex_helper.rs. All files
@@ -3,36 +3,45 @@ use regex_syntax::hir::Hir; /// Determine if a regex pattern contains a literal uppercase character. pub fn pattern_has_uppercase_char(pattern: &str) -> bool {- let mut parser = ParserBuilder::new().utf8(false).build();-- parser+ // The HIR includes case-folded alternatives introduced by inline flags.+ // Smart case must consider the original literals, before case folding.+ regex_syntax::ast::parse::Parser::new() .parse(pattern)- .map(|hir| hir_has_uppercase_char(&hir))+ .map(|ast| regex_syntax::ast::visit(&ast, UppercaseVisitor(false)).unwrap_or(false)) .unwrap_or(false) } -/// Determine if a regex expression contains a literal uppercase character.-fn hir_has_uppercase_char(hir: &Hir) -> bool {- use regex_syntax::hir::*;+struct UppercaseVisitor(bool); - match hir.kind() {- HirKind::Literal(Literal(bytes)) => match std::str::from_utf8(bytes) {- Ok(s) => s.chars().any(|c| c.is_uppercase()),- Err(_) => bytes.iter().any(|b| char::from(*b).is_uppercase()),- },- HirKind::Class(Class::Unicode(ranges)) => ranges- .iter()- .any(|r| r.start().is_uppercase() || r.end().is_uppercase()),- HirKind::Class(Class::Bytes(ranges)) => ranges- .iter()- .any(|r| char::from(r.start()).is_uppercase() || char::from(r.end()).is_uppercase()),- HirKind::Capture(Capture { sub, .. }) | HirKind::Repetition(Repetition { sub, .. }) => {- hir_has_uppercase_char(sub)- }- HirKind::Concat(hirs) | HirKind::Alternation(hirs) => {- hirs.iter().any(hir_has_uppercase_char)+impl regex_syntax::ast::Visitor for UppercaseVisitor {+ type Output = bool;+ type Err = std::convert::Infallible;++ fn finish(self) -> Result<bool, Self::Err> {+ Ok(self.0)+ }++ fn visit_pre(&mut self, ast: ®ex_syntax::ast::Ast) -> Result<(), Self::Err> {+ if let regex_syntax::ast::Ast::Literal(literal) = ast {+ self.0 |= literal.c.is_uppercase(); }- _ => false,+ Ok(())+ }++ fn visit_class_set_item_pre(+ &mut self,+ item: ®ex_syntax::ast::ClassSetItem,+ ) -> Result<(), Self::Err> {+ use regex_syntax::ast::ClassSetItem;++ self.0 |= match item {+ ClassSetItem::Literal(literal) => literal.c.is_uppercase(),+ ClassSetItem::Range(range) => {+ range.start.c.is_uppercase() || range.end.c.is_uppercase()+ }+ _ => false,+ };+ Ok(()) } } @@ -103,3 +112,19 @@ fn matches_strings_with_leading_dot_simple() { assert!(!pattern_matches_strings_with_leading_dot("\\.gitignore")); assert!(!pattern_matches_strings_with_leading_dot("^gitignore")); }++#[test]+fn pattern_has_uppercase_char_inline_flags() {+ for pattern in [+ "(?i)foo",+ "(?i:foo)",+ "(?i)[a-z]",+ "(?i)[a-z&&[^q]]",+ "(?i)é",+ ] {+ assert!(!pattern_has_uppercase_char(pattern), "{pattern}");+ }+ for pattern in ["(?i)Foo", "(?i)[A-Z]", r"(?i)\x46oo", "(?i)É"] {+ assert!(pattern_has_uppercase_char(pattern), "{pattern}");+ }+}