1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
"""Snapshot suite for the compile-path edge-case fixtures.
Each fixture builds a small, intentionally-tricky pattern and pins the
emitted regex string. The fixtures are the ones most likely to silently
drift when the compile path is refactored: deeply nested alternation,
combined lookaround, item-4 named backreferences, and constructs that
:mod:`edify` flags for ReDoS.
"""
from collections.abc import Callable
from pathlib import Path
import pytest
from edify import Pattern, RegexBuilder
from edify.compile.redos import ReDoSWarning
from edify.testing import assert_snapshot
_SNAPSHOT_ROOT = Path(__file__).parent.parent / "snapshots" / "fixtures"
def _deeply_nested_alternation():
inner_alt = RegexBuilder().any_of("abc", "def", "ghi")
outer_alt = RegexBuilder().any_of("first", "second", "third")
return (
RegexBuilder()
.start_of_input()
.subexpression(inner_alt)
.string("-")
.subexpression(outer_alt)
.end_of_input()
)
def _combined_lookaround_positive_and_negative():
return (
RegexBuilder()
.assert_ahead()
.digit()
.end()
.assert_not_ahead()
.string("0")
.end()
.assert_behind()
.string("$")
.end()
.digit()
.digit()
.digit()
)
def _named_backreference_pair():
return (
RegexBuilder()
.named_capture("open")
.any_of("[", "(", "{")
.end()
.zero_or_more()
.word()
.named_back_reference("open")
)
def _nested_unbounded_quantifier_redos():
return RegexBuilder().one_or_more().group().one_or_more().digit().end()
def _alternation_with_overlapping_prefixes_redos():
return RegexBuilder().one_or_more().any_of("aa", "aaa", "aaaa")
def _hex_color_pattern():
return (
RegexBuilder()
.start_of_input()
.string("#")
.any_of_chars("0123456789abcdefABCDEF")
.exactly(6)
.word()
.end_of_input()
)
def _email_shape_pattern():
return (
RegexBuilder()
.start_of_input()
.one_or_more()
.any_of_chars("a-zA-Z0-9._%+-")
.string("@")
.one_or_more()
.any_of_chars("a-zA-Z0-9.-")
.string(".")
.at_least(2)
.letter()
.end_of_input()
)
def _pattern_composition_via_use():
reusable = Pattern().at_least(2).letter()
return RegexBuilder().start_of_input().subexpression(reusable).end_of_input()
_FIXTURES = [
("deeply_nested_alternation", _deeply_nested_alternation),
("combined_lookaround", _combined_lookaround_positive_and_negative),
("named_backreference_pair", _named_backreference_pair),
("nested_unbounded_quantifier_redos", _nested_unbounded_quantifier_redos),
("alternation_with_overlapping_prefixes_redos", _alternation_with_overlapping_prefixes_redos),
("hex_color_pattern", _hex_color_pattern),
("email_shape_pattern", _email_shape_pattern),
("pattern_composition_via_use", _pattern_composition_via_use),
]
@pytest.mark.parametrize(
("fixture_name", "builder_factory"), _FIXTURES, ids=[name for name, _ in _FIXTURES]
)
def test_edge_case_fixture_emits_the_snapshotted_regex(
fixture_name: str, builder_factory: Callable[[], RegexBuilder]
):
builder = builder_factory()
emitted = builder.to_regex_string()
snapshot_path = _SNAPSHOT_ROOT / f"{fixture_name}.regex"
assert_snapshot(emitted, snapshot_path)
def test_nested_unbounded_quantifier_fixture_raises_the_redos_warning():
with pytest.warns(ReDoSWarning):
_nested_unbounded_quantifier_redos().to_regex()
|