1use 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 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 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 for target in old_state.epsilon {
812 let Some(mapped) = remap
813 .get(target)
814 .copied()
815 .filter(|&mapped| mapped != usize::MAX)
816 else {
817 return Err(NfaError::Unsupported);
818 };
819 self.states[new].epsilon.push(mapped);
820 }
821 for (set, target) in old_state.transitions {
822 let Some(mapped) = remap
823 .get(target)
824 .copied()
825 .filter(|&mapped| mapped != usize::MAX)
826 else {
827 return Err(NfaError::Unsupported);
828 };
829 self.states[new].transitions.push((set, mapped));
830 }
831 }
832 Ok(NfaFragment {
833 start: remap[fragment.start],
834 end: remap[fragment.end],
835 states: fragment.states.iter().map(|state| remap[*state]).collect(),
836 })
837 }
838}
839
840#[derive(Debug, Clone)]
841struct BranchFrame {
842 owner: usize,
843 sequence: usize,
844 group_start: Option<usize>,
845 last_atom: Option<usize>,
846 restore_flags: Option<RegexFlags>,
847}
848
849#[derive(Debug, Clone, Copy)]
850struct Quantifier {
851 minimum: u32,
852 maximum: Option<u32>,
853 possessive: bool,
854}
855
856#[derive(Debug)]
857struct ClassAtom {
858 start: usize,
859 end: usize,
860 value: Option<char>,
861 set: Option<CharacterSet>,
862 kind: NormalizedKind,
863 construct: Option<String>,
864 backspace: bool,
865 incomplete: Option<&'static str>,
866}
867
868#[derive(Debug)]
869struct EscapeValue {
870 end: usize,
871 anchor: bool,
872 construct: Option<String>,
873 first_set: Option<CharacterSet>,
874 incomplete: Option<&'static str>,
875 backspace: bool,
876}
877
878pub(crate) fn embedded_facts(node: Node<'_>, source: &str) -> Vec<EmbeddedLeafFact> {
882 if node.kind() == "call" {
883 return dynamic_pattern_fact(node, source).into_iter().collect();
884 }
885 if !matches!(node.kind(), "string" | "concatenated_string")
886 || (node.kind() == "string"
887 && node
888 .parent()
889 .is_some_and(|parent| parent.kind() == "concatenated_string"))
890 {
891 return Vec::new();
892 }
893 let Some((call, api)) = pattern_call(node, source) else {
894 return Vec::new();
895 };
896 let decoded = match decode_regex_pattern_string(node, source) {
897 Ok(decoded) => decoded,
898 Err(reason) => return vec![incomplete_fact(node, source, reason)],
899 };
900 let flags = call_flags(call, api, source);
901 let facts = RegexParser::new(&decoded, flags, api).parse();
902 facts_to_embedded(facts, &decoded, source)
903}
904
905pub(crate) fn module_assigned_facts(root: Node<'_>, source: &str) -> Vec<EmbeddedLeafFact> {
906 assert_eq!(root.kind(), "module");
907 let mut assignments = HashMap::<String, (usize, Option<Node<'_>>)>::new();
908 let mut calls = Vec::new();
909 let mut pending = vec![root];
910 while let Some(node) = pending.pop() {
911 if module_level_statement(node)
912 .is_some_and(|statement| statement.kind() == "expression_statement")
913 && node.kind() == "assignment"
914 && let (Some(left), Some(right)) = (
915 node.child_by_field_name("left"),
916 node.child_by_field_name("right"),
917 )
918 && left.kind() == "identifier"
919 {
920 let candidate =
921 matches!(right.kind(), "string" | "concatenated_string").then_some(right);
922 let entry = assignments
923 .entry(node_text(left, source).to_owned())
924 .or_insert((0, None));
925 entry.0 += 1;
926 entry.1 = (entry.0 == 1).then_some(candidate).flatten();
927 }
928 if node.kind() == "call"
929 && let Some(function) = node.child_by_field_name("function")
930 && let Some(api) = recognized_api(function, source)
931 && let Some(arguments) = node.child_by_field_name("arguments")
932 && let Some(pattern) = selected_pattern_argument(arguments, source)
933 && pattern.kind() == "identifier"
934 {
935 calls.push((node, pattern, api, node_text(pattern, source).to_owned()));
936 }
937 let mut cursor = node.walk();
938 let children = node.named_children(&mut cursor).collect::<Vec<_>>();
939 pending.extend(children.into_iter().rev());
940 }
941
942 let mut facts = Vec::new();
943 for (call, pattern, api, name) in calls {
944 let resolved = assignments
945 .get(&name)
946 .and_then(|(writes, candidate)| {
947 (*writes == 1)
948 .then_some(*candidate)
949 .flatten()
950 .filter(|candidate| candidate.end_byte() < call.start_byte())
951 })
952 .filter(|_| module_pattern_binding_is_visible(&name, pattern, source));
953 let Some(selected) = resolved else {
954 let range = node_range(pattern, source);
955 facts.push(EmbeddedLeafFact {
956 kind: NormalizedKind::RegexIncomplete,
957 range,
958 parent: None,
959 construct: Some(format!("dynamic-pattern:{}", api.flags_position)),
960 occurrence_role: None,
961 });
962 continue;
963 };
964 let decoded = match decode_regex_pattern_string(selected, source) {
965 Ok(decoded) => decoded,
966 Err(reason) => {
967 facts.push(incomplete_fact(selected, source, reason));
968 continue;
969 }
970 };
971 let flags = call_flags(call, api, source);
972 let mut parsed = facts_to_embedded(
973 RegexParser::new(&decoded, flags, api).parse(),
974 &decoded,
975 source,
976 );
977 let base = facts.len();
978 for fact in &mut parsed {
979 fact.parent = fact.parent.map(|parent| base + parent);
980 }
981 facts.extend(parsed);
982 }
983 facts
984}
985
986fn module_pattern_binding_is_visible(name: &str, reference: Node<'_>, source: &str) -> bool {
987 if python_comprehension_binds_name_at(name, reference, source)
988 || matches!(
989 python_name_resolution_at(name, reference, source),
990 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
991 )
992 {
993 return false;
994 }
995
996 let mut current = reference;
997 while let Some(parent) = current.parent() {
998 match parent.kind() {
999 "function_definition" | "lambda" => break,
1000 "class_definition" => {
1001 return python_module_or_class_scope_binds_name_bounded(
1002 parent,
1003 name,
1004 source,
1005 || true,
1006 ) == Some(false);
1007 }
1008 _ => current = parent,
1009 }
1010 }
1011 true
1012}
1013
1014fn module_level_statement<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
1015 let mut current = node;
1016 loop {
1017 let parent = current.parent()?;
1018 if parent.kind() == "module" {
1019 return Some(current);
1020 }
1021 current = parent;
1022 }
1023}
1024
1025fn incomplete_fact(node: Node<'_>, source: &str, reason: &str) -> EmbeddedLeafFact {
1026 let range = node_range(node, source);
1027 EmbeddedLeafFact {
1028 kind: NormalizedKind::RegexIncomplete,
1029 range,
1030 parent: None,
1031 construct: Some(reason.to_owned()),
1032 occurrence_role: None,
1033 }
1034}
1035
1036fn dynamic_pattern_fact(node: Node<'_>, source: &str) -> Option<EmbeddedLeafFact> {
1037 let function = node.child_by_field_name("function")?;
1038 let api = recognized_api(function, source)?;
1039 let arguments = node.child_by_field_name("arguments")?;
1040 let pattern = selected_pattern_argument(arguments, source)?;
1041 if matches!(pattern.kind(), "string" | "concatenated_string") {
1042 return None;
1043 }
1044 if pattern.kind() == "identifier" {
1045 return None;
1046 }
1047 let range = node_range(pattern, source);
1048 Some(EmbeddedLeafFact {
1049 kind: NormalizedKind::RegexIncomplete,
1050 range,
1051 parent: None,
1052 construct: Some(format!("dynamic-pattern:{}", api.flags_position)),
1053 occurrence_role: None,
1054 })
1055}
1056
1057fn pattern_call<'tree>(node: Node<'tree>, source: &str) -> Option<(Node<'tree>, Api)> {
1058 let argument = node.parent()?;
1059 let (arguments, is_pattern_keyword) = match argument.kind() {
1060 "keyword_argument" => {
1061 let name = argument.child_by_field_name("name")?;
1062 let value = argument.child_by_field_name("value")?;
1063 (
1064 argument.parent()?,
1065 value.id() == node.id() && node_text(name, source) == "pattern",
1066 )
1067 }
1068 "argument_list" => (argument, false),
1069 _ => return None,
1070 };
1071 if arguments.kind() != "argument_list" {
1072 return None;
1073 }
1074 let call = arguments.parent()?;
1075 if call.kind() != "call" {
1076 return None;
1077 }
1078 let function = call.child_by_field_name("function")?;
1079 let api = recognized_api(function, source)?;
1080 let selected = if is_pattern_keyword {
1081 true
1082 } else {
1083 selected_pattern_argument(arguments, source)
1084 .is_some_and(|pattern| pattern.id() == node.id())
1085 };
1086 selected.then_some((call, api))
1087}
1088
1089fn selected_pattern_argument<'tree>(arguments: Node<'tree>, source: &str) -> Option<Node<'tree>> {
1090 let mut positional = 0usize;
1091 let mut cursor = arguments.walk();
1092 for argument in arguments.named_children(&mut cursor) {
1093 match argument.kind() {
1094 "comment" => {}
1095 "keyword_argument" => {
1096 let name = argument.child_by_field_name("name")?;
1097 if node_text(name, source) == "pattern" {
1098 return argument.child_by_field_name("value");
1099 }
1100 }
1101 _ => {
1102 if positional == 0 {
1103 return Some(argument);
1104 }
1105 positional += 1;
1106 }
1107 }
1108 }
1109 None
1110}
1111
1112fn recognized_api(function: Node<'_>, source: &str) -> Option<Api> {
1113 let path = python_static_attribute_path(function)?;
1114 if path.len() != 2 || node_text(path[0], source) != "re" {
1115 return None;
1116 }
1117 let (flags_position, scans_from_each_offset) = match node_text(path[1], source) {
1118 "compile" => (1, false),
1119 "search" | "findall" | "finditer" => (2, true),
1120 "match" | "fullmatch" => (2, false),
1121 "split" => (3, true),
1122 "sub" | "subn" => (4, true),
1123 _ => return None,
1124 };
1125 Some(Api {
1126 flags_position,
1127 scans_from_each_offset,
1128 })
1129}
1130
1131fn call_flags(call: Node<'_>, api: Api, source: &str) -> RegexFlags {
1132 let mut flags_node = None;
1133 if let Some(arguments) = call.child_by_field_name("arguments") {
1134 let mut positional = 0usize;
1135 let mut cursor = arguments.walk();
1136 for argument in arguments.named_children(&mut cursor) {
1137 match argument.kind() {
1138 "comment" => {}
1139 "keyword_argument" => {
1140 if argument
1141 .child_by_field_name("name")
1142 .is_some_and(|name| node_text(name, source) == "flags")
1143 {
1144 flags_node = argument.child_by_field_name("value");
1145 }
1146 }
1147 _ => {
1148 if positional == api.flags_position {
1149 flags_node = Some(argument);
1150 }
1151 positional += 1;
1152 }
1153 }
1154 }
1155 }
1156 let Some(flags_node) = flags_node else {
1157 return RegexFlags {
1158 dotall: false,
1159 multiline: false,
1160 ignorecase: false,
1161 ascii: false,
1162 verbose: false,
1163 complete: true,
1164 };
1165 };
1166 let mut pending = vec![flags_node];
1167 let mut flags = RegexFlags {
1168 dotall: false,
1169 multiline: false,
1170 ignorecase: false,
1171 ascii: false,
1172 verbose: false,
1173 complete: true,
1174 };
1175 while let Some(node) = pending.pop() {
1176 match node.kind() {
1177 "integer" => {
1178 let text = node_text(node, source).replace('_', "");
1179 match python_integer_value(&text) {
1180 Some(value) => {
1181 flags.ignorecase |= value & 2 != 0;
1182 flags.dotall |= value & 16 != 0;
1183 flags.multiline |= value & 8 != 0;
1184 flags.ascii |= value & 256 != 0;
1185 flags.complete &=
1186 value & !(2 | 8 | 16 | 32 | 256) == 0 && value & (64 | 128) == 0;
1187 }
1188 None => flags.complete = false,
1189 }
1190 }
1191 "attribute" | "identifier" => {
1192 let path = python_static_attribute_path(node);
1193 let name = path.as_ref().and_then(|path| {
1194 (path.len() >= 2
1195 && node_text(path[0], source) == "re"
1196 && path[1..path.len() - 1]
1197 .iter()
1198 .all(|part| node_text(*part, source) == "re"))
1199 .then(|| node_text(*path.last().expect("flag path is nonempty"), source))
1200 });
1201 match name {
1202 Some("S" | "DOTALL") => flags.dotall = true,
1203 Some("M" | "MULTILINE") => flags.multiline = true,
1204 Some("I" | "IGNORECASE") => flags.ignorecase = true,
1205 Some("A" | "ASCII") => flags.ascii = true,
1206 Some("X" | "VERBOSE") => flags.verbose = true,
1207 Some("U" | "UNICODE") => {}
1208 Some("DEBUG") => flags.complete = false,
1209 _ => flags.complete = false,
1210 }
1211 }
1212 "binary_operator" => {
1213 if let (Some(left), Some(right)) = (
1214 node.child_by_field_name("left"),
1215 node.child_by_field_name("right"),
1216 ) {
1217 pending.push(right);
1218 pending.push(left);
1219 } else {
1220 flags.complete = false;
1221 }
1222 }
1223 _ => flags.complete = false,
1224 }
1225 }
1226 flags
1227}
1228
1229fn parse_inline_flags(
1230 chars: &[char],
1231 mut index: usize,
1232 mut flags: RegexFlags,
1233) -> Option<(usize, RegexFlags)> {
1234 let mut enabled = true;
1235 let mut saw_flag = false;
1236 let mut ascii_mode = None;
1237 while let Some(character) = chars.get(index).copied() {
1238 match character {
1239 ')' | ':' if saw_flag => {
1240 if ascii_mode == Some((true, true)) {
1241 return None;
1242 }
1243 return Some((index, flags));
1244 }
1245 '-' if enabled && saw_flag => enabled = false,
1246 'i' | 'm' | 's' | 'x' | 'a' | 'u' => {
1247 saw_flag = true;
1248 match character {
1249 'i' => flags.ignorecase = enabled,
1250 'm' => flags.multiline = enabled,
1251 's' => flags.dotall = enabled,
1252 'x' => flags.verbose = enabled,
1253 'a' => {
1254 flags.ascii = enabled;
1255 ascii_mode =
1256 Some((enabled, ascii_mode.is_some_and(|(_, unicode)| unicode)));
1257 }
1258 'u' => {
1259 flags.ascii = !enabled;
1260 ascii_mode = Some((ascii_mode.is_some_and(|(ascii, _)| ascii), enabled));
1261 }
1262 _ => unreachable!("inline flag arm includes only Python regex flags"),
1263 }
1264 }
1265 _ => return None,
1266 }
1267 index += 1;
1268 }
1269 None
1270}
1271
1272fn python_integer_value(text: &str) -> Option<u64> {
1273 let (radix, digits) =
1274 if let Some(digits) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
1275 (16, digits)
1276 } else if let Some(digits) = text.strip_prefix("0o").or_else(|| text.strip_prefix("0O")) {
1277 (8, digits)
1278 } else if let Some(digits) = text.strip_prefix("0b").or_else(|| text.strip_prefix("0B")) {
1279 (2, digits)
1280 } else {
1281 (10, text)
1282 };
1283 u64::from_str_radix(digits, radix).ok()
1284}
1285
1286fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1287 node.utf8_text(source.as_bytes())
1288 .expect("tree-sitter node ranges are valid UTF-8 source")
1289}
1290
1291fn node_range(node: Node<'_>, source: &str) -> Range {
1292 let line_starts = compute_line_starts(source);
1293 let start_byte = node.start_byte();
1294 let end_byte = node.end_byte();
1295 Range {
1296 start_byte,
1297 end_byte,
1298 start_line: find_line_index_for_offset(&line_starts, start_byte) + 1,
1299 end_line: find_line_index_for_offset(
1300 &line_starts,
1301 end_byte.saturating_sub(1).max(start_byte),
1302 ) + 1,
1303 }
1304}
1305
1306fn decode_python_string(node: Node<'_>, source: &str) -> Result<DecodedValue, &'static str> {
1307 let decoded = decode_string_literal(node, source)?;
1308 if decoded.is_bytes {
1309 return Err("non-text-or-formatted-string");
1310 }
1311 Ok(decoded)
1312}
1313
1314fn decode_regex_pattern_string(node: Node<'_>, source: &str) -> Result<DecodedValue, &'static str> {
1315 decode_string_literal(node, source)
1316}
1317
1318fn decode_string_literal(node: Node<'_>, source: &str) -> Result<DecodedValue, &'static str> {
1319 let mut strings = Vec::new();
1320 if node.kind() == "string" {
1321 strings.push(node);
1322 } else {
1323 let mut stack = vec![node];
1324 while let Some(current) = stack.pop() {
1325 if current.kind() == "string" {
1326 strings.push(current);
1327 continue;
1328 }
1329 let mut children = Vec::new();
1330 let mut cursor = current.walk();
1331 for child in current.named_children(&mut cursor) {
1332 if child.kind() == "concatenated_string" || child.kind() == "string" {
1333 children.push(child);
1334 } else {
1335 return Err("non-literal-string-segment");
1336 }
1337 }
1338 for child in children.into_iter().rev() {
1339 stack.push(child);
1340 }
1341 }
1342 }
1343 if strings.is_empty() {
1344 return Err("empty-concatenated-string");
1345 }
1346 let mut value = String::new();
1347 let mut chars = Vec::new();
1348 let mut byte_string = None;
1349 for string in strings {
1350 let is_bytes = decode_string_segment(string, source, &mut value, &mut chars)?;
1351 if byte_string.is_some_and(|previous| previous != is_bytes) {
1352 return Err("mixed-bytes-and-text-concatenation");
1353 }
1354 byte_string = Some(is_bytes);
1355 if chars.len() > MAX_PATTERN_CHARS {
1356 return Err("pattern-character-budget");
1357 }
1358 }
1359 Ok(DecodedValue {
1360 value,
1361 chars,
1362 source_range: node_range(node, source),
1363 is_bytes: byte_string.expect("non-empty literal expression has a string segment"),
1364 })
1365}
1366
1367pub fn decode_python_string_value(node: Node<'_>, source: &str) -> Result<String, &'static str> {
1372 decode_python_string(node, source).map(|decoded| decoded.value)
1373}
1374
1375pub fn decode_python_fstring_literal_segments(
1379 node: Node<'_>,
1380 source: &str,
1381) -> Result<Vec<String>, &'static str> {
1382 if node.kind() != "string" {
1383 return Err("not-a-python-fstring");
1384 }
1385 let mut start_token = None;
1386 let mut segments: Vec<Vec<Node<'_>>> = vec![Vec::new()];
1387 let mut cursor = node.walk();
1388 for child in node.named_children(&mut cursor) {
1389 match child.kind() {
1390 "string_start" if start_token.is_none() => start_token = Some(child),
1391 "string_start" => return Err("duplicate-fstring-start"),
1392 "string_end" => break,
1393 "interpolation" => segments.push(Vec::new()),
1394 "string_content" | "escape_sequence" => segments
1395 .last_mut()
1396 .expect("f-string literal segments retain a current segment")
1397 .push(child),
1398 _ => return Err("unsupported-fstring-prefix-node"),
1399 }
1400 }
1401 let start_token = start_token.ok_or("missing-fstring-start")?;
1402 let prefix = string_prefix(node_text(start_token, source));
1403 if !prefix.contains('f')
1404 || prefix
1405 .chars()
1406 .any(|character| !matches!(character, 'f' | 'r'))
1407 || prefix.matches('f').count() != 1
1408 || prefix.matches('r').count() > 1
1409 {
1410 return Err("unsupported-fstring-prefix");
1411 }
1412 let raw = prefix.contains('r');
1413 segments
1414 .into_iter()
1415 .map(|parts| {
1416 let mut value = String::new();
1417 let mut mapped = Vec::new();
1418 for part in parts {
1419 if raw {
1420 append_mapped_text(
1421 node_text(part, source),
1422 part.start_byte(),
1423 part.end_byte(),
1424 &mut value,
1425 &mut mapped,
1426 )?;
1427 } else if part.kind() == "escape_sequence" {
1428 decode_literal_escape(
1429 node_text(part, source),
1430 part.start_byte(),
1431 &mut value,
1432 &mut mapped,
1433 false,
1434 )?;
1435 } else {
1436 decode_string_content(part, source, &mut value, &mut mapped, false)?;
1437 }
1438 }
1439 Ok(value.replace("{{", "{").replace("}}", "}"))
1440 })
1441 .collect()
1442}
1443
1444fn decode_string_segment(
1445 node: Node<'_>,
1446 source: &str,
1447 value: &mut String,
1448 output: &mut Vec<MappedChar>,
1449) -> Result<bool, &'static str> {
1450 let mut start_token = None;
1451 let mut body = Vec::new();
1452 let mut cursor = node.walk();
1453 for child in node.named_children(&mut cursor) {
1454 match child.kind() {
1455 "string_start" => start_token = Some(child),
1456 "string_end" => {}
1457 "string_content" | "escape_sequence" => body.push(child),
1458 "interpolation" => return Err("formatted-string-interpolation"),
1459 _ => return Err("unsupported-string-segment"),
1460 }
1461 }
1462 let start_token = start_token.ok_or("missing-string-start")?;
1463 let prefix = string_prefix(node_text(start_token, source));
1464 if prefix.contains('f') || prefix.contains('t') {
1465 return Err("non-text-or-formatted-string");
1466 }
1467 if prefix
1468 .chars()
1469 .any(|prefix| !matches!(prefix, 'r' | 'u' | 'b'))
1470 || prefix.matches('r').count() > 1
1471 || prefix.matches('u').count() > 1
1472 || prefix.matches('b').count() > 1
1473 || (prefix.contains('r') && prefix.contains('u'))
1474 || (prefix.contains('b') && prefix.contains('u'))
1475 {
1476 return Err("unsupported-string-prefix");
1477 }
1478 let is_bytes = prefix.contains('b');
1479 if is_bytes && !node_text(node, source).is_ascii() {
1480 return Err("non-ascii-byte-literal");
1481 }
1482 let raw = prefix.contains('r');
1483 for part in body {
1484 let raw_text = node_text(part, source);
1485 if raw && matches!(part.kind(), "escape_sequence" | "string_content") {
1486 append_mapped_text(raw_text, part.start_byte(), part.end_byte(), value, output)?;
1487 } else if !raw && part.kind() == "escape_sequence" {
1488 decode_literal_escape(raw_text, part.start_byte(), value, output, is_bytes)?;
1489 } else if !raw && part.kind() == "string_content" {
1490 decode_string_content(part, source, value, output, is_bytes)?;
1491 } else {
1492 append_mapped_text(raw_text, part.start_byte(), part.end_byte(), value, output)?;
1493 }
1494 if output.len() > MAX_PATTERN_CHARS {
1495 return Err("pattern-character-budget");
1496 }
1497 }
1498 if is_bytes && value.chars().any(|character| character as u32 > 0xff) {
1499 return Err("byte-pattern-value-out-of-range");
1500 }
1501 Ok(is_bytes)
1502}
1503
1504fn string_prefix(start: &str) -> String {
1505 start
1506 .chars()
1507 .take_while(|character| !matches!(character, '\'' | '"'))
1508 .map(|character| character.to_ascii_lowercase())
1509 .collect()
1510}
1511
1512fn append_mapped_text(
1513 text: &str,
1514 source_start: usize,
1515 source_end: usize,
1516 output_text: &mut String,
1517 output: &mut Vec<MappedChar>,
1518) -> Result<(), &'static str> {
1519 for (offset, value) in text.char_indices() {
1520 if output.len() >= MAX_PATTERN_CHARS {
1521 return Err("pattern-character-budget");
1522 }
1523 let start = source_start + offset;
1524 let end = start + value.len_utf8();
1525 output_text.push(value);
1526 output.push(MappedChar {
1527 source_start: start,
1528 source_end: end,
1529 });
1530 }
1531 debug_assert_eq!(source_start + text.len(), source_end);
1532 Ok(())
1533}
1534
1535fn decode_string_content(
1536 node: Node<'_>,
1537 source: &str,
1538 output_text: &mut String,
1539 output: &mut Vec<MappedChar>,
1540 bytes: bool,
1541) -> Result<(), &'static str> {
1542 let mut position = node.start_byte();
1543 let mut cursor = node.walk();
1544 for child in node.named_children(&mut cursor) {
1545 assert_eq!(child.kind(), "escape_sequence");
1546 assert!(position <= child.start_byte());
1547 if position < child.start_byte() {
1548 let plain = source
1549 .get(position..child.start_byte())
1550 .expect("tree-sitter string ranges are valid UTF-8 source");
1551 append_mapped_text(plain, position, child.start_byte(), output_text, output)?;
1552 }
1553 decode_literal_escape(
1554 node_text(child, source),
1555 child.start_byte(),
1556 output_text,
1557 output,
1558 bytes,
1559 )?;
1560 position = child.end_byte();
1561 }
1562 assert!(position <= node.end_byte());
1563 if position < node.end_byte() {
1564 let plain = source
1565 .get(position..node.end_byte())
1566 .expect("tree-sitter string ranges are valid UTF-8 source");
1567 append_mapped_text(plain, position, node.end_byte(), output_text, output)?;
1568 }
1569 Ok(())
1570}
1571
1572fn decode_literal_escape(
1573 raw: &str,
1574 source_start: usize,
1575 output_text: &mut String,
1576 output: &mut Vec<MappedChar>,
1577 bytes: bool,
1578) -> Result<(), &'static str> {
1579 let chars: Vec<char> = raw.chars().collect();
1580 if chars.first() != Some(&'\\') || chars.len() < 2 {
1581 return Err("malformed-literal-escape");
1582 }
1583 if bytes && matches!(chars.get(1), Some('u' | 'U' | 'N')) {
1584 return append_mapped_text(
1585 raw,
1586 source_start,
1587 source_start + raw.len(),
1588 output_text,
1589 output,
1590 );
1591 }
1592 let escaped = chars[1];
1593 let replacement = match escaped {
1594 '\n' => None,
1595 '\r' if chars.get(2) == Some(&'\n') => None,
1596 '\r' => Some('\r'),
1597 '\\' => Some('\\'),
1598 '\'' => Some('\''),
1599 '"' => Some('"'),
1600 'a' => Some('\u{7}'),
1601 'b' => Some('\u{8}'),
1602 'f' => Some('\u{c}'),
1603 'n' => Some('\n'),
1604 'r' => Some('\r'),
1605 't' => Some('\t'),
1606 'v' => Some('\u{b}'),
1607 'x' => Some(
1608 char::from_u32(hex_value(&chars, 2, 2).ok_or("invalid-hex-escape")?)
1609 .ok_or("invalid-hex-scalar")?,
1610 ),
1611 'u' => Some(
1612 char::from_u32(hex_value(&chars, 2, 4).ok_or("invalid-unicode-escape")?)
1613 .ok_or("invalid-unicode-scalar")?,
1614 ),
1615 'U' => Some(
1616 char::from_u32(hex_value(&chars, 2, 8).ok_or("invalid-unicode-escape")?)
1617 .ok_or("invalid-unicode-scalar")?,
1618 ),
1619 'N' => return Err("named-unicode-escape"),
1620 '0'..='7' => {
1621 let mut value = (escaped as u32) - ('0' as u32);
1622 let mut digits = 1usize;
1623 while digits < 3 {
1624 let Some(digit @ '0'..='7') = chars.get(1 + digits).copied() else {
1625 break;
1626 };
1627 value = value * 8 + (digit as u32 - '0' as u32);
1628 digits += 1;
1629 }
1630 Some(char::from_u32(value).ok_or("invalid-octal-scalar")?)
1631 }
1632 other => {
1635 if output.len().saturating_add(2) > MAX_PATTERN_CHARS {
1636 return Err("pattern-character-budget");
1637 }
1638 output_text.push('\\');
1639 output.push(MappedChar {
1640 source_start,
1641 source_end: source_start + 1,
1642 });
1643 output_text.push(other);
1644 output.push(MappedChar {
1645 source_start: source_start + 1,
1646 source_end: source_start + 1 + other.len_utf8(),
1647 });
1648 return Ok(());
1649 }
1650 };
1651 if let Some(replacement) = replacement {
1652 if output.len() >= MAX_PATTERN_CHARS {
1653 return Err("pattern-character-budget");
1654 }
1655 output_text.push(replacement);
1656 output.push(MappedChar {
1657 source_start,
1658 source_end: source_start + raw.len(),
1659 });
1660 }
1661 Ok(())
1662}
1663
1664fn hex_value(chars: &[char], start: usize, digits: usize) -> Option<u32> {
1665 chars
1666 .get(start..start + digits)?
1667 .iter()
1668 .try_fold(0u32, |value, digit| {
1669 digit.to_digit(16).map(|digit| value * 16 + digit)
1670 })
1671}
1672
1673struct RegexParser<'a> {
1674 decoded: &'a DecodedValue,
1675 flags: RegexFlags,
1676 api: Api,
1677 chars: Vec<char>,
1678 index: usize,
1679 facts: Vec<Fact>,
1680 frames: Vec<BranchFrame>,
1681 pending_risks: Vec<(NormalizedKind, usize, usize, usize, String)>,
1682 verbose_comments: Vec<(usize, usize)>,
1683 incomplete: Option<(usize, usize, String)>,
1684 exceeded: bool,
1685}
1686
1687impl<'a> RegexParser<'a> {
1688 fn new(decoded: &'a DecodedValue, flags: RegexFlags, api: Api) -> Self {
1689 Self {
1690 decoded,
1691 flags,
1692 api,
1693 chars: decoded.value.chars().collect(),
1694 index: 0,
1695 facts: Vec::new(),
1696 frames: Vec::new(),
1697 pending_risks: Vec::new(),
1698 verbose_comments: Vec::new(),
1699 incomplete: None,
1700 exceeded: false,
1701 }
1702 }
1703
1704 fn parse(mut self) -> Vec<Fact> {
1705 let length = self.chars.len();
1706 if !self.flags.complete {
1707 self.mark_incomplete(0, length, "unsupported-regex-flags");
1708 }
1709 let pattern = self.add_fact(
1710 NormalizedKind::RegexPattern,
1711 0,
1712 length,
1713 None,
1714 Some(self.decoded.value.clone()),
1715 None,
1716 );
1717 let sequence = self.add_fact(
1718 NormalizedKind::RegexSequence,
1719 0,
1720 length,
1721 Some(pattern),
1722 None,
1723 None,
1724 );
1725 self.frames.push(BranchFrame {
1726 owner: pattern,
1727 sequence,
1728 group_start: None,
1729 last_atom: None,
1730 restore_flags: None,
1731 });
1732
1733 while self.index < length {
1734 if self.exceeded {
1735 break;
1736 }
1737 if self.flags.verbose {
1738 if matches!(
1739 self.chars[self.index],
1740 ' ' | '\t' | '\n' | '\r' | '\x0c' | '\x0b'
1741 ) {
1742 self.index += 1;
1743 continue;
1744 }
1745 if self.chars[self.index] == '#' {
1746 let start = self.index;
1747 while self.index < length && self.chars[self.index] != '\n' {
1748 self.index += 1;
1749 }
1750 self.verbose_comments.push((start, self.index));
1751 continue;
1752 }
1753 }
1754 if self.facts.len() >= MAX_PATTERN_FACTS - 2 {
1755 self.exceeded = true;
1756 break;
1757 }
1758 match self.chars[self.index] {
1759 '(' => self.open_group(),
1760 ')' => self.close_group(),
1761 '|' => self.alternation(),
1762 '[' => self.character_class(),
1763 '\\' => self.escape(false),
1764 '.' => self.wildcard(),
1765 '^' | '$' => self.anchor(),
1766 '*' | '+' | '?' => self.quantifier(),
1767 '{' if self.braced_quantifier_end().is_some() => self.quantifier(),
1768 _ => self.literal(),
1769 }
1770 }
1771
1772 if self.frames.len() > 1 {
1773 let start = self
1774 .frames
1775 .last()
1776 .and_then(|frame| frame.group_start)
1777 .unwrap_or(length);
1778 self.mark_incomplete(start, length, "unclosed-group");
1779 for frame in self.frames.iter().skip(1) {
1780 self.facts[frame.owner].end = length;
1781 self.facts[frame.sequence].end = length;
1782 }
1783 }
1784 if self.exceeded {
1785 return self.budget_result();
1786 }
1787 self.compute_minimum_widths();
1788 self.classify_verbose_comment_duplicates();
1789 self.classify_unmatchable_anchors();
1790 self.classify_hostname();
1791 self.classify_bad_tag();
1792 self.classify_polynomial_redos();
1793 self.classify_redos();
1794 self.append_pending_facts();
1795 if self.exceeded {
1796 return self.budget_result();
1797 }
1798 self.facts
1799 }
1800
1801 fn classify_verbose_comment_duplicates(&mut self) {
1802 if self.verbose_comments.is_empty()
1803 || !self
1804 .facts
1805 .iter()
1806 .any(|fact| fact.kind == NormalizedKind::RegexCharacterClass)
1807 {
1808 return;
1809 }
1810 let mut positions = HashMap::<char, Vec<usize>>::new();
1811 for &(start, end) in &self.verbose_comments {
1812 for (offset, character) in self.chars[start..end].iter().copied().enumerate() {
1813 if !character.is_whitespace() {
1814 positions.entry(character).or_default().push(start + offset);
1815 }
1816 }
1817 }
1818 let mut duplicates = positions
1819 .into_iter()
1820 .filter_map(|(character, offsets)| {
1821 (offsets.len() > 1).then(|| (character, offsets[0], offsets[1]))
1822 })
1823 .collect::<Vec<_>>();
1824 duplicates.sort_unstable_by_key(|(character, _, _)| *character);
1825 if let Some((character, _, _)) = duplicates.first() {
1826 self.pending_risks.push((
1827 NormalizedKind::RegexRiskDuplicateClassCharacter,
1828 0,
1829 0,
1830 self.chars.len(),
1831 format!("duplicate-verbose-comment-character:{character}"),
1832 ));
1833 }
1834 }
1835
1836 fn budget_result(&self) -> Vec<Fact> {
1837 let length = self.chars.len();
1838 vec![
1839 self.facts[0].clone(),
1840 self.facts[1].clone(),
1841 Fact {
1842 kind: NormalizedKind::RegexIncomplete,
1843 start: 0,
1844 end: length,
1845 parent: Some(0),
1846 construct: Some("regex-fact-budget".to_owned()),
1847 first_set: None,
1848 escaped_literal: None,
1849 quantifier: None,
1850 minimum_width: None,
1851 },
1852 ]
1853 }
1854
1855 fn add_fact(
1856 &mut self,
1857 kind: NormalizedKind,
1858 start: usize,
1859 end: usize,
1860 parent: Option<usize>,
1861 construct: Option<String>,
1862 first_set: Option<CharacterSet>,
1863 ) -> usize {
1864 let id = self.facts.len();
1865 assert!(
1866 id < MAX_PATTERN_FACTS,
1867 "regex fact budget checked before insertion"
1868 );
1869 let minimum_width = match kind {
1870 NormalizedKind::RegexLiteral
1871 | NormalizedKind::RegexWildcard
1872 | NormalizedKind::RegexCharacterClass
1873 | NormalizedKind::RegexCharacterRange
1874 | NormalizedKind::RegexEscape => Some(1),
1875 NormalizedKind::RegexAnchor => Some(0),
1876 _ => None,
1877 };
1878 self.facts.push(Fact {
1879 kind,
1880 start,
1881 end,
1882 parent,
1883 construct,
1884 first_set,
1885 escaped_literal: None,
1886 quantifier: None,
1887 minimum_width,
1888 });
1889 id
1890 }
1891
1892 fn mark_incomplete(&mut self, start: usize, end: usize, reason: &str) {
1893 if self.incomplete.is_none() {
1894 self.incomplete = Some((start, end, reason.to_owned()));
1895 }
1896 }
1897
1898 fn current_sequence(&self) -> usize {
1899 self.frames
1900 .last()
1901 .expect("regex parser always has an outer frame")
1902 .sequence
1903 }
1904
1905 fn record_atom(&mut self, fact: usize) {
1906 self.frames
1907 .last_mut()
1908 .expect("regex parser always has an outer frame")
1909 .last_atom = Some(fact);
1910 }
1911
1912 fn add_atom(
1913 &mut self,
1914 kind: NormalizedKind,
1915 start: usize,
1916 end: usize,
1917 construct: Option<String>,
1918 first_set: Option<CharacterSet>,
1919 ) -> usize {
1920 let fact = self.add_fact(
1921 kind,
1922 start,
1923 end,
1924 Some(self.current_sequence()),
1925 construct,
1926 first_set,
1927 );
1928 self.record_atom(fact);
1929 fact
1930 }
1931
1932 fn open_group(&mut self) {
1933 let start = self.index;
1934 let mut content_start = start + 1;
1935 let mut group_type = "capturing";
1936 let mut missing_special_group_end = None;
1937 let mut restore_flags = None;
1938 if self.chars.get(content_start) == Some(&'?') {
1939 match (
1940 self.chars.get(content_start + 1),
1941 self.chars.get(content_start + 2),
1942 ) {
1943 (Some(':'), _) => {
1944 group_type = "noncapturing";
1945 content_start += 2;
1946 }
1947 (Some('P'), Some('<')) => {
1948 group_type = "named-capturing";
1949 let mut close = content_start + 3;
1950 while close < self.chars.len() && self.chars[close] != '>' {
1951 close += 1;
1952 }
1953 if close == self.chars.len() {
1954 self.mark_incomplete(start, self.chars.len(), "unclosed-group-name");
1955 self.index = self.chars.len();
1956 return;
1957 }
1958 content_start = close + 1;
1959 }
1960 (Some('#'), _) => {
1961 let mut close = content_start + 2;
1962 while close < self.chars.len() && self.chars[close] != ')' {
1963 close += 1;
1964 }
1965 if close == self.chars.len() {
1966 self.mark_incomplete(start, close, "unclosed-comment-group");
1967 self.index = close;
1968 return;
1969 }
1970 let group = self.add_fact(
1971 NormalizedKind::RegexGroup,
1972 start,
1973 close + 1,
1974 Some(self.current_sequence()),
1975 Some("comment".to_owned()),
1976 None,
1977 );
1978 self.facts[group].minimum_width = Some(0);
1979 self.index = close + 1;
1980 self.record_atom(group);
1981 return;
1982 }
1983 (Some('='), _) => {
1984 group_type = "lookahead-positive";
1985 content_start += 2;
1986 }
1987 (Some('!'), _) => {
1988 group_type = "lookahead-negative";
1989 content_start += 2;
1990 }
1991 (Some('<'), Some('=')) => {
1992 group_type = "lookbehind-positive";
1993 content_start += 3;
1994 }
1995 (Some('<'), Some('!')) => {
1996 group_type = "lookbehind-negative";
1997 content_start += 3;
1998 }
1999 (Some('i' | 'm' | 's' | 'x' | 'a' | 'u' | '-'), _) => {
2000 let flags_start = content_start + 1;
2001 let Some((delimiter, flags)) =
2002 parse_inline_flags(&self.chars, flags_start, self.flags)
2003 else {
2004 self.mark_incomplete(
2005 start,
2006 (start + 3).min(self.chars.len()),
2007 "invalid-inline-flags",
2008 );
2009 self.index = (start + 3).min(self.chars.len());
2010 return;
2011 };
2012 match self.chars[delimiter] {
2013 ')' => {
2014 self.flags = flags;
2015 self.index = delimiter + 1;
2016 return;
2017 }
2018 ':' => {
2019 group_type = "scoped-flags";
2020 restore_flags = Some(self.flags);
2021 self.flags = flags;
2022 content_start = delimiter + 1;
2023 }
2024 _ => unreachable!("inline flags stop at a group delimiter"),
2025 }
2026 }
2027 _ => {
2028 self.mark_incomplete(
2029 start,
2030 (start + 3).min(self.chars.len()),
2031 "unsupported-special-group",
2032 );
2033 content_start += 1;
2034 }
2035 }
2036 } else if self.chars.get(content_start) == Some(&'P')
2037 && self.chars.get(content_start + 1) == Some(&'<')
2038 {
2039 let mut close = content_start + 2;
2040 while close < self.chars.len() && self.chars[close] != '>' {
2041 close += 1;
2042 }
2043 let name = &self.chars[content_start + 2..close.min(self.chars.len())];
2044 let valid_name = name
2045 .first()
2046 .is_some_and(|first| first.is_alphabetic() || *first == '_')
2047 && name
2048 .iter()
2049 .all(|character| character.is_alphanumeric() || *character == '_');
2050 if close < self.chars.len() && valid_name {
2051 missing_special_group_end = Some(close + 1);
2052 }
2053 }
2054 if self.frames.len() >= MAX_GROUP_DEPTH {
2055 self.mark_incomplete(
2056 start,
2057 (start + 1).min(self.chars.len()),
2058 "regex-group-depth-budget",
2059 );
2060 self.exceeded = true;
2061 return;
2062 }
2063 let owner = self.current_sequence();
2064 let group = self.add_fact(
2065 NormalizedKind::RegexGroup,
2066 start,
2067 self.chars.len(),
2068 Some(owner),
2069 Some(group_type.to_owned()),
2070 None,
2071 );
2072 if let Some(end) = missing_special_group_end {
2073 self.pending_risks.push((
2074 NormalizedKind::RegexRiskIncompleteSpecialGroup,
2075 group,
2076 start,
2077 end,
2078 "named-group-missing-question-mark".to_owned(),
2079 ));
2080 }
2081 let sequence = self.add_fact(
2082 NormalizedKind::RegexSequence,
2083 content_start,
2084 self.chars.len(),
2085 Some(group),
2086 None,
2087 None,
2088 );
2089 self.frames.push(BranchFrame {
2090 owner: group,
2091 sequence,
2092 group_start: Some(start),
2093 last_atom: None,
2094 restore_flags,
2095 });
2096 self.index = content_start;
2097 }
2098
2099 fn close_group(&mut self) {
2100 if self.frames.len() == 1 {
2101 self.mark_incomplete(self.index, self.index + 1, "unmatched-close-parenthesis");
2102 self.index += 1;
2103 return;
2104 }
2105 let close = self.index;
2106 self.index += 1;
2107 let frame = self
2108 .frames
2109 .pop()
2110 .expect("a nested regex group frame exists");
2111 self.facts[frame.sequence].end = close;
2112 self.facts[frame.owner].end = self.index;
2113 self.facts[frame.owner].first_set = self.sequence_first_set(frame.sequence);
2114 self.facts[frame.sequence].minimum_width = self.sequence_minimum_width(frame.sequence);
2115 self.facts[frame.owner].minimum_width = if matches!(
2116 self.facts[frame.owner].construct.as_deref(),
2117 Some(
2118 "lookahead-positive"
2119 | "lookahead-negative"
2120 | "lookbehind-positive"
2121 | "lookbehind-negative"
2122 )
2123 ) {
2124 Some(0)
2125 } else {
2126 self.facts[frame.sequence].minimum_width
2127 };
2128 if self.facts[frame.owner]
2129 .construct
2130 .as_deref()
2131 .is_some_and(|construct| construct.starts_with("look"))
2132 {
2133 self.facts[frame.owner].first_set = None;
2134 }
2135 if let Some(flags) = frame.restore_flags {
2136 self.flags = flags;
2137 }
2138 self.record_atom(frame.owner);
2139 }
2140
2141 fn alternation(&mut self) {
2142 let start = self.index;
2143 self.index += 1;
2144 let sequence = self.current_sequence();
2145 self.add_fact(
2146 NormalizedKind::RegexAlternation,
2147 start,
2148 self.index,
2149 Some(sequence),
2150 None,
2151 None,
2152 );
2153 let frame = self.frames.last_mut().expect("outer regex frame exists");
2154 frame.last_atom = None;
2155 }
2159
2160 fn literal(&mut self) {
2161 let start = self.index;
2162 let value = self.chars[self.index];
2163 self.index += 1;
2164 let set = CharacterSet::one(value);
2165 let set = if self.flags.ignorecase {
2166 set.with_ascii_case_variants()
2167 } else {
2168 set
2169 };
2170 self.add_atom(
2171 NormalizedKind::RegexLiteral,
2172 start,
2173 self.index,
2174 Some(value.to_string()),
2175 Some(set),
2176 );
2177 }
2178
2179 fn wildcard(&mut self) {
2180 let start = self.index;
2181 self.index += 1;
2182 let set = if self.flags.dotall {
2183 CharacterSet {
2184 ranges: vec![(0, 0x10ffff)],
2185 }
2186 } else {
2187 CharacterSet::any_except_newline()
2188 };
2189 self.add_atom(
2190 NormalizedKind::RegexWildcard,
2191 start,
2192 self.index,
2193 Some("dot".to_owned()),
2194 Some(set),
2195 );
2196 }
2197
2198 fn anchor(&mut self) {
2199 let start = self.index;
2200 let value = self.chars[self.index];
2201 self.index += 1;
2202 self.add_fact(
2203 NormalizedKind::RegexAnchor,
2204 start,
2205 self.index,
2206 Some(self.current_sequence()),
2207 Some(value.to_string()),
2208 None,
2209 );
2210 self.frames
2211 .last_mut()
2212 .expect("outer regex frame exists")
2213 .last_atom = None;
2214 }
2215
2216 fn quantifier(&mut self) {
2217 let start = self.index;
2218 let Some(atom) = self.frames.last().and_then(|frame| frame.last_atom) else {
2219 self.mark_incomplete(
2220 start,
2221 (start + 1).min(self.chars.len()),
2222 "quantifier-without-atom",
2223 );
2224 self.index += 1;
2225 return;
2226 };
2227 if self.facts[atom].quantifier.is_some() {
2228 self.mark_incomplete(
2229 start,
2230 (start + 1).min(self.chars.len()),
2231 "nested-quantifier",
2232 );
2233 self.index += 1;
2234 return;
2235 }
2236 let mut quantifier = match self.chars[self.index] {
2237 '*' => {
2238 self.index += 1;
2239 Quantifier {
2240 minimum: 0,
2241 maximum: None,
2242 possessive: false,
2243 }
2244 }
2245 '+' => {
2246 self.index += 1;
2247 Quantifier {
2248 minimum: 1,
2249 maximum: None,
2250 possessive: false,
2251 }
2252 }
2253 '?' => {
2254 self.index += 1;
2255 Quantifier {
2256 minimum: 0,
2257 maximum: Some(1),
2258 possessive: false,
2259 }
2260 }
2261 '{' => match self.braced_quantifier_end() {
2262 Some((end, quantifier)) => {
2263 self.index = end;
2264 quantifier
2265 }
2266 None => {
2267 self.mark_incomplete(
2268 start,
2269 (start + 1).min(self.chars.len()),
2270 "malformed-braced-quantifier",
2271 );
2272 self.index += 1;
2273 return;
2274 }
2275 },
2276 _ => unreachable!("quantifier dispatch uses a quantifier character"),
2277 };
2278 if matches!(self.chars.get(self.index), Some('?') | Some('+')) {
2279 quantifier.possessive = self.chars[self.index] == '+';
2280 self.index += 1;
2281 }
2282 let end = self.index;
2283 let fact = self.add_fact(
2284 NormalizedKind::RegexQuantifier,
2285 start,
2286 end,
2287 Some(atom),
2288 Some(format!(
2289 "min={},max={},possessive={}",
2290 quantifier.minimum,
2291 quantifier
2292 .maximum
2293 .map_or_else(|| "*".to_owned(), |maximum| maximum.to_string()),
2294 quantifier.possessive
2295 )),
2296 None,
2297 );
2298 self.facts[fact].quantifier = Some(quantifier);
2299 self.facts[atom].minimum_width = self.facts[atom]
2300 .minimum_width
2301 .and_then(|width| width.checked_mul(quantifier.minimum));
2302 self.facts[atom].end = end;
2303 }
2304
2305 fn braced_quantifier_end(&self) -> Option<(usize, Quantifier)> {
2306 let mut index = self.index + 1;
2307 let number_start = index;
2308 let mut minimum = 0u32;
2309 while let Some(digit @ '0'..='9') = self.chars.get(index).copied() {
2310 minimum = minimum
2311 .checked_mul(10)?
2312 .checked_add(digit as u32 - '0' as u32)?;
2313 index += 1;
2314 }
2315 if index == number_start {
2316 return None;
2317 }
2318 let mut maximum = Some(minimum);
2319 if self.chars.get(index) == Some(&',') {
2320 index += 1;
2321 let upper_start = index;
2322 let mut upper = 0u32;
2323 while let Some(digit @ '0'..='9') = self.chars.get(index).copied() {
2324 upper = upper
2325 .checked_mul(10)?
2326 .checked_add(digit as u32 - '0' as u32)?;
2327 index += 1;
2328 }
2329 maximum = if upper_start == index {
2330 None
2331 } else {
2332 Some(upper)
2333 };
2334 }
2335 if self.chars.get(index) != Some(&'}') || maximum.is_some_and(|maximum| maximum < minimum) {
2336 return None;
2337 }
2338 Some((
2339 index + 1,
2340 Quantifier {
2341 minimum,
2342 maximum,
2343 possessive: false,
2344 },
2345 ))
2346 }
2347
2348 fn escape(&mut self, in_class: bool) {
2349 let start = self.index;
2350 let Some(escape) = parse_regex_escape(&self.chars, start, in_class, self.flags.ascii)
2351 else {
2352 self.mark_incomplete(
2353 start,
2354 (start + 2).min(self.chars.len()),
2355 "unsupported-escape",
2356 );
2357 self.index = (start + 2).min(self.chars.len());
2358 return;
2359 };
2360 self.index = escape.end;
2361 if let Some(reason) = escape.incomplete {
2362 self.mark_incomplete(start, self.index, reason);
2363 }
2364 let kind = if escape.anchor && !in_class {
2365 NormalizedKind::RegexAnchor
2366 } else {
2367 NormalizedKind::RegexEscape
2368 };
2369 let escaped_literal = escape.first_set.as_ref().and_then(single_character);
2370 let first_set = escape.first_set.map(|set| {
2371 if self.flags.ignorecase {
2372 set.with_ascii_case_variants()
2373 } else {
2374 set
2375 }
2376 });
2377 let fact = self.add_fact(
2378 kind,
2379 start,
2380 self.index,
2381 Some(self.current_sequence()),
2382 escape.construct,
2383 first_set,
2384 );
2385 self.facts[fact].escaped_literal = escaped_literal;
2386 if escape.anchor && !in_class {
2387 self.frames
2388 .last_mut()
2389 .expect("outer regex frame exists")
2390 .last_atom = None;
2391 } else {
2392 self.record_atom(fact);
2393 }
2394 if escape.backspace && in_class {
2395 self.pending_risks.push((
2396 NormalizedKind::RegexRiskBackspaceEscape,
2397 fact,
2398 start,
2399 self.index,
2400 "backspace-in-character-class".to_owned(),
2401 ));
2402 }
2403 }
2404
2405 fn character_class(&mut self) {
2406 let start = self.index;
2407 self.index += 1;
2408 let class = self.add_fact(
2409 NormalizedKind::RegexCharacterClass,
2410 start,
2411 self.chars.len(),
2412 Some(self.current_sequence()),
2413 None,
2414 None,
2415 );
2416 let negated = self.chars.get(self.index) == Some(&'^');
2417 if negated {
2418 self.index += 1;
2419 }
2420 let mut members = CharacterSet { ranges: Vec::new() };
2421 let mut members_known = true;
2422 let mut saw_member = false;
2423 let mut closed = false;
2424 let mut explicit_members = HashSet::<String>::new();
2425 let mut range_members = Vec::<(usize, CharacterSet, u32)>::new();
2426 let mut category_members = Vec::new();
2427 while self.index < self.chars.len() {
2428 if self.facts.len() >= MAX_PATTERN_FACTS - 2 {
2429 self.exceeded = true;
2430 break;
2431 }
2432 if self.chars[self.index] == ']' && saw_member {
2433 self.index += 1;
2434 closed = true;
2435 break;
2436 }
2437 let Some(left) = parse_class_atom(&self.chars, self.index, self.flags.ascii) else {
2438 self.mark_incomplete(
2439 self.index,
2440 (self.index + 1).min(self.chars.len()),
2441 "unsupported-class-escape",
2442 );
2443 self.index += 1;
2444 members_known = false;
2445 continue;
2446 };
2447 self.index = left.end;
2448 if let Some(reason) = left.incomplete {
2449 self.mark_incomplete(left.start, left.end, reason);
2450 members_known = false;
2451 }
2452 let has_range = self.chars.get(self.index) == Some(&'-')
2453 && self
2454 .chars
2455 .get(self.index + 1)
2456 .is_some_and(|next| *next != ']');
2457 if has_range {
2458 let dash = self.index;
2459 self.index += 1;
2460 let Some(right) = parse_class_atom(&self.chars, self.index, self.flags.ascii)
2461 else {
2462 self.mark_incomplete(dash, dash + 1, "unsupported-class-range");
2463 members_known = false;
2464 continue;
2465 };
2466 self.index = right.end;
2467 if let Some(reason) = right.incomplete {
2468 self.mark_incomplete(right.start, right.end, reason);
2469 members_known = false;
2470 }
2471 let Some((left_value, right_value)) = left.value.zip(right.value) else {
2472 self.mark_incomplete(left.start, right.end, "non-literal-class-range");
2473 members_known = false;
2474 continue;
2475 };
2476 if left_value > right_value {
2477 let range_fact = self.add_fact(
2478 NormalizedKind::RegexCharacterRange,
2479 left.start,
2480 right.end,
2481 Some(class),
2482 Some(format!(
2483 "U+{:04X}-U+{:04X}",
2484 left_value as u32, right_value as u32
2485 )),
2486 Some(CharacterSet { ranges: Vec::new() }),
2487 );
2488 self.pending_risks.push((
2489 NormalizedKind::RegexRiskRange,
2490 range_fact,
2491 left.start,
2492 right.end,
2493 "empty-descending-range".to_owned(),
2494 ));
2495 self.mark_incomplete(left.start, right.end, "descending-class-range");
2496 members_known = false;
2497 saw_member = true;
2498 continue;
2499 }
2500 let range_set = CharacterSet {
2501 ranges: vec![(left_value as u32, right_value as u32)],
2502 };
2503 let range_set = if self.flags.ignorecase {
2504 range_set.with_ascii_case_variants()
2505 } else {
2506 range_set
2507 };
2508 let compatibility_set = codeql_range_character_set(left_value, right_value);
2509 let range_fact = self.add_fact(
2510 NormalizedKind::RegexCharacterRange,
2511 left.start,
2512 right.end,
2513 Some(class),
2514 Some(format!(
2515 "U+{:04X}-U+{:04X}",
2516 left_value as u32, right_value as u32
2517 )),
2518 Some(range_set.clone()),
2519 );
2520 if let Some((previous_fact, _, previous_width)) = range_members
2521 .iter()
2522 .find(|(_, previous, _)| previous.intersects(&compatibility_set))
2523 {
2524 let current_width = compatibility_set
2525 .ranges
2526 .iter()
2527 .map(|(start, end)| end.saturating_sub(*start))
2528 .max()
2529 .unwrap_or_default();
2530 let previous_start = self.facts[*previous_fact].start;
2531 let selected_fact = if current_width > *previous_width
2532 || (current_width == *previous_width && left.start < previous_start)
2533 {
2534 range_fact
2535 } else {
2536 *previous_fact
2537 };
2538 self.pending_risks.push((
2539 NormalizedKind::RegexRiskRange,
2540 selected_fact,
2541 self.facts[selected_fact].start,
2542 self.facts[selected_fact].end,
2543 "overlapping-class-range".to_owned(),
2544 ));
2545 }
2546 if category_members
2547 .iter()
2548 .any(|previous: &CharacterSet| previous.intersects(&compatibility_set))
2549 {
2550 self.pending_risks.push((
2551 NormalizedKind::RegexRiskRange,
2552 range_fact,
2553 left.start,
2554 right.end,
2555 "range-overlaps-character-class-escape".to_owned(),
2556 ));
2557 }
2558 let compatibility_width = compatibility_set
2559 .ranges
2560 .iter()
2561 .map(|(start, end)| end.saturating_sub(*start))
2562 .max()
2563 .unwrap_or_default();
2564 range_members.push((range_fact, compatibility_set, compatibility_width));
2565 members.union_with(&range_set);
2566 saw_member = true;
2567 if is_overly_broad_range(left_value, right_value) {
2568 self.pending_risks.push((
2569 NormalizedKind::RegexRiskRange,
2570 range_fact,
2571 left.start,
2572 right.end,
2573 "ascii-punctuation-between-letter-ranges".to_owned(),
2574 ));
2575 }
2576 } else {
2577 let semantic_set = left.set.as_ref().map(|set| {
2578 if self.flags.ignorecase {
2579 set.clone().with_ascii_case_variants()
2580 } else {
2581 set.clone()
2582 }
2583 });
2584 let is_category = left
2585 .construct
2586 .as_deref()
2587 .is_some_and(|construct| construct.starts_with("category:"));
2588 let member_fact = self.add_fact(
2589 left.kind,
2590 left.start,
2591 left.end,
2592 Some(class),
2593 left.construct,
2594 semantic_set.clone(),
2595 );
2596 if let Some(set) = &semantic_set {
2597 if left.value.is_some() {
2598 let syntax = self.chars[left.start..left.end].iter().collect::<String>();
2599 if !explicit_members.insert(syntax) {
2600 self.pending_risks.push((
2601 NormalizedKind::RegexRiskDuplicateClassCharacter,
2602 member_fact,
2603 left.start,
2604 left.end,
2605 "duplicate-class-member".to_owned(),
2606 ));
2607 }
2608 }
2609 if is_category {
2610 if range_members
2611 .iter()
2612 .any(|(_, range, _)| range.intersects(set))
2613 {
2614 self.pending_risks.push((
2615 NormalizedKind::RegexRiskRange,
2616 member_fact,
2617 left.start,
2618 left.end,
2619 "range-overlaps-character-class-escape".to_owned(),
2620 ));
2621 }
2622 category_members.push(set.clone());
2623 }
2624 members.union_with(set);
2625 } else {
2626 members_known = false;
2627 }
2628 saw_member = true;
2629 if left.backspace {
2630 self.pending_risks.push((
2631 NormalizedKind::RegexRiskBackspaceEscape,
2632 member_fact,
2633 left.start,
2634 left.end,
2635 "backspace-in-character-class".to_owned(),
2636 ));
2637 }
2638 }
2639 }
2640 self.facts[class].end = self.index;
2641 if !closed {
2642 self.mark_incomplete(start, self.index, "unclosed-character-class");
2643 }
2644 if !saw_member {
2645 self.mark_incomplete(start, self.index, "empty-character-class");
2646 }
2647 if negated && members_known {
2648 members = members.complement();
2649 }
2650 self.facts[class].first_set = members_known.then_some(members);
2651 self.record_atom(class);
2652 }
2653
2654 fn sequence_first_set(&self, sequence: usize) -> Option<CharacterSet> {
2655 let branches = self.branch_first_sets(sequence);
2656 if branches.iter().any(Option::is_none) {
2657 return None;
2658 }
2659 let mut result = CharacterSet { ranges: Vec::new() };
2660 for branch in branches.into_iter().flatten() {
2661 result.union_with(&branch);
2662 }
2663 Some(result)
2664 }
2665
2666 fn branch_first_sets(&self, sequence: usize) -> Vec<Option<CharacterSet>> {
2667 let mut branches = Vec::new();
2668 let mut current = None;
2669 let mut consumed = false;
2670 for fact in self
2671 .facts
2672 .iter()
2673 .filter(|fact| fact.parent == Some(sequence))
2674 {
2675 if fact.kind == NormalizedKind::RegexAlternation {
2676 branches.push(current.take());
2677 consumed = false;
2678 continue;
2679 }
2680 if consumed || fact.kind == NormalizedKind::RegexAnchor {
2681 continue;
2682 }
2683 match fact.kind {
2684 NormalizedKind::RegexLiteral
2685 | NormalizedKind::RegexWildcard
2686 | NormalizedKind::RegexCharacterClass
2687 | NormalizedKind::RegexEscape
2688 | NormalizedKind::RegexGroup => {
2689 current = fact.first_set.clone();
2690 consumed = true;
2691 }
2692 _ => {}
2693 }
2694 }
2695 branches.push(current);
2696 branches
2697 }
2698
2699 fn compute_minimum_widths(&mut self) {
2700 for id in (0..self.facts.len()).rev() {
2701 match self.facts[id].kind {
2702 NormalizedKind::RegexSequence => {
2703 self.facts[id].minimum_width = self.sequence_minimum_width(id);
2704 }
2705 NormalizedKind::RegexGroup => {
2706 let sequence = self.facts.iter().position(|fact| {
2707 fact.kind == NormalizedKind::RegexSequence && fact.parent == Some(id)
2708 });
2709 self.facts[id].minimum_width = if self.facts[id]
2710 .construct
2711 .as_deref()
2712 .is_some_and(|construct| construct.starts_with("look"))
2713 {
2714 Some(0)
2715 } else {
2716 sequence.and_then(|sequence| self.facts[sequence].minimum_width)
2717 };
2718 }
2719 _ => {}
2720 }
2721 }
2722 }
2723
2724 fn sequence_minimum_width(&self, sequence: usize) -> Option<u32> {
2725 let mut branches = Vec::new();
2726 let mut width = Some(0u32);
2727 for fact in self
2728 .facts
2729 .iter()
2730 .filter(|fact| fact.parent == Some(sequence))
2731 {
2732 if fact.kind == NormalizedKind::RegexAlternation {
2733 branches.push(width.take()?);
2734 width = Some(0);
2735 continue;
2736 }
2737 width = width?.checked_add(fact.minimum_width?);
2738 }
2739 branches.push(width?);
2740 branches.into_iter().min()
2741 }
2742
2743 fn classify_unmatchable_anchors(&mut self) {
2744 if self.incomplete.is_some() || self.flags.multiline {
2745 return;
2746 }
2747 let sequences = self
2748 .facts
2749 .iter()
2750 .enumerate()
2751 .filter(|(_, fact)| fact.kind == NormalizedKind::RegexSequence)
2752 .map(|(id, _)| id)
2753 .collect::<Vec<_>>();
2754 for sequence in sequences {
2755 let branches = regex_sequence_branches(
2756 &self.facts,
2757 self.facts
2758 .iter()
2759 .enumerate()
2760 .filter(|(_, fact)| fact.parent == Some(sequence))
2761 .map(|(id, _)| id),
2762 );
2763 for branch in branches {
2764 let mut prefix_width = Some(0u32);
2765 for (index, &id) in branch.iter().enumerate() {
2766 let fact = &self.facts[id];
2767 if fact.kind == NormalizedKind::RegexAnchor
2768 && fact.construct.as_deref() == Some("^")
2769 && (prefix_width.is_some_and(|width| width > 0)
2770 || branch[..index].iter().any(|&prefix| {
2771 self.facts[prefix].construct.as_deref()
2772 == Some("lookbehind-positive")
2773 && self.facts.iter().any(|body| {
2774 body.kind == NormalizedKind::RegexSequence
2775 && body.parent == Some(prefix)
2776 && body.minimum_width.is_some_and(|width| width > 0)
2777 })
2778 }))
2779 {
2780 self.pending_risks.push((
2781 NormalizedKind::RegexRiskUnmatchableCaret,
2782 id,
2783 fact.start,
2784 fact.end,
2785 "caret-after-consuming-term".to_owned(),
2786 ));
2787 } else if fact.kind == NormalizedKind::RegexAnchor
2788 && fact.construct.as_deref() == Some("$")
2789 {
2790 let mut suffix_width = Some(0u32);
2791 for &suffix_id in &branch[index + 1..] {
2792 suffix_width = suffix_width.and_then(|width| {
2793 self.facts[suffix_id]
2794 .minimum_width
2795 .and_then(|suffix| width.checked_add(suffix))
2796 });
2797 }
2798 if suffix_width.is_some_and(|width| width > 0)
2799 || branch[index + 1..].iter().any(|&suffix| {
2800 self.facts[suffix].construct.as_deref()
2801 == Some("lookahead-positive")
2802 && self.facts.iter().any(|body| {
2803 body.kind == NormalizedKind::RegexSequence
2804 && body.parent == Some(suffix)
2805 && body.minimum_width.is_some_and(|width| width > 0)
2806 })
2807 })
2808 {
2809 self.pending_risks.push((
2810 NormalizedKind::RegexRiskUnmatchableDollar,
2811 id,
2812 fact.start,
2813 fact.end,
2814 "dollar-before-consuming-term".to_owned(),
2815 ));
2816 }
2817 }
2818 prefix_width =
2819 prefix_width.and_then(|width| width.checked_add(fact.minimum_width?));
2820 }
2821 }
2822 }
2823 }
2824
2825 fn classify_hostname(&mut self) {
2826 if self.incomplete.is_some() {
2827 return;
2828 }
2829
2830 #[derive(Clone, Copy)]
2836 enum Token {
2837 Character(char),
2838 WildcardDot(usize),
2839 StartAnchor,
2840 EndAnchor,
2841 }
2842 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
2843 enum LabelEnd {
2844 AlphaOne,
2845 AlphaMany,
2846 Alphanumeric,
2847 Hyphen,
2848 }
2849 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
2850 enum Shape {
2851 Start,
2852 Prefix(&'static str),
2853 FirstLabel,
2854 NextLabel,
2855 Label(bool, LabelEnd),
2856 Slash,
2857 End,
2858 }
2859 impl Shape {
2860 fn accepts(self) -> bool {
2861 matches!(
2862 self,
2863 Self::Label(true, LabelEnd::AlphaMany) | Self::Slash | Self::End
2864 )
2865 }
2866
2867 fn advance(self, token: Token) -> Vec<Self> {
2868 let character = match token {
2869 Token::StartAnchor => {
2870 return if self == Self::Start {
2871 vec![self]
2872 } else {
2873 Vec::new()
2874 };
2875 }
2876 Token::EndAnchor => {
2877 return if self.accepts() {
2878 vec![Self::End]
2879 } else {
2880 Vec::new()
2881 };
2882 }
2883 Token::Character(character) => character,
2884 Token::WildcardDot(_) => '.',
2885 };
2886 let first = if character.is_ascii_alphabetic() {
2887 Some(LabelEnd::AlphaOne)
2888 } else if character.is_ascii_digit() {
2889 Some(LabelEnd::Alphanumeric)
2890 } else {
2891 None
2892 };
2893 match self {
2894 Self::Start => {
2895 let mut next = first
2896 .map(|end| Self::Label(false, end))
2897 .into_iter()
2898 .collect::<Vec<_>>();
2899 if character == 'h' {
2900 next.extend([Self::Prefix("ttp://"), Self::Prefix("ttps://")]);
2901 }
2902 next
2903 }
2904 Self::Prefix(rest) => {
2905 if rest.starts_with(character) {
2906 let rest = &rest[character.len_utf8()..];
2907 vec![if rest.is_empty() {
2908 Self::FirstLabel
2909 } else {
2910 Self::Prefix(rest)
2911 }]
2912 } else {
2913 Vec::new()
2914 }
2915 }
2916 Self::FirstLabel | Self::NextLabel => first
2917 .map(|end| Self::Label(self == Self::NextLabel, end))
2918 .into_iter()
2919 .collect(),
2920 Self::Label(separated, end) => {
2921 let next = if character.is_ascii_alphabetic() {
2922 Some(Self::Label(
2923 separated,
2924 match end {
2925 LabelEnd::AlphaOne | LabelEnd::AlphaMany => LabelEnd::AlphaMany,
2926 LabelEnd::Alphanumeric | LabelEnd::Hyphen => {
2927 LabelEnd::Alphanumeric
2928 }
2929 },
2930 ))
2931 } else if character.is_ascii_digit() {
2932 Some(Self::Label(separated, LabelEnd::Alphanumeric))
2933 } else if character == '-' {
2934 Some(Self::Label(separated, LabelEnd::Hyphen))
2935 } else if character == '.' && end != LabelEnd::Hyphen {
2936 Some(Self::NextLabel)
2937 } else if character == '/' && self.accepts() {
2938 Some(Self::Slash)
2939 } else {
2940 None
2941 };
2942 next.into_iter().collect()
2943 }
2944 Self::Slash | Self::End => Vec::new(),
2945 }
2946 }
2947 }
2948
2949 let mut children = vec![Vec::new(); self.facts.len()];
2954 for (id, fact) in self.facts.iter().enumerate() {
2955 if let Some(parent) = fact.parent {
2956 children[parent].push(id);
2957 }
2958 }
2959 let mut edges = vec![Vec::<(usize, Option<Token>)>::new(); self.facts.len() * 2];
2960 for (id, fact) in self.facts.iter().enumerate() {
2961 if children[id]
2962 .iter()
2963 .any(|&child| self.facts[child].kind == NormalizedKind::RegexQuantifier)
2964 {
2965 continue;
2966 }
2967 let start = id * 2;
2968 let end = start + 1;
2969 match fact.kind {
2970 NormalizedKind::RegexPattern | NormalizedKind::RegexGroup
2971 if fact.kind == NormalizedKind::RegexPattern
2972 || matches!(
2973 fact.construct.as_deref(),
2974 Some("capturing" | "named-capturing" | "noncapturing" | "scoped-flags")
2975 ) =>
2976 {
2977 for &sequence in &children[id] {
2978 if self.facts[sequence].kind == NormalizedKind::RegexSequence {
2979 edges[start].push((sequence * 2, None));
2980 edges[sequence * 2 + 1].push((end, None));
2981 }
2982 }
2983 }
2984 NormalizedKind::RegexSequence => {
2985 for branch in regex_sequence_branches(&self.facts, children[id].iter().copied())
2986 {
2987 let mut previous = start;
2988 for child in branch {
2989 edges[previous].push((child * 2, None));
2990 previous = child * 2 + 1;
2991 }
2992 edges[previous].push((end, None));
2993 }
2994 }
2995 _ => {
2996 let token = match fact.kind {
2997 NormalizedKind::RegexWildcard
2998 if fact.construct.as_deref() == Some("dot") =>
2999 {
3000 Some(Token::WildcardDot(id))
3001 }
3002 NormalizedKind::RegexAnchor => match fact.construct.as_deref() {
3003 Some("^" | "boundary:A") => Some(Token::StartAnchor),
3004 Some("$" | "boundary:Z" | "boundary:z") => Some(Token::EndAnchor),
3005 _ => None,
3006 },
3007 NormalizedKind::RegexLiteral => {
3008 fact.construct.as_deref().and_then(|value| {
3009 let mut chars = value.chars();
3010 let first = chars.next()?;
3011 chars.next().is_none().then_some(Token::Character(first))
3012 })
3013 }
3014 NormalizedKind::RegexEscape => fact.escaped_literal.map(Token::Character),
3015 NormalizedKind::RegexCharacterClass => fact
3016 .first_set
3017 .as_ref()
3018 .and_then(single_character)
3019 .map(Token::Character),
3020 _ => None,
3021 };
3022 if let Some(token) = token {
3023 edges[start].push((end, Some(token)));
3024 }
3025 }
3026 }
3027 }
3028 let start = (0, Shape::Start);
3029 let mut reverse = HashMap::<_, Vec<(_, Option<usize>)>>::from([(start, Vec::new())]);
3030 let mut pending = VecDeque::from([start]);
3031 while let Some((boundary, shape)) = pending.pop_front() {
3032 for &(target, token) in &edges[boundary] {
3033 let next_shapes = token.map_or_else(|| vec![shape], |token| shape.advance(token));
3034 let risk = match token {
3035 Some(Token::WildcardDot(id)) => Some(id),
3036 _ => None,
3037 };
3038 for next_shape in next_shapes {
3039 let next = (target, next_shape);
3040 let newly_seen = !reverse.contains_key(&next);
3041 reverse
3042 .entry(next)
3043 .or_default()
3044 .push(((boundary, shape), risk));
3045 if newly_seen {
3046 pending.push_back(next);
3047 }
3048 }
3049 }
3050 }
3051 let mut pending = reverse
3052 .keys()
3053 .copied()
3054 .filter(|&(boundary, shape)| boundary == 1 && shape.accepts())
3055 .collect::<Vec<_>>();
3056 let mut visited = HashSet::new();
3057 let mut risks = HashSet::new();
3058 while let Some(node) = pending.pop() {
3059 if !visited.insert(node) {
3060 continue;
3061 }
3062 for &(previous, risk) in &reverse[&node] {
3063 risks.extend(risk);
3064 pending.push(previous);
3065 }
3066 }
3067 let mut risks = risks.into_iter().collect::<Vec<_>>();
3068 risks.sort_unstable();
3069 for dot in risks {
3070 let fact = &self.facts[dot];
3071 self.pending_risks.push((
3072 NormalizedKind::RegexRiskHostname,
3073 dot,
3074 fact.start,
3075 fact.end,
3076 "unescaped-dot-in-hostname-like-pattern".to_owned(),
3077 ));
3078 }
3079 }
3080
3081 fn classify_bad_tag(&mut self) {
3082 if self.incomplete.is_some() || !self.flags.complete {
3083 return;
3084 }
3085 let value = &self.decoded.value;
3086 if !(value.contains('<') && value.contains('>'))
3087 && !["-->", "--!>", "<--", "<!--"]
3088 .iter()
3089 .any(|marker| value.contains(marker))
3090 {
3091 return;
3092 }
3093 let rust_pattern = value
3094 .replace("\\/", "/")
3095 .replace("\\\"", "\"")
3096 .replace("\\'", "'");
3097 let mut builder = regex::RegexBuilder::new(&rust_pattern);
3098 builder
3099 .case_insensitive(self.flags.ignorecase)
3100 .multi_line(self.flags.multiline)
3101 .dot_matches_new_line(self.flags.dotall)
3102 .unicode(!self.flags.ascii)
3103 .ignore_whitespace(self.flags.verbose);
3104 let Ok(regex) = builder.build() else {
3105 return;
3106 };
3107 let capture_nfa = match RegexNfa::compile(&self.facts, 0, None) {
3108 Ok(nfa) => Some(nfa),
3109 Err(error) => {
3110 self.mark_incomplete(
3111 0,
3112 self.chars.len(),
3113 match error {
3114 NfaError::Unsupported => "bad-tag-nfa-unsupported",
3115 NfaError::Budget => "bad-tag-nfa-state-budget",
3116 },
3117 );
3118 return;
3119 }
3120 };
3121 let matches = |sample: &str| {
3122 capture_nfa.as_ref().map_or_else(
3123 || regex.is_match(sample),
3124 |nfa| nfa.match_prefix_capture_groups(sample).0,
3125 )
3126 };
3127 let groups = |sample: &str| {
3128 capture_nfa.as_ref().map_or_else(
3129 || {
3130 regex
3131 .captures(sample)
3132 .map(|captures| {
3133 captures
3134 .iter()
3135 .enumerate()
3136 .skip(1)
3137 .filter_map(|(index, capture)| capture.map(|_| index))
3138 .collect::<HashSet<_>>()
3139 })
3140 .unwrap_or_default()
3141 },
3142 |nfa| nfa.match_prefix_capture_groups(sample).1,
3143 )
3144 };
3145 let comment = "<!-- foo -->";
3146 let comment_bang = "<!-- foo --!>";
3147 let comment_groups = groups(comment);
3148 let comment_bang_groups = groups(comment_bang);
3149 let comment_group_mismatch = matches(comment)
3150 && matches(comment_bang)
3151 && comment_groups.iter().any(|group| {
3152 !comment_bang_groups.contains(group)
3153 && comment_bang_groups.iter().any(|other| group != other)
3154 });
3155 let plain_tag_groups = groups("<foo>");
3156 let only_comment_group = groups(comment).iter().any(|group| {
3157 plain_tag_groups.iter().any(|other| other != group)
3158 && !matches(comment_bang)
3159 && !groups("<!- foo ->").contains(group)
3160 && !plain_tag_groups.contains(group)
3161 && !groups("<script>").contains(group)
3162 });
3163 let bounded_script_opener = value
3164 .to_ascii_lowercase()
3165 .find("<script")
3166 .map(|start| {
3167 let start = value[..start].chars().count() + "<script".len();
3168 self.facts
3169 .iter()
3170 .filter(|fact| {
3171 fact.kind == NormalizedKind::RegexLiteral
3172 && fact.construct.as_deref() == Some(">")
3173 && fact.start >= start
3174 && !fact.parent.is_some_and(|parent| {
3175 self.facts[parent].kind == NormalizedKind::RegexCharacterClass
3176 })
3177 })
3178 .map(|fact| fact.start)
3179 .min()
3180 .is_some_and(|tag_end| {
3181 self.facts.iter().enumerate().any(|(id, fact)| {
3182 fact.kind == NormalizedKind::RegexCharacterClass
3183 && fact.start >= start
3184 && fact.end <= tag_end
3185 && fact
3186 .first_set
3187 .as_ref()
3188 .is_some_and(|set| !set.contains('>'))
3189 && self.facts.iter().any(|quantifier| {
3190 quantifier.kind == NormalizedKind::RegexQuantifier
3191 && quantifier.parent == Some(id)
3192 && quantifier
3193 .quantifier
3194 .is_some_and(|quantifier| quantifier.maximum.is_none())
3195 })
3196 })
3197 })
3198 })
3199 .unwrap_or(false);
3200 let script_warning = !bounded_script_opener
3201 && ((matches("<script>foo</script>")
3202 && matches("<script src=\"foo\"></script>")
3203 && !matches("<foo ></foo>")
3204 && (!matches("<script \n>foo</script>") || !matches("<script >foo\n</script>")))
3205 || (matches("<script>foo</script>")
3206 && matches("<script src=\"foo\"></script>")
3207 && !matches("<script src='foo'></script>")
3208 && !matches("<foo>"))
3209 || (matches("<script>foo</script>")
3210 && matches("<script src='foo'></script>")
3211 && !matches("<script src=\"foo\"></script>")
3212 && !matches("<foo>"))
3213 || (matches("<script>foo</script>")
3214 && matches("<script src='foo'></script>")
3215 && !matches("<script\tsrc='foo'></script>")
3216 && !matches("<foo>")
3217 && !matches("<foo src=\"foo\"></foo>"))
3218 || (matches("<script>foo</script>")
3219 && !self.flags.ignorecase
3220 && !matches("<foo>")
3221 && !matches("<foo ></foo>")
3222 && (!matches("<SCRIPT>foo</SCRIPT>")
3223 || (matches("<SCRIPT>foo</SCRIPT>") && !matches("<sCrIpT>foo</ScRiPt>"))))
3224 || (matches("<script src=\"foo\"></script>")
3225 && !matches("<foo>")
3226 && !matches("<foo ></foo>")
3227 && (!matches("<script src=\"foo\">foo</script >")
3228 || !matches("<script src=\"foo\">foo</script foo=\"bar\">")
3229 || !matches("<script src=\"foo\">foo</script\t\n bar>"))));
3230 let warning = comment_group_mismatch
3231 || only_comment_group
3232 || (matches("-->") && !matches("--!>") && !matches("--"))
3233 || (matches(comment)
3234 && !matches("<!-- foo\n -->")
3235 && !matches("<!- foo ->")
3236 && !matches("<foo>")
3237 && !matches("<script>"))
3238 || script_warning;
3239 if warning {
3240 self.pending_risks.push((
3241 NormalizedKind::RegexRiskBadTag,
3242 0,
3243 0,
3244 self.chars.len(),
3245 "matches-html-samples-inconsistently".to_owned(),
3246 ));
3247 }
3248 }
3249
3250 fn classify_polynomial_redos(&mut self) {
3251 if self.incomplete.is_some() || !self.flags.complete {
3252 return;
3253 }
3254 let sequences = self
3255 .facts
3256 .iter()
3257 .enumerate()
3258 .filter(|(_, fact)| fact.kind == NormalizedKind::RegexSequence)
3259 .map(|(id, _)| id)
3260 .collect::<Vec<_>>();
3261 for sequence in sequences {
3262 let mut branches = vec![Vec::new()];
3263 for (id, fact) in self.facts.iter().enumerate() {
3264 if fact.parent != Some(sequence) {
3265 continue;
3266 }
3267 if fact.kind == NormalizedKind::RegexAlternation {
3268 branches.push(Vec::new());
3269 } else {
3270 branches
3271 .last_mut()
3272 .expect("regex sequence always has a branch")
3273 .push(id);
3274 }
3275 }
3276 for branch in branches {
3277 let Some(&terminal_anchor) = branch.last() else {
3278 continue;
3279 };
3280 let terminal = &self.facts[terminal_anchor];
3281 if terminal.kind != NormalizedKind::RegexAnchor
3282 || terminal.construct.as_deref() != Some("$")
3283 {
3284 continue;
3285 }
3286
3287 let mut repeated = Vec::new();
3288 for (position, &atom) in branch[..branch.len() - 1].iter().enumerate() {
3289 if !matches!(
3290 self.facts[atom].kind,
3291 NormalizedKind::RegexLiteral
3292 | NormalizedKind::RegexEscape
3293 | NormalizedKind::RegexCharacterClass
3294 | NormalizedKind::RegexWildcard
3295 ) || self.facts[atom].minimum_width != Some(1)
3296 {
3297 continue;
3298 }
3299 let Some((quantifier_id, quantifier)) =
3300 self.facts.iter().enumerate().find_map(|(id, fact)| {
3301 (fact.kind == NormalizedKind::RegexQuantifier
3302 && fact.parent == Some(atom))
3303 .then_some((id, fact.quantifier?))
3304 })
3305 else {
3306 continue;
3307 };
3308 if quantifier.minimum == 1
3309 && quantifier.maximum.is_none()
3310 && !quantifier.possessive
3311 {
3312 repeated.push((
3313 position,
3314 atom,
3315 quantifier_id,
3316 self.facts[atom].first_set.clone(),
3317 ));
3318 }
3319 }
3320 if self.flags.ignorecase && !self.flags.ascii && !repeated.is_empty() {
3321 self.mark_incomplete(
3322 self.facts[sequence].start,
3323 self.facts[sequence].end,
3324 "redos-unicode-casefold-bound",
3325 );
3326 continue;
3327 }
3328
3329 let anchored_at_start = branch
3330 .iter()
3331 .take_while(|&&id| {
3332 (self.facts[id].kind == NormalizedKind::RegexAnchor
3333 && self.facts[id].construct.as_deref() == Some("^"))
3334 || (self.facts[id].kind == NormalizedKind::RegexGroup
3335 && self.facts[id].construct.as_deref() == Some("comment"))
3336 })
3337 .any(|&id| {
3338 self.facts[id].kind == NormalizedKind::RegexAnchor
3339 && self.facts[id].construct.as_deref() == Some("^")
3340 });
3341 let mut risky_quantifier = None;
3342 let mut overlapping_repeated_runs = false;
3343 'pairs: for left in 0..repeated.len() {
3344 for right in left + 1..repeated.len() {
3345 let (left_position, left_atom, _, _) = &repeated[left];
3346 let (right_position, right_atom, right_quantifier, _) = &repeated[right];
3347 if !polynomial_atoms_overlap(
3348 &self.facts[*left_atom],
3349 &self.facts[*right_atom],
3350 ) {
3351 continue;
3352 }
3353 let separable =
3354 branch[left_position + 1..*right_position]
3355 .iter()
3356 .all(|&separator| {
3357 if !matches!(
3358 self.facts[separator].kind,
3359 NormalizedKind::RegexLiteral
3360 | NormalizedKind::RegexEscape
3361 | NormalizedKind::RegexCharacterClass
3362 | NormalizedKind::RegexWildcard
3363 ) {
3364 return false;
3365 }
3366 let Some(quantifier) = self
3367 .facts
3368 .iter()
3369 .find(|fact| {
3370 fact.kind == NormalizedKind::RegexQuantifier
3371 && fact.parent == Some(separator)
3372 })
3373 .and_then(|fact| fact.quantifier)
3374 else {
3375 return false;
3376 };
3377 quantifier.minimum == 0
3378 && quantifier.maximum == Some(1)
3379 && !quantifier.possessive
3380 });
3381 if separable {
3382 risky_quantifier = Some(*right_quantifier);
3383 overlapping_repeated_runs = true;
3384 break 'pairs;
3385 }
3386 }
3387 }
3388
3389 if risky_quantifier.is_none() && self.api.scans_from_each_offset {
3390 risky_quantifier = (!anchored_at_start)
3391 .then(|| {
3392 repeated
3393 .first()
3394 .map(|(_, _, quantifier_id, _)| *quantifier_id)
3395 })
3396 .flatten();
3397 }
3398
3399 if let Some(quantifier) = risky_quantifier {
3400 self.pending_risks.push((
3401 NormalizedKind::RegexRiskPolynomial,
3402 quantifier,
3403 self.facts[quantifier].start,
3404 self.facts[quantifier].end,
3405 if overlapping_repeated_runs {
3406 "overlapping-repeated-runs".to_owned()
3407 } else {
3408 "unanchored-repeated-run-before-end-anchor".to_owned()
3409 },
3410 ));
3411 }
3412 }
3413 }
3414 }
3415
3416 fn has_rejecting_suffix(&self, repeated_group: usize) -> bool {
3417 let mut member = repeated_group;
3418 loop {
3419 let Some(sequence) = self.facts[member].parent else {
3420 return false;
3421 };
3422 if self.facts[sequence].kind != NormalizedKind::RegexSequence {
3423 return false;
3424 }
3425 let mut after_member = false;
3426 for (id, fact) in self.facts.iter().enumerate() {
3427 if fact.parent != Some(sequence) {
3428 continue;
3429 }
3430 if id == member {
3431 after_member = true;
3432 continue;
3433 }
3434 if !after_member {
3435 continue;
3436 }
3437 if fact.kind == NormalizedKind::RegexAlternation {
3438 break;
3439 }
3440 if fact.kind == NormalizedKind::RegexAnchor
3441 && matches!(
3442 fact.construct.as_deref(),
3443 Some("$" | "boundary:Z" | "boundary:z")
3444 )
3445 {
3446 return true;
3447 }
3448 if fact.kind == NormalizedKind::RegexGroup
3449 && fact.construct.as_deref() == Some("lookahead-positive")
3450 && self.facts.iter().any(|body| {
3451 body.kind == NormalizedKind::RegexSequence
3452 && body.parent == Some(id)
3453 && body.minimum_width.is_some_and(|width| width > 0)
3454 })
3455 {
3456 return true;
3457 }
3458 if fact.minimum_width.is_some_and(|width| width > 0) {
3459 return true;
3460 }
3461 }
3462 let Some(owner) = self.facts[sequence].parent else {
3463 return false;
3464 };
3465 if self.facts[owner].kind != NormalizedKind::RegexGroup {
3466 return false;
3467 }
3468 member = owner;
3469 }
3470 }
3471
3472 fn suffix_is_covered_by_repeated_body(&self, repeated_group: usize) -> bool {
3473 let Some(body_set) = self.facts[repeated_group].first_set.as_ref() else {
3474 return false;
3475 };
3476 let mut member = repeated_group;
3477 loop {
3478 let Some(sequence) = self.facts[member].parent else {
3479 return false;
3480 };
3481 if self.facts[sequence].kind != NormalizedKind::RegexSequence {
3482 return false;
3483 }
3484 let mut after_member = false;
3485 let mut found_suffix = false;
3486 for (id, fact) in self.facts.iter().enumerate() {
3487 if fact.parent != Some(sequence) {
3488 continue;
3489 }
3490 if fact.kind == NormalizedKind::RegexAlternation {
3491 break;
3492 }
3493 if id == member {
3494 after_member = true;
3495 continue;
3496 }
3497 if !after_member {
3498 continue;
3499 }
3500 if fact.kind == NormalizedKind::RegexAnchor {
3501 return false;
3502 }
3503 if fact.minimum_width.is_some_and(|width| width > 0) {
3504 found_suffix = true;
3505 if fact.first_set.as_ref() != Some(body_set) {
3506 return false;
3507 }
3508 }
3509 }
3510 if found_suffix {
3511 return true;
3512 }
3513 let Some(owner) = self.facts[sequence].parent else {
3514 return false;
3515 };
3516 if self.facts[owner].kind != NormalizedKind::RegexGroup {
3517 return false;
3518 }
3519 member = owner;
3520 }
3521 }
3522
3523 fn classify_redos(&mut self) {
3524 if self.incomplete.is_some() {
3525 return;
3526 }
3527 let groups = (0..self.facts.len())
3528 .filter(|&id| self.facts[id].kind == NormalizedKind::RegexGroup)
3529 .collect::<Vec<_>>();
3530 let mut repeated_groups = Vec::new();
3531 for group in groups {
3532 let Some(sequence) = self.facts.iter().position(|fact| {
3533 fact.kind == NormalizedKind::RegexSequence && fact.parent == Some(group)
3534 }) else {
3535 continue;
3536 };
3537 let Some(outer_quantifier) = self.facts.iter().position(|fact| {
3538 fact.kind == NormalizedKind::RegexQuantifier && fact.parent == Some(group)
3539 }) else {
3540 continue;
3541 };
3542 let Some(quantifier) = self.facts[outer_quantifier].quantifier else {
3543 continue;
3544 };
3545 if quantifier.maximum.is_some() {
3546 continue;
3547 }
3548 if quantifier.possessive {
3549 continue;
3550 }
3551 repeated_groups.push((group, sequence, outer_quantifier));
3552 }
3553 if repeated_groups.len() > MAX_REDOSE_GROUPS {
3554 self.mark_incomplete(0, self.chars.len(), "redos-group-budget");
3555 return;
3556 }
3557 let mut relation_budget = MAX_NFA_PRODUCT_PAIRS;
3558 for (group, sequence, outer_quantifier) in repeated_groups {
3559 let mut risk_found = false;
3560 let branch_count = self
3561 .facts
3562 .iter()
3563 .filter(|fact| {
3564 fact.kind == NormalizedKind::RegexAlternation && fact.parent == Some(sequence)
3565 })
3566 .count()
3567 + 1;
3568 if branch_count > 1 {
3569 if branch_count > MAX_REDOSE_ALTERNATIVES {
3570 self.mark_incomplete(
3571 self.facts[group].start,
3572 self.facts[group].end,
3573 "redos-alternative-budget",
3574 );
3575 continue;
3576 }
3577 if self.flags.ignorecase && !self.flags.ascii {
3578 self.mark_incomplete(
3579 self.facts[group].start,
3580 self.facts[group].end,
3581 "redos-unicode-casefold-bound",
3582 );
3583 }
3584 let branches = (0..branch_count)
3585 .map(|branch| RegexNfa::compile(&self.facts, sequence, Some(branch)))
3586 .collect::<Result<Vec<_>, _>>();
3587 let repeated = RegexNfa::compile(&self.facts, sequence, None)
3588 .and_then(RegexNfa::positive_closure);
3589 match (branches, repeated) {
3590 (Ok(branches), Ok(repeated)) => {
3591 let mut overlap = false;
3592 let mut budget_exceeded = false;
3593 'pairs: for left in 0..branches.len() {
3594 for right in left + 1..branches.len() {
3595 match branches[left].intersects_on_nonempty_string(
3596 &branches[right],
3597 &mut relation_budget,
3598 ) {
3599 Ok(true) => {
3600 overlap = true;
3601 break 'pairs;
3602 }
3603 Ok(false) => {}
3604 Err(NfaError::Budget) => {
3605 budget_exceeded = true;
3606 break 'pairs;
3607 }
3608 Err(NfaError::Unsupported) => unreachable!(
3609 "product comparison only rejects bounded automata"
3610 ),
3611 }
3612 match branches[left].has_repartitioned_prefix(
3613 &branches[right],
3614 &repeated,
3615 &mut relation_budget,
3616 ) {
3617 Ok(true) => {
3618 overlap = true;
3619 break 'pairs;
3620 }
3621 Ok(false) => {}
3622 Err(NfaError::Budget) => {
3623 budget_exceeded = true;
3624 break 'pairs;
3625 }
3626 Err(NfaError::Unsupported) => unreachable!(
3627 "prefix comparison only rejects bounded automata"
3628 ),
3629 }
3630 match branches[right].has_repartitioned_prefix(
3631 &branches[left],
3632 &repeated,
3633 &mut relation_budget,
3634 ) {
3635 Ok(true) => {
3636 overlap = true;
3637 break 'pairs;
3638 }
3639 Ok(false) => {}
3640 Err(NfaError::Budget) => {
3641 budget_exceeded = true;
3642 break 'pairs;
3643 }
3644 Err(NfaError::Unsupported) => unreachable!(
3645 "prefix comparison only rejects bounded automata"
3646 ),
3647 }
3648 }
3649 }
3650 if overlap && self.has_rejecting_suffix(group) {
3651 risk_found = true;
3652 self.pending_risks.push((
3653 NormalizedKind::RegexRiskExponential,
3654 outer_quantifier,
3655 self.facts[outer_quantifier].start,
3656 self.facts[outer_quantifier].end,
3657 "overlapping-repeated-alternatives".to_owned(),
3658 ));
3659 }
3660 if budget_exceeded {
3661 self.mark_incomplete(
3662 self.facts[group].start,
3663 self.facts[group].end,
3664 "redos-nfa-product-budget",
3665 );
3666 }
3667 }
3668 (Err(error), _) | (_, Err(error)) => self.mark_incomplete(
3669 self.facts[group].start,
3670 self.facts[group].end,
3671 match error {
3672 NfaError::Unsupported => "redos-nfa-unsupported",
3673 NfaError::Budget => "redos-nfa-state-budget",
3674 },
3675 ),
3676 }
3677 }
3678 let has_backtracking_child = (0..self.facts.len()).any(|candidate| {
3679 candidate != outer_quantifier
3680 && self.is_descendant(candidate, group)
3681 && matches!(
3682 self.facts[candidate].kind,
3683 NormalizedKind::RegexAlternation | NormalizedKind::RegexQuantifier
3684 )
3685 });
3686 if has_backtracking_child && !risk_found {
3687 if self.flags.ignorecase && !self.flags.ascii {
3688 self.mark_incomplete(
3689 self.facts[group].start,
3690 self.facts[group].end,
3691 "redos-unicode-casefold-bound",
3692 );
3693 }
3694 match RegexNfa::compile(&self.facts, group, None)
3695 .and_then(|nfa| nfa.has_pumpable_fork(&mut relation_budget))
3696 {
3697 Ok(true)
3698 if self.has_rejecting_suffix(group)
3699 && !self.suffix_is_covered_by_repeated_body(group) =>
3700 {
3701 self.pending_risks.push((
3702 NormalizedKind::RegexRiskExponential,
3703 outer_quantifier,
3704 self.facts[outer_quantifier].start,
3705 self.facts[outer_quantifier].end,
3706 "overlapping-nfa-cycles".to_owned(),
3707 ));
3708 }
3709 Ok(_) => {}
3710 Err(error) => self.mark_incomplete(
3711 self.facts[group].start,
3712 self.facts[group].end,
3713 match error {
3714 NfaError::Unsupported => "redos-nfa-unsupported",
3715 NfaError::Budget => "redos-nfa-product-budget",
3716 },
3717 ),
3718 }
3719 }
3720 }
3721 }
3722
3723 fn is_descendant(&self, mut fact: usize, ancestor: usize) -> bool {
3724 while let Some(parent) = self.facts[fact].parent {
3725 if parent == ancestor {
3726 return true;
3727 }
3728 fact = parent;
3729 }
3730 false
3731 }
3732
3733 fn append_pending_facts(&mut self) {
3734 for (kind, parent, start, end, reason) in std::mem::take(&mut self.pending_risks) {
3735 if self.facts.len() >= MAX_PATTERN_FACTS - 1 {
3736 self.exceeded = true;
3737 break;
3738 }
3739 self.add_fact(kind, start, end, Some(parent), Some(reason), None);
3740 }
3741 if let Some((start, end, reason)) = self.incomplete.take() {
3742 self.add_fact(
3743 NormalizedKind::RegexIncomplete,
3744 start,
3745 end,
3746 Some(0),
3747 Some(reason),
3748 None,
3749 );
3750 }
3751 }
3752}
3753
3754fn is_overly_broad_range(start: char, end: char) -> bool {
3755 let low = start as u32;
3756 let high = end as u32;
3757 let width = high.saturating_sub(low).saturating_add(1);
3758 if width < 10 {
3759 return false;
3760 }
3761 let alphanumeric = |character: char| character.is_ascii_alphanumeric();
3762 let wide_ascii = low < 128 && high < 128;
3763 let spans_letters = low <= 'Z' as u32 && high >= 'a' as u32;
3764 let spans_digits_to_letters = low <= '9' as u32 && high >= 'A' as u32;
3765 let has_non_alphanumeric_boundary = !alphanumeric(start) || !alphanumeric(end);
3766 let allowed = end == '~'
3767 || matches!(start, ' ' | '!')
3768 || start == '\0'
3769 || (start == '0' && end == '?')
3770 || (start == '@' && end == '_')
3771 || (start == '@' && end == 'Z');
3772 !allowed
3773 && (spans_letters
3774 || spans_digits_to_letters
3775 || (wide_ascii && has_non_alphanumeric_boundary))
3776}
3777
3778fn codeql_range_character_set(start: char, end: char) -> CharacterSet {
3779 let codepoint = |character: char| {
3780 let value = character as u32;
3781 if value > 0xffff { 0xfffd } else { value }
3782 };
3783 let start = codepoint(start);
3784 let end = codepoint(end);
3785 CharacterSet {
3786 ranges: (start <= end).then_some((start, end)).into_iter().collect(),
3787 }
3788}
3789
3790fn category_character_set(escaped: char, ascii: bool) -> Option<CharacterSet> {
3791 let (category, negated) = match escaped {
3792 'd' => ('d', false),
3793 'D' => ('d', true),
3794 's' => ('s', false),
3795 'S' => ('s', true),
3796 'w' => ('w', false),
3797 'W' => ('w', true),
3798 _ => return None,
3799 };
3800 let mut set = match category {
3801 'd' => CharacterSet {
3802 ranges: vec![(b'0' as u32, b'9' as u32)],
3803 },
3804 's' => CharacterSet {
3805 ranges: vec![(9, 13), (32, 32)],
3806 },
3807 'w' => CharacterSet {
3808 ranges: vec![
3809 (b'0' as u32, b'9' as u32),
3810 (b'A' as u32, b'Z' as u32),
3811 (b'_' as u32, b'_' as u32),
3812 (b'a' as u32, b'z' as u32),
3813 ],
3814 },
3815 _ => unreachable!("category is selected from d, s, w"),
3816 };
3817 if !ascii && negated {
3818 }
3821 if !ascii && negated {
3822 return None;
3823 }
3824 if negated {
3825 set = set.complement();
3826 }
3827 Some(set)
3828}
3829
3830fn parse_regex_escape(
3831 chars: &[char],
3832 start: usize,
3833 in_class: bool,
3834 ascii: bool,
3835) -> Option<EscapeValue> {
3836 if chars.get(start) != Some(&'\\') {
3837 return None;
3838 }
3839 let escaped = *chars.get(start + 1)?;
3840 let mut end = start + 2;
3841 let mut value = None;
3842 let mut anchor = false;
3843 let mut incomplete = None;
3844 let mut backspace = false;
3845 let construct = match escaped {
3846 'A' | 'Z' | 'z' | 'b' | 'B' if !in_class => {
3847 anchor = true;
3848 Some(format!("boundary:{escaped}"))
3849 }
3850 'd' | 'D' | 's' | 'S' | 'w' | 'W' => Some(format!("category:{escaped}")),
3851 'a' => {
3852 value = Some('\u{7}');
3853 Some("U+0007".to_owned())
3854 }
3855 'f' => {
3856 value = Some('\u{c}');
3857 Some("U+000C".to_owned())
3858 }
3859 'n' => {
3860 value = Some('\n');
3861 Some("U+000A".to_owned())
3862 }
3863 'r' => {
3864 value = Some('\r');
3865 Some("U+000D".to_owned())
3866 }
3867 't' => {
3868 value = Some('\t');
3869 Some("U+0009".to_owned())
3870 }
3871 'v' => {
3872 value = Some('\u{b}');
3873 Some("U+000B".to_owned())
3874 }
3875 'b' if in_class => {
3876 value = Some('\u{8}');
3877 backspace = true;
3878 Some("backspace".to_owned())
3879 }
3880 'x' | 'u' | 'U' => {
3881 let digits = match escaped {
3882 'x' => 2,
3883 'u' => 4,
3884 _ => 8,
3885 };
3886 let scalar = hex_value(chars, start + 2, digits);
3887 end = start + 2 + digits;
3888 match scalar.and_then(char::from_u32) {
3889 Some(character) => {
3890 value = Some(character);
3891 Some(format!("U+{:04X}", character as u32))
3892 }
3893 None => {
3894 incomplete = Some("invalid-hex-escape");
3895 Some(format!("invalid:{escaped}"))
3896 }
3897 }
3898 }
3899 'N' => {
3900 incomplete = Some("named-unicode-escape");
3901 if chars.get(start + 2) == Some(&'{') {
3902 let mut close = start + 3;
3903 while close < chars.len() && chars[close] != '}' {
3904 close += 1;
3905 }
3906 end = (close + 1).min(chars.len());
3907 }
3908 Some("unicode-name".to_owned())
3909 }
3910 '0'..='7' => {
3911 let mut codepoint = escaped as u32 - '0' as u32;
3912 let mut digits = 1usize;
3913 while digits < 3 {
3914 let Some(digit @ '0'..='7') = chars.get(start + 1 + digits).copied() else {
3915 break;
3916 };
3917 codepoint = codepoint * 8 + (digit as u32 - '0' as u32);
3918 digits += 1;
3919 }
3920 end = start + 1 + digits;
3921 match char::from_u32(codepoint) {
3922 Some(character) => {
3923 value = Some(character);
3924 Some(format!("U+{codepoint:04X}"))
3925 }
3926 None => {
3927 incomplete = Some("invalid-octal-escape");
3928 Some("invalid-octal".to_owned())
3929 }
3930 }
3931 }
3932 '1'..='9' if !in_class => {
3933 incomplete = Some("backreference-not-supported");
3934 Some(format!("backreference:{escaped}"))
3935 }
3936 'g' | 'k' if !in_class => {
3937 incomplete = Some("named-backreference-not-supported");
3938 Some(format!("backreference:{escaped}"))
3939 }
3940 other if other.is_ascii_alphabetic() => {
3941 incomplete = Some("unknown-regex-escape");
3942 Some(format!("unknown:{other}"))
3943 }
3944 other => {
3945 value = Some(other);
3946 Some(other.to_string())
3947 }
3948 };
3949 if end > chars.len() {
3950 return None;
3951 }
3952 let first_set = value
3953 .map(CharacterSet::one)
3954 .or_else(|| category_character_set(escaped, ascii));
3955 Some(EscapeValue {
3956 end,
3957 anchor,
3958 construct,
3959 first_set,
3960 incomplete,
3961 backspace,
3962 })
3963}
3964
3965fn parse_class_atom(chars: &[char], start: usize, ascii: bool) -> Option<ClassAtom> {
3966 let value = *chars.get(start)?;
3967 if value != '\\' {
3968 return Some(ClassAtom {
3969 start,
3970 end: start + 1,
3971 value: Some(value),
3972 set: Some(CharacterSet::one(value)),
3973 kind: NormalizedKind::RegexLiteral,
3974 construct: Some(value.to_string()),
3975 backspace: false,
3976 incomplete: None,
3977 });
3978 }
3979 let escape = parse_regex_escape(chars, start, true, ascii)?;
3980 Some(ClassAtom {
3981 start,
3982 end: escape.end,
3983 value: escape.first_set.as_ref().and_then(single_character),
3984 set: escape.first_set,
3985 kind: NormalizedKind::RegexEscape,
3986 construct: escape.construct,
3987 backspace: escape.backspace,
3988 incomplete: escape.incomplete,
3989 })
3990}
3991
3992fn single_character(set: &CharacterSet) -> Option<char> {
3993 let [(start, end)] = set.ranges.as_slice() else {
3994 return None;
3995 };
3996 (start == end).then(|| char::from_u32(*start)).flatten()
3997}
3998
3999fn facts_to_embedded(
4000 facts: Vec<Fact>,
4001 decoded: &DecodedValue,
4002 source: &str,
4003) -> Vec<EmbeddedLeafFact> {
4004 let mut children = vec![Vec::new(); facts.len()];
4005 let mut roots = Vec::new();
4006 for (id, fact) in facts.iter().enumerate() {
4007 if let Some(parent) = fact.parent {
4008 assert!(
4009 parent < id,
4010 "regex facts are parent-before-child before ordering"
4011 );
4012 children[parent].push(id);
4013 } else {
4014 roots.push(id);
4015 }
4016 }
4017 let order_children = |ids: &mut Vec<usize>| {
4018 ids.sort_by_key(|id| (facts[*id].start, facts[*id].end, *id));
4019 };
4020 order_children(&mut roots);
4021 for child_ids in &mut children {
4022 order_children(child_ids);
4023 }
4024 let mut order = Vec::with_capacity(facts.len());
4025 let mut stack = roots.into_iter().rev().collect::<Vec<_>>();
4026 while let Some(id) = stack.pop() {
4027 order.push(id);
4028 stack.extend(children[id].iter().rev().copied());
4029 }
4030 assert_eq!(
4031 order.len(),
4032 facts.len(),
4033 "regex AST fact tree reaches every node"
4034 );
4035 let mut remap = vec![0usize; facts.len()];
4036 for (new, old) in order.iter().copied().enumerate() {
4037 remap[old] = new;
4038 }
4039 let line_starts = compute_line_starts(source);
4040 order
4041 .into_iter()
4042 .map(|old| {
4043 let fact = &facts[old];
4044 let range = mapped_source_range(fact.start, fact.end, decoded, &line_starts);
4045 EmbeddedLeafFact {
4046 kind: fact.kind,
4047 range,
4048 parent: fact.parent.map(|parent| remap[parent]),
4049 construct: fact.construct.clone(),
4050 occurrence_role: None,
4051 }
4052 })
4053 .collect()
4054}
4055
4056fn mapped_source_range(
4057 start: usize,
4058 end: usize,
4059 decoded: &DecodedValue,
4060 line_starts: &[usize],
4061) -> Range {
4062 assert!(
4063 start <= end && end <= decoded.chars.len(),
4064 "regex parser ranges stay within the decoded pattern"
4065 );
4066 let (start_byte, end_byte) = if start < end {
4067 (
4068 decoded.chars[start].source_start,
4069 decoded.chars[end - 1].source_end,
4070 )
4071 } else {
4072 let boundary = decoded.chars.get(start).map_or_else(
4076 || {
4077 decoded
4078 .chars
4079 .last()
4080 .map_or(decoded.source_range.start_byte, |last| last.source_end)
4081 },
4082 |next| next.source_start,
4083 );
4084 (boundary, boundary)
4085 };
4086 Range {
4087 start_byte,
4088 end_byte,
4089 start_line: find_line_index_for_offset(line_starts, start_byte) + 1,
4090 end_line: find_line_index_for_offset(
4091 line_starts,
4092 end_byte.saturating_sub(1).max(start_byte),
4093 ) + 1,
4094 }
4095}
4096
4097#[cfg(test)]
4098mod tests {
4099 use super::*;
4100 use brokk_bifrost_test_macros::hardwon;
4101
4102 #[test]
4103 fn public_string_value_decoder_decodes_escaped_format_braces() {
4104 let source = r#"value = "\x7b0\x7d""#;
4105 let mut parser = tree_sitter::Parser::new();
4106 parser
4107 .set_language(&tree_sitter_python::LANGUAGE.into())
4108 .expect("Python grammar is valid");
4109 let tree = parser.parse(source, None).expect("parser returns a tree");
4110 let mut pending = vec![tree.root_node()];
4111 let mut string = None;
4112 while let Some(node) = pending.pop() {
4113 if node.kind() == "string" {
4114 string = Some(node);
4115 break;
4116 }
4117 let mut cursor = node.walk();
4118 pending.extend(node.named_children(&mut cursor));
4119 }
4120 let string = string.expect("fixture contains a string expression");
4121 let mut cursor = string.walk();
4122 let parts = string
4123 .named_children(&mut cursor)
4124 .map(|child| (child.kind().to_owned(), node_text(child, source).to_owned()))
4125 .collect::<Vec<_>>();
4126 assert_eq!(
4127 decode_python_string_value(string, source).expect("string literal decodes"),
4128 "{0}",
4129 "parts={parts:?}; string={}",
4130 string.to_sexp()
4131 );
4132 }
4133
4134 fn embedded_for_source(source: &str) -> Vec<EmbeddedLeafFact> {
4135 let mut parser = tree_sitter::Parser::new();
4136 parser
4137 .set_language(&tree_sitter_python::LANGUAGE.into())
4138 .expect("Python grammar is valid");
4139 let tree = parser.parse(source, None).expect("parser returns a tree");
4140 let root = tree.root_node();
4141 let mut pending = vec![root];
4142 let mut facts = module_assigned_facts(root, source);
4143 while let Some(node) = pending.pop() {
4144 if matches!(node.kind(), "call" | "string" | "concatenated_string") {
4145 facts.extend(embedded_facts(node, source));
4146 }
4147 let mut children = Vec::new();
4148 let mut cursor = node.walk();
4149 for child in node.named_children(&mut cursor) {
4150 children.push(child);
4151 }
4152 pending.extend(children.into_iter().rev());
4153 }
4154 facts
4155 }
4156
4157 #[hardwon(
4158 "#4874: bounded repetition cloned a fragment after concatenation linked its end outside"
4159 )]
4160 #[test]
4161 fn meshroom_assigned_regex_and_reduced_form_compile_without_incomplete_facts() {
4162 let patterns = [
4163 r"(?P<FILESTEM_PREFIX>.*?)(?P<FRAMEID_STR>[-._]\d+)?(?P<EXTENSION>\.\w{3,4})",
4164 r"(?P<run>a{3})",
4165 ];
4166
4167 for pattern in patterns {
4168 let source = format!("import re\npattern = r\"{pattern}\"\nre.compile(pattern)\n");
4169 let facts = embedded_for_source(&source);
4170 assert!(
4171 facts
4172 .iter()
4173 .any(|fact| fact.kind == NormalizedKind::RegexPattern),
4174 "pattern={pattern:?}; facts={facts:#?}"
4175 );
4176 assert!(
4177 facts
4178 .iter()
4179 .all(|fact| fact.kind != NormalizedKind::RegexIncomplete),
4180 "pattern={pattern:?}; facts={facts:#?}"
4181 );
4182 }
4183
4184 let unsupported = embedded_for_source(
4185 "import re\npattern = r\"(?P<anchored>^a)\"\nre.compile(pattern)\n",
4186 );
4187 assert!(
4188 unsupported.iter().any(|fact| {
4189 fact.kind == NormalizedKind::RegexIncomplete
4190 && fact.construct.as_deref() == Some("bad-tag-nfa-unsupported")
4191 }),
4192 "unsupported NFA construct must be explicit; facts={unsupported:#?}"
4193 );
4194 }
4195
4196 #[hardwon(
4197 "#4650: Python regex facts missed byte patterns, nested assigned values, Unicode ranges, and ReDoS"
4198 )]
4199 #[test]
4200 fn codeql_regex_facts_cover_byte_values_assignments_and_safe_redos_controls() {
4201 let source = r####"import re
4202re.compile(br"[\b\t ]")
4203re.compile(b"[AA]")
4204re.compile(br"(P<name>[\w]+)")
4205re.compile(b" ^abc")
4206re.compile(b"(?s) ^abc")
4207re.compile(b"abc$ ")
4208re.compile(r"[A-z]")
4209re.compile(r"^[z-a]$")
4210re.compile(r"[0-93-5]")
4211re.compile(r"[0-9\d]")
4212re.compile('[\U0001D173-\U0001D17A\U000E0020-\U000E007F\U000e0001]')
4213re.compile(r"<script.*?>.*?</script>", re.I)
4214re.compile(r"<script.*?>.*?</script>", re.re.DOTALL)
4215re.compile(r"""(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|<!(--.*?--\s*)+>)""", re.I)
4216VERBOSE_REGEX = r"""
4217 \[
4218 (?P<header>[^]]+) # very permissive!
4219 \]
4220 """
4221re.compile(VERBOSE_REGEX, re.VERBOSE)
4222re.compile(r"(a|.)*")
4223re.compile(r"(a|aa?)*b")
4224re.compile(r"^([a-z]+)+$")
4225MY_REG = r'X(\u0061|a)*Y'
4226def matcher(name):
4227 re.match(MY_REG, name)
4228def shadowed(name, MY_REG):
4229 re.match(MY_REG, name)
4230re.compile(r"((.|[^a])*)\"")
4231re.compile(r"((a|[^a])*)\"")
4232re.compile(r"(a+)+aaaaa*a+")
4233"####;
4234 let facts = embedded_for_source(source);
4235 let has = |kind| facts.iter().any(|fact| fact.kind == kind);
4236 assert!(has(NormalizedKind::RegexRiskBackspaceEscape), "{facts:#?}");
4237 assert!(
4238 has(NormalizedKind::RegexRiskDuplicateClassCharacter),
4239 "{facts:#?}"
4240 );
4241 assert!(
4242 has(NormalizedKind::RegexRiskIncompleteSpecialGroup),
4243 "{facts:#?}"
4244 );
4245 assert!(has(NormalizedKind::RegexRiskUnmatchableCaret), "{facts:#?}");
4246 assert!(
4247 has(NormalizedKind::RegexRiskUnmatchableDollar),
4248 "{facts:#?}"
4249 );
4250 assert!(has(NormalizedKind::RegexRiskRange), "{facts:#?}");
4251 assert!(has(NormalizedKind::RegexRiskBadTag), "{facts:#?}");
4252 assert!(has(NormalizedKind::RegexRiskExponential), "{facts:#?}");
4253 let overlapping_range_start = source
4254 .find("[0-93-5]")
4255 .expect("overlapping range sample is present")
4256 + 1;
4257 assert!(
4258 facts.iter().any(|fact| {
4259 fact.kind == NormalizedKind::RegexRiskRange
4260 && fact.range.start_byte == overlapping_range_start
4261 && fact.range.end_byte == overlapping_range_start + 3
4262 }),
4263 "{facts:#?}"
4264 );
4265 let line_with = |needle: &str| {
4266 source
4267 .lines()
4268 .position(|line| line.contains(needle))
4269 .expect("regex fixture line is present")
4270 + 1
4271 };
4272 let risk_lines = facts
4273 .iter()
4274 .filter(|fact| fact.kind == NormalizedKind::RegexRiskExponential)
4275 .map(|fact| fact.range.start_line)
4276 .collect::<HashSet<_>>();
4277 assert!(risk_lines.contains(&line_with("^([a-z]+)+$")), "{facts:#?}");
4278 assert!(risk_lines.contains(&line_with("MY_REG =")), "{facts:#?}");
4279 assert!(risk_lines.contains(&line_with("((.|[^a])*)")), "{facts:#?}");
4280 assert!(
4281 !risk_lines.contains(&line_with("((a|[^a])*)")),
4282 "{facts:#?}"
4283 );
4284 assert!(!risk_lines.contains(&line_with("aaaaa*a+")), "{facts:#?}");
4285 let shadowed_line = source
4286 .lines()
4287 .enumerate()
4288 .filter_map(|(index, line)| {
4289 line.contains("re.match(MY_REG, name)").then_some(index + 1)
4290 })
4291 .last()
4292 .expect("shadowed pattern reference is present");
4293 assert!(
4294 facts.iter().any(|fact| {
4295 fact.kind == NormalizedKind::RegexIncomplete
4296 && fact.range.start_line == shadowed_line
4297 && fact
4298 .construct
4299 .as_deref()
4300 .is_some_and(|reason| reason.starts_with("dynamic-pattern:"))
4301 }),
4302 "{facts:#?}"
4303 );
4304 assert!(
4305 facts.iter().any(|fact| {
4306 fact.kind == NormalizedKind::RegexRiskRange
4307 && fact.range.start_line == line_with("U0001D173")
4308 }),
4309 "{facts:#?}"
4310 );
4311 assert!(
4312 facts.iter().any(|fact| {
4313 fact.kind == NormalizedKind::RegexRiskDuplicateClassCharacter
4314 && fact.range.start_line == line_with("VERBOSE_REGEX =")
4315 }),
4316 "{facts:#?}"
4317 );
4318 assert!(
4319 facts.iter().any(|fact| {
4320 fact.kind == NormalizedKind::RegexRiskBadTag
4321 && fact.range.start_line == line_with("<[a-z")
4322 }),
4323 "{facts:#?}"
4324 );
4325 let nested_flag_line = source
4326 .lines()
4327 .position(|line| line.contains("re.re.DOTALL"))
4328 .expect("upstream nested DOTALL flag case is present")
4329 + 1;
4330 assert!(facts.iter().any(|fact| {
4331 fact.kind == NormalizedKind::RegexRiskBadTag
4332 && fact.range.start_line == nested_flag_line
4333 }));
4334 let safe_line = source
4335 .lines()
4336 .position(|line| line.contains("(a|.)*"))
4337 .expect("safe redos control is present")
4338 + 1;
4339 assert!(!facts.iter().any(|fact| {
4340 fact.kind == NormalizedKind::RegexRiskExponential && fact.range.start_line == safe_line
4341 }));
4342 assert!(facts.iter().any(|fact| {
4343 fact.kind == NormalizedKind::RegexPattern
4344 && fact
4345 .construct
4346 .as_deref()
4347 .is_some_and(|pattern| pattern.contains("very permissive"))
4348 }));
4349 }
4350
4351 #[test]
4352 fn decodes_python_unknown_escapes_and_adjacent_literals_before_regex_parse() {
4353 let source = r#"import re
4354re.compile("\d" "\." r"\.")
4355"#;
4356 let facts = embedded_for_source(source);
4357 let pattern = facts
4358 .iter()
4359 .find(|fact| fact.kind == NormalizedKind::RegexPattern)
4360 .expect("selected compile call has a regex pattern fact");
4361 assert_eq!(pattern.construct.as_deref(), Some(r"\d\.\."));
4362 assert!(
4363 facts
4364 .iter()
4365 .any(|fact| fact.kind == NormalizedKind::RegexEscape)
4366 );
4367 assert!(
4368 !facts
4369 .iter()
4370 .any(|fact| fact.kind == NormalizedKind::RegexIncomplete)
4371 );
4372 }
4373
4374 #[test]
4375 fn selects_only_recognized_pattern_arguments_and_marks_dynamic_values_incomplete() {
4376 let source = r#"import re
4377re.compile("selected")
4378re.sub("replacement", "pattern", "subject")
4379other.compile("unrecognized")
4380re.compile(pattern)
4381"#;
4382 let facts = embedded_for_source(source);
4383 let patterns = facts
4384 .iter()
4385 .filter(|fact| fact.kind == NormalizedKind::RegexPattern)
4386 .map(|fact| fact.construct.as_deref().expect("pattern stores value"))
4387 .collect::<Vec<_>>();
4388 assert_eq!(patterns, ["selected", "replacement"]);
4389 assert!(facts.iter().any(|fact| {
4390 fact.kind == NormalizedKind::RegexIncomplete
4391 && fact
4392 .construct
4393 .as_deref()
4394 .is_some_and(|construct| construct.starts_with("dynamic-pattern:"))
4395 }));
4396 assert!(!patterns.contains(&"pattern"));
4397 assert!(!patterns.contains(&"subject"));
4398 assert!(!patterns.contains(&"unrecognized"));
4399 }
4400
4401 #[test]
4402 fn supported_lookaround_groups_and_orphan_quantifiers_have_typed_facts() {
4403 let facts = embedded_for_source(
4404 "import re\nre.compile(r\"(?=lookahead)\")\nre.compile(r\"a$*\")\n",
4405 );
4406 assert!(facts.iter().any(|fact| {
4407 fact.kind == NormalizedKind::RegexGroup
4408 && fact.construct.as_deref() == Some("lookahead-positive")
4409 }));
4410 assert!(facts.iter().any(|fact| {
4411 fact.kind == NormalizedKind::RegexIncomplete
4412 && fact.construct.as_deref() == Some("quantifier-without-atom")
4413 }));
4414 }
4415
4416 #[hardwon(
4417 "#4454: zero-width regex facts escaped their embedded parent and aborted workspace indexing"
4418 )]
4419 #[test]
4420 fn zero_width_regex_facts_stay_within_their_embedded_parents() {
4421 for (source, malformed) in [
4422 ("import re\nre.compile(r'^(v)(.*)()$')\n", false),
4423 ("import re\nre.subn(\"()\"r' am I'\"??\")\n", false),
4424 (
4425 "import re\nre.compile(r\"invalid literal for int().*: '?'?\")\n",
4426 false,
4427 ),
4428 ("import re\nre.compile(\"ab()+cd\")\n", false),
4429 ("import re\nre.sub('()x', r'\\g<0>\\g<0>', 'xx')\n", false),
4430 ("import re\nre.search('(', 'test')\n", true),
4431 ] {
4432 let facts = embedded_for_source(source);
4433 assert!(
4434 facts
4435 .iter()
4436 .any(|fact| fact.kind == NormalizedKind::RegexPattern),
4437 "source={source:?}; facts={facts:#?}"
4438 );
4439 for (fact_id, fact) in facts.iter().enumerate() {
4440 if let Some(parent) = fact.parent {
4441 assert!(parent < fact_id, "source={source:?}; facts={facts:#?}");
4442 let parent_fact = &facts[parent];
4443 assert!(
4444 parent_fact.range.start_byte <= fact.range.start_byte
4445 && fact.range.end_byte <= parent_fact.range.end_byte,
4446 "source={source:?}; fact={fact:#?}; parent={parent_fact:#?}"
4447 );
4448 }
4449 }
4450 if !malformed {
4451 assert!(
4452 facts.iter().any(|fact| {
4453 fact.kind == NormalizedKind::RegexSequence
4454 && fact.range.start_byte == fact.range.end_byte
4455 }),
4456 "source={source:?} retains its zero-width empty-group sequence; facts={facts:#?}"
4457 );
4458 }
4459 if malformed {
4460 assert!(
4461 facts
4462 .iter()
4463 .any(|fact| fact.kind == NormalizedKind::RegexIncomplete),
4464 "malformed pattern remains structurally unavailable; facts={facts:#?}"
4465 );
4466 }
4467 }
4468 }
4469
4470 #[test]
4471 fn polynomial_redos_facts_cover_search_restarts_and_overlapping_repetitions() {
4472 for (source, expected) in [
4473 ("import re\nre.search(r'\\s+$', value)\n", true),
4474 ("import re\nre.search(r'^\\s+$', value)\n", false),
4475 ("import re\nre.search(r'^\\d+E?\\d+$', value)\n", true),
4476 ("import re\nre.search(r'^\\d+E\\d+$', value)\n", false),
4477 ("import re\nre.match(r'\\s+$', value)\n", false),
4478 ("import re\nre.compile(r'\\s+$')\n", false),
4479 ] {
4480 let facts = embedded_for_source(source);
4481 assert_eq!(
4482 facts
4483 .iter()
4484 .any(|fact| fact.kind == NormalizedKind::RegexRiskPolynomial),
4485 expected,
4486 "source={source:?}; facts={facts:#?}"
4487 );
4488 }
4489 }
4490
4491 #[test]
4492 #[hardwon("#4443: an empty embedded regex sequence escaped its parent source range")]
4493 fn unquantified_empty_group_keeps_embedded_regex_facts_inside_parents() {
4494 let source = "import re\nre.search(r'function (no)?watchplus()', webpage)\n";
4495 let facts = embedded_for_source(source);
4496 assert!(
4497 facts
4498 .iter()
4499 .any(|fact| fact.kind == NormalizedKind::RegexPattern)
4500 );
4501 for (index, fact) in facts.iter().enumerate() {
4502 if let Some(parent) = fact.parent {
4503 assert!(parent < index, "{fact:?}");
4504 let parent_fact = &facts[parent];
4505 assert!(
4506 parent_fact.range.start_byte <= fact.range.start_byte
4507 && fact.range.end_byte <= parent_fact.range.end_byte,
4508 "child {fact:?} is outside parent {parent_fact:?}"
4509 );
4510 }
4511 }
4512 let closing_paren = source.find("()'").expect("trailing empty group") + 1;
4513 assert!(facts.iter().any(|fact| {
4514 fact.kind == NormalizedKind::RegexSequence
4515 && fact.range.start_byte == closing_paren
4516 && fact.range.end_byte == closing_paren
4517 }));
4518 }
4519}