Repository: PyCQA/flake8

Issue:
warn on invalid codes in `ignore` / `extend-ignore`
### describe the request

for example https://github.com/getsentry/sentry-python/blob/b36d84a76bd6f8344c9b0a9694591939296e9c06/.flake8#L2-L13

this disables a code called `//` and a code called `I` (among many others) due to incorrect comment syntax

Comments:

Patch:
diff --git a/src/flake8/defaults.py b/src/flake8/defaults.py
index 4ba0048..e3edf32 100644
--- a/src/flake8/defaults.py
+++ b/src/flake8/defaults.py
@@ -43,3 +43,5 @@ NOQA_INLINE_REGEXP = re.compile(
 )
 
 NOQA_FILE = re.compile(r"\s*# flake8[:=]\s*noqa", re.I)
+
+VALID_CODE_PREFIX = re.compile("^[A-Z]{1,3}[0-9]{0,3}$", re.ASCII)
diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py
index e158737..a9ced1b 100644
--- a/src/flake8/options/config.py
+++ b/src/flake8/options/config.py
@@ -7,6 +7,7 @@ import os.path
 from typing import Any
 
 from flake8 import exceptions
+from flake8.defaults import VALID_CODE_PREFIX
 from flake8.options.manager import OptionManager
 
 LOG = logging.getLogger(__name__)
@@ -120,6 +121,16 @@ def parse_config(
         LOG.debug('Option "%s" returned value: %r', option_name, value)
 
         final_value = option.normalize(value, cfg_dir)
+
+        if option_name in {"ignore", "extend-ignore"}:
+            for error_code in final_value:
+                if not VALID_CODE_PREFIX.match(error_code):
+                    raise ValueError(
+                        f"Error code {error_code!r} "
+                        f"supplied to {option_name!r} option "
+                        f"does not match {VALID_CODE_PREFIX.pattern!r}"
+                    )
+
         assert option.config_name is not None
         config_dict[option.config_name] = final_value
 
diff --git a/src/flake8/plugins/finder.py b/src/flake8/plugins/finder.py
index c051488..4a43ccb 100644
--- a/src/flake8/plugins/finder.py
+++ b/src/flake8/plugins/finder.py
@@ -5,7 +5,6 @@ import configparser
 import inspect
 import itertools
 import logging
-import re
 import sys
 from typing import Any
 from typing import Generator
@@ -14,13 +13,12 @@ from typing import NamedTuple
 
 from flake8 import utils
 from flake8._compat import importlib_metadata
+from flake8.defaults import VALID_CODE_PREFIX
 from flake8.exceptions import ExecutionError
 from flake8.exceptions import FailedToLoadPlugin
 
 LOG = logging.getLogger(__name__)
 
-VALID_CODE = re.compile("^[A-Z]{1,3}[0-9]{0,3}$", re.ASCII)
-
 FLAKE8_GROUPS = frozenset(("flake8.extension", "flake8.report"))
 
 BANNED_PLUGINS = {
@@ -337,10 +335,10 @@ def _classify_plugins(
             raise NotImplementedError(f"what plugin type? {loaded}")
 
     for loaded in itertools.chain(tree, logical_line, physical_line):
-        if not VALID_CODE.match(loaded.entry_name):
+        if not VALID_CODE_PREFIX.match(loaded.entry_name):
             raise ExecutionError(
                 f"plugin code for `{loaded.display_name}` does not match "
-                f"{VALID_CODE.pattern}"
+                f"{VALID_CODE_PREFIX.pattern}"
             )
 
     return Plugins(


Test patch:
diff --git a/tests/unit/plugins/finder_test.py b/tests/unit/plugins/finder_test.py
index d526fd1..cd5cf4a 100644
--- a/tests/unit/plugins/finder_test.py
+++ b/tests/unit/plugins/finder_test.py
@@ -31,37 +31,6 @@ def _loaded(plugin=None, obj=None, parameters=None):
     return finder.LoadedPlugin(plugin, obj, parameters)
 
 
-@pytest.mark.parametrize(
-    "s",
-    (
-        "E",
-        "E1",
-        "E123",
-        "ABC",
-        "ABC1",
-        "ABC123",
-    ),
-)
-def test_valid_plugin_prefixes(s):
-    assert finder.VALID_CODE.match(s)
-
-
-@pytest.mark.parametrize(
-    "s",
-    (
-        "",
-        "A1234",
-        "ABCD",
-        "abc",
-        "a-b",
-        "☃",
-        "A𝟗",
-    ),
-)
-def test_invalid_plugin_prefixes(s):
-    assert finder.VALID_CODE.match(s) is None
-
-
 def test_loaded_plugin_entry_name_vs_display_name():
     loaded = _loaded(_plugin(package="package-name", ep=_ep(name="Q")))
     assert loaded.entry_name == "Q"
diff --git a/tests/unit/test_defaults.py b/tests/unit/test_defaults.py
new file mode 100644
index 0000000..822b8f0
--- /dev/null
+++ b/tests/unit/test_defaults.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+import pytest
+
+from flake8.defaults import VALID_CODE_PREFIX
+
+
+@pytest.mark.parametrize(
+    "s",
+    (
+        "E",
+        "E1",
+        "E123",
+        "ABC",
+        "ABC1",
+        "ABC123",
+    ),
+)
+def test_valid_plugin_prefixes(s):
+    assert VALID_CODE_PREFIX.match(s)
+
+
+@pytest.mark.parametrize(
+    "s",
+    (
+        "",
+        "A1234",
+        "ABCD",
+        "abc",
+        "a-b",
+        "☃",
+        "A𝟗",
+    ),
+)
+def test_invalid_plugin_prefixes(s):
+    assert VALID_CODE_PREFIX.match(s) is None
diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py
index 8c8f0cb..43d8104 100644
--- a/tests/unit/test_options_config.py
+++ b/tests/unit/test_options_config.py
@@ -220,3 +220,35 @@ def test_parse_config_ignores_unknowns(tmp_path, opt_manager, caplog):
 def test_load_config_missing_file_raises_exception(capsys):
     with pytest.raises(exceptions.ExecutionError):
         config.load_config("foo.cfg", [])
+
+
+def test_invalid_ignore_codes_raise_error(tmpdir, opt_manager):
+    tmpdir.join("setup.cfg").write("[flake8]\nignore = E203, //comment")
+    with tmpdir.as_cwd():
+        cfg, _ = config.load_config("setup.cfg", [], isolated=False)
+
+    with pytest.raises(ValueError) as excinfo:
+        config.parse_config(opt_manager, cfg, tmpdir)
+
+    expected = (
+        "Error code '//comment' supplied to 'ignore' option "
+        "does not match '^[A-Z]{1,3}[0-9]{0,3}$'"
+    )
+    (msg,) = excinfo.value.args
+    assert msg == expected
+
+
+def test_invalid_extend_ignore_codes_raise_error(tmpdir, opt_manager):
+    tmpdir.join("setup.cfg").write("[flake8]\nextend-ignore = E203, //comment")
+    with tmpdir.as_cwd():
+        cfg, _ = config.load_config("setup.cfg", [], isolated=False)
+
+    with pytest.raises(ValueError) as excinfo:
+        config.parse_config(opt_manager, cfg, tmpdir)
+
+    expected = (
+        "Error code '//comment' supplied to 'extend-ignore' option "
+        "does not match '^[A-Z]{1,3}[0-9]{0,3}$'"
+    )
+    (msg,) = excinfo.value.args
+    assert msg == expected