diff --git a/doc/whatsnew/fragments/3696.breaking b/doc/whatsnew/fragments/3696.breaking new file mode 100644 index 0000000000..ad93e45098 --- /dev/null +++ b/doc/whatsnew/fragments/3696.breaking @@ -0,0 +1,13 @@ +Enabling or disabling individual messages will now take effect even if an +``--enable=all`` or ``disable=all`` follows in the same configuration file +(or on the command line). + +This means for the following example, ``fixme`` messages will now be emitted:: + +.. code-block:: + + pylint my_module --enable=fixme --disable=all + +To regain the prior behavior, remove the superfluous earlier option. + +Closes #3696 diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py index 85602197e5..514bdcd815 100644 --- a/pylint/config/config_initialization.py +++ b/pylint/config/config_initialization.py @@ -13,7 +13,10 @@ from pylint import reporters from pylint.config.config_file_parser import _ConfigurationFileParser -from pylint.config.exceptions import _UnrecognizedOptionError +from pylint.config.exceptions import ( + ArgumentPreprocessingError, + _UnrecognizedOptionError, +) from pylint.utils import utils if TYPE_CHECKING: @@ -46,6 +49,9 @@ def _config_initialization( print(ex, file=sys.stderr) sys.exit(32) + # Order --enable=all or --disable=all to come first. + config_args = _order_all_first(config_args, joined=False) + # Run init hook, if present, before loading plugins if "init-hook" in config_data: exec(utils._unquote(config_data["init-hook"])) # pylint: disable=exec-used @@ -73,6 +79,7 @@ def _config_initialization( # Now we parse any options from the command line, so they can override # the configuration file + args_list = _order_all_first(args_list, joined=True) parsed_args_list = linter._parse_command_line_configuration(args_list) # Remove the positional arguments separator from the list of arguments if it exists @@ -147,3 +154,48 @@ def _config_initialization( for arg in parsed_args_list ) ) + + +def _order_all_first(config_args: list[str], *, joined: bool) -> list[str]: + """Reorder config_args such that --enable=all or --disable=all comes first. + + Raise if both are given. + + If joined is True, expect args in the form '--enable=all,for-any-all'. + If joined is False, expect args in the form '--enable', 'all,for-any-all'. + """ + indexes_to_prepend = [] + all_action = "" + + for i, arg in enumerate(config_args): + if joined and (arg.startswith("--enable=") or arg.startswith("--disable=")): + value = arg.split("=")[1] + elif arg in {"--enable", "--disable"}: + value = config_args[i + 1] + else: + continue + + if "all" not in (msg.strip() for msg in value.split(",")): + continue + + arg = arg.split("=")[0] + if all_action and (arg != all_action): + raise ArgumentPreprocessingError( + "--enable=all and --disable=all are incompatible." + ) + all_action = arg + + indexes_to_prepend.append(i) + if not joined: + indexes_to_prepend.append(i + 1) + + returned_args = [] + for i in indexes_to_prepend: + returned_args.append(config_args[i]) + + for i, arg in enumerate(config_args): + if i in indexes_to_prepend: + continue + returned_args.append(arg) + + return returned_args diff --git a/tests/config/functional/toml/toml_with_mutually_exclusive_disable_enable_all.toml b/tests/config/functional/toml/toml_with_mutually_exclusive_disable_enable_all.toml new file mode 100644 index 0000000000..9b422d8b7a --- /dev/null +++ b/tests/config/functional/toml/toml_with_mutually_exclusive_disable_enable_all.toml @@ -0,0 +1,3 @@ +[tool.pylint."messages control"] +disable = "all" +enable = "all" diff --git a/tests/config/functional/toml/toml_with_specific_disable_before_enable_all.toml b/tests/config/functional/toml/toml_with_specific_disable_before_enable_all.toml new file mode 100644 index 0000000000..120132a30a --- /dev/null +++ b/tests/config/functional/toml/toml_with_specific_disable_before_enable_all.toml @@ -0,0 +1,3 @@ +[tool.pylint."messages control"] +disable = "fixme" +enable = "all" diff --git a/tests/config/functional/toml/toml_with_specific_enable_before_disable_all.toml b/tests/config/functional/toml/toml_with_specific_enable_before_disable_all.toml new file mode 100644 index 0000000000..539597cc58 --- /dev/null +++ b/tests/config/functional/toml/toml_with_specific_enable_before_disable_all.toml @@ -0,0 +1,3 @@ +[tool.pylint."messages control"] +enable = "fixme" +disable = "all" diff --git a/tests/config/test_config.py b/tests/config/test_config.py index ff49d901ec..83c8b072e5 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -11,6 +11,7 @@ import pytest from pytest import CaptureFixture +from pylint.config.exceptions import ArgumentPreprocessingError from pylint.interfaces import CONFIDENCE_LEVEL_NAMES from pylint.lint import Run as LintRun from pylint.testutils import create_files @@ -20,6 +21,7 @@ HERE = Path(__file__).parent.absolute() REGRTEST_DATA_DIR = HERE / ".." / "regrtest_data" EMPTY_MODULE = REGRTEST_DATA_DIR / "empty.py" +FIXME_MODULE = REGRTEST_DATA_DIR / "fixme.py" def check_configuration_file_reader( @@ -175,3 +177,45 @@ def test_clear_cache_post_run() -> None: assert not run_before_edit.linter.stats.by_msg assert run_after_edit.linter.stats.by_msg + + +def test_enable_all_disable_all_mutually_exclusive() -> None: + with pytest.raises(ArgumentPreprocessingError): + runner = Run(["--enable=all", "--disable=all", str(EMPTY_MODULE)], exit=False) + + runner = Run(["--enable=all", "--enable=all", str(EMPTY_MODULE)], exit=False) + assert not runner.linter.stats.by_msg + + with pytest.raises(ArgumentPreprocessingError): + run_using_a_configuration_file( + HERE + / "functional" + / "toml" + / "toml_with_mutually_exclusive_disable_enable_all.toml", + ) + + +def test_disable_before_enable_all_takes_effect() -> None: + runner = Run(["--disable=fixme", "--enable=all", str(FIXME_MODULE)], exit=False) + assert not runner.linter.stats.by_msg + + _, _, toml_runner = run_using_a_configuration_file( + HERE + / "functional" + / "toml" + / "toml_with_specific_disable_before_enable_all.toml", + ) + assert not toml_runner.linter.is_message_enabled("fixme") + + +def test_enable_before_disable_all_takes_effect() -> None: + runner = Run(["--enable=fixme", "--disable=all", str(FIXME_MODULE)], exit=False) + assert runner.linter.stats.by_msg + + _, _, toml_runner = run_using_a_configuration_file( + HERE + / "functional" + / "toml" + / "toml_with_specific_enable_before_disable_all.toml", + ) + assert toml_runner.linter.is_message_enabled("fixme") diff --git a/tests/config/test_functional_config_loading.py b/tests/config/test_functional_config_loading.py index 0bbfe50ff6..0bce30701b 100644 --- a/tests/config/test_functional_config_loading.py +++ b/tests/config/test_functional_config_loading.py @@ -43,6 +43,11 @@ str(path.relative_to(FUNCTIONAL_DIR)) for ext in ACCEPTED_CONFIGURATION_EXTENSIONS for path in FUNCTIONAL_DIR.rglob(f"*.{ext}") + if (str_path := str(path)) + # The enable/disable all tests are not practical with this framework. + # They require manually listing ~400 messages, which will + # require constant updates. + and "enable_all" not in str_path and "disable_all" not in str_path ] diff --git a/tests/regrtest_data/fixme.py b/tests/regrtest_data/fixme.py new file mode 100644 index 0000000000..f0f7d062b2 --- /dev/null +++ b/tests/regrtest_data/fixme.py @@ -0,0 +1 @@ +# TODO: implement diff --git a/tests/test_self.py b/tests/test_self.py index 3970f22641..83e7421edf 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -266,7 +266,7 @@ def test_enable_all_works(self) -> None: """ ) self._test_output( - [module, "--disable=all", "--enable=all", "-rn"], expected_output=expected + [module, "--disable=I", "--enable=all", "-rn"], expected_output=expected ) def test_wrong_import_position_when_others_disabled(self) -> None: