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