Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions regexlint/checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,61 @@ def check_redundant_repetition(reg, errs):
errs.append((num, level, repeat.start, "should be +"))


def _whitespace_atom(branch):
"""Return the single atom of an alternation branch when it matches only
whitespace that ``\\s`` already covers (unwrapping a repetition such as
``\\s+``), else None.
"""
if len(branch.children) != 1:
return None
atom = branch.children[0]
if atom.type in Other.Repetition and len(atom.children) == 1:
atom = atom.children[0]
if (
(atom.type is Other.BuiltinCharclass and atom.data == "\\s")
or atom.type in (Other.Newline, Other.Tab)
or (atom.type is Other.Suspicious and atom.data == "\\r")
or (atom.type is Other.Literal and atom.data in " \t\n\r\x0b\x0c")
):
return atom
return None


def check_redundant_whitespace_alternation(reg, errs):
# https://github.com/pygments/pygments/pull/3186
num = "126"
level = logging.WARNING
msg = "Redundant whitespace alternation, simplify to \\s+ (\\s already matches \\n)"

outer = (
Other.Repetition.Plus,
Other.Repetition.NongreedyPlus,
Other.Repetition.Star,
Other.Repetition.NongreedyStar,
)
for rep in find_all_by_type(reg, Other.Repetition):
if rep.type not in outer or len(rep.children) != 1:
continue
group = rep.children[0]
if group.type not in Other.Open or len(group.children) != 1:
continue
alt = group.children[0]
if alt.type is not Other.Alternation:
continue
# Every branch must be \s-subsumed whitespace, and at least one must be
# \s itself (which is what makes the other branches redundant).
has_s = False
for branch in alt.children:
atom = _whitespace_atom(branch)
if atom is None:
break
if atom.type is Other.BuiltinCharclass:
has_s = True
else:
if has_s:
errs.append((num, level, rep.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
Expand Down
24 changes: 24 additions & 0 deletions tests/test_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
check_no_nulls,
check_prefix_ordering,
check_redundant_repetition,
check_redundant_whitespace_alternation,
check_single_character_classes,
check_suspicious_anchors,
check_unescaped_braces,
Expand Down Expand Up @@ -758,6 +759,29 @@ def test_redundant_repetition_star(self):
print(errs)
self.assertEqual(len(errs), 1)

def test_redundant_whitespace_alternation(self):
for pat in (r"(\n|\s+)+", r"(\n|\s)+", r"(\s|\n)+", r"(\s|\n)*"):
r = Regex.get_parse_tree(pat)
errs = []
check_redundant_whitespace_alternation(r, errs)
print(pat, errs)
self.assertEqual(len(errs), 1, pat)

def test_redundant_whitespace_alternation_noncapturing(self):
r = Regex.get_parse_tree(r"(?:\n|\s)+")
errs = []
check_redundant_whitespace_alternation(r, errs)
self.assertEqual(len(errs), 1)

def test_redundant_whitespace_alternation_ok(self):
# No \s branch, a non-whitespace branch, or no outer +/* repetition.
for pat in (r"(\n|\t)+", r"(\d|\s)+", r"(\n|\s)?", r"(\n|\s)", r"\s+"):
r = Regex.get_parse_tree(pat)
errs = []
check_redundant_whitespace_alternation(r, errs)
print(pat, errs)
self.assertEqual(len(errs), 0, pat)

def test_manual_empty_string(self):
r = Regex.get_parse_tree("")
errs = []
Expand Down