diff --git a/regexlint/checkers.py b/regexlint/checkers.py index 1550d8e..6abd190 100644 --- a/regexlint/checkers.py +++ b/regexlint/checkers.py @@ -507,6 +507,26 @@ def check_redundant_repetition(reg, errs): errs.append((num, level, repeat.start, "should be +")) +def check_dot_newline_alternation(reg, errs): + # https://github.com/pygments/pygments/pull/3187 + num = "126" + level = logging.WARNING + msg = "Alternation matching any character, simplify to [\\s\\S]" + + # (.|\n) matches every character, since . matches all but \n. It is the + # [\s\S] idiom spelled as a per-character alternation; matching a single + # character class is markedly faster: ~4x on a long comment body, ~13% + # end-to-end on comment-heavy sources (pygments PR #3187). + allowed = {Other.Dot, Other.Newline, Other.Tab, Other.Suspicious} + for alt in find_all_by_type(reg, Other.Alternation): + atoms = [b.children[0] for b in alt.children if len(b.children) == 1] + if len(atoms) != len(alt.children): + continue + types = {a.type for a in atoms} + if Other.Dot in types and Other.Newline in types and types <= allowed: + errs.append((num, level, alt.start or 0, msg)) + + def manual_check_for_empty_string_match(reg, errs, raw_pat): # Skip the check in the following conditions: # * Rules that use a callback, since they're used for indentation diff --git a/tests/test_checkers.py b/tests/test_checkers.py index f0ae425..f9d944a 100644 --- a/tests/test_checkers.py +++ b/tests/test_checkers.py @@ -31,6 +31,7 @@ check_charclass_negation, check_charclass_overlap, check_charclass_simplify, + check_dot_newline_alternation, check_multiline_anchors, check_no_bels, check_no_consecutive_dots, @@ -758,6 +759,23 @@ def test_redundant_repetition_star(self): print(errs) self.assertEqual(len(errs), 1) + def test_dot_newline_alternation(self): + for pat in (r"(.|\n)*?", r"(.|\n)", r".|\n", r"(\n|.)+", r"(.|\n|\r)*?"): + r = Regex.get_parse_tree(pat) + errs = [] + check_dot_newline_alternation(r, errs) + print(pat, errs) + self.assertEqual(len(errs), 1, pat) + + def test_dot_newline_alternation_ok(self): + # No \n branch, no . branch, or a multi-character branch. + for pat in (r"(.|a)+", r"(\n|a)+", r"(a|b)+", r"(.|\n|foo)+", r"[\s\S]*?"): + r = Regex.get_parse_tree(pat) + errs = [] + check_dot_newline_alternation(r, errs) + print(pat, errs) + self.assertEqual(len(errs), 0, pat) + def test_manual_empty_string(self): r = Regex.get_parse_tree("") errs = []