Skip to main content

brokk_bifrost_python/
regex.rs

1//! Python `re` pattern-value facts.
2//!
3//! Source structure comes from tree-sitter-python. This module decodes only a
4//! string AST node selected as the pattern argument of a recognized `re` API,
5//! then parses that decoded value with Python `re` rules. Unsupported syntax
6//! is emitted as an incomplete fact instead of being treated as harmless.
7
8use crate::bindings::{
9    PythonLexicalNameResolution, python_comprehension_binds_name_at,
10    python_module_or_class_scope_binds_name_bounded, python_name_resolution_at,
11};
12use crate::syntax::python_static_attribute_path;
13use brokk_bifrost_core::analyzer::Range;
14use brokk_bifrost_core::analyzer::structural::kinds::NormalizedKind;
15use brokk_bifrost_core::analyzer::structural::spec::EmbeddedLeafFact;
16use brokk_bifrost_core::text_utils::{compute_line_starts, find_line_index_for_offset};
17use std::collections::{HashMap, HashSet, VecDeque};
18use tree_sitter::Node;
19
20const MAX_PATTERN_CHARS: usize = 8192;
21const MAX_PATTERN_FACTS: usize = 4096;
22const MAX_GROUP_DEPTH: usize = 128;
23const MAX_NFA_STATES: usize = 512;
24const MAX_NFA_PRODUCT_PAIRS: usize = 65_536;
25const MAX_NFA_REPEAT: u32 = 32;
26const MAX_REDOSE_GROUPS: usize = 16;
27const MAX_REDOSE_ALTERNATIVES: usize = 8;
28
29#[derive(Debug)]
30struct MappedChar {
31    source_start: usize,
32    source_end: usize,
33}
34
35#[derive(Debug)]
36struct DecodedValue {
37    value: String,
38    chars: Vec<MappedChar>,
39    source_range: Range,
40    is_bytes: bool,
41}
42
43#[derive(Debug, Clone, Copy)]
44struct RegexFlags {
45    dotall: bool,
46    multiline: bool,
47    ignorecase: bool,
48    ascii: bool,
49    verbose: bool,
50    complete: bool,
51}
52
53#[derive(Debug, Clone, Copy)]
54struct Api {
55    flags_position: usize,
56    scans_from_each_offset: bool,
57}
58
59#[derive(Debug, Clone)]
60struct Fact {
61    kind: NormalizedKind,
62    start: usize,
63    end: usize,
64    parent: Option<usize>,
65    construct: Option<String>,
66    first_set: Option<CharacterSet>,
67    // Singleton escape value before case folding; retained only for this pattern.
68    escaped_literal: Option<char>,
69    quantifier: Option<Quantifier>,
70    minimum_width: Option<u32>,
71}
72
73fn regex_sequence_branches(
74    facts: &[Fact],
75    children: impl IntoIterator<Item = usize>,
76) -> Vec<Vec<usize>> {
77    let mut branches = vec![Vec::new()];
78    for child in children {
79        if facts[child].kind == NormalizedKind::RegexAlternation {
80            branches.push(Vec::new());
81        } else {
82            branches
83                .last_mut()
84                .expect("a regex sequence has a branch")
85                .push(child);
86        }
87    }
88    branches
89}
90
91fn polynomial_atoms_overlap(left: &Fact, right: &Fact) -> bool {
92    match (&left.first_set, &right.first_set) {
93        (Some(left), Some(right)) => left.intersects(right),
94        _ => matches!(
95            (left.construct.as_deref(), right.construct.as_deref()),
96            (Some("category:d"), Some("category:d" | "category:w"))
97                | (Some("category:w"), Some("category:d" | "category:w"))
98                | (Some("category:s"), Some("category:s"))
99        ),
100    }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104struct CharacterSet {
105    ranges: Vec<(u32, u32)>,
106}
107
108impl CharacterSet {
109    fn one(value: char) -> Self {
110        let value = value as u32;
111        Self {
112            ranges: vec![(value, value)],
113        }
114    }
115
116    fn any_except_newline() -> Self {
117        Self {
118            ranges: vec![(0, 9), (11, 0x10ffff)],
119        }
120    }
121
122    fn complement(mut self) -> Self {
123        let mut ranges = Vec::new();
124        let mut next = 0u32;
125        self.ranges.sort_unstable();
126        for (start, end) in self.ranges {
127            if next < start {
128                ranges.push((next, start - 1));
129            }
130            next = end.saturating_add(1);
131            if next > 0x10ffff {
132                break;
133            }
134        }
135        if next <= 0x10ffff {
136            ranges.push((next, 0x10ffff));
137        }
138        Self { ranges }
139    }
140
141    fn union_with(&mut self, other: &Self) {
142        self.ranges.extend_from_slice(&other.ranges);
143        self.ranges.sort_unstable();
144        let mut merged: Vec<(u32, u32)> = Vec::with_capacity(self.ranges.len());
145        for (start, end) in self.ranges.drain(..) {
146            if let Some((_, previous_end)) = merged.last_mut()
147                && start <= previous_end.saturating_add(1)
148            {
149                *previous_end = (*previous_end).max(end);
150            } else {
151                merged.push((start, end));
152            }
153        }
154        self.ranges = merged;
155    }
156
157    fn intersects(&self, other: &Self) -> bool {
158        self.ranges.iter().any(|(left_start, left_end)| {
159            other
160                .ranges
161                .iter()
162                .any(|(right_start, right_end)| left_start <= right_end && right_start <= left_end)
163        })
164    }
165
166    fn contains(&self, character: char) -> bool {
167        let value = character as u32;
168        self.ranges
169            .iter()
170            .any(|(start, end)| *start <= value && value <= *end)
171    }
172
173    fn with_ascii_case_variants(mut self) -> Self {
174        for lower in 'a'..='z' {
175            let upper = lower.to_ascii_uppercase();
176            match (self.contains(lower), self.contains(upper)) {
177                (true, false) => self.union_with(&Self::one(upper)),
178                (false, true) => self.union_with(&Self::one(lower)),
179                _ => {}
180            }
181        }
182        self
183    }
184}
185
186#[derive(Debug, Clone, Default)]
187struct NfaState {
188    epsilon: Vec<usize>,
189    transitions: Vec<(CharacterSet, usize)>,
190    capture_ends: Vec<usize>,
191}
192
193#[derive(Debug, Clone)]
194struct NfaFragment {
195    start: usize,
196    end: usize,
197    states: Vec<usize>,
198}
199
200#[derive(Debug)]
201struct RegexNfa {
202    states: Vec<NfaState>,
203    start: usize,
204    end: usize,
205}
206
207#[derive(Debug, Clone, Copy)]
208enum NfaError {
209    Unsupported,
210    Budget,
211}
212
213impl RegexNfa {
214    fn compile(
215        facts: &[Fact],
216        root: usize,
217        selected_branch: Option<usize>,
218    ) -> Result<Self, NfaError> {
219        let mut children = vec![Vec::new(); facts.len()];
220        for (id, fact) in facts.iter().enumerate() {
221            if let Some(parent) = fact.parent {
222                children[parent].push(id);
223            }
224        }
225        let mut capture_ids = vec![None; facts.len()];
226        let mut next_capture = 1;
227        for (id, fact) in facts.iter().enumerate() {
228            if fact.kind == NormalizedKind::RegexGroup
229                && matches!(
230                    fact.construct.as_deref(),
231                    Some("capturing" | "named-capturing")
232                )
233            {
234                capture_ids[id] = Some(next_capture);
235                next_capture += 1;
236            }
237        }
238        let mut builder = NfaBuilder {
239            facts,
240            children,
241            selected_branch: selected_branch.map(|branch| (root, branch)),
242            capture_ids: &capture_ids,
243            states: Vec::new(),
244        };
245        let mut fragments = vec![None; facts.len()];
246        let mut stack = vec![(root, false)];
247        while let Some((id, expanded)) = stack.pop() {
248            if fragments[id].is_some() {
249                continue;
250            }
251            if expanded {
252                fragments[id] = Some(builder.compile_fact(id, &fragments)?);
253                continue;
254            }
255            stack.push((id, true));
256            let dependencies = builder.dependencies(id);
257            for dependency in dependencies.into_iter().rev() {
258                stack.push((dependency, false));
259            }
260        }
261        let fragment = fragments[root].take().ok_or(NfaError::Unsupported)?;
262        Ok(Self {
263            states: builder.states,
264            start: fragment.start,
265            end: fragment.end,
266        })
267    }
268
269    fn match_prefix_capture_groups(&self, sample: &str) -> (bool, HashSet<usize>) {
270        let chars = sample.chars().collect::<Vec<_>>();
271        let width = chars.len() + 1;
272        let node_id = |state: usize, position: usize| state * width + position;
273        let mut reachable = vec![false; self.states.len() * width];
274        let mut reverse = vec![Vec::new(); self.states.len() * width];
275        let start = node_id(self.start, 0);
276        reachable[start] = true;
277        let mut pending = VecDeque::from([start]);
278        while let Some(node) = pending.pop_front() {
279            let state = node / width;
280            let position = node % width;
281            let mut successors = self.states[state]
282                .epsilon
283                .iter()
284                .map(|&target| node_id(target, position))
285                .collect::<Vec<_>>();
286            if let Some(&character) = chars.get(position) {
287                successors.extend(
288                    self.states[state]
289                        .transitions
290                        .iter()
291                        .filter(|(set, _)| set.contains(character))
292                        .map(|(_, target)| node_id(*target, position + 1)),
293                );
294            }
295            for successor in successors {
296                reverse[successor].push(node);
297                if !std::mem::replace(&mut reachable[successor], true) {
298                    pending.push_back(successor);
299                }
300            }
301        }
302
303        let accepting = (0..width)
304            .map(|position| node_id(self.end, position))
305            .filter(|&node| reachable[node])
306            .collect::<Vec<_>>();
307        if accepting.is_empty() {
308            return (false, HashSet::new());
309        }
310        let mut reaches_accept = vec![false; reachable.len()];
311        let mut pending = VecDeque::new();
312        for node in accepting {
313            reaches_accept[node] = true;
314            pending.push_back(node);
315        }
316        while let Some(node) = pending.pop_front() {
317            for &previous in &reverse[node] {
318                if !std::mem::replace(&mut reaches_accept[previous], true) {
319                    pending.push_back(previous);
320                }
321            }
322        }
323        let groups = self
324            .states
325            .iter()
326            .enumerate()
327            .filter(|(state, _)| {
328                (0..width).any(|position| {
329                    let node = node_id(*state, position);
330                    reachable[node] && reaches_accept[node]
331                })
332            })
333            .flat_map(|(_, state)| state.capture_ends.iter().copied())
334            .collect();
335        (true, groups)
336    }
337
338    fn intersects_on_nonempty_string(
339        &self,
340        other: &Self,
341        remaining_budget: &mut usize,
342    ) -> Result<bool, NfaError> {
343        let mut pending = VecDeque::from([(self.start, other.start, false)]);
344        let mut seen = HashSet::new();
345        while let Some((left, right, consumed)) = pending.pop_front() {
346            if !seen.insert((left, right, consumed)) {
347                continue;
348            }
349            if *remaining_budget == 0 {
350                return Err(NfaError::Budget);
351            }
352            *remaining_budget -= 1;
353            if consumed && left == self.end && right == other.end {
354                return Ok(true);
355            }
356            for &target in &self.states[left].epsilon {
357                pending.push_back((target, right, consumed));
358            }
359            for &target in &other.states[right].epsilon {
360                pending.push_back((left, target, consumed));
361            }
362            for (left_set, left_target) in &self.states[left].transitions {
363                for (right_set, right_target) in &other.states[right].transitions {
364                    if left_set.intersects(right_set) {
365                        pending.push_back((*left_target, *right_target, true));
366                    }
367                }
368            }
369        }
370        Ok(false)
371    }
372
373    fn has_repartitioned_prefix(
374        &self,
375        longer: &Self,
376        repeated: &Self,
377        remaining_budget: &mut usize,
378    ) -> Result<bool, NfaError> {
379        // Find a non-empty common prefix accepted by `self` while `longer`
380        // continues, then require that continuation to match the repeated
381        // alternation. This catches ambiguous partitions such as `__|.`.
382        let mut pending = VecDeque::from([(0u8, self.start, longer.start, false)]);
383        let mut seen = HashSet::new();
384        while let Some((phase, left, right, consumed)) = pending.pop_front() {
385            if !seen.insert((phase, left, right, consumed)) {
386                continue;
387            }
388            if *remaining_budget == 0 {
389                return Err(NfaError::Budget);
390            }
391            *remaining_budget -= 1;
392            if phase == 0 {
393                if consumed && left == self.end {
394                    pending.push_back((1, right, repeated.start, false));
395                }
396                for &target in &self.states[left].epsilon {
397                    pending.push_back((0, target, right, consumed));
398                }
399                for &target in &longer.states[right].epsilon {
400                    pending.push_back((0, left, target, consumed));
401                }
402                for (left_set, left_target) in &self.states[left].transitions {
403                    for (right_set, right_target) in &longer.states[right].transitions {
404                        if left_set.intersects(right_set) {
405                            pending.push_back((0, *left_target, *right_target, true));
406                        }
407                    }
408                }
409            } else {
410                if consumed && left == longer.end && right == repeated.end {
411                    return Ok(true);
412                }
413                for &target in &longer.states[left].epsilon {
414                    pending.push_back((1, target, right, consumed));
415                }
416                for &target in &repeated.states[right].epsilon {
417                    pending.push_back((1, left, target, consumed));
418                }
419                for (longer_set, longer_target) in &longer.states[left].transitions {
420                    for (repeated_set, repeated_target) in &repeated.states[right].transitions {
421                        if longer_set.intersects(repeated_set) {
422                            pending.push_back((1, *longer_target, *repeated_target, true));
423                        }
424                    }
425                }
426            }
427        }
428        Ok(false)
429    }
430
431    fn has_pumpable_fork(&self, remaining_budget: &mut usize) -> Result<bool, NfaError> {
432        let epsilon_closures = self.epsilon_closures();
433        for fork in 0..self.states.len() {
434            let mut outgoing = Vec::new();
435            for &source in &epsilon_closures[fork] {
436                for (set, target) in &self.states[source].transitions {
437                    outgoing.push((source, set, *target));
438                }
439            }
440            for left in 0..outgoing.len() {
441                for right in left + 1..outgoing.len() {
442                    if *remaining_budget == 0 {
443                        return Err(NfaError::Budget);
444                    }
445                    *remaining_budget -= 1;
446                    let (left_source, left_set, left_target) = outgoing[left];
447                    let (right_source, right_set, right_target) = outgoing[right];
448                    if left_source == right_source
449                        && left_target == right_target
450                        && left_set.ranges == right_set.ranges
451                    {
452                        continue;
453                    }
454                    if !left_set.intersects(right_set) {
455                        continue;
456                    }
457                    if self.product_reaches_fork(
458                        (left_target, right_target),
459                        fork,
460                        &epsilon_closures,
461                        remaining_budget,
462                    )? {
463                        return Ok(true);
464                    }
465                }
466            }
467        }
468        Ok(false)
469    }
470
471    fn epsilon_closures(&self) -> Vec<Vec<usize>> {
472        (0..self.states.len())
473            .map(|start| {
474                let mut seen = vec![false; self.states.len()];
475                let mut pending = vec![start];
476                let mut closure = Vec::new();
477                while let Some(state) = pending.pop() {
478                    if std::mem::replace(&mut seen[state], true) {
479                        continue;
480                    }
481                    closure.push(state);
482                    pending.extend(self.states[state].epsilon.iter().copied());
483                }
484                closure
485            })
486            .collect()
487    }
488
489    fn product_reaches_fork(
490        &self,
491        start: (usize, usize),
492        fork: usize,
493        epsilon_closures: &[Vec<usize>],
494        remaining_budget: &mut usize,
495    ) -> Result<bool, NfaError> {
496        let canonical = |(left, right): (usize, usize)| {
497            if left <= right {
498                (left, right)
499            } else {
500                (right, left)
501            }
502        };
503        let mut pending = VecDeque::from([canonical(start)]);
504        let mut seen = HashSet::new();
505        while let Some((left, right)) = pending.pop_front() {
506            if (left, right) == (fork, fork) {
507                return Ok(true);
508            }
509            if !seen.insert((left, right)) {
510                continue;
511            }
512            for &left_source in &epsilon_closures[left] {
513                for (left_set, left_target) in &self.states[left_source].transitions {
514                    for &right_source in &epsilon_closures[right] {
515                        for (right_set, right_target) in &self.states[right_source].transitions {
516                            if *remaining_budget == 0 {
517                                return Err(NfaError::Budget);
518                            }
519                            *remaining_budget -= 1;
520                            if left_set.intersects(right_set) {
521                                pending.push_back(canonical((*left_target, *right_target)));
522                            }
523                        }
524                    }
525                }
526            }
527        }
528        Ok(false)
529    }
530
531    fn positive_closure(mut self) -> Result<Self, NfaError> {
532        if self.states.len().saturating_add(2) > MAX_NFA_STATES {
533            return Err(NfaError::Budget);
534        }
535        let original_start = self.start;
536        let original_end = self.end;
537        let start = self.states.len();
538        let end = start + 1;
539        self.states.push(NfaState::default());
540        self.states.push(NfaState::default());
541        self.states[start].epsilon.push(original_start);
542        self.states[original_end]
543            .epsilon
544            .extend([end, original_start]);
545        self.start = start;
546        self.end = end;
547        Ok(self)
548    }
549}
550
551struct NfaBuilder<'a> {
552    facts: &'a [Fact],
553    children: Vec<Vec<usize>>,
554    selected_branch: Option<(usize, usize)>,
555    capture_ids: &'a [Option<usize>],
556    states: Vec<NfaState>,
557}
558
559impl NfaBuilder<'_> {
560    fn dependencies(&self, id: usize) -> Vec<usize> {
561        match self.facts[id].kind {
562            NormalizedKind::RegexPattern | NormalizedKind::RegexGroup => self.children[id]
563                .iter()
564                .copied()
565                .filter(|child| self.facts[*child].kind == NormalizedKind::RegexSequence)
566                .collect(),
567            NormalizedKind::RegexSequence => self.children[id]
568                .iter()
569                .copied()
570                .filter(|child| {
571                    !matches!(
572                        self.facts[*child].kind,
573                        NormalizedKind::RegexAlternation | NormalizedKind::RegexQuantifier
574                    )
575                })
576                .collect(),
577            _ => Vec::new(),
578        }
579    }
580
581    fn compile_fact(
582        &mut self,
583        id: usize,
584        fragments: &[Option<NfaFragment>],
585    ) -> Result<NfaFragment, NfaError> {
586        let fact = &self.facts[id];
587        let kind = fact.kind;
588        let is_comment = fact.construct.as_deref() == Some("comment");
589        let first_set = fact.first_set.clone();
590        let base = match kind {
591            NormalizedKind::RegexPattern | NormalizedKind::RegexGroup => {
592                if is_comment {
593                    self.empty_fragment()?
594                } else if kind == NormalizedKind::RegexGroup
595                    && fact
596                        .construct
597                        .as_deref()
598                        .is_some_and(|construct| construct.starts_with("look"))
599                {
600                    return Err(NfaError::Unsupported);
601                } else {
602                    let sequence = self.children[id]
603                        .iter()
604                        .copied()
605                        .find(|child| self.facts[*child].kind == NormalizedKind::RegexSequence)
606                        .ok_or(NfaError::Unsupported)?;
607                    fragments[sequence].clone().ok_or(NfaError::Unsupported)?
608                }
609            }
610            NormalizedKind::RegexSequence => self.compile_sequence(id, fragments)?,
611            NormalizedKind::RegexAnchor => return Err(NfaError::Unsupported),
612            NormalizedKind::RegexLiteral
613            | NormalizedKind::RegexWildcard
614            | NormalizedKind::RegexCharacterClass
615            | NormalizedKind::RegexCharacterRange
616            | NormalizedKind::RegexEscape => {
617                let set = first_set.ok_or(NfaError::Unsupported)?;
618                self.character_fragment(set)?
619            }
620            _ => return Err(NfaError::Unsupported),
621        };
622        let base = if let Some(capture) = self.capture_ids[id] {
623            self.capture_group(base, capture)?
624        } else {
625            base
626        };
627        self.apply_quantifier(id, base)
628    }
629
630    fn capture_group(
631        &mut self,
632        mut inner: NfaFragment,
633        capture: usize,
634    ) -> Result<NfaFragment, NfaError> {
635        let start = self.new_state()?;
636        let end = self.new_state()?;
637        self.states[start].epsilon.push(inner.start);
638        self.states[inner.end].epsilon.push(end);
639        self.states[end].capture_ends.push(capture);
640        inner.states.extend([start, end]);
641        inner.start = start;
642        inner.end = end;
643        Ok(inner)
644    }
645
646    fn compile_sequence(
647        &mut self,
648        sequence: usize,
649        fragments: &[Option<NfaFragment>],
650    ) -> Result<NfaFragment, NfaError> {
651        let branches = regex_sequence_branches(self.facts, self.children[sequence].iter().copied())
652            .into_iter()
653            .map(|terms| {
654                let terms = terms
655                    .into_iter()
656                    .filter(|&child| self.facts[child].kind != NormalizedKind::RegexQuantifier)
657                    .map(|child| fragments[child].clone().ok_or(NfaError::Unsupported))
658                    .collect::<Result<Vec<_>, _>>()?;
659                self.concatenate(terms)
660            })
661            .collect::<Result<Vec<_>, _>>()?;
662        if let Some((selected_sequence, selected_index)) = self.selected_branch
663            && selected_sequence == sequence
664        {
665            return branches
666                .get(selected_index)
667                .cloned()
668                .ok_or(NfaError::Unsupported);
669        }
670        self.alternate(branches)
671    }
672
673    fn apply_quantifier(&mut self, id: usize, base: NfaFragment) -> Result<NfaFragment, NfaError> {
674        let Some(quantifier_id) = self.children[id]
675            .iter()
676            .copied()
677            .find(|child| self.facts[*child].kind == NormalizedKind::RegexQuantifier)
678        else {
679            return Ok(base);
680        };
681        let quantifier = self.facts[quantifier_id]
682            .quantifier
683            .ok_or(NfaError::Unsupported)?;
684        if quantifier.minimum > MAX_NFA_REPEAT
685            || quantifier
686                .maximum
687                .is_some_and(|maximum| maximum > MAX_NFA_REPEAT)
688        {
689            return Err(NfaError::Budget);
690        }
691        let mut result = if quantifier.minimum == 0 {
692            self.empty_fragment()?
693        } else {
694            base.clone()
695        };
696        let mut copies = Vec::new();
697        for _ in 1..quantifier.minimum {
698            copies.push(self.clone_fragment(&base)?);
699        }
700        let unbounded = match quantifier.maximum {
701            Some(maximum) => {
702                for _ in quantifier.minimum..maximum {
703                    let clone = self.clone_fragment(&base)?;
704                    copies.push(self.optional(clone)?);
705                }
706                None
707            }
708            None => {
709                let clone = self.clone_fragment(&base)?;
710                Some(self.star(clone)?)
711            }
712        };
713        for copy in copies {
714            result = self.concatenate(vec![result, copy])?;
715        }
716        if let Some(unbounded) = unbounded {
717            result = self.concatenate(vec![result, unbounded])?;
718        }
719        Ok(result)
720    }
721
722    fn new_state(&mut self) -> Result<usize, NfaError> {
723        if self.states.len() >= MAX_NFA_STATES {
724            return Err(NfaError::Budget);
725        }
726        let state = self.states.len();
727        self.states.push(NfaState::default());
728        Ok(state)
729    }
730
731    fn empty_fragment(&mut self) -> Result<NfaFragment, NfaError> {
732        let start = self.new_state()?;
733        let end = self.new_state()?;
734        self.states[start].epsilon.push(end);
735        Ok(NfaFragment {
736            start,
737            end,
738            states: vec![start, end],
739        })
740    }
741
742    fn character_fragment(&mut self, set: CharacterSet) -> Result<NfaFragment, NfaError> {
743        let start = self.new_state()?;
744        let end = self.new_state()?;
745        self.states[start].transitions.push((set, end));
746        Ok(NfaFragment {
747            start,
748            end,
749            states: vec![start, end],
750        })
751    }
752
753    fn concatenate(&mut self, mut fragments: Vec<NfaFragment>) -> Result<NfaFragment, NfaError> {
754        let Some(mut result) = fragments.pop() else {
755            return self.empty_fragment();
756        };
757        while let Some(next) = fragments.pop() {
758            self.states[next.end].epsilon.push(result.start);
759            result.start = next.start;
760            result.states.extend(next.states);
761        }
762        Ok(result)
763    }
764
765    fn alternate(&mut self, fragments: Vec<NfaFragment>) -> Result<NfaFragment, NfaError> {
766        if fragments.len() == 1 {
767            return Ok(fragments
768                .into_iter()
769                .next()
770                .expect("one alternative exists"));
771        }
772        let start = self.new_state()?;
773        let end = self.new_state()?;
774        let mut states = vec![start, end];
775        for fragment in fragments {
776            self.states[start].epsilon.push(fragment.start);
777            self.states[fragment.end].epsilon.push(end);
778            states.extend(fragment.states);
779        }
780        Ok(NfaFragment { start, end, states })
781    }
782
783    fn optional(&mut self, inner: NfaFragment) -> Result<NfaFragment, NfaError> {
784        let start = self.new_state()?;
785        let end = self.new_state()?;
786        self.states[start].epsilon.extend([end, inner.start]);
787        self.states[inner.end].epsilon.push(end);
788        let mut states = vec![start, end];
789        states.extend(inner.states);
790        Ok(NfaFragment { start, end, states })
791    }
792
793    fn star(&mut self, inner: NfaFragment) -> Result<NfaFragment, NfaError> {
794        let start = self.new_state()?;
795        let end = self.new_state()?;
796        self.states[start].epsilon.extend([end, inner.start]);
797        self.states[inner.end].epsilon.extend([end, inner.start]);
798        let mut states = vec![start, end];
799        states.extend(inner.states);
800        Ok(NfaFragment { start, end, states })
801    }
802
803    fn clone_fragment(&mut self, fragment: &NfaFragment) -> Result<NfaFragment, NfaError> {
804        let mut remap = vec![usize::MAX; self.states.len()];
805        for &old in &fragment.states {
806            remap[old] = self.new_state()?;
807        }
808        for &old in &fragment.states {
809            let new = remap[old];
810            let old_state = self.states[old].clone();
811            self.states[new].capture_ends = old_state.capture_ends;
812            for target in old_state.epsilon {
813                let Some(mapped) = remap
814                    .get(target)
815                    .copied()
816                    .filter(|&mapped| mapped != usize::MAX)
817                else {
818                    return Err(NfaError::Unsupported);
819                };
820                self.states[new].epsilon.push(mapped);
821            }
822            for (set, target) in old_state.transitions {
823                let Some(mapped) = remap
824                    .get(target)
825                    .copied()
826                    .filter(|&mapped| mapped != usize::MAX)
827                else {
828                    return Err(NfaError::Unsupported);
829                };
830                self.states[new].transitions.push((set, mapped));
831            }
832        }
833        Ok(NfaFragment {
834            start: remap[fragment.start],
835            end: remap[fragment.end],
836            states: fragment.states.iter().map(|state| remap[*state]).collect(),
837        })
838    }
839}
840
841#[derive(Debug, Clone)]
842struct BranchFrame {
843    owner: usize,
844    sequence: usize,
845    group_start: Option<usize>,
846    last_atom: Option<usize>,
847    restore_flags: Option<RegexFlags>,
848}
849
850#[derive(Debug, Clone, Copy)]
851struct Quantifier {
852    minimum: u32,
853    maximum: Option<u32>,
854    possessive: bool,
855}
856
857#[derive(Debug)]
858struct ClassAtom {
859    start: usize,
860    end: usize,
861    value: Option<char>,
862    set: Option<CharacterSet>,
863    kind: NormalizedKind,
864    construct: Option<String>,
865    backspace: bool,
866    incomplete: Option<&'static str>,
867}
868
869#[derive(Debug)]
870struct EscapeValue {
871    end: usize,
872    anchor: bool,
873    construct: Option<String>,
874    first_set: Option<CharacterSet>,
875    incomplete: Option<&'static str>,
876    backspace: bool,
877}
878
879/// Return facts for a string-literal node or a call node in the Python AST.
880/// A string is analyzed only when it is the first positional argument or the
881/// `pattern` keyword of a statically named `re` API call.
882pub(crate) fn embedded_facts(node: Node<'_>, source: &str) -> Vec<EmbeddedLeafFact> {
883    if node.kind() == "call" {
884        return dynamic_pattern_fact(node, source).into_iter().collect();
885    }
886    if !matches!(node.kind(), "string" | "concatenated_string")
887        || (node.kind() == "string"
888            && node
889                .parent()
890                .is_some_and(|parent| parent.kind() == "concatenated_string"))
891    {
892        return Vec::new();
893    }
894    let Some((call, api)) = pattern_call(node, source) else {
895        return Vec::new();
896    };
897    let decoded = match decode_regex_pattern_string(node, source) {
898        Ok(decoded) => decoded,
899        Err(reason) => return vec![incomplete_fact(node, source, reason)],
900    };
901    let flags = call_flags(call, api, source);
902    let facts = RegexParser::new(&decoded, flags, api).parse();
903    facts_to_embedded(facts, &decoded, source)
904}
905
906pub(crate) fn module_assigned_facts(root: Node<'_>, source: &str) -> Vec<EmbeddedLeafFact> {
907    assert_eq!(root.kind(), "module");
908    let mut assignments = HashMap::<String, (usize, Option<Node<'_>>)>::new();
909    let mut calls = Vec::new();
910    let mut pending = vec![root];
911    while let Some(node) = pending.pop() {
912        if module_level_statement(node)
913            .is_some_and(|statement| statement.kind() == "expression_statement")
914            && node.kind() == "assignment"
915            && let (Some(left), Some(right)) = (
916                node.child_by_field_name("left"),
917                node.child_by_field_name("right"),
918            )
919            && left.kind() == "identifier"
920        {
921            let candidate =
922                matches!(right.kind(), "string" | "concatenated_string").then_some(right);
923            let entry = assignments
924                .entry(node_text(left, source).to_owned())
925                .or_insert((0, None));
926            entry.0 += 1;
927            entry.1 = (entry.0 == 1).then_some(candidate).flatten();
928        }
929        if node.kind() == "call"
930            && let Some(function) = node.child_by_field_name("function")
931            && let Some(api) = recognized_api(function, source)
932            && let Some(arguments) = node.child_by_field_name("arguments")
933            && let Some(pattern) = selected_pattern_argument(arguments, source)
934            && pattern.kind() == "identifier"
935        {
936            calls.push((node, pattern, api, node_text(pattern, source).to_owned()));
937        }
938        let mut cursor = node.walk();
939        let children = node.named_children(&mut cursor).collect::<Vec<_>>();
940        pending.extend(children.into_iter().rev());
941    }
942
943    let mut facts = Vec::new();
944    for (call, pattern, api, name) in calls {
945        let resolved = assignments
946            .get(&name)
947            .and_then(|(writes, candidate)| {
948                (*writes == 1)
949                    .then_some(*candidate)
950                    .flatten()
951                    .filter(|candidate| candidate.end_byte() < call.start_byte())
952            })
953            .filter(|_| module_pattern_binding_is_visible(&name, pattern, source));
954        let Some(selected) = resolved else {
955            let range = node_range(pattern, source);
956            facts.push(EmbeddedLeafFact {
957                kind: NormalizedKind::RegexIncomplete,
958                range,
959                parent: None,
960                construct: Some(format!("dynamic-pattern:{}", api.flags_position)),
961                occurrence_role: None,
962            });
963            continue;
964        };
965        let decoded = match decode_regex_pattern_string(selected, source) {
966            Ok(decoded) => decoded,
967            Err(reason) => {
968                facts.push(incomplete_fact(selected, source, reason));
969                continue;
970            }
971        };
972        let flags = call_flags(call, api, source);
973        let mut parsed = facts_to_embedded(
974            RegexParser::new(&decoded, flags, api).parse(),
975            &decoded,
976            source,
977        );
978        let base = facts.len();
979        for fact in &mut parsed {
980            fact.parent = fact.parent.map(|parent| base + parent);
981        }
982        facts.extend(parsed);
983    }
984    facts
985}
986
987fn module_pattern_binding_is_visible(name: &str, reference: Node<'_>, source: &str) -> bool {
988    if python_comprehension_binds_name_at(name, reference, source)
989        || matches!(
990            python_name_resolution_at(name, reference, source),
991            PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
992        )
993    {
994        return false;
995    }
996
997    let mut current = reference;
998    while let Some(parent) = current.parent() {
999        match parent.kind() {
1000            "function_definition" | "lambda" => break,
1001            "class_definition" => {
1002                return python_module_or_class_scope_binds_name_bounded(
1003                    parent,
1004                    name,
1005                    source,
1006                    || true,
1007                ) == Some(false);
1008            }
1009            _ => current = parent,
1010        }
1011    }
1012    true
1013}
1014
1015fn module_level_statement<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
1016    let mut current = node;
1017    loop {
1018        let parent = current.parent()?;
1019        if parent.kind() == "module" {
1020            return Some(current);
1021        }
1022        current = parent;
1023    }
1024}
1025
1026fn incomplete_fact(node: Node<'_>, source: &str, reason: &str) -> EmbeddedLeafFact {
1027    let range = node_range(node, source);
1028    EmbeddedLeafFact {
1029        kind: NormalizedKind::RegexIncomplete,
1030        range,
1031        parent: None,
1032        construct: Some(reason.to_owned()),
1033        occurrence_role: None,
1034    }
1035}
1036
1037fn dynamic_pattern_fact(node: Node<'_>, source: &str) -> Option<EmbeddedLeafFact> {
1038    let function = node.child_by_field_name("function")?;
1039    let api = recognized_api(function, source)?;
1040    let arguments = node.child_by_field_name("arguments")?;
1041    let pattern = selected_pattern_argument(arguments, source)?;
1042    if matches!(pattern.kind(), "string" | "concatenated_string") {
1043        return None;
1044    }
1045    if pattern.kind() == "identifier" {
1046        return None;
1047    }
1048    let range = node_range(pattern, source);
1049    Some(EmbeddedLeafFact {
1050        kind: NormalizedKind::RegexIncomplete,
1051        range,
1052        parent: None,
1053        construct: Some(format!("dynamic-pattern:{}", api.flags_position)),
1054        occurrence_role: None,
1055    })
1056}
1057
1058fn pattern_call<'tree>(node: Node<'tree>, source: &str) -> Option<(Node<'tree>, Api)> {
1059    let argument = node.parent()?;
1060    let (arguments, is_pattern_keyword) = match argument.kind() {
1061        "keyword_argument" => {
1062            let name = argument.child_by_field_name("name")?;
1063            let value = argument.child_by_field_name("value")?;
1064            (
1065                argument.parent()?,
1066                value.id() == node.id() && node_text(name, source) == "pattern",
1067            )
1068        }
1069        "argument_list" => (argument, false),
1070        _ => return None,
1071    };
1072    if arguments.kind() != "argument_list" {
1073        return None;
1074    }
1075    let call = arguments.parent()?;
1076    if call.kind() != "call" {
1077        return None;
1078    }
1079    let function = call.child_by_field_name("function")?;
1080    let api = recognized_api(function, source)?;
1081    let selected = if is_pattern_keyword {
1082        true
1083    } else {
1084        selected_pattern_argument(arguments, source)
1085            .is_some_and(|pattern| pattern.id() == node.id())
1086    };
1087    selected.then_some((call, api))
1088}
1089
1090fn selected_pattern_argument<'tree>(arguments: Node<'tree>, source: &str) -> Option<Node<'tree>> {
1091    let mut positional = 0usize;
1092    let mut cursor = arguments.walk();
1093    for argument in arguments.named_children(&mut cursor) {
1094        match argument.kind() {
1095            "comment" => {}
1096            "keyword_argument" => {
1097                let name = argument.child_by_field_name("name")?;
1098                if node_text(name, source) == "pattern" {
1099                    return argument.child_by_field_name("value");
1100                }
1101            }
1102            _ => {
1103                if positional == 0 {
1104                    return Some(argument);
1105                }
1106                positional += 1;
1107            }
1108        }
1109    }
1110    None
1111}
1112
1113fn recognized_api(function: Node<'_>, source: &str) -> Option<Api> {
1114    let path = python_static_attribute_path(function)?;
1115    if path.len() != 2 || node_text(path[0], source) != "re" {
1116        return None;
1117    }
1118    let (flags_position, scans_from_each_offset) = match node_text(path[1], source) {
1119        "compile" => (1, false),
1120        "search" | "findall" | "finditer" => (2, true),
1121        "match" | "fullmatch" => (2, false),
1122        "split" => (3, true),
1123        "sub" | "subn" => (4, true),
1124        _ => return None,
1125    };
1126    Some(Api {
1127        flags_position,
1128        scans_from_each_offset,
1129    })
1130}
1131
1132fn call_flags(call: Node<'_>, api: Api, source: &str) -> RegexFlags {
1133    let mut flags_node = None;
1134    if let Some(arguments) = call.child_by_field_name("arguments") {
1135        let mut positional = 0usize;
1136        let mut cursor = arguments.walk();
1137        for argument in arguments.named_children(&mut cursor) {
1138            match argument.kind() {
1139                "comment" => {}
1140                "keyword_argument" => {
1141                    if argument
1142                        .child_by_field_name("name")
1143                        .is_some_and(|name| node_text(name, source) == "flags")
1144                    {
1145                        flags_node = argument.child_by_field_name("value");
1146                    }
1147                }
1148                _ => {
1149                    if positional == api.flags_position {
1150                        flags_node = Some(argument);
1151                    }
1152                    positional += 1;
1153                }
1154            }
1155        }
1156    }
1157    let Some(flags_node) = flags_node else {
1158        return RegexFlags {
1159            dotall: false,
1160            multiline: false,
1161            ignorecase: false,
1162            ascii: false,
1163            verbose: false,
1164            complete: true,
1165        };
1166    };
1167    let mut pending = vec![flags_node];
1168    let mut flags = RegexFlags {
1169        dotall: false,
1170        multiline: false,
1171        ignorecase: false,
1172        ascii: false,
1173        verbose: false,
1174        complete: true,
1175    };
1176    while let Some(node) = pending.pop() {
1177        match node.kind() {
1178            "integer" => {
1179                let text = node_text(node, source).replace('_', "");
1180                match python_integer_value(&text) {
1181                    Some(value) => {
1182                        flags.ignorecase |= value & 2 != 0;
1183                        flags.dotall |= value & 16 != 0;
1184                        flags.multiline |= value & 8 != 0;
1185                        flags.ascii |= value & 256 != 0;
1186                        flags.complete &=
1187                            value & !(2 | 8 | 16 | 32 | 256) == 0 && value & (64 | 128) == 0;
1188                    }
1189                    None => flags.complete = false,
1190                }
1191            }
1192            "attribute" | "identifier" => {
1193                let path = python_static_attribute_path(node);
1194                let name = path.as_ref().and_then(|path| {
1195                    (path.len() >= 2
1196                        && node_text(path[0], source) == "re"
1197                        && path[1..path.len() - 1]
1198                            .iter()
1199                            .all(|part| node_text(*part, source) == "re"))
1200                    .then(|| node_text(*path.last().expect("flag path is nonempty"), source))
1201                });
1202                match name {
1203                    Some("S" | "DOTALL") => flags.dotall = true,
1204                    Some("M" | "MULTILINE") => flags.multiline = true,
1205                    Some("I" | "IGNORECASE") => flags.ignorecase = true,
1206                    Some("A" | "ASCII") => flags.ascii = true,
1207                    Some("X" | "VERBOSE") => flags.verbose = true,
1208                    Some("U" | "UNICODE") => {}
1209                    Some("DEBUG") => flags.complete = false,
1210                    _ => flags.complete = false,
1211                }
1212            }
1213            "binary_operator" => {
1214                if let (Some(left), Some(right)) = (
1215                    node.child_by_field_name("left"),
1216                    node.child_by_field_name("right"),
1217                ) {
1218                    pending.push(right);
1219                    pending.push(left);
1220                } else {
1221                    flags.complete = false;
1222                }
1223            }
1224            _ => flags.complete = false,
1225        }
1226    }
1227    flags
1228}
1229
1230fn parse_inline_flags(
1231    chars: &[char],
1232    mut index: usize,
1233    mut flags: RegexFlags,
1234) -> Option<(usize, RegexFlags)> {
1235    let mut enabled = true;
1236    let mut saw_flag = false;
1237    let mut ascii_mode = None;
1238    while let Some(character) = chars.get(index).copied() {
1239        match character {
1240            ')' | ':' if saw_flag && (enabled || character == ':') => {
1241                if ascii_mode == Some((true, true)) {
1242                    return None;
1243                }
1244                return Some((index, flags));
1245            }
1246            '-' if enabled => {
1247                enabled = false;
1248                saw_flag = false;
1249            }
1250            'i' | 'm' | 's' | 'x' | 'a' | 'u' => {
1251                if !enabled && matches!(character, 'a' | 'u') {
1252                    return None;
1253                }
1254                saw_flag = true;
1255                match character {
1256                    'i' => flags.ignorecase = enabled,
1257                    'm' => flags.multiline = enabled,
1258                    's' => flags.dotall = enabled,
1259                    'x' => flags.verbose = enabled,
1260                    'a' => {
1261                        flags.ascii = enabled;
1262                        ascii_mode =
1263                            Some((enabled, ascii_mode.is_some_and(|(_, unicode)| unicode)));
1264                    }
1265                    'u' => {
1266                        flags.ascii = !enabled;
1267                        ascii_mode = Some((ascii_mode.is_some_and(|(ascii, _)| ascii), enabled));
1268                    }
1269                    _ => unreachable!("inline flag arm includes only Python regex flags"),
1270                }
1271            }
1272            _ => return None,
1273        }
1274        index += 1;
1275    }
1276    None
1277}
1278
1279fn python_integer_value(text: &str) -> Option<u64> {
1280    let (radix, digits) =
1281        if let Some(digits) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
1282            (16, digits)
1283        } else if let Some(digits) = text.strip_prefix("0o").or_else(|| text.strip_prefix("0O")) {
1284            (8, digits)
1285        } else if let Some(digits) = text.strip_prefix("0b").or_else(|| text.strip_prefix("0B")) {
1286            (2, digits)
1287        } else {
1288            (10, text)
1289        };
1290    u64::from_str_radix(digits, radix).ok()
1291}
1292
1293fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1294    node.utf8_text(source.as_bytes())
1295        .expect("tree-sitter node ranges are valid UTF-8 source")
1296}
1297
1298fn node_range(node: Node<'_>, source: &str) -> Range {
1299    let line_starts = compute_line_starts(source);
1300    let start_byte = node.start_byte();
1301    let end_byte = node.end_byte();
1302    Range {
1303        start_byte,
1304        end_byte,
1305        start_line: find_line_index_for_offset(&line_starts, start_byte) + 1,
1306        end_line: find_line_index_for_offset(
1307            &line_starts,
1308            end_byte.saturating_sub(1).max(start_byte),
1309        ) + 1,
1310    }
1311}
1312
1313fn decode_python_string(node: Node<'_>, source: &str) -> Result<DecodedValue, &'static str> {
1314    let decoded = decode_string_literal(node, source)?;
1315    if decoded.is_bytes {
1316        return Err("non-text-or-formatted-string");
1317    }
1318    Ok(decoded)
1319}
1320
1321fn decode_regex_pattern_string(node: Node<'_>, source: &str) -> Result<DecodedValue, &'static str> {
1322    decode_string_literal(node, source)
1323}
1324
1325fn decode_string_literal(node: Node<'_>, source: &str) -> Result<DecodedValue, &'static str> {
1326    let mut strings = Vec::new();
1327    if node.kind() == "string" {
1328        strings.push(node);
1329    } else {
1330        let mut stack = vec![node];
1331        while let Some(current) = stack.pop() {
1332            if current.kind() == "string" {
1333                strings.push(current);
1334                continue;
1335            }
1336            let mut children = Vec::new();
1337            let mut cursor = current.walk();
1338            for child in current.named_children(&mut cursor) {
1339                if child.kind() == "concatenated_string" || child.kind() == "string" {
1340                    children.push(child);
1341                } else {
1342                    return Err("non-literal-string-segment");
1343                }
1344            }
1345            for child in children.into_iter().rev() {
1346                stack.push(child);
1347            }
1348        }
1349    }
1350    if strings.is_empty() {
1351        return Err("empty-concatenated-string");
1352    }
1353    let mut value = String::new();
1354    let mut chars = Vec::new();
1355    let mut byte_string = None;
1356    for string in strings {
1357        let is_bytes = decode_string_segment(string, source, &mut value, &mut chars)?;
1358        if byte_string.is_some_and(|previous| previous != is_bytes) {
1359            return Err("mixed-bytes-and-text-concatenation");
1360        }
1361        byte_string = Some(is_bytes);
1362        if chars.len() > MAX_PATTERN_CHARS {
1363            return Err("pattern-character-budget");
1364        }
1365    }
1366    Ok(DecodedValue {
1367        value,
1368        chars,
1369        source_range: node_range(node, source),
1370        is_bytes: byte_string.expect("non-empty literal expression has a string segment"),
1371    })
1372}
1373
1374/// Decode the value of a tree-sitter-selected Python string expression.
1375///
1376/// Callers must select the literal from syntax before invoking this helper;
1377/// this function classifies only the selected expression's value.
1378pub fn decode_python_string_value(node: Node<'_>, source: &str) -> Result<String, &'static str> {
1379    decode_python_string(node, source).map(|decoded| decoded.value)
1380}
1381
1382/// Decode each literal segment in a selected Python f-string, omitting its
1383/// interpolation expressions. The parse tree chooses the segments; this
1384/// helper reads only their decoded values.
1385pub fn decode_python_fstring_literal_segments(
1386    node: Node<'_>,
1387    source: &str,
1388) -> Result<Vec<String>, &'static str> {
1389    if node.kind() != "string" {
1390        return Err("not-a-python-fstring");
1391    }
1392    let mut start_token = None;
1393    let mut segments: Vec<Vec<Node<'_>>> = vec![Vec::new()];
1394    let mut cursor = node.walk();
1395    for child in node.named_children(&mut cursor) {
1396        match child.kind() {
1397            "string_start" if start_token.is_none() => start_token = Some(child),
1398            "string_start" => return Err("duplicate-fstring-start"),
1399            "string_end" => break,
1400            "interpolation" => segments.push(Vec::new()),
1401            "string_content" | "escape_sequence" => segments
1402                .last_mut()
1403                .expect("f-string literal segments retain a current segment")
1404                .push(child),
1405            _ => return Err("unsupported-fstring-prefix-node"),
1406        }
1407    }
1408    let start_token = start_token.ok_or("missing-fstring-start")?;
1409    let prefix = string_prefix(node_text(start_token, source));
1410    if !prefix.contains('f')
1411        || prefix
1412            .chars()
1413            .any(|character| !matches!(character, 'f' | 'r'))
1414        || prefix.matches('f').count() != 1
1415        || prefix.matches('r').count() > 1
1416    {
1417        return Err("unsupported-fstring-prefix");
1418    }
1419    let raw = prefix.contains('r');
1420    segments
1421        .into_iter()
1422        .map(|parts| {
1423            let mut value = String::new();
1424            let mut mapped = Vec::new();
1425            for part in parts {
1426                if raw {
1427                    append_mapped_text(
1428                        node_text(part, source),
1429                        part.start_byte(),
1430                        part.end_byte(),
1431                        &mut value,
1432                        &mut mapped,
1433                    )?;
1434                } else if part.kind() == "escape_sequence" {
1435                    decode_literal_escape(
1436                        node_text(part, source),
1437                        part.start_byte(),
1438                        &mut value,
1439                        &mut mapped,
1440                        false,
1441                    )?;
1442                } else {
1443                    decode_string_content(part, source, &mut value, &mut mapped, false)?;
1444                }
1445            }
1446            Ok(value.replace("{{", "{").replace("}}", "}"))
1447        })
1448        .collect()
1449}
1450
1451fn decode_string_segment(
1452    node: Node<'_>,
1453    source: &str,
1454    value: &mut String,
1455    output: &mut Vec<MappedChar>,
1456) -> Result<bool, &'static str> {
1457    let mut start_token = None;
1458    let mut body = Vec::new();
1459    let mut cursor = node.walk();
1460    for child in node.named_children(&mut cursor) {
1461        match child.kind() {
1462            "string_start" => start_token = Some(child),
1463            "string_end" => {}
1464            "string_content" | "escape_sequence" => body.push(child),
1465            "interpolation" => return Err("formatted-string-interpolation"),
1466            _ => return Err("unsupported-string-segment"),
1467        }
1468    }
1469    let start_token = start_token.ok_or("missing-string-start")?;
1470    let prefix = string_prefix(node_text(start_token, source));
1471    if prefix.contains('f') || prefix.contains('t') {
1472        return Err("non-text-or-formatted-string");
1473    }
1474    if prefix
1475        .chars()
1476        .any(|prefix| !matches!(prefix, 'r' | 'u' | 'b'))
1477        || prefix.matches('r').count() > 1
1478        || prefix.matches('u').count() > 1
1479        || prefix.matches('b').count() > 1
1480        || (prefix.contains('r') && prefix.contains('u'))
1481        || (prefix.contains('b') && prefix.contains('u'))
1482    {
1483        return Err("unsupported-string-prefix");
1484    }
1485    let is_bytes = prefix.contains('b');
1486    if is_bytes && !node_text(node, source).is_ascii() {
1487        return Err("non-ascii-byte-literal");
1488    }
1489    let raw = prefix.contains('r');
1490    for part in body {
1491        let raw_text = node_text(part, source);
1492        if raw && matches!(part.kind(), "escape_sequence" | "string_content") {
1493            append_mapped_text(raw_text, part.start_byte(), part.end_byte(), value, output)?;
1494        } else if !raw && part.kind() == "escape_sequence" {
1495            decode_literal_escape(raw_text, part.start_byte(), value, output, is_bytes)?;
1496        } else if !raw && part.kind() == "string_content" {
1497            decode_string_content(part, source, value, output, is_bytes)?;
1498        } else {
1499            append_mapped_text(raw_text, part.start_byte(), part.end_byte(), value, output)?;
1500        }
1501        if output.len() > MAX_PATTERN_CHARS {
1502            return Err("pattern-character-budget");
1503        }
1504    }
1505    if is_bytes && value.chars().any(|character| character as u32 > 0xff) {
1506        return Err("byte-pattern-value-out-of-range");
1507    }
1508    Ok(is_bytes)
1509}
1510
1511fn string_prefix(start: &str) -> String {
1512    start
1513        .chars()
1514        .take_while(|character| !matches!(character, '\'' | '"'))
1515        .map(|character| character.to_ascii_lowercase())
1516        .collect()
1517}
1518
1519fn append_mapped_text(
1520    text: &str,
1521    source_start: usize,
1522    source_end: usize,
1523    output_text: &mut String,
1524    output: &mut Vec<MappedChar>,
1525) -> Result<(), &'static str> {
1526    for (offset, value) in text.char_indices() {
1527        if output.len() >= MAX_PATTERN_CHARS {
1528            return Err("pattern-character-budget");
1529        }
1530        let start = source_start + offset;
1531        let end = start + value.len_utf8();
1532        output_text.push(value);
1533        output.push(MappedChar {
1534            source_start: start,
1535            source_end: end,
1536        });
1537    }
1538    debug_assert_eq!(source_start + text.len(), source_end);
1539    Ok(())
1540}
1541
1542fn decode_string_content(
1543    node: Node<'_>,
1544    source: &str,
1545    output_text: &mut String,
1546    output: &mut Vec<MappedChar>,
1547    bytes: bool,
1548) -> Result<(), &'static str> {
1549    let mut position = node.start_byte();
1550    let mut cursor = node.walk();
1551    for child in node.named_children(&mut cursor) {
1552        assert_eq!(child.kind(), "escape_sequence");
1553        assert!(position <= child.start_byte());
1554        if position < child.start_byte() {
1555            let plain = source
1556                .get(position..child.start_byte())
1557                .expect("tree-sitter string ranges are valid UTF-8 source");
1558            append_mapped_text(plain, position, child.start_byte(), output_text, output)?;
1559        }
1560        decode_literal_escape(
1561            node_text(child, source),
1562            child.start_byte(),
1563            output_text,
1564            output,
1565            bytes,
1566        )?;
1567        position = child.end_byte();
1568    }
1569    assert!(position <= node.end_byte());
1570    if position < node.end_byte() {
1571        let plain = source
1572            .get(position..node.end_byte())
1573            .expect("tree-sitter string ranges are valid UTF-8 source");
1574        append_mapped_text(plain, position, node.end_byte(), output_text, output)?;
1575    }
1576    Ok(())
1577}
1578
1579fn decode_literal_escape(
1580    raw: &str,
1581    source_start: usize,
1582    output_text: &mut String,
1583    output: &mut Vec<MappedChar>,
1584    bytes: bool,
1585) -> Result<(), &'static str> {
1586    let chars: Vec<char> = raw.chars().collect();
1587    if chars.first() != Some(&'\\') || chars.len() < 2 {
1588        return Err("malformed-literal-escape");
1589    }
1590    if bytes && matches!(chars.get(1), Some('u' | 'U' | 'N')) {
1591        return append_mapped_text(
1592            raw,
1593            source_start,
1594            source_start + raw.len(),
1595            output_text,
1596            output,
1597        );
1598    }
1599    let escaped = chars[1];
1600    let replacement = match escaped {
1601        '\n' => None,
1602        '\r' if chars.get(2) == Some(&'\n') => None,
1603        '\r' => Some('\r'),
1604        '\\' => Some('\\'),
1605        '\'' => Some('\''),
1606        '"' => Some('"'),
1607        'a' => Some('\u{7}'),
1608        'b' => Some('\u{8}'),
1609        'f' => Some('\u{c}'),
1610        'n' => Some('\n'),
1611        'r' => Some('\r'),
1612        't' => Some('\t'),
1613        'v' => Some('\u{b}'),
1614        'x' => Some(
1615            char::from_u32(hex_value(&chars, 2, 2).ok_or("invalid-hex-escape")?)
1616                .ok_or("invalid-hex-scalar")?,
1617        ),
1618        'u' => Some(
1619            char::from_u32(hex_value(&chars, 2, 4).ok_or("invalid-unicode-escape")?)
1620                .ok_or("invalid-unicode-scalar")?,
1621        ),
1622        'U' => Some(
1623            char::from_u32(hex_value(&chars, 2, 8).ok_or("invalid-unicode-escape")?)
1624                .ok_or("invalid-unicode-scalar")?,
1625        ),
1626        'N' => return Err("named-unicode-escape"),
1627        '0'..='7' => {
1628            let mut value = (escaped as u32) - ('0' as u32);
1629            let mut digits = 1usize;
1630            while digits < 3 {
1631                let Some(digit @ '0'..='7') = chars.get(1 + digits).copied() else {
1632                    break;
1633                };
1634                value = value * 8 + (digit as u32 - '0' as u32);
1635                digits += 1;
1636            }
1637            Some(char::from_u32(value).ok_or("invalid-octal-scalar")?)
1638        }
1639        // Python preserves unrecognized escapes, including punctuation; the
1640        // regex parser must receive the backslash to classify them correctly.
1641        other => {
1642            if output.len().saturating_add(2) > MAX_PATTERN_CHARS {
1643                return Err("pattern-character-budget");
1644            }
1645            output_text.push('\\');
1646            output.push(MappedChar {
1647                source_start,
1648                source_end: source_start + 1,
1649            });
1650            output_text.push(other);
1651            output.push(MappedChar {
1652                source_start: source_start + 1,
1653                source_end: source_start + 1 + other.len_utf8(),
1654            });
1655            return Ok(());
1656        }
1657    };
1658    if let Some(replacement) = replacement {
1659        if output.len() >= MAX_PATTERN_CHARS {
1660            return Err("pattern-character-budget");
1661        }
1662        output_text.push(replacement);
1663        output.push(MappedChar {
1664            source_start,
1665            source_end: source_start + raw.len(),
1666        });
1667    }
1668    Ok(())
1669}
1670
1671fn hex_value(chars: &[char], start: usize, digits: usize) -> Option<u32> {
1672    chars
1673        .get(start..start + digits)?
1674        .iter()
1675        .try_fold(0u32, |value, digit| {
1676            digit.to_digit(16).map(|digit| value * 16 + digit)
1677        })
1678}
1679
1680struct RegexParser<'a> {
1681    decoded: &'a DecodedValue,
1682    flags: RegexFlags,
1683    api: Api,
1684    chars: Vec<char>,
1685    index: usize,
1686    facts: Vec<Fact>,
1687    frames: Vec<BranchFrame>,
1688    pending_risks: Vec<(NormalizedKind, usize, usize, usize, String)>,
1689    incomplete: Option<(usize, usize, String)>,
1690    exceeded: bool,
1691}
1692
1693impl<'a> RegexParser<'a> {
1694    fn new(decoded: &'a DecodedValue, flags: RegexFlags, api: Api) -> Self {
1695        Self {
1696            decoded,
1697            flags,
1698            api,
1699            chars: decoded.value.chars().collect(),
1700            index: 0,
1701            facts: Vec::new(),
1702            frames: Vec::new(),
1703            pending_risks: Vec::new(),
1704            incomplete: None,
1705            exceeded: false,
1706        }
1707    }
1708
1709    fn parse(mut self) -> Vec<Fact> {
1710        let length = self.chars.len();
1711        if !self.flags.complete {
1712            self.mark_incomplete(0, length, "unsupported-regex-flags");
1713        }
1714        let pattern = self.add_fact(
1715            NormalizedKind::RegexPattern,
1716            0,
1717            length,
1718            None,
1719            Some(self.decoded.value.clone()),
1720            None,
1721        );
1722        let sequence = self.add_fact(
1723            NormalizedKind::RegexSequence,
1724            0,
1725            length,
1726            Some(pattern),
1727            None,
1728            None,
1729        );
1730        self.frames.push(BranchFrame {
1731            owner: pattern,
1732            sequence,
1733            group_start: None,
1734            last_atom: None,
1735            restore_flags: None,
1736        });
1737
1738        while self.index < length {
1739            if self.exceeded {
1740                break;
1741            }
1742            if self.flags.verbose {
1743                if matches!(
1744                    self.chars[self.index],
1745                    ' ' | '\t' | '\n' | '\r' | '\x0c' | '\x0b'
1746                ) {
1747                    self.index += 1;
1748                    continue;
1749                }
1750                if self.chars[self.index] == '#' {
1751                    while self.index < length && self.chars[self.index] != '\n' {
1752                        self.index += 1;
1753                    }
1754                    continue;
1755                }
1756            }
1757            if self.facts.len() >= MAX_PATTERN_FACTS - 2 {
1758                self.exceeded = true;
1759                break;
1760            }
1761            match self.chars[self.index] {
1762                '(' => self.open_group(),
1763                ')' => self.close_group(),
1764                '|' => self.alternation(),
1765                '[' => self.character_class(),
1766                '\\' => self.escape(false),
1767                '.' => self.wildcard(),
1768                '^' | '$' => self.anchor(),
1769                '*' | '+' | '?' => self.quantifier(),
1770                '{' if self.braced_quantifier_end().is_some() => self.quantifier(),
1771                _ => self.literal(),
1772            }
1773        }
1774
1775        if self.frames.len() > 1 {
1776            let start = self
1777                .frames
1778                .last()
1779                .and_then(|frame| frame.group_start)
1780                .unwrap_or(length);
1781            self.mark_incomplete(start, length, "unclosed-group");
1782            for frame in self.frames.iter().skip(1) {
1783                self.facts[frame.owner].end = length;
1784                self.facts[frame.sequence].end = length;
1785            }
1786        }
1787        if self.exceeded {
1788            return self.budget_result();
1789        }
1790        self.compute_minimum_widths();
1791        self.classify_unmatchable_anchors();
1792        self.classify_hostname();
1793        self.classify_bad_tag();
1794        self.classify_polynomial_redos();
1795        self.classify_redos();
1796        self.append_pending_facts();
1797        if self.exceeded {
1798            return self.budget_result();
1799        }
1800        self.facts
1801    }
1802
1803    fn budget_result(&self) -> Vec<Fact> {
1804        let length = self.chars.len();
1805        vec![
1806            self.facts[0].clone(),
1807            self.facts[1].clone(),
1808            Fact {
1809                kind: NormalizedKind::RegexIncomplete,
1810                start: 0,
1811                end: length,
1812                parent: Some(0),
1813                construct: Some("regex-fact-budget".to_owned()),
1814                first_set: None,
1815                escaped_literal: None,
1816                quantifier: None,
1817                minimum_width: None,
1818            },
1819        ]
1820    }
1821
1822    fn add_fact(
1823        &mut self,
1824        kind: NormalizedKind,
1825        start: usize,
1826        end: usize,
1827        parent: Option<usize>,
1828        construct: Option<String>,
1829        first_set: Option<CharacterSet>,
1830    ) -> usize {
1831        let id = self.facts.len();
1832        assert!(
1833            id < MAX_PATTERN_FACTS,
1834            "regex fact budget checked before insertion"
1835        );
1836        let minimum_width = match kind {
1837            NormalizedKind::RegexLiteral
1838            | NormalizedKind::RegexWildcard
1839            | NormalizedKind::RegexCharacterClass
1840            | NormalizedKind::RegexCharacterRange
1841            | NormalizedKind::RegexEscape => Some(1),
1842            NormalizedKind::RegexAnchor => Some(0),
1843            _ => None,
1844        };
1845        self.facts.push(Fact {
1846            kind,
1847            start,
1848            end,
1849            parent,
1850            construct,
1851            first_set,
1852            escaped_literal: None,
1853            quantifier: None,
1854            minimum_width,
1855        });
1856        id
1857    }
1858
1859    fn mark_incomplete(&mut self, start: usize, end: usize, reason: &str) {
1860        if self.incomplete.is_none() {
1861            self.incomplete = Some((start, end, reason.to_owned()));
1862        }
1863    }
1864
1865    fn current_sequence(&self) -> usize {
1866        self.frames
1867            .last()
1868            .expect("regex parser always has an outer frame")
1869            .sequence
1870    }
1871
1872    fn record_atom(&mut self, fact: usize) {
1873        self.frames
1874            .last_mut()
1875            .expect("regex parser always has an outer frame")
1876            .last_atom = Some(fact);
1877    }
1878
1879    fn add_atom(
1880        &mut self,
1881        kind: NormalizedKind,
1882        start: usize,
1883        end: usize,
1884        construct: Option<String>,
1885        first_set: Option<CharacterSet>,
1886    ) -> usize {
1887        let fact = self.add_fact(
1888            kind,
1889            start,
1890            end,
1891            Some(self.current_sequence()),
1892            construct,
1893            first_set,
1894        );
1895        self.record_atom(fact);
1896        fact
1897    }
1898
1899    fn open_group(&mut self) {
1900        let start = self.index;
1901        let mut content_start = start + 1;
1902        let mut group_type = "capturing";
1903        let mut missing_special_group_end = None;
1904        let mut restore_flags = None;
1905        if self.chars.get(content_start) == Some(&'?') {
1906            match (
1907                self.chars.get(content_start + 1),
1908                self.chars.get(content_start + 2),
1909            ) {
1910                (Some(':'), _) => {
1911                    group_type = "noncapturing";
1912                    content_start += 2;
1913                }
1914                (Some('P'), Some('<')) => {
1915                    group_type = "named-capturing";
1916                    let mut close = content_start + 3;
1917                    while close < self.chars.len() && self.chars[close] != '>' {
1918                        close += 1;
1919                    }
1920                    if close == self.chars.len() {
1921                        self.mark_incomplete(start, self.chars.len(), "unclosed-group-name");
1922                        self.index = self.chars.len();
1923                        return;
1924                    }
1925                    content_start = close + 1;
1926                }
1927                (Some('#'), _) => {
1928                    let mut close = content_start + 2;
1929                    while close < self.chars.len() && self.chars[close] != ')' {
1930                        close += 1;
1931                    }
1932                    if close == self.chars.len() {
1933                        self.mark_incomplete(start, close, "unclosed-comment-group");
1934                        self.index = close;
1935                        return;
1936                    }
1937                    let group = self.add_fact(
1938                        NormalizedKind::RegexGroup,
1939                        start,
1940                        close + 1,
1941                        Some(self.current_sequence()),
1942                        Some("comment".to_owned()),
1943                        None,
1944                    );
1945                    self.facts[group].minimum_width = Some(0);
1946                    self.index = close + 1;
1947                    self.record_atom(group);
1948                    return;
1949                }
1950                (Some('='), _) => {
1951                    group_type = "lookahead-positive";
1952                    content_start += 2;
1953                }
1954                (Some('!'), _) => {
1955                    group_type = "lookahead-negative";
1956                    content_start += 2;
1957                }
1958                (Some('<'), Some('=')) => {
1959                    group_type = "lookbehind-positive";
1960                    content_start += 3;
1961                }
1962                (Some('<'), Some('!')) => {
1963                    group_type = "lookbehind-negative";
1964                    content_start += 3;
1965                }
1966                (Some('i' | 'm' | 's' | 'x' | 'a' | 'u' | '-'), _) => {
1967                    let flags_start = content_start + 1;
1968                    let Some((delimiter, flags)) =
1969                        parse_inline_flags(&self.chars, flags_start, self.flags)
1970                    else {
1971                        self.mark_incomplete(
1972                            start,
1973                            (start + 3).min(self.chars.len()),
1974                            "invalid-inline-flags",
1975                        );
1976                        self.index = (start + 3).min(self.chars.len());
1977                        return;
1978                    };
1979                    match self.chars[delimiter] {
1980                        ')' => {
1981                            self.flags = flags;
1982                            self.index = delimiter + 1;
1983                            return;
1984                        }
1985                        ':' => {
1986                            group_type = "scoped-flags";
1987                            restore_flags = Some(self.flags);
1988                            self.flags = flags;
1989                            content_start = delimiter + 1;
1990                        }
1991                        _ => unreachable!("inline flags stop at a group delimiter"),
1992                    }
1993                }
1994                _ => {
1995                    self.mark_incomplete(
1996                        start,
1997                        (start + 3).min(self.chars.len()),
1998                        "unsupported-special-group",
1999                    );
2000                    content_start += 1;
2001                }
2002            }
2003        } else if self.chars.get(content_start) == Some(&'P')
2004            && self.chars.get(content_start + 1) == Some(&'<')
2005        {
2006            let mut close = content_start + 2;
2007            while close < self.chars.len() && self.chars[close] != '>' {
2008                close += 1;
2009            }
2010            let name = &self.chars[content_start + 2..close.min(self.chars.len())];
2011            let valid_name = name
2012                .first()
2013                .is_some_and(|first| first.is_alphabetic() || *first == '_')
2014                && name
2015                    .iter()
2016                    .all(|character| character.is_alphanumeric() || *character == '_');
2017            if close < self.chars.len() && valid_name {
2018                missing_special_group_end = Some(close + 1);
2019            }
2020        }
2021        if self.frames.len() >= MAX_GROUP_DEPTH {
2022            self.mark_incomplete(
2023                start,
2024                (start + 1).min(self.chars.len()),
2025                "regex-group-depth-budget",
2026            );
2027            self.exceeded = true;
2028            return;
2029        }
2030        let owner = self.current_sequence();
2031        let group = self.add_fact(
2032            NormalizedKind::RegexGroup,
2033            start,
2034            self.chars.len(),
2035            Some(owner),
2036            Some(group_type.to_owned()),
2037            None,
2038        );
2039        if let Some(end) = missing_special_group_end {
2040            self.pending_risks.push((
2041                NormalizedKind::RegexRiskIncompleteSpecialGroup,
2042                group,
2043                start,
2044                end,
2045                "named-group-missing-question-mark".to_owned(),
2046            ));
2047        }
2048        let sequence = self.add_fact(
2049            NormalizedKind::RegexSequence,
2050            content_start,
2051            self.chars.len(),
2052            Some(group),
2053            None,
2054            None,
2055        );
2056        self.frames.push(BranchFrame {
2057            owner: group,
2058            sequence,
2059            group_start: Some(start),
2060            last_atom: None,
2061            restore_flags,
2062        });
2063        self.index = content_start;
2064    }
2065
2066    fn close_group(&mut self) {
2067        if self.frames.len() == 1 {
2068            self.mark_incomplete(self.index, self.index + 1, "unmatched-close-parenthesis");
2069            self.index += 1;
2070            return;
2071        }
2072        let close = self.index;
2073        self.index += 1;
2074        let frame = self
2075            .frames
2076            .pop()
2077            .expect("a nested regex group frame exists");
2078        self.facts[frame.sequence].end = close;
2079        self.facts[frame.owner].end = self.index;
2080        self.facts[frame.owner].first_set = self.sequence_first_set(frame.sequence);
2081        self.facts[frame.sequence].minimum_width = self.sequence_minimum_width(frame.sequence);
2082        self.facts[frame.owner].minimum_width = if matches!(
2083            self.facts[frame.owner].construct.as_deref(),
2084            Some(
2085                "lookahead-positive"
2086                    | "lookahead-negative"
2087                    | "lookbehind-positive"
2088                    | "lookbehind-negative"
2089            )
2090        ) {
2091            Some(0)
2092        } else {
2093            self.facts[frame.sequence].minimum_width
2094        };
2095        if self.facts[frame.owner]
2096            .construct
2097            .as_deref()
2098            .is_some_and(|construct| construct.starts_with("look"))
2099        {
2100            self.facts[frame.owner].first_set = None;
2101        }
2102        if let Some(flags) = frame.restore_flags {
2103            self.flags = flags;
2104        }
2105        self.record_atom(frame.owner);
2106    }
2107
2108    fn alternation(&mut self) {
2109        let start = self.index;
2110        self.index += 1;
2111        let sequence = self.current_sequence();
2112        self.add_fact(
2113            NormalizedKind::RegexAlternation,
2114            start,
2115            self.index,
2116            Some(sequence),
2117            None,
2118            None,
2119        );
2120        let frame = self.frames.last_mut().expect("outer regex frame exists");
2121        frame.last_atom = None;
2122        // A dollar assertion at the end of a branch does not constrain the
2123        // next branch. Anchors are classified after the complete tree exists.
2124        // (The branch boundary itself is represented by the alternation fact.)
2125    }
2126
2127    fn literal(&mut self) {
2128        let start = self.index;
2129        let value = self.chars[self.index];
2130        self.index += 1;
2131        let set = CharacterSet::one(value);
2132        let set = if self.flags.ignorecase {
2133            set.with_ascii_case_variants()
2134        } else {
2135            set
2136        };
2137        self.add_atom(
2138            NormalizedKind::RegexLiteral,
2139            start,
2140            self.index,
2141            Some(value.to_string()),
2142            Some(set),
2143        );
2144    }
2145
2146    fn wildcard(&mut self) {
2147        let start = self.index;
2148        self.index += 1;
2149        let set = if self.flags.dotall {
2150            CharacterSet {
2151                ranges: vec![(0, 0x10ffff)],
2152            }
2153        } else {
2154            CharacterSet::any_except_newline()
2155        };
2156        self.add_atom(
2157            NormalizedKind::RegexWildcard,
2158            start,
2159            self.index,
2160            Some("dot".to_owned()),
2161            Some(set),
2162        );
2163    }
2164
2165    fn anchor(&mut self) {
2166        let start = self.index;
2167        let value = self.chars[self.index];
2168        self.index += 1;
2169        self.add_fact(
2170            NormalizedKind::RegexAnchor,
2171            start,
2172            self.index,
2173            Some(self.current_sequence()),
2174            Some(value.to_string()),
2175            None,
2176        );
2177        self.frames
2178            .last_mut()
2179            .expect("outer regex frame exists")
2180            .last_atom = None;
2181    }
2182
2183    fn quantifier(&mut self) {
2184        let start = self.index;
2185        let Some(atom) = self.frames.last().and_then(|frame| frame.last_atom) else {
2186            self.mark_incomplete(
2187                start,
2188                (start + 1).min(self.chars.len()),
2189                "quantifier-without-atom",
2190            );
2191            self.index += 1;
2192            return;
2193        };
2194        if self
2195            .facts
2196            .iter()
2197            .any(|fact| fact.parent == Some(atom) && fact.kind == NormalizedKind::RegexQuantifier)
2198        {
2199            self.mark_incomplete(
2200                start,
2201                (start + 1).min(self.chars.len()),
2202                "nested-quantifier",
2203            );
2204            self.index += 1;
2205            return;
2206        }
2207        let mut quantifier = match self.chars[self.index] {
2208            '*' => {
2209                self.index += 1;
2210                Quantifier {
2211                    minimum: 0,
2212                    maximum: None,
2213                    possessive: false,
2214                }
2215            }
2216            '+' => {
2217                self.index += 1;
2218                Quantifier {
2219                    minimum: 1,
2220                    maximum: None,
2221                    possessive: false,
2222                }
2223            }
2224            '?' => {
2225                self.index += 1;
2226                Quantifier {
2227                    minimum: 0,
2228                    maximum: Some(1),
2229                    possessive: false,
2230                }
2231            }
2232            '{' => match self.braced_quantifier_end() {
2233                Some((end, quantifier)) => {
2234                    self.index = end;
2235                    quantifier
2236                }
2237                None => {
2238                    self.mark_incomplete(
2239                        start,
2240                        (start + 1).min(self.chars.len()),
2241                        "malformed-braced-quantifier",
2242                    );
2243                    self.index += 1;
2244                    return;
2245                }
2246            },
2247            _ => unreachable!("quantifier dispatch uses a quantifier character"),
2248        };
2249        if matches!(self.chars.get(self.index), Some('?') | Some('+')) {
2250            quantifier.possessive = self.chars[self.index] == '+';
2251            self.index += 1;
2252        }
2253        let end = self.index;
2254        let fact = self.add_fact(
2255            NormalizedKind::RegexQuantifier,
2256            start,
2257            end,
2258            Some(atom),
2259            Some(format!(
2260                "min={},max={},possessive={}",
2261                quantifier.minimum,
2262                quantifier
2263                    .maximum
2264                    .map_or_else(|| "*".to_owned(), |maximum| maximum.to_string()),
2265                quantifier.possessive
2266            )),
2267            None,
2268        );
2269        self.facts[fact].quantifier = Some(quantifier);
2270        self.facts[atom].minimum_width = self.facts[atom]
2271            .minimum_width
2272            .and_then(|width| width.checked_mul(quantifier.minimum));
2273        self.facts[atom].end = end;
2274    }
2275
2276    fn braced_quantifier_end(&self) -> Option<(usize, Quantifier)> {
2277        let mut index = self.index + 1;
2278        let number_start = index;
2279        let mut minimum = 0u32;
2280        while let Some(digit @ '0'..='9') = self.chars.get(index).copied() {
2281            minimum = minimum
2282                .checked_mul(10)?
2283                .checked_add(digit as u32 - '0' as u32)?;
2284            index += 1;
2285        }
2286        if index == number_start {
2287            return None;
2288        }
2289        let mut maximum = Some(minimum);
2290        if self.chars.get(index) == Some(&',') {
2291            index += 1;
2292            let upper_start = index;
2293            let mut upper = 0u32;
2294            while let Some(digit @ '0'..='9') = self.chars.get(index).copied() {
2295                upper = upper
2296                    .checked_mul(10)?
2297                    .checked_add(digit as u32 - '0' as u32)?;
2298                index += 1;
2299            }
2300            maximum = if upper_start == index {
2301                None
2302            } else {
2303                Some(upper)
2304            };
2305        }
2306        if self.chars.get(index) != Some(&'}') || maximum.is_some_and(|maximum| maximum < minimum) {
2307            return None;
2308        }
2309        Some((
2310            index + 1,
2311            Quantifier {
2312                minimum,
2313                maximum,
2314                possessive: false,
2315            },
2316        ))
2317    }
2318
2319    fn escape(&mut self, in_class: bool) {
2320        let start = self.index;
2321        let Some(escape) = parse_regex_escape(&self.chars, start, in_class, self.flags.ascii)
2322        else {
2323            self.mark_incomplete(
2324                start,
2325                (start + 2).min(self.chars.len()),
2326                "unsupported-escape",
2327            );
2328            self.index = (start + 2).min(self.chars.len());
2329            return;
2330        };
2331        self.index = escape.end;
2332        if let Some(reason) = escape.incomplete {
2333            self.mark_incomplete(start, self.index, reason);
2334        }
2335        let kind = if escape.anchor && !in_class {
2336            NormalizedKind::RegexAnchor
2337        } else {
2338            NormalizedKind::RegexEscape
2339        };
2340        let escaped_literal = escape.first_set.as_ref().and_then(single_character);
2341        let first_set = escape.first_set.map(|set| {
2342            if self.flags.ignorecase {
2343                set.with_ascii_case_variants()
2344            } else {
2345                set
2346            }
2347        });
2348        let fact = self.add_fact(
2349            kind,
2350            start,
2351            self.index,
2352            Some(self.current_sequence()),
2353            escape.construct,
2354            first_set,
2355        );
2356        self.facts[fact].escaped_literal = escaped_literal;
2357        if escape.anchor && !in_class {
2358            self.frames
2359                .last_mut()
2360                .expect("outer regex frame exists")
2361                .last_atom = None;
2362        } else {
2363            self.record_atom(fact);
2364        }
2365        if escape.backspace && in_class {
2366            self.pending_risks.push((
2367                NormalizedKind::RegexRiskBackspaceEscape,
2368                fact,
2369                start,
2370                self.index,
2371                "backspace-in-character-class".to_owned(),
2372            ));
2373        }
2374    }
2375
2376    fn character_class(&mut self) {
2377        let start = self.index;
2378        self.index += 1;
2379        let class = self.add_fact(
2380            NormalizedKind::RegexCharacterClass,
2381            start,
2382            self.chars.len(),
2383            Some(self.current_sequence()),
2384            None,
2385            None,
2386        );
2387        let negated = self.chars.get(self.index) == Some(&'^');
2388        if negated {
2389            self.index += 1;
2390        }
2391        let mut members = CharacterSet { ranges: Vec::new() };
2392        let mut members_known = true;
2393        let mut saw_member = false;
2394        let mut closed = false;
2395        let mut explicit_members = HashSet::<String>::new();
2396        let mut range_members = Vec::<(usize, CharacterSet, u32)>::new();
2397        let mut category_members = Vec::new();
2398        while self.index < self.chars.len() {
2399            if self.facts.len() >= MAX_PATTERN_FACTS - 2 {
2400                self.exceeded = true;
2401                break;
2402            }
2403            if self.chars[self.index] == ']' && saw_member {
2404                self.index += 1;
2405                closed = true;
2406                break;
2407            }
2408            let Some(left) = parse_class_atom(&self.chars, self.index, self.flags.ascii) else {
2409                self.mark_incomplete(
2410                    self.index,
2411                    (self.index + 1).min(self.chars.len()),
2412                    "unsupported-class-escape",
2413                );
2414                self.index += 1;
2415                members_known = false;
2416                continue;
2417            };
2418            self.index = left.end;
2419            if let Some(reason) = left.incomplete {
2420                self.mark_incomplete(left.start, left.end, reason);
2421                members_known = false;
2422            }
2423            let has_range = self.chars.get(self.index) == Some(&'-')
2424                && self
2425                    .chars
2426                    .get(self.index + 1)
2427                    .is_some_and(|next| *next != ']');
2428            if has_range {
2429                let dash = self.index;
2430                self.index += 1;
2431                let Some(right) = parse_class_atom(&self.chars, self.index, self.flags.ascii)
2432                else {
2433                    self.mark_incomplete(dash, dash + 1, "unsupported-class-range");
2434                    members_known = false;
2435                    continue;
2436                };
2437                self.index = right.end;
2438                if let Some(reason) = right.incomplete {
2439                    self.mark_incomplete(right.start, right.end, reason);
2440                    members_known = false;
2441                }
2442                let Some((left_value, right_value)) = left.value.zip(right.value) else {
2443                    self.mark_incomplete(left.start, right.end, "non-literal-class-range");
2444                    members_known = false;
2445                    continue;
2446                };
2447                if left_value > right_value {
2448                    let range_fact = self.add_fact(
2449                        NormalizedKind::RegexCharacterRange,
2450                        left.start,
2451                        right.end,
2452                        Some(class),
2453                        Some(format!(
2454                            "U+{:04X}-U+{:04X}",
2455                            left_value as u32, right_value as u32
2456                        )),
2457                        Some(CharacterSet { ranges: Vec::new() }),
2458                    );
2459                    self.pending_risks.push((
2460                        NormalizedKind::RegexRiskRange,
2461                        range_fact,
2462                        left.start,
2463                        right.end,
2464                        "empty-descending-range".to_owned(),
2465                    ));
2466                    self.mark_incomplete(left.start, right.end, "descending-class-range");
2467                    members_known = false;
2468                    saw_member = true;
2469                    continue;
2470                }
2471                let range_set = CharacterSet {
2472                    ranges: vec![(left_value as u32, right_value as u32)],
2473                };
2474                let range_set = if self.flags.ignorecase {
2475                    range_set.with_ascii_case_variants()
2476                } else {
2477                    range_set
2478                };
2479                let compatibility_set = codeql_range_character_set(left_value, right_value);
2480                let range_fact = self.add_fact(
2481                    NormalizedKind::RegexCharacterRange,
2482                    left.start,
2483                    right.end,
2484                    Some(class),
2485                    Some(format!(
2486                        "U+{:04X}-U+{:04X}",
2487                        left_value as u32, right_value as u32
2488                    )),
2489                    Some(range_set.clone()),
2490                );
2491                if let Some((previous_fact, _, previous_width)) = range_members
2492                    .iter()
2493                    .find(|(_, previous, _)| previous.intersects(&compatibility_set))
2494                {
2495                    let current_width = compatibility_set
2496                        .ranges
2497                        .iter()
2498                        .map(|(start, end)| end.saturating_sub(*start))
2499                        .max()
2500                        .unwrap_or_default();
2501                    let previous_start = self.facts[*previous_fact].start;
2502                    let selected_fact = if current_width > *previous_width
2503                        || (current_width == *previous_width && left.start < previous_start)
2504                    {
2505                        range_fact
2506                    } else {
2507                        *previous_fact
2508                    };
2509                    self.pending_risks.push((
2510                        NormalizedKind::RegexRiskRange,
2511                        selected_fact,
2512                        self.facts[selected_fact].start,
2513                        self.facts[selected_fact].end,
2514                        "overlapping-class-range".to_owned(),
2515                    ));
2516                }
2517                if category_members
2518                    .iter()
2519                    .any(|previous: &CharacterSet| previous.intersects(&compatibility_set))
2520                {
2521                    self.pending_risks.push((
2522                        NormalizedKind::RegexRiskRange,
2523                        range_fact,
2524                        left.start,
2525                        right.end,
2526                        "range-overlaps-character-class-escape".to_owned(),
2527                    ));
2528                }
2529                let compatibility_width = compatibility_set
2530                    .ranges
2531                    .iter()
2532                    .map(|(start, end)| end.saturating_sub(*start))
2533                    .max()
2534                    .unwrap_or_default();
2535                range_members.push((range_fact, compatibility_set, compatibility_width));
2536                members.union_with(&range_set);
2537                saw_member = true;
2538                if is_overly_broad_range(left_value, right_value) {
2539                    self.pending_risks.push((
2540                        NormalizedKind::RegexRiskRange,
2541                        range_fact,
2542                        left.start,
2543                        right.end,
2544                        "ascii-punctuation-between-letter-ranges".to_owned(),
2545                    ));
2546                }
2547            } else {
2548                let semantic_set = left.set.as_ref().map(|set| {
2549                    if self.flags.ignorecase {
2550                        set.clone().with_ascii_case_variants()
2551                    } else {
2552                        set.clone()
2553                    }
2554                });
2555                let is_category = left
2556                    .construct
2557                    .as_deref()
2558                    .is_some_and(|construct| construct.starts_with("category:"));
2559                let member_fact = self.add_fact(
2560                    left.kind,
2561                    left.start,
2562                    left.end,
2563                    Some(class),
2564                    left.construct,
2565                    semantic_set.clone(),
2566                );
2567                if let Some(set) = &semantic_set {
2568                    if left.value.is_some() {
2569                        let syntax = self.chars[left.start..left.end].iter().collect::<String>();
2570                        if !explicit_members.insert(syntax) {
2571                            self.pending_risks.push((
2572                                NormalizedKind::RegexRiskDuplicateClassCharacter,
2573                                member_fact,
2574                                left.start,
2575                                left.end,
2576                                "duplicate-class-member".to_owned(),
2577                            ));
2578                        }
2579                    }
2580                    if is_category {
2581                        if range_members
2582                            .iter()
2583                            .any(|(_, range, _)| range.intersects(set))
2584                        {
2585                            self.pending_risks.push((
2586                                NormalizedKind::RegexRiskRange,
2587                                member_fact,
2588                                left.start,
2589                                left.end,
2590                                "range-overlaps-character-class-escape".to_owned(),
2591                            ));
2592                        }
2593                        category_members.push(set.clone());
2594                    }
2595                    members.union_with(set);
2596                } else {
2597                    members_known = false;
2598                }
2599                saw_member = true;
2600                if left.backspace {
2601                    self.pending_risks.push((
2602                        NormalizedKind::RegexRiskBackspaceEscape,
2603                        member_fact,
2604                        left.start,
2605                        left.end,
2606                        "backspace-in-character-class".to_owned(),
2607                    ));
2608                }
2609            }
2610        }
2611        self.facts[class].end = self.index;
2612        if !closed {
2613            self.mark_incomplete(start, self.index, "unclosed-character-class");
2614        }
2615        if !saw_member {
2616            self.mark_incomplete(start, self.index, "empty-character-class");
2617        }
2618        if negated && members_known {
2619            members = members.complement();
2620        }
2621        self.facts[class].first_set = members_known.then_some(members);
2622        self.record_atom(class);
2623    }
2624
2625    fn sequence_first_set(&self, sequence: usize) -> Option<CharacterSet> {
2626        let branches = self.branch_first_sets(sequence);
2627        if branches.iter().any(Option::is_none) {
2628            return None;
2629        }
2630        let mut result = CharacterSet { ranges: Vec::new() };
2631        for branch in branches.into_iter().flatten() {
2632            result.union_with(&branch);
2633        }
2634        Some(result)
2635    }
2636
2637    fn branch_first_sets(&self, sequence: usize) -> Vec<Option<CharacterSet>> {
2638        let mut branches = Vec::new();
2639        let mut current = None;
2640        let mut consumed = false;
2641        for fact in self
2642            .facts
2643            .iter()
2644            .filter(|fact| fact.parent == Some(sequence))
2645        {
2646            if fact.kind == NormalizedKind::RegexAlternation {
2647                branches.push(current.take());
2648                consumed = false;
2649                continue;
2650            }
2651            if consumed || fact.kind == NormalizedKind::RegexAnchor {
2652                continue;
2653            }
2654            match fact.kind {
2655                NormalizedKind::RegexLiteral
2656                | NormalizedKind::RegexWildcard
2657                | NormalizedKind::RegexCharacterClass
2658                | NormalizedKind::RegexEscape
2659                | NormalizedKind::RegexGroup => {
2660                    current = fact.first_set.clone();
2661                    consumed = true;
2662                }
2663                _ => {}
2664            }
2665        }
2666        branches.push(current);
2667        branches
2668    }
2669
2670    fn compute_minimum_widths(&mut self) {
2671        for id in (0..self.facts.len()).rev() {
2672            match self.facts[id].kind {
2673                NormalizedKind::RegexSequence => {
2674                    self.facts[id].minimum_width = self.sequence_minimum_width(id);
2675                }
2676                NormalizedKind::RegexGroup => {
2677                    let sequence = self.facts.iter().position(|fact| {
2678                        fact.kind == NormalizedKind::RegexSequence && fact.parent == Some(id)
2679                    });
2680                    self.facts[id].minimum_width = if self.facts[id]
2681                        .construct
2682                        .as_deref()
2683                        .is_some_and(|construct| {
2684                            construct.starts_with("look") || construct == "comment"
2685                        }) {
2686                        Some(0)
2687                    } else {
2688                        sequence.and_then(|sequence| self.facts[sequence].minimum_width)
2689                    };
2690                    let minimum = self
2691                        .facts
2692                        .iter()
2693                        .find(|fact| {
2694                            fact.parent == Some(id) && fact.kind == NormalizedKind::RegexQuantifier
2695                        })
2696                        .and_then(|fact| fact.quantifier)
2697                        .map_or(1, |quantifier| quantifier.minimum);
2698                    self.facts[id].minimum_width = self.facts[id]
2699                        .minimum_width
2700                        .and_then(|width| width.checked_mul(minimum));
2701                }
2702                _ => {}
2703            }
2704        }
2705    }
2706
2707    fn sequence_minimum_width(&self, sequence: usize) -> Option<u32> {
2708        let mut branches = Vec::new();
2709        let mut width = Some(0u32);
2710        for fact in self
2711            .facts
2712            .iter()
2713            .filter(|fact| fact.parent == Some(sequence))
2714        {
2715            if fact.kind == NormalizedKind::RegexAlternation {
2716                branches.push(width.take()?);
2717                width = Some(0);
2718                continue;
2719            }
2720            width = width?.checked_add(fact.minimum_width?);
2721        }
2722        branches.push(width?);
2723        branches.into_iter().min()
2724    }
2725
2726    fn classify_unmatchable_anchors(&mut self) {
2727        if self.incomplete.is_some() || self.flags.multiline {
2728            return;
2729        }
2730        let sequences = self
2731            .facts
2732            .iter()
2733            .enumerate()
2734            .filter(|(_, fact)| fact.kind == NormalizedKind::RegexSequence)
2735            .map(|(id, _)| id)
2736            .collect::<Vec<_>>();
2737        for sequence in sequences {
2738            let branches = regex_sequence_branches(
2739                &self.facts,
2740                self.facts
2741                    .iter()
2742                    .enumerate()
2743                    .filter(|(_, fact)| fact.parent == Some(sequence))
2744                    .map(|(id, _)| id),
2745            );
2746            for branch in branches {
2747                let mut prefix_width = Some(0u32);
2748                for (index, &id) in branch.iter().enumerate() {
2749                    let fact = &self.facts[id];
2750                    if fact.kind == NormalizedKind::RegexAnchor
2751                        && fact.construct.as_deref() == Some("^")
2752                        && (prefix_width.is_some_and(|width| width > 0)
2753                            || branch[..index].iter().any(|&prefix| {
2754                                self.facts[prefix].construct.as_deref()
2755                                    == Some("lookbehind-positive")
2756                                    && self.facts.iter().any(|body| {
2757                                        body.kind == NormalizedKind::RegexSequence
2758                                            && body.parent == Some(prefix)
2759                                            && body.minimum_width.is_some_and(|width| width > 0)
2760                                    })
2761                            }))
2762                    {
2763                        self.pending_risks.push((
2764                            NormalizedKind::RegexRiskUnmatchableCaret,
2765                            id,
2766                            fact.start,
2767                            fact.end,
2768                            "caret-after-consuming-term".to_owned(),
2769                        ));
2770                    } else if fact.kind == NormalizedKind::RegexAnchor
2771                        && fact.construct.as_deref() == Some("$")
2772                    {
2773                        let mut suffix_width = Some(0u32);
2774                        for &suffix_id in &branch[index + 1..] {
2775                            suffix_width = suffix_width.and_then(|width| {
2776                                self.facts[suffix_id]
2777                                    .minimum_width
2778                                    .and_then(|suffix| width.checked_add(suffix))
2779                            });
2780                        }
2781                        if suffix_width.is_some_and(|width| width > 0)
2782                            || branch[index + 1..].iter().any(|&suffix| {
2783                                self.facts[suffix].construct.as_deref()
2784                                    == Some("lookahead-positive")
2785                                    && self.facts.iter().any(|body| {
2786                                        body.kind == NormalizedKind::RegexSequence
2787                                            && body.parent == Some(suffix)
2788                                            && body.minimum_width.is_some_and(|width| width > 0)
2789                                    })
2790                            })
2791                        {
2792                            self.pending_risks.push((
2793                                NormalizedKind::RegexRiskUnmatchableDollar,
2794                                id,
2795                                fact.start,
2796                                fact.end,
2797                                "dollar-before-consuming-term".to_owned(),
2798                            ));
2799                        }
2800                    }
2801                    prefix_width =
2802                        prefix_width.and_then(|width| width.checked_add(fact.minimum_width?));
2803                }
2804            }
2805        }
2806    }
2807
2808    fn classify_hostname(&mut self) {
2809        if self.incomplete.is_some() {
2810            return;
2811        }
2812
2813        // Recognize hostname-LIKE values, not the intent of the regex's caller.
2814        // A wildcard is projected to a separator only while recognizing this
2815        // shape. Escaped dots and singleton dot classes consume the same shape
2816        // character but are not risks. The accepted path must contain the dot;
2817        // a suffix in another alternative cannot supply its hostname context.
2818        #[derive(Clone, Copy)]
2819        enum Token {
2820            Character(char),
2821            WildcardDot(usize),
2822            StartAnchor,
2823            EndAnchor,
2824        }
2825        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
2826        enum LabelEnd {
2827            AlphaOne,
2828            AlphaMany,
2829            Alphanumeric,
2830            Hyphen,
2831        }
2832        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
2833        enum Shape {
2834            Start,
2835            Prefix(&'static str),
2836            FirstLabel,
2837            NextLabel,
2838            Label(bool, LabelEnd),
2839            Slash,
2840            End,
2841        }
2842        impl Shape {
2843            fn accepts(self) -> bool {
2844                matches!(
2845                    self,
2846                    Self::Label(true, LabelEnd::AlphaMany) | Self::Slash | Self::End
2847                )
2848            }
2849
2850            fn advance(self, token: Token) -> Vec<Self> {
2851                let character = match token {
2852                    Token::StartAnchor => {
2853                        return if self == Self::Start {
2854                            vec![self]
2855                        } else {
2856                            Vec::new()
2857                        };
2858                    }
2859                    Token::EndAnchor => {
2860                        return if self.accepts() {
2861                            vec![Self::End]
2862                        } else {
2863                            Vec::new()
2864                        };
2865                    }
2866                    Token::Character(character) => character,
2867                    Token::WildcardDot(_) => '.',
2868                };
2869                let first = if character.is_ascii_alphabetic() {
2870                    Some(LabelEnd::AlphaOne)
2871                } else if character.is_ascii_digit() {
2872                    Some(LabelEnd::Alphanumeric)
2873                } else {
2874                    None
2875                };
2876                match self {
2877                    Self::Start => {
2878                        let mut next = first
2879                            .map(|end| Self::Label(false, end))
2880                            .into_iter()
2881                            .collect::<Vec<_>>();
2882                        if character == 'h' {
2883                            next.extend([Self::Prefix("ttp://"), Self::Prefix("ttps://")]);
2884                        }
2885                        next
2886                    }
2887                    Self::Prefix(rest) => {
2888                        if rest.starts_with(character) {
2889                            let rest = &rest[character.len_utf8()..];
2890                            vec![if rest.is_empty() {
2891                                Self::FirstLabel
2892                            } else {
2893                                Self::Prefix(rest)
2894                            }]
2895                        } else {
2896                            Vec::new()
2897                        }
2898                    }
2899                    Self::FirstLabel | Self::NextLabel => first
2900                        .map(|end| Self::Label(self == Self::NextLabel, end))
2901                        .into_iter()
2902                        .collect(),
2903                    Self::Label(separated, end) => {
2904                        let next = if character.is_ascii_alphabetic() {
2905                            Some(Self::Label(
2906                                separated,
2907                                match end {
2908                                    LabelEnd::AlphaOne | LabelEnd::AlphaMany => LabelEnd::AlphaMany,
2909                                    LabelEnd::Alphanumeric | LabelEnd::Hyphen => {
2910                                        LabelEnd::Alphanumeric
2911                                    }
2912                                },
2913                            ))
2914                        } else if character.is_ascii_digit() {
2915                            Some(Self::Label(separated, LabelEnd::Alphanumeric))
2916                        } else if character == '-' {
2917                            Some(Self::Label(separated, LabelEnd::Hyphen))
2918                        } else if character == '.' && end != LabelEnd::Hyphen {
2919                            Some(Self::NextLabel)
2920                        } else if character == '/' && self.accepts() {
2921                            Some(Self::Slash)
2922                        } else {
2923                            None
2924                        };
2925                        next.into_iter().collect()
2926                    }
2927                    Self::Slash | Self::End => Vec::new(),
2928                }
2929            }
2930        }
2931
2932        // Two boundaries per existing fact form an acyclic graph. Groups and
2933        // alternatives reuse their fact boundaries rather than expanding the
2934        // Cartesian product of label choices. Both the graph and its product
2935        // with the fixed-size Shape state set are bounded by MAX_PATTERN_FACTS.
2936        let mut children = vec![Vec::new(); self.facts.len()];
2937        for (id, fact) in self.facts.iter().enumerate() {
2938            if let Some(parent) = fact.parent {
2939                children[parent].push(id);
2940            }
2941        }
2942        let mut edges = vec![Vec::<(usize, Option<Token>)>::new(); self.facts.len() * 2];
2943        for (id, fact) in self.facts.iter().enumerate() {
2944            if children[id]
2945                .iter()
2946                .any(|&child| self.facts[child].kind == NormalizedKind::RegexQuantifier)
2947            {
2948                continue;
2949            }
2950            let start = id * 2;
2951            let end = start + 1;
2952            match fact.kind {
2953                NormalizedKind::RegexPattern | NormalizedKind::RegexGroup
2954                    if fact.kind == NormalizedKind::RegexPattern
2955                        || matches!(
2956                            fact.construct.as_deref(),
2957                            Some("capturing" | "named-capturing" | "noncapturing" | "scoped-flags")
2958                        ) =>
2959                {
2960                    for &sequence in &children[id] {
2961                        if self.facts[sequence].kind == NormalizedKind::RegexSequence {
2962                            edges[start].push((sequence * 2, None));
2963                            edges[sequence * 2 + 1].push((end, None));
2964                        }
2965                    }
2966                }
2967                NormalizedKind::RegexSequence => {
2968                    for branch in regex_sequence_branches(&self.facts, children[id].iter().copied())
2969                    {
2970                        let mut previous = start;
2971                        for child in branch {
2972                            edges[previous].push((child * 2, None));
2973                            previous = child * 2 + 1;
2974                        }
2975                        edges[previous].push((end, None));
2976                    }
2977                }
2978                _ => {
2979                    let token = match fact.kind {
2980                        NormalizedKind::RegexWildcard
2981                            if fact.construct.as_deref() == Some("dot") =>
2982                        {
2983                            Some(Token::WildcardDot(id))
2984                        }
2985                        NormalizedKind::RegexAnchor => match fact.construct.as_deref() {
2986                            Some("^" | "boundary:A") => Some(Token::StartAnchor),
2987                            Some("$" | "boundary:Z" | "boundary:z") => Some(Token::EndAnchor),
2988                            _ => None,
2989                        },
2990                        NormalizedKind::RegexLiteral => {
2991                            fact.construct.as_deref().and_then(|value| {
2992                                let mut chars = value.chars();
2993                                let first = chars.next()?;
2994                                chars.next().is_none().then_some(Token::Character(first))
2995                            })
2996                        }
2997                        NormalizedKind::RegexEscape => fact.escaped_literal.map(Token::Character),
2998                        NormalizedKind::RegexCharacterClass => fact
2999                            .first_set
3000                            .as_ref()
3001                            .and_then(single_character)
3002                            .map(Token::Character),
3003                        _ => None,
3004                    };
3005                    if let Some(token) = token {
3006                        edges[start].push((end, Some(token)));
3007                    }
3008                }
3009            }
3010        }
3011        let start = (0, Shape::Start);
3012        let mut reverse = HashMap::<_, Vec<(_, Option<usize>)>>::from([(start, Vec::new())]);
3013        let mut pending = VecDeque::from([start]);
3014        while let Some((boundary, shape)) = pending.pop_front() {
3015            for &(target, token) in &edges[boundary] {
3016                let next_shapes = token.map_or_else(|| vec![shape], |token| shape.advance(token));
3017                let risk = match token {
3018                    Some(Token::WildcardDot(id)) => Some(id),
3019                    _ => None,
3020                };
3021                for next_shape in next_shapes {
3022                    let next = (target, next_shape);
3023                    let newly_seen = !reverse.contains_key(&next);
3024                    reverse
3025                        .entry(next)
3026                        .or_default()
3027                        .push(((boundary, shape), risk));
3028                    if newly_seen {
3029                        pending.push_back(next);
3030                    }
3031                }
3032            }
3033        }
3034        let mut pending = reverse
3035            .keys()
3036            .copied()
3037            .filter(|&(boundary, shape)| boundary == 1 && shape.accepts())
3038            .collect::<Vec<_>>();
3039        let mut visited = HashSet::new();
3040        let mut risks = HashSet::new();
3041        while let Some(node) = pending.pop() {
3042            if !visited.insert(node) {
3043                continue;
3044            }
3045            for &(previous, risk) in &reverse[&node] {
3046                risks.extend(risk);
3047                pending.push(previous);
3048            }
3049        }
3050        let mut risks = risks.into_iter().collect::<Vec<_>>();
3051        risks.sort_unstable();
3052        for dot in risks {
3053            let fact = &self.facts[dot];
3054            self.pending_risks.push((
3055                NormalizedKind::RegexRiskHostname,
3056                dot,
3057                fact.start,
3058                fact.end,
3059                "unescaped-dot-in-hostname-like-pattern".to_owned(),
3060            ));
3061        }
3062    }
3063
3064    fn classify_bad_tag(&mut self) {
3065        if self.incomplete.is_some() || !self.flags.complete {
3066            return;
3067        }
3068        let value = &self.decoded.value;
3069        if !(value.contains('<') && value.contains('>'))
3070            && !["-->", "--!>", "<--", "<!--"]
3071                .iter()
3072                .any(|marker| value.contains(marker))
3073        {
3074            return;
3075        }
3076        let rust_pattern = value
3077            .replace("\\/", "/")
3078            .replace("\\\"", "\"")
3079            .replace("\\'", "'");
3080        let mut builder = regex::RegexBuilder::new(&rust_pattern);
3081        builder
3082            .case_insensitive(self.flags.ignorecase)
3083            .multi_line(self.flags.multiline)
3084            .dot_matches_new_line(self.flags.dotall)
3085            .unicode(!self.flags.ascii)
3086            .ignore_whitespace(self.flags.verbose);
3087        let Ok(regex) = builder.build() else {
3088            return;
3089        };
3090        let capture_nfa = match RegexNfa::compile(&self.facts, 0, None) {
3091            Ok(nfa) => Some(nfa),
3092            Err(error) => {
3093                self.mark_incomplete(
3094                    0,
3095                    self.chars.len(),
3096                    match error {
3097                        NfaError::Unsupported => "bad-tag-nfa-unsupported",
3098                        NfaError::Budget => "bad-tag-nfa-state-budget",
3099                    },
3100                );
3101                return;
3102            }
3103        };
3104        let matches = |sample: &str| {
3105            capture_nfa.as_ref().map_or_else(
3106                || regex.is_match(sample),
3107                |nfa| nfa.match_prefix_capture_groups(sample).0,
3108            )
3109        };
3110        let groups = |sample: &str| {
3111            capture_nfa.as_ref().map_or_else(
3112                || {
3113                    regex
3114                        .captures(sample)
3115                        .map(|captures| {
3116                            captures
3117                                .iter()
3118                                .enumerate()
3119                                .skip(1)
3120                                .filter_map(|(index, capture)| capture.map(|_| index))
3121                                .collect::<HashSet<_>>()
3122                        })
3123                        .unwrap_or_default()
3124                },
3125                |nfa| nfa.match_prefix_capture_groups(sample).1,
3126            )
3127        };
3128        let comment = "<!-- foo -->";
3129        let comment_bang = "<!-- foo --!>";
3130        let comment_groups = groups(comment);
3131        let comment_bang_groups = groups(comment_bang);
3132        let comment_group_mismatch = matches(comment)
3133            && matches(comment_bang)
3134            && comment_groups.iter().any(|group| {
3135                !comment_bang_groups.contains(group)
3136                    && comment_bang_groups.iter().any(|other| group != other)
3137            });
3138        let plain_tag_groups = groups("<foo>");
3139        let only_comment_group = groups(comment).iter().any(|group| {
3140            plain_tag_groups.iter().any(|other| other != group)
3141                && !matches(comment_bang)
3142                && !groups("<!- foo ->").contains(group)
3143                && !plain_tag_groups.contains(group)
3144                && !groups("<script>").contains(group)
3145        });
3146        let bounded_script_opener = value
3147            .to_ascii_lowercase()
3148            .find("<script")
3149            .map(|start| {
3150                let start = value[..start].chars().count() + "<script".len();
3151                self.facts
3152                    .iter()
3153                    .filter(|fact| {
3154                        fact.kind == NormalizedKind::RegexLiteral
3155                            && fact.construct.as_deref() == Some(">")
3156                            && fact.start >= start
3157                            && !fact.parent.is_some_and(|parent| {
3158                                self.facts[parent].kind == NormalizedKind::RegexCharacterClass
3159                            })
3160                    })
3161                    .map(|fact| fact.start)
3162                    .min()
3163                    .is_some_and(|tag_end| {
3164                        self.facts.iter().enumerate().any(|(id, fact)| {
3165                            fact.kind == NormalizedKind::RegexCharacterClass
3166                                && fact.start >= start
3167                                && fact.end <= tag_end
3168                                && fact
3169                                    .first_set
3170                                    .as_ref()
3171                                    .is_some_and(|set| !set.contains('>'))
3172                                && self.facts.iter().any(|quantifier| {
3173                                    quantifier.kind == NormalizedKind::RegexQuantifier
3174                                        && quantifier.parent == Some(id)
3175                                        && quantifier
3176                                            .quantifier
3177                                            .is_some_and(|quantifier| quantifier.maximum.is_none())
3178                                })
3179                        })
3180                    })
3181            })
3182            .unwrap_or(false);
3183        let script_warning = !bounded_script_opener
3184            && ((matches("<script>foo</script>")
3185                && matches("<script src=\"foo\"></script>")
3186                && !matches("<foo ></foo>")
3187                && (!matches("<script \n>foo</script>") || !matches("<script >foo\n</script>")))
3188                || (matches("<script>foo</script>")
3189                    && matches("<script src=\"foo\"></script>")
3190                    && !matches("<script src='foo'></script>")
3191                    && !matches("<foo>"))
3192                || (matches("<script>foo</script>")
3193                    && matches("<script src='foo'></script>")
3194                    && !matches("<script src=\"foo\"></script>")
3195                    && !matches("<foo>"))
3196                || (matches("<script>foo</script>")
3197                    && matches("<script src='foo'></script>")
3198                    && !matches("<script\tsrc='foo'></script>")
3199                    && !matches("<foo>")
3200                    && !matches("<foo src=\"foo\"></foo>"))
3201                || (matches("<script>foo</script>")
3202                    && !self.flags.ignorecase
3203                    && !matches("<foo>")
3204                    && !matches("<foo ></foo>")
3205                    && (!matches("<SCRIPT>foo</SCRIPT>")
3206                        || (matches("<SCRIPT>foo</SCRIPT>") && !matches("<sCrIpT>foo</ScRiPt>"))))
3207                || (matches("<script src=\"foo\"></script>")
3208                    && !matches("<foo>")
3209                    && !matches("<foo ></foo>")
3210                    && (!matches("<script src=\"foo\">foo</script >")
3211                        || !matches("<script src=\"foo\">foo</script foo=\"bar\">")
3212                        || !matches("<script src=\"foo\">foo</script\t\n bar>"))));
3213        let warning = comment_group_mismatch
3214            || only_comment_group
3215            || (matches("-->") && !matches("--!>") && !matches("--"))
3216            || (matches(comment)
3217                && !matches("<!-- foo\n -->")
3218                && !matches("<!- foo ->")
3219                && !matches("<foo>")
3220                && !matches("<script>"))
3221            || script_warning;
3222        if warning {
3223            self.pending_risks.push((
3224                NormalizedKind::RegexRiskBadTag,
3225                0,
3226                0,
3227                self.chars.len(),
3228                "matches-html-samples-inconsistently".to_owned(),
3229            ));
3230        }
3231    }
3232
3233    fn classify_polynomial_redos(&mut self) {
3234        if self.incomplete.is_some() || !self.flags.complete {
3235            return;
3236        }
3237        let sequences = self
3238            .facts
3239            .iter()
3240            .enumerate()
3241            .filter(|(_, fact)| fact.kind == NormalizedKind::RegexSequence)
3242            .map(|(id, _)| id)
3243            .collect::<Vec<_>>();
3244        for sequence in sequences {
3245            let mut branches = vec![Vec::new()];
3246            for (id, fact) in self.facts.iter().enumerate() {
3247                if fact.parent != Some(sequence) {
3248                    continue;
3249                }
3250                if fact.kind == NormalizedKind::RegexAlternation {
3251                    branches.push(Vec::new());
3252                } else {
3253                    branches
3254                        .last_mut()
3255                        .expect("regex sequence always has a branch")
3256                        .push(id);
3257                }
3258            }
3259            for branch in branches {
3260                let Some(&terminal_anchor) = branch.last() else {
3261                    continue;
3262                };
3263                let terminal = &self.facts[terminal_anchor];
3264                if terminal.kind != NormalizedKind::RegexAnchor
3265                    || terminal.construct.as_deref() != Some("$")
3266                {
3267                    continue;
3268                }
3269
3270                let mut repeated = Vec::new();
3271                for (position, &atom) in branch[..branch.len() - 1].iter().enumerate() {
3272                    if !matches!(
3273                        self.facts[atom].kind,
3274                        NormalizedKind::RegexLiteral
3275                            | NormalizedKind::RegexEscape
3276                            | NormalizedKind::RegexCharacterClass
3277                            | NormalizedKind::RegexWildcard
3278                    ) || self.facts[atom].minimum_width != Some(1)
3279                    {
3280                        continue;
3281                    }
3282                    let Some((quantifier_id, quantifier)) =
3283                        self.facts.iter().enumerate().find_map(|(id, fact)| {
3284                            (fact.kind == NormalizedKind::RegexQuantifier
3285                                && fact.parent == Some(atom))
3286                            .then_some((id, fact.quantifier?))
3287                        })
3288                    else {
3289                        continue;
3290                    };
3291                    if quantifier.minimum == 1
3292                        && quantifier.maximum.is_none()
3293                        && !quantifier.possessive
3294                    {
3295                        repeated.push((
3296                            position,
3297                            atom,
3298                            quantifier_id,
3299                            self.facts[atom].first_set.clone(),
3300                        ));
3301                    }
3302                }
3303                if self.flags.ignorecase && !self.flags.ascii && !repeated.is_empty() {
3304                    self.mark_incomplete(
3305                        self.facts[sequence].start,
3306                        self.facts[sequence].end,
3307                        "redos-unicode-casefold-bound",
3308                    );
3309                    continue;
3310                }
3311
3312                let anchored_at_start = branch
3313                    .iter()
3314                    .take_while(|&&id| {
3315                        (self.facts[id].kind == NormalizedKind::RegexAnchor
3316                            && self.facts[id].construct.as_deref() == Some("^"))
3317                            || (self.facts[id].kind == NormalizedKind::RegexGroup
3318                                && self.facts[id].construct.as_deref() == Some("comment"))
3319                    })
3320                    .any(|&id| {
3321                        self.facts[id].kind == NormalizedKind::RegexAnchor
3322                            && self.facts[id].construct.as_deref() == Some("^")
3323                    });
3324                let mut risky_quantifier = None;
3325                let mut overlapping_repeated_runs = false;
3326                'pairs: for left in 0..repeated.len() {
3327                    for right in left + 1..repeated.len() {
3328                        let (left_position, left_atom, _, _) = &repeated[left];
3329                        let (right_position, right_atom, right_quantifier, _) = &repeated[right];
3330                        if !polynomial_atoms_overlap(
3331                            &self.facts[*left_atom],
3332                            &self.facts[*right_atom],
3333                        ) {
3334                            continue;
3335                        }
3336                        let separable =
3337                            branch[left_position + 1..*right_position]
3338                                .iter()
3339                                .all(|&separator| {
3340                                    if !matches!(
3341                                        self.facts[separator].kind,
3342                                        NormalizedKind::RegexLiteral
3343                                            | NormalizedKind::RegexEscape
3344                                            | NormalizedKind::RegexCharacterClass
3345                                            | NormalizedKind::RegexWildcard
3346                                    ) {
3347                                        return false;
3348                                    }
3349                                    let Some(quantifier) = self
3350                                        .facts
3351                                        .iter()
3352                                        .find(|fact| {
3353                                            fact.kind == NormalizedKind::RegexQuantifier
3354                                                && fact.parent == Some(separator)
3355                                        })
3356                                        .and_then(|fact| fact.quantifier)
3357                                    else {
3358                                        return false;
3359                                    };
3360                                    quantifier.minimum == 0
3361                                        && quantifier.maximum == Some(1)
3362                                        && !quantifier.possessive
3363                                });
3364                        if separable {
3365                            risky_quantifier = Some(*right_quantifier);
3366                            overlapping_repeated_runs = true;
3367                            break 'pairs;
3368                        }
3369                    }
3370                }
3371
3372                if risky_quantifier.is_none() && self.api.scans_from_each_offset {
3373                    risky_quantifier = (!anchored_at_start)
3374                        .then(|| {
3375                            repeated
3376                                .first()
3377                                .map(|(_, _, quantifier_id, _)| *quantifier_id)
3378                        })
3379                        .flatten();
3380                }
3381
3382                if let Some(quantifier) = risky_quantifier {
3383                    self.pending_risks.push((
3384                        NormalizedKind::RegexRiskPolynomial,
3385                        quantifier,
3386                        self.facts[quantifier].start,
3387                        self.facts[quantifier].end,
3388                        if overlapping_repeated_runs {
3389                            "overlapping-repeated-runs".to_owned()
3390                        } else {
3391                            "unanchored-repeated-run-before-end-anchor".to_owned()
3392                        },
3393                    ));
3394                }
3395            }
3396        }
3397    }
3398
3399    fn has_rejecting_suffix(&self, repeated_group: usize) -> bool {
3400        let mut member = repeated_group;
3401        loop {
3402            let Some(sequence) = self.facts[member].parent else {
3403                return false;
3404            };
3405            if self.facts[sequence].kind != NormalizedKind::RegexSequence {
3406                return false;
3407            }
3408            let mut after_member = false;
3409            for (id, fact) in self.facts.iter().enumerate() {
3410                if fact.parent != Some(sequence) {
3411                    continue;
3412                }
3413                if id == member {
3414                    after_member = true;
3415                    continue;
3416                }
3417                if !after_member {
3418                    continue;
3419                }
3420                if fact.kind == NormalizedKind::RegexAlternation {
3421                    break;
3422                }
3423                if fact.kind == NormalizedKind::RegexAnchor
3424                    && matches!(
3425                        fact.construct.as_deref(),
3426                        Some("$" | "boundary:Z" | "boundary:z")
3427                    )
3428                {
3429                    return true;
3430                }
3431                if fact.kind == NormalizedKind::RegexGroup
3432                    && fact.construct.as_deref() == Some("lookahead-positive")
3433                    && self.facts.iter().any(|body| {
3434                        body.kind == NormalizedKind::RegexSequence
3435                            && body.parent == Some(id)
3436                            && body.minimum_width.is_some_and(|width| width > 0)
3437                    })
3438                {
3439                    return true;
3440                }
3441                if fact.minimum_width.is_some_and(|width| width > 0) {
3442                    return true;
3443                }
3444            }
3445            let Some(owner) = self.facts[sequence].parent else {
3446                return false;
3447            };
3448            if self.facts[owner].kind != NormalizedKind::RegexGroup {
3449                return false;
3450            }
3451            member = owner;
3452        }
3453    }
3454
3455    fn suffix_is_covered_by_repeated_body(&self, repeated_group: usize) -> bool {
3456        let Some(body_set) = self.facts[repeated_group].first_set.as_ref() else {
3457            return false;
3458        };
3459        let mut member = repeated_group;
3460        loop {
3461            let Some(sequence) = self.facts[member].parent else {
3462                return false;
3463            };
3464            if self.facts[sequence].kind != NormalizedKind::RegexSequence {
3465                return false;
3466            }
3467            let mut after_member = false;
3468            let mut found_suffix = false;
3469            for (id, fact) in self.facts.iter().enumerate() {
3470                if fact.parent != Some(sequence) {
3471                    continue;
3472                }
3473                if fact.kind == NormalizedKind::RegexAlternation {
3474                    break;
3475                }
3476                if id == member {
3477                    after_member = true;
3478                    continue;
3479                }
3480                if !after_member {
3481                    continue;
3482                }
3483                if fact.kind == NormalizedKind::RegexAnchor {
3484                    return false;
3485                }
3486                if fact.minimum_width.is_some_and(|width| width > 0) {
3487                    found_suffix = true;
3488                    if fact.first_set.as_ref() != Some(body_set) {
3489                        return false;
3490                    }
3491                }
3492            }
3493            if found_suffix {
3494                return true;
3495            }
3496            let Some(owner) = self.facts[sequence].parent else {
3497                return false;
3498            };
3499            if self.facts[owner].kind != NormalizedKind::RegexGroup {
3500                return false;
3501            }
3502            member = owner;
3503        }
3504    }
3505
3506    fn classify_redos(&mut self) {
3507        if self.incomplete.is_some() {
3508            return;
3509        }
3510        let groups = (0..self.facts.len())
3511            .filter(|&id| self.facts[id].kind == NormalizedKind::RegexGroup)
3512            .collect::<Vec<_>>();
3513        let mut repeated_groups = Vec::new();
3514        for group in groups {
3515            let Some(sequence) = self.facts.iter().position(|fact| {
3516                fact.kind == NormalizedKind::RegexSequence && fact.parent == Some(group)
3517            }) else {
3518                continue;
3519            };
3520            let Some(outer_quantifier) = self.facts.iter().position(|fact| {
3521                fact.kind == NormalizedKind::RegexQuantifier && fact.parent == Some(group)
3522            }) else {
3523                continue;
3524            };
3525            let Some(quantifier) = self.facts[outer_quantifier].quantifier else {
3526                continue;
3527            };
3528            if quantifier.maximum.is_some() {
3529                continue;
3530            }
3531            if quantifier.possessive {
3532                continue;
3533            }
3534            repeated_groups.push((group, sequence, outer_quantifier));
3535        }
3536        if repeated_groups.len() > MAX_REDOSE_GROUPS {
3537            self.mark_incomplete(0, self.chars.len(), "redos-group-budget");
3538            return;
3539        }
3540        let mut relation_budget = MAX_NFA_PRODUCT_PAIRS;
3541        for (group, sequence, outer_quantifier) in repeated_groups {
3542            let mut risk_found = false;
3543            let branch_count = self
3544                .facts
3545                .iter()
3546                .filter(|fact| {
3547                    fact.kind == NormalizedKind::RegexAlternation && fact.parent == Some(sequence)
3548                })
3549                .count()
3550                + 1;
3551            if branch_count > 1 {
3552                if branch_count > MAX_REDOSE_ALTERNATIVES {
3553                    self.mark_incomplete(
3554                        self.facts[group].start,
3555                        self.facts[group].end,
3556                        "redos-alternative-budget",
3557                    );
3558                    continue;
3559                }
3560                if self.flags.ignorecase && !self.flags.ascii {
3561                    self.mark_incomplete(
3562                        self.facts[group].start,
3563                        self.facts[group].end,
3564                        "redos-unicode-casefold-bound",
3565                    );
3566                }
3567                let branches = (0..branch_count)
3568                    .map(|branch| RegexNfa::compile(&self.facts, sequence, Some(branch)))
3569                    .collect::<Result<Vec<_>, _>>();
3570                let repeated = RegexNfa::compile(&self.facts, sequence, None)
3571                    .and_then(RegexNfa::positive_closure);
3572                match (branches, repeated) {
3573                    (Ok(branches), Ok(repeated)) => {
3574                        let mut overlap = false;
3575                        let mut budget_exceeded = false;
3576                        'pairs: for left in 0..branches.len() {
3577                            for right in left + 1..branches.len() {
3578                                match branches[left].intersects_on_nonempty_string(
3579                                    &branches[right],
3580                                    &mut relation_budget,
3581                                ) {
3582                                    Ok(true) => {
3583                                        overlap = true;
3584                                        break 'pairs;
3585                                    }
3586                                    Ok(false) => {}
3587                                    Err(NfaError::Budget) => {
3588                                        budget_exceeded = true;
3589                                        break 'pairs;
3590                                    }
3591                                    Err(NfaError::Unsupported) => unreachable!(
3592                                        "product comparison only rejects bounded automata"
3593                                    ),
3594                                }
3595                                match branches[left].has_repartitioned_prefix(
3596                                    &branches[right],
3597                                    &repeated,
3598                                    &mut relation_budget,
3599                                ) {
3600                                    Ok(true) => {
3601                                        overlap = true;
3602                                        break 'pairs;
3603                                    }
3604                                    Ok(false) => {}
3605                                    Err(NfaError::Budget) => {
3606                                        budget_exceeded = true;
3607                                        break 'pairs;
3608                                    }
3609                                    Err(NfaError::Unsupported) => unreachable!(
3610                                        "prefix comparison only rejects bounded automata"
3611                                    ),
3612                                }
3613                                match branches[right].has_repartitioned_prefix(
3614                                    &branches[left],
3615                                    &repeated,
3616                                    &mut relation_budget,
3617                                ) {
3618                                    Ok(true) => {
3619                                        overlap = true;
3620                                        break 'pairs;
3621                                    }
3622                                    Ok(false) => {}
3623                                    Err(NfaError::Budget) => {
3624                                        budget_exceeded = true;
3625                                        break 'pairs;
3626                                    }
3627                                    Err(NfaError::Unsupported) => unreachable!(
3628                                        "prefix comparison only rejects bounded automata"
3629                                    ),
3630                                }
3631                            }
3632                        }
3633                        if overlap && self.has_rejecting_suffix(group) {
3634                            risk_found = true;
3635                            self.pending_risks.push((
3636                                NormalizedKind::RegexRiskExponential,
3637                                outer_quantifier,
3638                                self.facts[outer_quantifier].start,
3639                                self.facts[outer_quantifier].end,
3640                                "overlapping-repeated-alternatives".to_owned(),
3641                            ));
3642                        }
3643                        if budget_exceeded {
3644                            self.mark_incomplete(
3645                                self.facts[group].start,
3646                                self.facts[group].end,
3647                                "redos-nfa-product-budget",
3648                            );
3649                        }
3650                    }
3651                    (Err(error), _) | (_, Err(error)) => self.mark_incomplete(
3652                        self.facts[group].start,
3653                        self.facts[group].end,
3654                        match error {
3655                            NfaError::Unsupported => "redos-nfa-unsupported",
3656                            NfaError::Budget => "redos-nfa-state-budget",
3657                        },
3658                    ),
3659                }
3660            }
3661            let has_backtracking_child = (0..self.facts.len()).any(|candidate| {
3662                candidate != outer_quantifier
3663                    && self.is_descendant(candidate, group)
3664                    && matches!(
3665                        self.facts[candidate].kind,
3666                        NormalizedKind::RegexAlternation | NormalizedKind::RegexQuantifier
3667                    )
3668            });
3669            if has_backtracking_child && !risk_found {
3670                if self.flags.ignorecase && !self.flags.ascii {
3671                    self.mark_incomplete(
3672                        self.facts[group].start,
3673                        self.facts[group].end,
3674                        "redos-unicode-casefold-bound",
3675                    );
3676                }
3677                match RegexNfa::compile(&self.facts, group, None)
3678                    .and_then(|nfa| nfa.has_pumpable_fork(&mut relation_budget))
3679                {
3680                    Ok(true)
3681                        if self.has_rejecting_suffix(group)
3682                            && !self.suffix_is_covered_by_repeated_body(group) =>
3683                    {
3684                        self.pending_risks.push((
3685                            NormalizedKind::RegexRiskExponential,
3686                            outer_quantifier,
3687                            self.facts[outer_quantifier].start,
3688                            self.facts[outer_quantifier].end,
3689                            "overlapping-nfa-cycles".to_owned(),
3690                        ));
3691                    }
3692                    Ok(_) => {}
3693                    Err(error) => self.mark_incomplete(
3694                        self.facts[group].start,
3695                        self.facts[group].end,
3696                        match error {
3697                            NfaError::Unsupported => "redos-nfa-unsupported",
3698                            NfaError::Budget => "redos-nfa-product-budget",
3699                        },
3700                    ),
3701                }
3702            }
3703        }
3704    }
3705
3706    fn is_descendant(&self, mut fact: usize, ancestor: usize) -> bool {
3707        while let Some(parent) = self.facts[fact].parent {
3708            if parent == ancestor {
3709                return true;
3710            }
3711            fact = parent;
3712        }
3713        false
3714    }
3715
3716    fn append_pending_facts(&mut self) {
3717        for (kind, parent, start, end, reason) in std::mem::take(&mut self.pending_risks) {
3718            if self.facts.len() >= MAX_PATTERN_FACTS - 1 {
3719                self.exceeded = true;
3720                break;
3721            }
3722            self.add_fact(kind, start, end, Some(parent), Some(reason), None);
3723        }
3724        if let Some((start, end, reason)) = self.incomplete.take() {
3725            self.add_fact(
3726                NormalizedKind::RegexIncomplete,
3727                start,
3728                end,
3729                Some(0),
3730                Some(reason),
3731                None,
3732            );
3733        }
3734    }
3735}
3736
3737fn is_overly_broad_range(start: char, end: char) -> bool {
3738    let low = start as u32;
3739    let high = end as u32;
3740    let width = high.saturating_sub(low).saturating_add(1);
3741    if width < 10 {
3742        return false;
3743    }
3744    let alphanumeric = |character: char| character.is_ascii_alphanumeric();
3745    let wide_ascii = low < 128 && high < 128;
3746    let spans_letters = low <= 'Z' as u32 && high >= 'a' as u32;
3747    let spans_digits_to_letters = low <= '9' as u32 && high >= 'A' as u32;
3748    let has_non_alphanumeric_boundary = !alphanumeric(start) || !alphanumeric(end);
3749    let allowed = end == '~'
3750        || matches!(start, ' ' | '!')
3751        || start == '\0'
3752        || (start == '0' && end == '?')
3753        || (start == '@' && end == '_')
3754        || (start == '@' && end == 'Z');
3755    !allowed
3756        && (spans_letters
3757            || spans_digits_to_letters
3758            || (wide_ascii && has_non_alphanumeric_boundary))
3759}
3760
3761fn codeql_range_character_set(start: char, end: char) -> CharacterSet {
3762    let codepoint = |character: char| {
3763        let value = character as u32;
3764        if value > 0xffff { 0xfffd } else { value }
3765    };
3766    let start = codepoint(start);
3767    let end = codepoint(end);
3768    CharacterSet {
3769        ranges: (start <= end).then_some((start, end)).into_iter().collect(),
3770    }
3771}
3772
3773fn category_character_set(escaped: char, ascii: bool) -> Option<CharacterSet> {
3774    let (category, negated) = match escaped {
3775        'd' => ('d', false),
3776        'D' => ('d', true),
3777        's' => ('s', false),
3778        'S' => ('s', true),
3779        'w' => ('w', false),
3780        'W' => ('w', true),
3781        _ => return None,
3782    };
3783    let mut set = match category {
3784        'd' => CharacterSet {
3785            ranges: vec![(b'0' as u32, b'9' as u32)],
3786        },
3787        's' => CharacterSet {
3788            ranges: vec![(9, 13), (32, 32)],
3789        },
3790        'w' => CharacterSet {
3791            ranges: vec![
3792                (b'0' as u32, b'9' as u32),
3793                (b'A' as u32, b'Z' as u32),
3794                (b'_' as u32, b'_' as u32),
3795                (b'a' as u32, b'z' as u32),
3796            ],
3797        },
3798        _ => unreachable!("category is selected from d, s, w"),
3799    };
3800    if !ascii && negated {
3801        // A Unicode negated category is not the complement of its ASCII
3802        // subset. Leave it unknown rather than claim a false overlap.
3803    }
3804    if !ascii && negated {
3805        return None;
3806    }
3807    if negated {
3808        set = set.complement();
3809    }
3810    Some(set)
3811}
3812
3813fn parse_regex_escape(
3814    chars: &[char],
3815    start: usize,
3816    in_class: bool,
3817    ascii: bool,
3818) -> Option<EscapeValue> {
3819    if chars.get(start) != Some(&'\\') {
3820        return None;
3821    }
3822    let escaped = *chars.get(start + 1)?;
3823    let mut end = start + 2;
3824    let mut value = None;
3825    let mut anchor = false;
3826    let mut incomplete = None;
3827    let mut backspace = false;
3828    let construct = match escaped {
3829        'A' | 'Z' | 'z' | 'b' | 'B' if !in_class => {
3830            anchor = true;
3831            Some(format!("boundary:{escaped}"))
3832        }
3833        'd' | 'D' | 's' | 'S' | 'w' | 'W' => Some(format!("category:{escaped}")),
3834        'a' => {
3835            value = Some('\u{7}');
3836            Some("U+0007".to_owned())
3837        }
3838        'f' => {
3839            value = Some('\u{c}');
3840            Some("U+000C".to_owned())
3841        }
3842        'n' => {
3843            value = Some('\n');
3844            Some("U+000A".to_owned())
3845        }
3846        'r' => {
3847            value = Some('\r');
3848            Some("U+000D".to_owned())
3849        }
3850        't' => {
3851            value = Some('\t');
3852            Some("U+0009".to_owned())
3853        }
3854        'v' => {
3855            value = Some('\u{b}');
3856            Some("U+000B".to_owned())
3857        }
3858        'b' if in_class => {
3859            value = Some('\u{8}');
3860            backspace = true;
3861            Some("backspace".to_owned())
3862        }
3863        'x' | 'u' | 'U' => {
3864            let digits = match escaped {
3865                'x' => 2,
3866                'u' => 4,
3867                _ => 8,
3868            };
3869            let scalar = hex_value(chars, start + 2, digits);
3870            end = start + 2 + digits;
3871            match scalar.and_then(char::from_u32) {
3872                Some(character) => {
3873                    value = Some(character);
3874                    Some(format!("U+{:04X}", character as u32))
3875                }
3876                None => {
3877                    incomplete = Some("invalid-hex-escape");
3878                    Some(format!("invalid:{escaped}"))
3879                }
3880            }
3881        }
3882        'N' => {
3883            incomplete = Some("named-unicode-escape");
3884            if chars.get(start + 2) == Some(&'{') {
3885                let mut close = start + 3;
3886                while close < chars.len() && chars[close] != '}' {
3887                    close += 1;
3888                }
3889                end = (close + 1).min(chars.len());
3890            }
3891            Some("unicode-name".to_owned())
3892        }
3893        '0'..='7' => {
3894            let mut codepoint = escaped as u32 - '0' as u32;
3895            let mut digits = 1usize;
3896            while digits < 3 {
3897                let Some(digit @ '0'..='7') = chars.get(start + 1 + digits).copied() else {
3898                    break;
3899                };
3900                codepoint = codepoint * 8 + (digit as u32 - '0' as u32);
3901                digits += 1;
3902            }
3903            end = start + 1 + digits;
3904            match char::from_u32(codepoint) {
3905                Some(character) => {
3906                    value = Some(character);
3907                    Some(format!("U+{codepoint:04X}"))
3908                }
3909                None => {
3910                    incomplete = Some("invalid-octal-escape");
3911                    Some("invalid-octal".to_owned())
3912                }
3913            }
3914        }
3915        '1'..='9' if !in_class => {
3916            incomplete = Some("backreference-not-supported");
3917            Some(format!("backreference:{escaped}"))
3918        }
3919        'g' | 'k' if !in_class => {
3920            incomplete = Some("named-backreference-not-supported");
3921            Some(format!("backreference:{escaped}"))
3922        }
3923        other if other.is_ascii_alphabetic() => {
3924            incomplete = Some("unknown-regex-escape");
3925            Some(format!("unknown:{other}"))
3926        }
3927        other => {
3928            value = Some(other);
3929            Some(other.to_string())
3930        }
3931    };
3932    if end > chars.len() {
3933        return None;
3934    }
3935    let first_set = value
3936        .map(CharacterSet::one)
3937        .or_else(|| category_character_set(escaped, ascii));
3938    Some(EscapeValue {
3939        end,
3940        anchor,
3941        construct,
3942        first_set,
3943        incomplete,
3944        backspace,
3945    })
3946}
3947
3948fn parse_class_atom(chars: &[char], start: usize, ascii: bool) -> Option<ClassAtom> {
3949    let value = *chars.get(start)?;
3950    if value != '\\' {
3951        return Some(ClassAtom {
3952            start,
3953            end: start + 1,
3954            value: Some(value),
3955            set: Some(CharacterSet::one(value)),
3956            kind: NormalizedKind::RegexLiteral,
3957            construct: Some(value.to_string()),
3958            backspace: false,
3959            incomplete: None,
3960        });
3961    }
3962    let escape = parse_regex_escape(chars, start, true, ascii)?;
3963    Some(ClassAtom {
3964        start,
3965        end: escape.end,
3966        value: escape.first_set.as_ref().and_then(single_character),
3967        set: escape.first_set,
3968        kind: NormalizedKind::RegexEscape,
3969        construct: escape.construct,
3970        backspace: escape.backspace,
3971        incomplete: escape.incomplete,
3972    })
3973}
3974
3975fn single_character(set: &CharacterSet) -> Option<char> {
3976    let [(start, end)] = set.ranges.as_slice() else {
3977        return None;
3978    };
3979    (start == end).then(|| char::from_u32(*start)).flatten()
3980}
3981
3982fn facts_to_embedded(
3983    facts: Vec<Fact>,
3984    decoded: &DecodedValue,
3985    source: &str,
3986) -> Vec<EmbeddedLeafFact> {
3987    let mut children = vec![Vec::new(); facts.len()];
3988    let mut roots = Vec::new();
3989    for (id, fact) in facts.iter().enumerate() {
3990        if let Some(parent) = fact.parent {
3991            assert!(
3992                parent < id,
3993                "regex facts are parent-before-child before ordering"
3994            );
3995            children[parent].push(id);
3996        } else {
3997            roots.push(id);
3998        }
3999    }
4000    let order_children = |ids: &mut Vec<usize>| {
4001        ids.sort_by_key(|id| (facts[*id].start, facts[*id].end, *id));
4002    };
4003    order_children(&mut roots);
4004    for child_ids in &mut children {
4005        order_children(child_ids);
4006    }
4007    let mut order = Vec::with_capacity(facts.len());
4008    let mut stack = roots.into_iter().rev().collect::<Vec<_>>();
4009    while let Some(id) = stack.pop() {
4010        order.push(id);
4011        stack.extend(children[id].iter().rev().copied());
4012    }
4013    assert_eq!(
4014        order.len(),
4015        facts.len(),
4016        "regex AST fact tree reaches every node"
4017    );
4018    let mut remap = vec![0usize; facts.len()];
4019    for (new, old) in order.iter().copied().enumerate() {
4020        remap[old] = new;
4021    }
4022    let line_starts = compute_line_starts(source);
4023    order
4024        .into_iter()
4025        .map(|old| {
4026            let fact = &facts[old];
4027            let range = mapped_source_range(fact.start, fact.end, decoded, &line_starts);
4028            EmbeddedLeafFact {
4029                kind: fact.kind,
4030                range,
4031                parent: fact.parent.map(|parent| remap[parent]),
4032                construct: fact.construct.clone(),
4033                occurrence_role: None,
4034            }
4035        })
4036        .collect()
4037}
4038
4039fn mapped_source_range(
4040    start: usize,
4041    end: usize,
4042    decoded: &DecodedValue,
4043    line_starts: &[usize],
4044) -> Range {
4045    assert!(
4046        start <= end && end <= decoded.chars.len(),
4047        "regex parser ranges stay within the decoded pattern"
4048    );
4049    let (start_byte, end_byte) = if start < end {
4050        (
4051            decoded.chars[start].source_start,
4052            decoded.chars[end - 1].source_end,
4053        )
4054    } else {
4055        // An empty regex sequence (for example the body of `()`) still has a
4056        // precise source boundary. Mapping it to the whole Python literal
4057        // makes that child extend outside its enclosing group.
4058        let boundary = decoded.chars.get(start).map_or_else(
4059            || {
4060                decoded
4061                    .chars
4062                    .last()
4063                    .map_or(decoded.source_range.start_byte, |last| last.source_end)
4064            },
4065            |next| next.source_start,
4066        );
4067        (boundary, boundary)
4068    };
4069    Range {
4070        start_byte,
4071        end_byte,
4072        start_line: find_line_index_for_offset(line_starts, start_byte) + 1,
4073        end_line: find_line_index_for_offset(
4074            line_starts,
4075            end_byte.saturating_sub(1).max(start_byte),
4076        ) + 1,
4077    }
4078}
4079
4080#[cfg(test)]
4081mod tests {
4082    use super::*;
4083    use brokk_bifrost_test_macros::hardwon;
4084
4085    #[test]
4086    fn public_string_value_decoder_decodes_escaped_format_braces() {
4087        let source = r#"value = "\x7b0\x7d""#;
4088        let mut parser = tree_sitter::Parser::new();
4089        parser
4090            .set_language(&tree_sitter_python::LANGUAGE.into())
4091            .expect("Python grammar is valid");
4092        let tree = parser.parse(source, None).expect("parser returns a tree");
4093        let mut pending = vec![tree.root_node()];
4094        let mut string = None;
4095        while let Some(node) = pending.pop() {
4096            if node.kind() == "string" {
4097                string = Some(node);
4098                break;
4099            }
4100            let mut cursor = node.walk();
4101            pending.extend(node.named_children(&mut cursor));
4102        }
4103        let string = string.expect("fixture contains a string expression");
4104        let mut cursor = string.walk();
4105        let parts = string
4106            .named_children(&mut cursor)
4107            .map(|child| (child.kind().to_owned(), node_text(child, source).to_owned()))
4108            .collect::<Vec<_>>();
4109        assert_eq!(
4110            decode_python_string_value(string, source).expect("string literal decodes"),
4111            "{0}",
4112            "parts={parts:?}; string={}",
4113            string.to_sexp()
4114        );
4115    }
4116
4117    fn embedded_for_source(source: &str) -> Vec<EmbeddedLeafFact> {
4118        let mut parser = tree_sitter::Parser::new();
4119        parser
4120            .set_language(&tree_sitter_python::LANGUAGE.into())
4121            .expect("Python grammar is valid");
4122        let tree = parser.parse(source, None).expect("parser returns a tree");
4123        let root = tree.root_node();
4124        let mut pending = vec![root];
4125        let mut facts = module_assigned_facts(root, source);
4126        while let Some(node) = pending.pop() {
4127            if matches!(node.kind(), "call" | "string" | "concatenated_string") {
4128                facts.extend(embedded_facts(node, source));
4129            }
4130            let mut children = Vec::new();
4131            let mut cursor = node.walk();
4132            for child in node.named_children(&mut cursor) {
4133                children.push(child);
4134            }
4135            pending.extend(children.into_iter().rev());
4136        }
4137        facts
4138    }
4139
4140    #[hardwon(
4141        "#4874: bounded repetition cloned a fragment after concatenation linked its end outside"
4142    )]
4143    #[test]
4144    fn meshroom_assigned_regex_and_reduced_form_compile_without_incomplete_facts() {
4145        let patterns = [
4146            r"(?P<FILESTEM_PREFIX>.*?)(?P<FRAMEID_STR>[-._]\d+)?(?P<EXTENSION>\.\w{3,4})",
4147            r"(?P<run>a{3})",
4148        ];
4149
4150        for pattern in patterns {
4151            let source = format!("import re\npattern = r\"{pattern}\"\nre.compile(pattern)\n");
4152            let facts = embedded_for_source(&source);
4153            assert!(
4154                facts
4155                    .iter()
4156                    .any(|fact| fact.kind == NormalizedKind::RegexPattern),
4157                "pattern={pattern:?}; facts={facts:#?}"
4158            );
4159            assert!(
4160                facts
4161                    .iter()
4162                    .all(|fact| fact.kind != NormalizedKind::RegexIncomplete),
4163                "pattern={pattern:?}; facts={facts:#?}"
4164            );
4165        }
4166
4167        let unsupported = embedded_for_source(
4168            "import re\npattern = r\"(?P<anchored>^a)\"\nre.compile(pattern)\n",
4169        );
4170        assert!(
4171            unsupported.iter().any(|fact| {
4172                fact.kind == NormalizedKind::RegexIncomplete
4173                    && fact.construct.as_deref() == Some("bad-tag-nfa-unsupported")
4174            }),
4175            "unsupported NFA construct must be explicit; facts={unsupported:#?}"
4176        );
4177    }
4178
4179    #[hardwon(
4180        "#4650: Python regex facts missed byte patterns, nested assigned values, Unicode ranges, and ReDoS"
4181    )]
4182    #[test]
4183    fn codeql_regex_facts_cover_byte_values_assignments_and_safe_redos_controls() {
4184        let source = r####"import re
4185re.compile(br"[\b\t ]")
4186re.compile(b"[AA]")
4187re.compile(br"(P<name>[\w]+)")
4188re.compile(b" ^abc")
4189re.compile(b"(?s) ^abc")
4190re.compile(b"abc$ ")
4191re.compile(r"[A-z]")
4192re.compile(r"^[z-a]$")
4193re.compile(r"[0-93-5]")
4194re.compile(r"[0-9\d]")
4195re.compile('[\U0001D173-\U0001D17A\U000E0020-\U000E007F\U000e0001]')
4196re.compile(r"<script.*?>.*?</script>", re.I)
4197re.compile(r"<script.*?>.*?</script>", re.re.DOTALL)
4198re.compile(r"""(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|<!(--.*?--\s*)+>)""", re.I)
4199VERBOSE_REGEX = r"""
4200        \[
4201        (?P<header>[^]]+)                  # very permissive!
4202        \]
4203        """
4204re.compile(VERBOSE_REGEX, re.VERBOSE)
4205re.compile(r"(a|.)*")
4206re.compile(r"(a|aa?)*b")
4207re.compile(r"^([a-z]+)+$")
4208MY_REG = r'X(\u0061|a)*Y'
4209def matcher(name):
4210    re.match(MY_REG, name)
4211def shadowed(name, MY_REG):
4212    re.match(MY_REG, name)
4213re.compile(r"((.|[^a])*)\"")
4214re.compile(r"((a|[^a])*)\"")
4215re.compile(r"(a+)+aaaaa*a+")
4216"####;
4217        let facts = embedded_for_source(source);
4218        let has = |kind| facts.iter().any(|fact| fact.kind == kind);
4219        assert!(has(NormalizedKind::RegexRiskBackspaceEscape), "{facts:#?}");
4220        assert!(
4221            has(NormalizedKind::RegexRiskDuplicateClassCharacter),
4222            "{facts:#?}"
4223        );
4224        assert!(
4225            has(NormalizedKind::RegexRiskIncompleteSpecialGroup),
4226            "{facts:#?}"
4227        );
4228        assert!(has(NormalizedKind::RegexRiskUnmatchableCaret), "{facts:#?}");
4229        assert!(
4230            has(NormalizedKind::RegexRiskUnmatchableDollar),
4231            "{facts:#?}"
4232        );
4233        assert!(has(NormalizedKind::RegexRiskRange), "{facts:#?}");
4234        assert!(has(NormalizedKind::RegexRiskBadTag), "{facts:#?}");
4235        assert!(has(NormalizedKind::RegexRiskExponential), "{facts:#?}");
4236        let overlapping_range_start = source
4237            .find("[0-93-5]")
4238            .expect("overlapping range sample is present")
4239            + 1;
4240        assert!(
4241            facts.iter().any(|fact| {
4242                fact.kind == NormalizedKind::RegexRiskRange
4243                    && fact.range.start_byte == overlapping_range_start
4244                    && fact.range.end_byte == overlapping_range_start + 3
4245            }),
4246            "{facts:#?}"
4247        );
4248        let line_with = |needle: &str| {
4249            source
4250                .lines()
4251                .position(|line| line.contains(needle))
4252                .expect("regex fixture line is present")
4253                + 1
4254        };
4255        let risk_lines = facts
4256            .iter()
4257            .filter(|fact| fact.kind == NormalizedKind::RegexRiskExponential)
4258            .map(|fact| fact.range.start_line)
4259            .collect::<HashSet<_>>();
4260        assert!(risk_lines.contains(&line_with("^([a-z]+)+$")), "{facts:#?}");
4261        assert!(risk_lines.contains(&line_with("MY_REG =")), "{facts:#?}");
4262        assert!(risk_lines.contains(&line_with("((.|[^a])*)")), "{facts:#?}");
4263        assert!(
4264            !risk_lines.contains(&line_with("((a|[^a])*)")),
4265            "{facts:#?}"
4266        );
4267        assert!(!risk_lines.contains(&line_with("aaaaa*a+")), "{facts:#?}");
4268        let shadowed_line = source
4269            .lines()
4270            .enumerate()
4271            .filter_map(|(index, line)| {
4272                line.contains("re.match(MY_REG, name)").then_some(index + 1)
4273            })
4274            .last()
4275            .expect("shadowed pattern reference is present");
4276        assert!(
4277            facts.iter().any(|fact| {
4278                fact.kind == NormalizedKind::RegexIncomplete
4279                    && fact.range.start_line == shadowed_line
4280                    && fact
4281                        .construct
4282                        .as_deref()
4283                        .is_some_and(|reason| reason.starts_with("dynamic-pattern:"))
4284            }),
4285            "{facts:#?}"
4286        );
4287        assert!(
4288            facts.iter().any(|fact| {
4289                fact.kind == NormalizedKind::RegexRiskRange
4290                    && fact.range.start_line == line_with("U0001D173")
4291            }),
4292            "{facts:#?}"
4293        );
4294        assert!(
4295            !facts.iter().any(|fact| {
4296                fact.kind == NormalizedKind::RegexRiskDuplicateClassCharacter
4297                    && fact.range.start_line == line_with("VERBOSE_REGEX =")
4298            }),
4299            "{facts:#?}"
4300        );
4301        assert!(
4302            facts.iter().any(|fact| {
4303                fact.kind == NormalizedKind::RegexRiskBadTag
4304                    && fact.range.start_line == line_with("<[a-z")
4305            }),
4306            "{facts:#?}"
4307        );
4308        let nested_flag_line = source
4309            .lines()
4310            .position(|line| line.contains("re.re.DOTALL"))
4311            .expect("upstream nested DOTALL flag case is present")
4312            + 1;
4313        assert!(facts.iter().any(|fact| {
4314            fact.kind == NormalizedKind::RegexRiskBadTag
4315                && fact.range.start_line == nested_flag_line
4316        }));
4317        let safe_line = source
4318            .lines()
4319            .position(|line| line.contains("(a|.)*"))
4320            .expect("safe redos control is present")
4321            + 1;
4322        assert!(!facts.iter().any(|fact| {
4323            fact.kind == NormalizedKind::RegexRiskExponential && fact.range.start_line == safe_line
4324        }));
4325        assert!(facts.iter().any(|fact| {
4326            fact.kind == NormalizedKind::RegexPattern
4327                && fact
4328                    .construct
4329                    .as_deref()
4330                    .is_some_and(|pattern| pattern.contains("very permissive"))
4331        }));
4332    }
4333
4334    #[test]
4335    fn decodes_python_unknown_escapes_and_adjacent_literals_before_regex_parse() {
4336        let source = r#"import re
4337re.compile("\d" "\." r"\.")
4338"#;
4339        let facts = embedded_for_source(source);
4340        let pattern = facts
4341            .iter()
4342            .find(|fact| fact.kind == NormalizedKind::RegexPattern)
4343            .expect("selected compile call has a regex pattern fact");
4344        assert_eq!(pattern.construct.as_deref(), Some(r"\d\.\."));
4345        assert!(
4346            facts
4347                .iter()
4348                .any(|fact| fact.kind == NormalizedKind::RegexEscape)
4349        );
4350        assert!(
4351            !facts
4352                .iter()
4353                .any(|fact| fact.kind == NormalizedKind::RegexIncomplete)
4354        );
4355    }
4356
4357    #[test]
4358    fn selects_only_recognized_pattern_arguments_and_marks_dynamic_values_incomplete() {
4359        let source = r#"import re
4360re.compile("selected")
4361re.sub("replacement", "pattern", "subject")
4362other.compile("unrecognized")
4363re.compile(pattern)
4364"#;
4365        let facts = embedded_for_source(source);
4366        let patterns = facts
4367            .iter()
4368            .filter(|fact| fact.kind == NormalizedKind::RegexPattern)
4369            .map(|fact| fact.construct.as_deref().expect("pattern stores value"))
4370            .collect::<Vec<_>>();
4371        assert_eq!(patterns, ["selected", "replacement"]);
4372        assert!(facts.iter().any(|fact| {
4373            fact.kind == NormalizedKind::RegexIncomplete
4374                && fact
4375                    .construct
4376                    .as_deref()
4377                    .is_some_and(|construct| construct.starts_with("dynamic-pattern:"))
4378        }));
4379        assert!(!patterns.contains(&"pattern"));
4380        assert!(!patterns.contains(&"subject"));
4381        assert!(!patterns.contains(&"unrecognized"));
4382    }
4383
4384    #[test]
4385    fn supported_lookaround_groups_and_orphan_quantifiers_have_typed_facts() {
4386        let facts = embedded_for_source(
4387            "import re\nre.compile(r\"(?=lookahead)\")\nre.compile(r\"a$*\")\n",
4388        );
4389        assert!(facts.iter().any(|fact| {
4390            fact.kind == NormalizedKind::RegexGroup
4391                && fact.construct.as_deref() == Some("lookahead-positive")
4392        }));
4393        assert!(facts.iter().any(|fact| {
4394            fact.kind == NormalizedKind::RegexIncomplete
4395                && fact.construct.as_deref() == Some("quantifier-without-atom")
4396        }));
4397    }
4398
4399    #[hardwon(
4400        "#4454: zero-width regex facts escaped their embedded parent and aborted workspace indexing"
4401    )]
4402    #[test]
4403    fn zero_width_regex_facts_stay_within_their_embedded_parents() {
4404        for (source, malformed) in [
4405            ("import re\nre.compile(r'^(v)(.*)()$')\n", false),
4406            ("import re\nre.subn(\"()\"r' am I'\"??\")\n", false),
4407            (
4408                "import re\nre.compile(r\"invalid literal for int().*: '?'?\")\n",
4409                false,
4410            ),
4411            ("import re\nre.compile(\"ab()+cd\")\n", false),
4412            ("import re\nre.sub('()x', r'\\g<0>\\g<0>', 'xx')\n", false),
4413            ("import re\nre.search('(', 'test')\n", true),
4414        ] {
4415            let facts = embedded_for_source(source);
4416            assert!(
4417                facts
4418                    .iter()
4419                    .any(|fact| fact.kind == NormalizedKind::RegexPattern),
4420                "source={source:?}; facts={facts:#?}"
4421            );
4422            for (fact_id, fact) in facts.iter().enumerate() {
4423                if let Some(parent) = fact.parent {
4424                    assert!(parent < fact_id, "source={source:?}; facts={facts:#?}");
4425                    let parent_fact = &facts[parent];
4426                    assert!(
4427                        parent_fact.range.start_byte <= fact.range.start_byte
4428                            && fact.range.end_byte <= parent_fact.range.end_byte,
4429                        "source={source:?}; fact={fact:#?}; parent={parent_fact:#?}"
4430                    );
4431                }
4432            }
4433            if !malformed {
4434                assert!(
4435                    facts.iter().any(|fact| {
4436                        fact.kind == NormalizedKind::RegexSequence
4437                            && fact.range.start_byte == fact.range.end_byte
4438                    }),
4439                    "source={source:?} retains its zero-width empty-group sequence; facts={facts:#?}"
4440                );
4441            }
4442            if malformed {
4443                assert!(
4444                    facts
4445                        .iter()
4446                        .any(|fact| fact.kind == NormalizedKind::RegexIncomplete),
4447                    "malformed pattern remains structurally unavailable; facts={facts:#?}"
4448                );
4449            }
4450        }
4451    }
4452
4453    #[test]
4454    fn polynomial_redos_facts_cover_search_restarts_and_overlapping_repetitions() {
4455        for (source, expected) in [
4456            ("import re\nre.search(r'\\s+$', value)\n", true),
4457            ("import re\nre.search(r'^\\s+$', value)\n", false),
4458            ("import re\nre.search(r'^\\d+E?\\d+$', value)\n", true),
4459            ("import re\nre.search(r'^\\d+E\\d+$', value)\n", false),
4460            ("import re\nre.match(r'\\s+$', value)\n", false),
4461            ("import re\nre.compile(r'\\s+$')\n", false),
4462        ] {
4463            let facts = embedded_for_source(source);
4464            assert_eq!(
4465                facts
4466                    .iter()
4467                    .any(|fact| fact.kind == NormalizedKind::RegexRiskPolynomial),
4468                expected,
4469                "source={source:?}; facts={facts:#?}"
4470            );
4471        }
4472    }
4473
4474    #[test]
4475    #[hardwon("#4443: an empty embedded regex sequence escaped its parent source range")]
4476    fn unquantified_empty_group_keeps_embedded_regex_facts_inside_parents() {
4477        let source = "import re\nre.search(r'function (no)?watchplus()', webpage)\n";
4478        let facts = embedded_for_source(source);
4479        assert!(
4480            facts
4481                .iter()
4482                .any(|fact| fact.kind == NormalizedKind::RegexPattern)
4483        );
4484        for (index, fact) in facts.iter().enumerate() {
4485            if let Some(parent) = fact.parent {
4486                assert!(parent < index, "{fact:?}");
4487                let parent_fact = &facts[parent];
4488                assert!(
4489                    parent_fact.range.start_byte <= fact.range.start_byte
4490                        && fact.range.end_byte <= parent_fact.range.end_byte,
4491                    "child {fact:?} is outside parent {parent_fact:?}"
4492                );
4493            }
4494        }
4495        let closing_paren = source.find("()'").expect("trailing empty group") + 1;
4496        assert!(facts.iter().any(|fact| {
4497            fact.kind == NormalizedKind::RegexSequence
4498                && fact.range.start_byte == closing_paren
4499                && fact.range.end_byte == closing_paren
4500        }));
4501    }
4502    #[test]
4503    fn qs6_regex_decoder_and_fact_budgets_keep_ranges_and_parent_links() {
4504        for (pattern, reason, fact_count) in [
4505            (
4506                "f'{dynamic}'".to_owned(),
4507                "formatted-string-interpolation",
4508                1,
4509            ),
4510            (
4511                format!("r'{}'", "a".repeat(8193)),
4512                "pattern-character-budget",
4513                1,
4514            ),
4515            (
4516                format!("r'{}'", "a|".repeat(2200) + "a"),
4517                "regex-fact-budget",
4518                3,
4519            ),
4520        ] {
4521            let prefix = "# \u{e9}\nimport re\nre.compile(";
4522            let source = format!("{prefix}{pattern})\n");
4523            let facts = embedded_for_source(&source);
4524            assert_eq!(facts.len(), fact_count, "reason={reason}; facts={facts:?}");
4525            let incomplete = facts
4526                .iter()
4527                .find(|fact| fact.kind == NormalizedKind::RegexIncomplete)
4528                .expect("limit explicitly reported");
4529            assert_eq!(
4530                incomplete.construct.as_deref(),
4531                Some(reason),
4532                "{incomplete:?}"
4533            );
4534            let expected = if fact_count == 1 {
4535                prefix.len()..prefix.len() + pattern.len()
4536            } else {
4537                prefix.len() + 2..prefix.len() + pattern.len() - 1
4538            };
4539            assert_eq!(
4540                incomplete.range.start_byte..incomplete.range.end_byte,
4541                expected
4542            );
4543            for (index, fact) in facts.iter().enumerate() {
4544                assert!(source.is_char_boundary(fact.range.start_byte));
4545                assert!(source.is_char_boundary(fact.range.end_byte));
4546                if let Some(parent) = fact.parent {
4547                    assert!(parent < index, "{facts:?}");
4548                    assert!(
4549                        facts[parent].range.start_byte <= fact.range.start_byte
4550                            && fact.range.end_byte <= facts[parent].range.end_byte,
4551                        "{facts:?}"
4552                    );
4553                }
4554            }
4555        }
4556        assert!(
4557            embedded_for_source("import re\nre.compile(r'ab')\n")
4558                .iter()
4559                .any(|f| f.kind == NormalizedKind::RegexPattern)
4560        );
4561    }
4562
4563    #[test]
4564    fn qs6_integer_flags_have_radix_independent_semantics() {
4565        for (mask, complete) in [
4566            (0_u64, true),
4567            (2, true),
4568            (8, true),
4569            (16, true),
4570            (256, true),
4571            (282, true),
4572            (1, false),
4573            (64, false),
4574        ] {
4575            let mut reference = None;
4576            for digits in [
4577                mask.to_string(),
4578                format!("0x{mask:x}"),
4579                format!("0o{mask:o}"),
4580                format!("0b{mask:b}"),
4581            ] {
4582                // Python permits separators between digits in every radix.
4583                let (prefix, digits) = if digits.starts_with('0') && digits.len() > 1 {
4584                    digits.split_at(2)
4585                } else {
4586                    ("", digits.as_str())
4587                };
4588                let separated = digits
4589                    .chars()
4590                    .map(|ch| ch.to_string())
4591                    .collect::<Vec<_>>()
4592                    .join("_");
4593                let source = format!("import re\nre.compile(r'^a.\\w$', {prefix}{separated})\n");
4594                let tree = crate::declarations::parse_python_tree(&source).unwrap();
4595                let call = tree
4596                    .root_node()
4597                    .named_child(1)
4598                    .unwrap()
4599                    .named_child(0)
4600                    .unwrap();
4601                assert_eq!(call.kind(), "call");
4602                let integer = call
4603                    .child_by_field_name("arguments")
4604                    .unwrap()
4605                    .named_child(1)
4606                    .unwrap();
4607                assert_eq!(integer.kind(), "integer");
4608                assert_eq!(node_text(integer, &source), format!("{prefix}{separated}"));
4609                let api =
4610                    recognized_api(call.child_by_field_name("function").unwrap(), &source).unwrap();
4611                let flags = call_flags(call, api, &source);
4612                let expected = match mask {
4613                    2 => (true, false, false, false),
4614                    8 => (false, true, false, false),
4615                    16 => (false, false, true, false),
4616                    256 => (false, false, false, true),
4617                    282 => (true, true, true, true),
4618                    _ => (false, false, false, false),
4619                };
4620                assert_eq!(
4621                    (flags.ignorecase, flags.multiline, flags.dotall, flags.ascii),
4622                    expected
4623                );
4624                let facts = embedded_for_source(&source);
4625                assert!(
4626                    facts
4627                        .iter()
4628                        .any(|fact| fact.kind == NormalizedKind::RegexPattern)
4629                );
4630                assert_eq!(
4631                    !facts
4632                        .iter()
4633                        .any(|fact| fact.construct.as_deref() == Some("unsupported-regex-flags")),
4634                    complete,
4635                    "mask={mask}: {facts:?}"
4636                );
4637                let semantic = facts
4638                    .iter()
4639                    .map(|fact| (fact.kind, fact.parent, fact.construct.clone()))
4640                    .collect::<Vec<_>>();
4641                if let Some(reference) = &reference {
4642                    assert_eq!(&semantic, reference);
4643                } else {
4644                    reference = Some(semantic);
4645                }
4646            }
4647        }
4648        for overflow in [
4649            "18446744073709551616",
4650            "0x1_0000_0000_0000_0000",
4651            "0o2_0000_0000_0000_0000_0000_00",
4652            "0b1_0000000000000000000000000000000000000000000000000000000000000000",
4653        ] {
4654            let source = format!("import re\nre.compile('a', {overflow})\n");
4655            assert!(
4656                embedded_for_source(&source)
4657                    .iter()
4658                    .any(|fact| fact.construct.as_deref() == Some("unsupported-regex-flags")),
4659                "{overflow}"
4660            );
4661        }
4662    }
4663    #[test]
4664    #[hardwon(
4665        "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: ignored comments produced duplicate-class warnings"
4666    )]
4667    fn qs6_verbose_comments_are_ignored() {
4668        let facts = embedded_for_source("import re\nre.compile('[a] # xx', re.X)\n");
4669        assert!(
4670            facts
4671                .iter()
4672                .any(|f| f.kind == NormalizedKind::RegexCharacterClass)
4673        );
4674        assert!(
4675            !facts
4676                .iter()
4677                .any(|f| f.kind == NormalizedKind::RegexRiskDuplicateClassCharacter),
4678            "{facts:?}"
4679        );
4680    }
4681
4682    #[test]
4683    #[hardwon(
4684        "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: group widths lost quantifiers and comment emptiness"
4685    )]
4686    fn qs6_group_width_anchor_regression() {
4687        for (pattern, risk) in [
4688            ("(a)*^", false),
4689            ("(a)?^", false),
4690            ("(a){0,3}^", false),
4691            ("(a)+^", true),
4692            ("(?#ignored)a^", true),
4693        ] {
4694            let facts = embedded_for_source(&format!("import re\nre.compile(r'{pattern}')\n"));
4695            assert_eq!(
4696                facts
4697                    .iter()
4698                    .any(|f| f.kind == NormalizedKind::RegexRiskUnmatchableCaret),
4699                risk,
4700                "{pattern}: {facts:?}"
4701            );
4702        }
4703    }
4704
4705    #[test]
4706    #[hardwon(
4707        "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: multiple repetition operators were accepted"
4708    )]
4709    fn qs6_multiple_repeat_is_incomplete() {
4710        for pattern in ["a**", "a*+*", "(ab){2}{3}"] {
4711            let facts = embedded_for_source(&format!("import re\nre.compile(r'{pattern}')\n"));
4712            assert!(
4713                facts
4714                    .iter()
4715                    .any(|f| f.construct.as_deref() == Some("nested-quantifier")),
4716                "{pattern}: {facts:?}"
4717            );
4718        }
4719    }
4720
4721    #[test]
4722    #[hardwon(
4723        "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: optional clones lost HTML capture evidence"
4724    )]
4725    fn qs6_optional_captures_retain_bad_tag_evidence() {
4726        let facts = embedded_for_source("import re\nre.compile(r'(?:(<!--.*?--)|(<foo))?>')\n");
4727        assert!(
4728            facts
4729                .iter()
4730                .any(|f| f.kind == NormalizedKind::RegexRiskBadTag),
4731            "{facts:?}"
4732        );
4733    }
4734
4735    fn qs6_parse(pattern: &str) -> Vec<Fact> {
4736        let decoded = DecodedValue {
4737            value: pattern.to_owned(),
4738            chars: Vec::new(),
4739            source_range: Range {
4740                start_byte: 0,
4741                end_byte: pattern.len(),
4742                start_line: 1,
4743                end_line: 1,
4744            },
4745            is_bytes: false,
4746        };
4747        RegexParser::new(
4748            &decoded,
4749            RegexFlags {
4750                dotall: false,
4751                multiline: false,
4752                ignorecase: false,
4753                ascii: false,
4754                verbose: false,
4755                complete: true,
4756            },
4757            Api {
4758                flags_position: 1,
4759                scans_from_each_offset: false,
4760            },
4761        )
4762        .parse()
4763    }
4764
4765    // CPython re.match oracle, generated over every word through length three
4766    // on {a, b, A, newline}, plus repeat-limit boundary witnesses.
4767    #[test]
4768    fn qs6_cpython_prefix_language_corpus() {
4769        let samples = [
4770            "",
4771            "a",
4772            "b",
4773            "A",
4774            "\n",
4775            "aa",
4776            "ab",
4777            "aA",
4778            "a\n",
4779            "ba",
4780            "bb",
4781            "bA",
4782            "b\n",
4783            "Aa",
4784            "Ab",
4785            "AA",
4786            "A\n",
4787            "\na",
4788            "\nb",
4789            "\nA",
4790            "\n\n",
4791            "aaa",
4792            "aab",
4793            "aaA",
4794            "aa\n",
4795            "aba",
4796            "abb",
4797            "abA",
4798            "ab\n",
4799            "aAa",
4800            "aAb",
4801            "aAA",
4802            "aA\n",
4803            "a\na",
4804            "a\nb",
4805            "a\nA",
4806            "a\n\n",
4807            "baa",
4808            "bab",
4809            "baA",
4810            "ba\n",
4811            "bba",
4812            "bbb",
4813            "bbA",
4814            "bb\n",
4815            "bAa",
4816            "bAb",
4817            "bAA",
4818            "bA\n",
4819            "b\na",
4820            "b\nb",
4821            "b\nA",
4822            "b\n\n",
4823            "Aaa",
4824            "Aab",
4825            "AaA",
4826            "Aa\n",
4827            "Aba",
4828            "Abb",
4829            "AbA",
4830            "Ab\n",
4831            "AAa",
4832            "AAb",
4833            "AAA",
4834            "AA\n",
4835            "A\na",
4836            "A\nb",
4837            "A\nA",
4838            "A\n\n",
4839            "\naa",
4840            "\nab",
4841            "\naA",
4842            "\na\n",
4843            "\nba",
4844            "\nbb",
4845            "\nbA",
4846            "\nb\n",
4847            "\nAa",
4848            "\nAb",
4849            "\nAA",
4850            "\nA\n",
4851            "\n\na",
4852            "\n\nb",
4853            "\n\nA",
4854            "\n\n\n",
4855            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
4856            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
4857            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
4858        ];
4859        for (pattern, expected) in [
4860            (
4861                "a",
4862                "0100011110000000000001111111111111111000000000000000000000000000000000000000000000000111",
4863            ),
4864            (
4865                "b",
4866                "0010000001111000000000000000000000000111111111111111100000000000000000000000000000000000",
4867            ),
4868            (
4869                "[a]",
4870                "0100011110000000000001111111111111111000000000000000000000000000000000000000000000000111",
4871            ),
4872            (
4873                "[a-a]",
4874                "0100011110000000000001111111111111111000000000000000000000000000000000000000000000000111",
4875            ),
4876            (
4877                "[a-b]",
4878                "0110011111111000000001111111111111111111111111111111100000000000000000000000000000000111",
4879            ),
4880            (
4881                "[ab]",
4882                "0110011111111000000001111111111111111111111111111111100000000000000000000000000000000111",
4883            ),
4884            (
4885                "a?",
4886                "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
4887            ),
4888            (
4889                "ab?",
4890                "0100011110000000000001111111111111111000000000000000000000000000000000000000000000000111",
4891            ),
4892            (
4893                "a{0}",
4894                "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
4895            ),
4896            (
4897                "a{1}",
4898                "0100011110000000000001111111111111111000000000000000000000000000000000000000000000000111",
4899            ),
4900            (
4901                "a{3}",
4902                "0000000000000000000001000000000000000000000000000000000000000000000000000000000000000111",
4903            ),
4904            (
4905                "a{3,3}",
4906                "0000000000000000000001000000000000000000000000000000000000000000000000000000000000000111",
4907            ),
4908            (
4909                "a{2,3}",
4910                "0000010000000000000001111000000000000000000000000000000000000000000000000000000000000111",
4911            ),
4912            (
4913                "a{32}",
4914                "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000011",
4915            ),
4916            (
4917                ".",
4918                "0111011111111111100001111111111111111111111111111111111111111111111110000000000000000111",
4919            ),
4920            (
4921                "(?s:.)",
4922                "0111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
4923            ),
4924            (
4925                "(?ai:a)",
4926                "0101011110000111100001111111111111111000000000000000011111111111111110000000000000000111",
4927            ),
4928            (
4929                "(?ai:A)",
4930                "0101011110000111100001111111111111111000000000000000011111111111111110000000000000000111",
4931            ),
4932            (
4933                "(?ai:[a-z])",
4934                "0111011111111111100001111111111111111111111111111111111111111111111110000000000000000111",
4935            ),
4936            (
4937                "(?ai:[A-Z])",
4938                "0111011111111111100001111111111111111111111111111111111111111111111110000000000000000111",
4939            ),
4940            (
4941                "(a|aa)",
4942                "0100011110000000000001111111111111111000000000000000000000000000000000000000000000000111",
4943            ),
4944            (
4945                "(?:ab|b)*",
4946                "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
4947            ),
4948            (
4949                "(?:a|b)+",
4950                "0110011111111000000001111111111111111111111111111111100000000000000000000000000000000111",
4951            ),
4952            (
4953                "a{0,2}b",
4954                "0010001001111000000000100111100000000111111111111111100000000000000000000000000000000000",
4955            ),
4956        ] {
4957            let facts = qs6_parse(pattern);
4958            assert!(
4959                !facts
4960                    .iter()
4961                    .any(|f| f.kind == NormalizedKind::RegexIncomplete),
4962                "{pattern}: {facts:?}"
4963            );
4964            let nfa = RegexNfa::compile(&facts, 0, None).unwrap();
4965            for (sample, expected) in samples.iter().zip(expected.bytes()) {
4966                assert_eq!(
4967                    nfa.match_prefix_capture_groups(sample).0,
4968                    expected == b'1',
4969                    "{pattern:?} on {sample:?}"
4970                );
4971            }
4972        }
4973    }
4974
4975    #[test]
4976    fn qs6_finite_language_relations() {
4977        let patterns = ["a?", "b?", "a", "aa", "aaa", "ab", "ba", "(?:a|aa)"];
4978        let nfas = patterns.map(|p| RegexNfa::compile(&qs6_parse(p), 0, None).unwrap());
4979        // These finite languages are independently enumerated by CPython fullmatch.
4980        let languages: [&[&str]; 8] = [
4981            &["", "a"],
4982            &["", "b"],
4983            &["a"],
4984            &["aa"],
4985            &["aaa"],
4986            &["ab"],
4987            &["ba"],
4988            &["a", "aa"],
4989        ];
4990        for (i, left) in nfas.iter().enumerate() {
4991            for (j, right) in nfas.iter().enumerate() {
4992                let expected = languages[i]
4993                    .iter()
4994                    .any(|w| !w.is_empty() && languages[j].contains(w));
4995                assert_eq!(
4996                    left.intersects_on_nonempty_string(right, &mut 65536)
4997                        .unwrap(),
4998                    expected,
4999                    "{i}, {j}"
5000                );
5001                for (k, pattern) in patterns.iter().enumerate() {
5002                    let repeated = RegexNfa::compile(&qs6_parse(pattern), 0, None)
5003                        .unwrap()
5004                        .positive_closure()
5005                        .unwrap();
5006                    let expected = languages[j].iter().any(|w| {
5007                        (1..w.len()).any(|split| {
5008                            if !languages[i].contains(&&w[..split]) {
5009                                return false;
5010                            }
5011                            // Independent word-break oracle for a positive closure.
5012                            let suffix = &w[split..];
5013                            let mut reachable = vec![false; suffix.len() + 1];
5014                            reachable[0] = true;
5015                            for offset in 0..suffix.len() {
5016                                if reachable[offset] {
5017                                    for part in languages[k].iter().filter(|w| !w.is_empty()) {
5018                                        if suffix[offset..].starts_with(part) {
5019                                            reachable[offset + part.len()] = true;
5020                                        }
5021                                    }
5022                                }
5023                            }
5024                            reachable[suffix.len()]
5025                        })
5026                    });
5027                    assert_eq!(
5028                        left.has_repartitioned_prefix(right, &repeated, &mut 65536)
5029                            .unwrap(),
5030                        expected,
5031                        "{i}, {j}, {k}"
5032                    );
5033                }
5034            }
5035        }
5036    }
5037
5038    #[test]
5039    fn qs6_nfa_fork_and_resource_boundaries() {
5040        // Two one-character paths either reconverge on the same letter or
5041        // cannot synchronize. Starting at the fork itself also reaches it.
5042        for right in ['a', 'b'] {
5043            let nfa = RegexNfa {
5044                start: 0,
5045                end: 2,
5046                states: vec![
5047                    NfaState {
5048                        transitions: vec![(CharacterSet::one('a'), 2)],
5049                        ..Default::default()
5050                    },
5051                    NfaState {
5052                        transitions: vec![(CharacterSet::one(right), 2)],
5053                        ..Default::default()
5054                    },
5055                    NfaState::default(),
5056                ],
5057            };
5058            let closures = nfa.epsilon_closures();
5059            assert_eq!(
5060                nfa.product_reaches_fork((0, 1), 2, &closures, &mut 65536)
5061                    .unwrap(),
5062                right == 'a'
5063            );
5064            assert!(
5065                nfa.product_reaches_fork((2, 2), 2, &closures, &mut 65536)
5066                    .unwrap()
5067            );
5068        }
5069        for count in [31, 32, 33] {
5070            let facts = qs6_parse(&format!("a{{{count}}}"));
5071            assert_eq!(RegexNfa::compile(&facts, 0, None).is_ok(), count <= 32);
5072        }
5073        // A linear 256-character language uses exactly the 512-state budget.
5074        // Redundant alternation wrappers must not consume its remaining states.
5075        for length in [255, 256, 257] {
5076            let facts = qs6_parse(&"a".repeat(length));
5077            assert_eq!(RegexNfa::compile(&facts, 0, None).is_ok(), length <= 256);
5078        }
5079        let mut builder = NfaBuilder {
5080            facts: &[],
5081            children: Vec::new(),
5082            selected_branch: None,
5083            capture_ids: &[],
5084            states: Vec::new(),
5085        };
5086        for _ in 0..512 {
5087            builder.new_state().unwrap();
5088        }
5089        assert!(matches!(builder.new_state(), Err(NfaError::Budget)));
5090        for depth in [126, 127, 128] {
5091            let facts = qs6_parse(&format!("{}a{}", "(".repeat(depth), ")".repeat(depth)));
5092            assert_eq!(
5093                facts
5094                    .iter()
5095                    .any(|f| f.kind == NormalizedKind::RegexIncomplete),
5096                depth >= 128
5097            );
5098        }
5099    }
5100
5101    #[test]
5102    fn qs6_literal_grammar_and_mapping_corpus() {
5103        // CPython ast.literal_eval values, preserving each grammar-selected
5104        // literal's source mapping rather than searching the source for syntax.
5105        for (literal, expected) in [
5106            (r"'\u0061'", "a"),
5107            (r"'\U00000061'", "a"),
5108            (r"b'\x61'", "a"),
5109            (r"b'\n'", "\n"),
5110            (r"r'\u0061'", r"\u0061"),
5111        ] {
5112            let tree = crate::declarations::parse_python_tree(literal).unwrap();
5113            let node = tree
5114                .root_node()
5115                .named_child(0)
5116                .unwrap()
5117                .named_child(0)
5118                .unwrap();
5119            assert_eq!(node.kind(), "string");
5120            let decoded = decode_string_literal(node, literal).unwrap();
5121            assert_eq!(decoded.value, expected, "{literal}");
5122        }
5123        for count in [8191, 8192, 8193] {
5124            let source = format!("rf'{}{{value}}'", "a".repeat(count));
5125            let tree = crate::declarations::parse_python_tree(&source).unwrap();
5126            let node = tree
5127                .root_node()
5128                .named_child(0)
5129                .unwrap()
5130                .named_child(0)
5131                .unwrap();
5132            let result = decode_python_fstring_literal_segments(node, &source);
5133            if count <= 8192 {
5134                assert_eq!(result.unwrap(), ["a".repeat(count), String::new()]);
5135            } else {
5136                assert_eq!(result, Err("pattern-character-budget"));
5137            }
5138        }
5139        for source in [
5140            "import re\nre.compile(r'''a\nb''')\n",
5141            "import re\nre.compile('a' '\\n' 'b')\n",
5142        ] {
5143            let facts = embedded_for_source(source);
5144            assert!(facts.iter().any(|f| f.kind == NormalizedKind::RegexLiteral));
5145            for fact in facts {
5146                let range = fact.range;
5147                assert_eq!(
5148                    range.start_line,
5149                    source[..range.start_byte]
5150                        .bytes()
5151                        .filter(|b| *b == b'\n')
5152                        .count()
5153                        + 1
5154                );
5155                assert_eq!(
5156                    range.end_line,
5157                    source[..range.end_byte.saturating_sub(1).max(range.start_byte)]
5158                        .bytes()
5159                        .filter(|b| *b == b'\n')
5160                        .count()
5161                        + 1
5162                );
5163            }
5164        }
5165    }
5166    #[test]
5167    fn qs6_anchor_and_redos_fact_corpus() {
5168        // Safe/unsafe controls follow zero-width assertion semantics and
5169        // whether a rejecting suffix forces enumeration of ambiguous partitions.
5170        for (pattern, flags, caret, dollar, exponential) in [
5171            ("^^a", "0", false, false, false),
5172            ("a$$", "0", false, false, false),
5173            ("(?=a)^a", "0", false, false, false),
5174            ("a^", "0", true, false, false),
5175            ("(|a)^", "0", false, false, false),
5176            ("(a|)^", "0", false, false, false),
5177            ("a$z", "0", false, true, false),
5178            (r"a\n^b", "re.M", false, false, false),
5179            ("([ab]+)+[a-b]+", "0", false, false, false),
5180            ("([a-b]+)+[a-b]+", "0", false, false, false),
5181            ("(a|aa)*(?=b)", "0", false, false, true),
5182            ("(a|aa)*(?=bb)", "0", false, false, true),
5183            ("(a|aa)*", "0", false, false, false),
5184        ] {
5185            let facts =
5186                embedded_for_source(&format!("import re\nre.compile(r'{pattern}', {flags})\n"));
5187            let has = |kind| facts.iter().any(|f| f.kind == kind);
5188            assert!(has(NormalizedKind::RegexPattern));
5189            assert!(
5190                !has(NormalizedKind::RegexIncomplete),
5191                "{pattern}: {facts:?}"
5192            );
5193            assert_eq!(
5194                (
5195                    has(NormalizedKind::RegexRiskUnmatchableCaret),
5196                    has(NormalizedKind::RegexRiskUnmatchableDollar),
5197                    has(NormalizedKind::RegexRiskExponential)
5198                ),
5199                (caret, dollar, exponential),
5200                "{pattern}: {facts:?}"
5201            );
5202        }
5203        for pattern in ["(ab)", "(^ab)", "(?:ab)"] {
5204            let facts = qs6_parse(pattern);
5205            let group = facts
5206                .iter()
5207                .find(|f| f.kind == NormalizedKind::RegexGroup)
5208                .unwrap();
5209            let first = group.first_set.as_ref().unwrap();
5210            assert!(first.contains('a'));
5211            assert!(!first.contains('b'), "{pattern}: {facts:?}");
5212        }
5213        for (pattern, broad) in [
5214            ("[:-B]", false),
5215            ("[:-C]", true),
5216            ("[:-D]", true),
5217            ("[A-Z]", false),
5218            ("[0-?]", false),
5219        ] {
5220            let facts = embedded_for_source(&format!("import re\nre.compile(r'{pattern}')\n"));
5221            assert_eq!(
5222                facts
5223                    .iter()
5224                    .any(|f| f.kind == NormalizedKind::RegexRiskRange),
5225                broad,
5226                "{pattern}: {facts:?}"
5227            );
5228        }
5229    }
5230
5231    #[test]
5232    fn qs6_selected_arguments_and_invalid_token_spans() {
5233        let source = "import re\nre.compile(pattern='a')\nre.search(string='subject', pattern='b')\nre.compile(other='unselected')\nre.sub('c', 'replacement', 'subject')\n";
5234        let facts = embedded_for_source(source);
5235        assert_eq!(
5236            facts
5237                .iter()
5238                .filter(|f| f.kind == NormalizedKind::RegexPattern)
5239                .map(|f| f.construct.as_deref().unwrap())
5240                .collect::<Vec<_>>(),
5241            ["a", "b", "c"]
5242        );
5243        for pattern in ["a|*", "a|+", "a|?", "(?P=name)", "a(?P=name)", "(abc"] {
5244            let prefix = "import re\nre.compile(r'";
5245            let source = format!("{prefix}{pattern}')\n");
5246            let facts = embedded_for_source(&source);
5247            let incomplete = facts
5248                .iter()
5249                .find(|f| f.kind == NormalizedKind::RegexIncomplete)
5250                .unwrap();
5251            let expected_start = if pattern.starts_with("a|") {
5252                2
5253            } else if pattern == "a(?P=name)" {
5254                1
5255            } else {
5256                0
5257            };
5258            let expected_end = if pattern.starts_with("a|") || pattern == "(?P=name)" {
5259                3
5260            } else if pattern == "a(?P=name)" {
5261                4
5262            } else {
5263                pattern.len()
5264            };
5265            assert_eq!(
5266                incomplete.range.start_byte..incomplete.range.end_byte,
5267                prefix.len() + expected_start..prefix.len() + expected_end,
5268                "{pattern}: {facts:?}"
5269            );
5270            // The public fact stream traverses siblings in lexical source order.
5271            for parent in 0..facts.len() {
5272                let children = facts
5273                    .iter()
5274                    .filter(|f| f.parent == Some(parent))
5275                    .map(|f| f.range.start_byte)
5276                    .collect::<Vec<_>>();
5277                assert!(
5278                    children.windows(2).all(|w| w[0] <= w[1]),
5279                    "{pattern}: {facts:?}"
5280                );
5281            }
5282        }
5283    }
5284
5285    #[test]
5286    fn qs6_pending_fact_budget_reserves_incomplete_slot() {
5287        // Every repeated class member queues a diagnostic. Exercise the point
5288        // where parsed facts plus pending diagnostics exhaust the shared cap.
5289        for members in [2044, 2045, 2046, 2047, 2048] {
5290            let facts = qs6_parse(&format!("[{}]", "a".repeat(members)));
5291            assert!(facts.len() <= 4096, "members={members}: {}", facts.len());
5292            if members >= 2047 {
5293                assert!(
5294                    facts
5295                        .iter()
5296                        .any(|f| f.construct.as_deref() == Some("regex-fact-budget")),
5297                    "members={members}: {}",
5298                    facts.len()
5299                );
5300            }
5301        }
5302    }
5303
5304    #[test]
5305    #[hardwon(
5306        "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: scoped flags could not start by disabling a flag"
5307    )]
5308    fn qs6_scoped_flags_can_disable_without_enabling() {
5309        for pattern in ["(?i:a)(?-i:b)", "(?-i:ab)"] {
5310            let facts = qs6_parse(pattern);
5311            assert!(
5312                !facts
5313                    .iter()
5314                    .any(|f| f.kind == NormalizedKind::RegexIncomplete),
5315                "{facts:?}"
5316            );
5317            let nfa = RegexNfa::compile(&facts, 0, None).unwrap();
5318            assert!(nfa.match_prefix_capture_groups("ab").0);
5319            assert!(!nfa.match_prefix_capture_groups("aB").0);
5320        }
5321        for pattern in ["(?-a:a)", "(?-u:a)", "(?-i)", "(?i-:a)"] {
5322            let facts = qs6_parse(pattern);
5323            assert!(
5324                facts
5325                    .iter()
5326                    .any(|f| f.construct.as_deref() == Some("invalid-inline-flags")),
5327                "{pattern}: {facts:?}"
5328            );
5329        }
5330    }
5331}