aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-15 14:58:16 +0530
committernatsuoto <[email protected]>2026-07-15 14:58:16 +0530
commitcf84fbf5af3b3f321582d74ed34d53b69d8c0002 (patch)
tree5f10e1334bdf8bb9519f514c202814ccb8cdd3ef /tests
parent5a771b525a59d94c1d339d302eae118e0e46ab1e (diff)
downloadedify-cf84fbf5af3b3f321582d74ed34d53b69d8c0002.tar.xz
edify-cf84fbf5af3b3f321582d74ed34d53b69d8c0002.zip
feat!(compile): normalize char-class escaping to minimal correct form; add equivalence corpus
Diffstat (limited to 'tests')
-rw-r--r--tests/builder/builder.test.py8
-rw-r--r--tests/compile/escape.test.py115
2 files changed, 119 insertions, 4 deletions
diff --git a/tests/builder/builder.test.py b/tests/builder/builder.test.py
index 82aa1cf..5f96b5c 100644
--- a/tests/builder/builder.test.py
+++ b/tests/builder/builder.test.py
@@ -404,14 +404,14 @@ def test_end_of_input():
def test_any_of_chars():
expr = RegexBuilder().any_of_chars("aeiou.-")
- regex_equality("[aeiou\\.\\-]", expr)
- regex_compilation("[aeiou\\.\\-]", expr)
+ regex_equality("[aeiou.-]", expr)
+ regex_compilation("[aeiou.-]", expr)
def test_anything_but_chars():
expr = RegexBuilder().anything_but_chars("aeiou.-")
- regex_equality("[^aeiou\\.\\-]", expr)
- regex_compilation("[^aeiou\\.\\-]", expr)
+ regex_equality("[^aeiou.-]", expr)
+ regex_compilation("[^aeiou.-]", expr)
def test_anything_but_string():
diff --git a/tests/compile/escape.test.py b/tests/compile/escape.test.py
new file mode 100644
index 0000000..e9ab657
--- /dev/null
+++ b/tests/compile/escape.test.py
@@ -0,0 +1,115 @@
+"""Tests for the character-class escape normalization.
+
+The minimal-correct form escapes only ``\\``, ``]``, first-position ``^``,
+and interior ``-`` — everything else passes through as a literal. Match
+behavior against a representative corpus must stay identical to the
+previous over-escaped form.
+"""
+
+import re
+
+import pytest
+
+from edify import RegexBuilder
+
+_METACHARS_UNRELATED_TO_CLASS = "#?!@$%^&*"
+_MIXED_CORPUS = [
+ "",
+ "a",
+ "z",
+ "#",
+ "?",
+ "!",
+ "@",
+ "$",
+ "%",
+ "^",
+ "&",
+ "*",
+ "-",
+ ".",
+ "abc",
+ "a#b",
+ "1-2",
+ " ",
+ "foo.bar",
+ "hello world",
+ "a^b",
+ "]",
+ "[a]",
+ "\\",
+]
+
+
+def test_any_of_chars_emits_minimal_form_for_class_safe_metachars():
+ emitted = RegexBuilder().any_of_chars(_METACHARS_UNRELATED_TO_CLASS).to_regex_string()
+ assert emitted == "[#?!@$%^&*]"
+
+
+def test_any_of_chars_escapes_backslash_and_closing_bracket_only():
+ emitted = RegexBuilder().any_of_chars("a\\b]c").to_regex_string()
+ assert emitted == "[a\\\\b\\]c]"
+
+
+def test_any_of_chars_escapes_first_position_caret_only():
+ first_caret = RegexBuilder().any_of_chars("^abc").to_regex_string()
+ later_caret = RegexBuilder().any_of_chars("a^bc").to_regex_string()
+ assert first_caret == "[\\^abc]"
+ assert later_caret == "[a^bc]"
+
+
+def test_any_of_chars_escapes_interior_dash_only():
+ edge_dashes = RegexBuilder().any_of_chars("-a-").to_regex_string()
+ interior_dash = RegexBuilder().any_of_chars("a-b").to_regex_string()
+ assert edge_dashes == "[-a-]"
+ assert interior_dash == "[a\\-b]"
+
+
+def test_any_of_chars_leaves_dot_and_asterisk_unescaped_inside_a_class():
+ emitted = RegexBuilder().any_of_chars(".*").to_regex_string()
+ assert emitted == "[.*]"
+
+
+def test_anything_but_chars_leading_position_of_body_is_not_caret():
+ emitted = RegexBuilder().anything_but_chars("^abc").to_regex_string()
+ assert emitted == "[^\\^abc]"
+
+
+def test_anything_but_chars_normalizes_the_same_way_as_any_of_chars():
+ emitted = RegexBuilder().anything_but_chars("#?!@$%^&*").to_regex_string()
+ assert emitted == "[^#?!@$%^&*]"
+
+
+def test_string_terminal_still_uses_full_escape_outside_the_class():
+ emitted = RegexBuilder().string("a.b").to_regex_string()
+ assert emitted == "a\\.b"
+
+
+def test_char_terminal_still_uses_full_escape_outside_the_class():
+ emitted = RegexBuilder().char(".").to_regex_string()
+ assert emitted == "\\."
+
+
[email protected]("candidate", _MIXED_CORPUS)
+def test_normalized_and_over_escaped_char_classes_match_the_same_inputs(candidate):
+ normalized_pattern = (
+ RegexBuilder().any_of_chars(_METACHARS_UNRELATED_TO_CLASS).to_regex_string()
+ )
+ over_escaped_pattern = f"[{re.escape(_METACHARS_UNRELATED_TO_CLASS)}]"
+ normalized_hits = re.findall(normalized_pattern, candidate)
+ over_escaped_hits = re.findall(over_escaped_pattern, candidate)
+ assert normalized_hits == over_escaped_hits
+
+
[email protected]("candidate", _MIXED_CORPUS)
+def test_normalized_and_over_escaped_negated_classes_match_the_same_inputs(candidate):
+ normalized_pattern = RegexBuilder().anything_but_chars("aeiou.-").to_regex_string()
+ over_escaped_pattern = "[^aeiou\\.\\-]"
+ normalized_hits = re.findall(normalized_pattern, candidate)
+ over_escaped_hits = re.findall(over_escaped_pattern, candidate)
+ assert normalized_hits == over_escaped_hits
+
+
+def test_any_of_chars_empty_class_still_renders_empty():
+ emitted = RegexBuilder().any_of_chars("").to_regex_string()
+ assert emitted == "[]"