diff --git a/dev_tools/check_useless_exclude_paths_hooks.py b/dev_tools/check_useless_exclude_paths_hooks.py index 4fec637..34eef23 100644 --- a/dev_tools/check_useless_exclude_paths_hooks.py +++ b/dev_tools/check_useless_exclude_paths_hooks.py @@ -4,6 +4,7 @@ from __future__ import annotations import itertools +import re import sys from collections import Counter from pathlib import Path @@ -12,6 +13,7 @@ from ruamel.yaml import YAML CONFIG_FILE = ".pre-commit-config.yaml" +REGEX_ESCAPE_SEQUENCE = re.compile(r"\\([.^$*+?{}\[\]()|/\\])") class Hook: @@ -62,7 +64,29 @@ def count_excluded_files(self) -> int: def is_regex_pattern(exclude: str) -> bool: - return any(regex_key in exclude for regex_key in ["*", "$", "^"]) + return any(_contains_unescaped_char(exclude, regex_key) for regex_key in ["*", "$", "^"]) + + +def _contains_unescaped_char(text: str, char: str) -> bool: + escaped = False + + for current_char in text: + if escaped: + escaped = False + continue + + if current_char == "\\": + escaped = True + continue + + if current_char == char: + return True + + return False + + +def _unescape_literal_regex_elements(text: str) -> str: + return REGEX_ESCAPE_SEQUENCE.sub(r"\1", text) def extract_literal_exclude_paths(exclude_regex: str) -> list[str]: @@ -76,7 +100,7 @@ def extract_literal_exclude_paths(exclude_regex: str) -> list[str]: .split("|") ) - return [exclude for exclude in exclude_list if not is_regex_pattern(exclude)] + return [_unescape_literal_regex_elements(exclude) for exclude in exclude_list if not is_regex_pattern(exclude)] def _remove_verbose_regex_comments(exclude: str) -> str: diff --git a/tests/test_check_useless_exclude_paths_hooks.py b/tests/test_check_useless_exclude_paths_hooks.py index 4bcf73e..fc2f30c 100644 --- a/tests/test_check_useless_exclude_paths_hooks.py +++ b/tests/test_check_useless_exclude_paths_hooks.py @@ -27,6 +27,11 @@ def test_is_regex_pattern_for_regex_should_be_true(pattern: str) -> None: assert is_regex_pattern(pattern) +@pytest.mark.parametrize("pattern", [r"foo\$bar", r"foo\^bar", r"foo\*bar"]) +def test_is_regex_pattern_for_escaped_regex_characters_should_be_false(pattern: str) -> None: + assert is_regex_pattern(pattern) is False + + def test_is_regex_pattern_for_no_regex_should_be_false() -> None: assert is_regex_pattern("packages/thirdparty/") is False @@ -58,6 +63,14 @@ def test_extract_literal_exclude_paths_for_multiple_literals_should_return_paths ] +def test_extract_literal_exclude_paths_should_unescape_literal_regex_elements() -> None: + assert extract_literal_exclude_paths(r"(?x)^(foo\.txt|bar\/baz|foo\$bar)") == [ + "foo.txt", + "bar/baz", + "foo$bar", + ] + + def test_from_hook_config_for_single_path(fs: FakeFilesystem) -> None: root_directory = Path("Test_directory/") fs.create_dir(root_directory)