diff --git a/doc/whatsnew/fragments/7229.bugfix b/doc/whatsnew/fragments/7229.bugfix new file mode 100644 index 0000000000..33c7b578ed --- /dev/null +++ b/doc/whatsnew/fragments/7229.bugfix @@ -0,0 +1,5 @@ +When parsing comma-separated lists of regular expressions in the config, ignore +commas that are inside braces since those indicate quantiers, not dilineation +between expressions. + +Closes #7229 diff --git a/pylint/config/argument.py b/pylint/config/argument.py index 3c29515176..8c6b8a2cb3 100644 --- a/pylint/config/argument.py +++ b/pylint/config/argument.py @@ -102,7 +102,7 @@ def _py_version_transformer(value: str) -> tuple[int, ...]: def _regexp_csv_transfomer(value: str) -> Sequence[Pattern[str]]: """Transforms a comma separated list of regular expressions.""" patterns: list[Pattern[str]] = [] - for pattern in _csv_transformer(value): + for pattern in pylint_utils._check_regexp_csv(value): patterns.append(re.compile(pattern)) return patterns diff --git a/pylint/utils/__init__.py b/pylint/utils/__init__.py index bc5011db95..9265d0d13e 100644 --- a/pylint/utils/__init__.py +++ b/pylint/utils/__init__.py @@ -14,6 +14,7 @@ HAS_ISORT_5, IsortDriver, _check_csv, + _check_regexp_csv, _format_option_value, _splitstrip, _unquote, @@ -34,6 +35,7 @@ "HAS_ISORT_5", "IsortDriver", "_check_csv", + "_check_regexp_csv", "_format_option_value", "_splitstrip", "_unquote", diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 6a4277642b..e33ba81253 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -21,7 +21,8 @@ import textwrap import tokenize import warnings -from collections.abc import Sequence +from collections import deque +from collections.abc import Iterable, Sequence from io import BufferedReader, BytesIO from typing import ( TYPE_CHECKING, @@ -328,6 +329,29 @@ def _check_csv(value: list[str] | tuple[str] | str) -> Sequence[str]: return _splitstrip(value) +def _check_regexp_csv(value: list[str] | tuple[str] | str) -> Iterable[str]: + if isinstance(value, (list, tuple)): + yield from value + else: + # None is a sentinel value here + regexps: deque[deque[str] | None] = deque([None]) + open_braces = False + for char in value: + if char == "{": + open_braces = True + elif char == "}" and open_braces: + open_braces = False + + if char == "," and not open_braces: + regexps.append(None) + elif regexps[-1] is None: + regexps.pop() + regexps.append(deque([char])) + else: + regexps[-1].append(char) + yield from ("".join(regexp).strip() for regexp in regexps if regexp is not None) + + def _comment(string: str) -> str: """Return string as a comment.""" lines = [line.strip() for line in string.splitlines()] diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 47891aee25..5296d67810 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -5,6 +5,8 @@ from __future__ import annotations import os +import re +import timeit from pathlib import Path import pytest @@ -111,6 +113,31 @@ def test_unknown_py_version(capsys: CaptureFixture) -> None: assert "the-newest has an invalid format, should be a version string." in output.err +CSV_REGEX_COMMA_CASES = [ + ("foo", ["foo"]), + ("foo,bar", ["foo", "bar"]), + ("foo, bar", ["foo", "bar"]), + ("foo, bar{1,3}", ["foo", "bar{1,3}"]), +] + + +@pytest.mark.parametrize("in_string,expected", CSV_REGEX_COMMA_CASES) +def test_csv_regex_comma_in_quantifier(in_string, expected) -> None: + """Check that we correctly parse a comma-separated regex when there are one + or more commas within quantifier expressions. + """ + + def _template_run(in_string): + r = Run( + [str(EMPTY_MODULE), rf"--bad-names-rgx={in_string}"], + exit=False, + ) + return r.linter.config.bad_names_rgxs + + assert _template_run(in_string) == [re.compile(regex) for regex in expected] + + + def test_short_verbose(capsys: CaptureFixture) -> None: """Check that we correctly handle the -v flag.""" Run([str(EMPTY_MODULE), "-v"], exit=False)