Repository: akaihola/darker

Issue:
Remove the `-L` / `--lint` option
If #465 is implemented and the linting support implementation is moved to a separate Python package, the next major release is an opportunity to drop linting support from Darker completely.

See #304 for discussion leading to this proposal.

Discussion:
akaihola: @okuuva hey since the idea to separate formatter and linter support into two tools came originally from you, would you be interested to do a quick review of this PR?
okuuva: > @okuuva hey since the idea to separate formatter and linter support into two tools came originally from you, would you be interested to do a quick review of this PR?

Sure!

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 83facb2..28174ab 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -38,7 +38,7 @@ If applicable, add copy/paste the output or attach a screenshots to help explain
  - Git version [e.g. 2.36.0]
  - Darker version [e.g. 2.1.1]
  - Black version [e.g. 22.3.0]
- - other reformatter and linter versions [e.g. `isort==5.10.1`, `mypy==0.942`
+ - other reformatter versions [e.g. `isort==5.10.1`, `flynt==1.0.1`]
 
 **Additional context**
 Add any other context about the problem here.
diff --git a/.github/workflows/darker-flake8.yml b/.github/workflows/darker-flake8.yml
deleted file mode 100644
index 841ba19..0000000
--- a/.github/workflows/darker-flake8.yml
+++ /dev/null
@@ -1,21 +0,0 @@
----
-name: Test linting using the Darker GitHub Action
-
-on: push  # yamllint disable-line rule:truthy
-
-jobs:
-  darker-github-action-linting-test:
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-      - name: Run Darker's own GitHub action straight from the repository
-        uses: ./
-        with:
-          options: --check --diff --color
-          src: action release_tools setup.py src/darker
-          revision: origin/master...
-          lint: flake8
-          version: "@${{ github.ref_name }}"
diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml
index 07600d6..8545afa 100644
--- a/.github/workflows/mypy.yml
+++ b/.github/workflows/mypy.yml
@@ -15,7 +15,6 @@ jobs:
             black \
             git+https://github.com/akaihola/darkgraylib.git@main \
             flynt \
-            git+https://github.com/akaihola/graylint.git@main \
             isort \
             mypy>=0.990 \
             pytest \
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 1812ad9..084a442 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -16,8 +16,6 @@ jobs:
             black \
             git+https://github.com/akaihola/darkgraylib.git@main \
             defusedxml \
-            git+https://github.com/akaihola/graylint.git@main \
-            pip-requirements-parser \
             pygments \
             pylint \
             pytest>=6.2.0 \
diff --git a/CHANGES.rst b/CHANGES.rst
index a5fd39f..0973556 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -13,6 +13,11 @@ Added
 - The ``--preview`` configuration flag is now supported in the configuration files for
   Darker and Black
 
+Removed
+-------
+- **Backwards incompatible change:** Baseline linting support (``-L``/``--lint`` option)
+  has been removed. Use the Graylint_ tool instead.
+
 Fixed
 -----
 - Update ``darkgray-dev-tools`` for Pip >= 24.1 compatibility.
diff --git a/README.rst b/README.rst
index 5121952..ecf3c33 100644
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,6 @@
-=================================================
- Darker – reformat and lint modified Python code
-=================================================
+========================================
+ Darker – reformat modified Python code
+========================================
 
 |build-badge| |license-badge| |pypi-badge| |downloads-badge| |black-badge| |changelog-badge|
 
@@ -32,12 +32,8 @@ What?
 
 This utility reformats and checks Python source code files.
 However, when run in a Git repository, it compares an old revision of the source tree
-to a newer revision (or the working tree). It then
-
-- only applies reformatting in regions which have changed in the Git working tree
-  between the two revisions, and
-- only reports those linting messages which appeared after the modifications to the
-  source code files.
+to a newer revision (or the working tree). It then only applies reformatting
+in regions which have changed in the Git working tree between the two revisions.
 
 The reformatters supported are:
 
@@ -45,7 +41,7 @@ The reformatters supported are:
 - isort_ for sorting imports
 - flynt_ for turning old-style format strings to f-strings
 
-See `Using linters`_ below for the list of supported linters.
+**NOTE:** Baseline linting support has been moved to the Graylint_ package.
 
 To easily run Darker as a Pytest_ plugin, see pytest-darker_.
 
@@ -168,12 +164,16 @@ You can enable additional features with command line options:
   your modules to determine whether they are first or third party modules.
 - ``-f`` / ``--flynt``: Also convert string formatting to use f-strings using the
   ``flynt`` package
-- ``-L <linter>`` / ``--lint <linter>``: Run a supported linter (see `Using linters`_)
 
 *New in version 1.1.0:* The ``-L`` / ``--lint`` option.
+
 *New in version 1.2.2:* Package available in conda-forge_.
+
 *New in version 1.7.0:* The ``-f`` / ``--flynt`` option
 
+*New in version 3.0.0:* Removed the ``-L`` / ``--lint`` functionality and moved it into
+the Graylint_ package.
+
 .. _Conda: https://conda.io/
 .. _conda-forge: https://conda-forge.org/
 
@@ -280,8 +280,8 @@ instead of the last commit:
    $ darker --revision master .
 
 
-Customizing ``darker``, Black_, isort_, flynt_ and linter behavior
-==================================================================
+Customizing ``darker``, Black_, isort_ and flynt_ behavior
+==========================================================
 
 ``darker`` invokes Black_ and isort_ internals directly instead of running their
 binaries, so it needs to read and pass configuration options to them explicitly.
@@ -289,20 +289,13 @@ Project-specific default options for ``darker`` itself, Black_ and isort_ are re
 the project's ``pyproject.toml`` file in the repository root. isort_ does also look for
 a few other places for configuration.
 
-Mypy_, Pylint_, Flake8_ and other compatible linters are invoked as
-subprocesses by ``darker``, so normal configuration mechanisms apply for each of those
-tools. Linters can also be configured on the command line, for example::
-
-    darker -L "mypy --strict" .
-    darker --lint "pylint --errors-only" .
-  
 flynt_ (option ``-f`` / ``--flynt``) is also invoked as a subprocess, but passing
 command line options to it is currently not supported. Configuration files need to be
 used instead.
 
 Darker does honor exclusion options in Black configuration files when recursing
-directories, but the exclusions are only applied to Black reformatting. Isort and
-linters are still run on excluded files. Also, individual files explicitly listed on the
+directories, but the exclusions are only applied to Black reformatting.
+Isort is still run on excluded files. Also, individual files explicitly listed on the
 command line are still reformatted even if they match exclusion patterns.
 
 For more details, see:
@@ -329,8 +322,7 @@ The following `command line arguments`_ can also be used to modify the defaults:
        (``HEAD..:STDIN:`` being the default if ``--stdin-filename`` is enabled).
 -c PATH, --config PATH
        Make ``darker``, ``black`` and ``isort`` read configuration from ``PATH``. Note
-       that other tools like ``flynt``, ``mypy``, ``pylint`` or ``flake8`` won't use
-       this configuration file.
+       that other tools like ``flynt`` won't use this configuration file.
 -v, --verbose
        Show steps taken and summarize modifications
 -q, --quiet
@@ -362,11 +354,7 @@ The following `command line arguments`_ can also be used to modify the defaults:
        In Black, enable potentially disruptive style changes that may be added to Black
        in the future
 -L CMD, --lint CMD
-       Run a linter on changed files. ``CMD`` can be a name or path of the linter
-       binary, or a full quoted command line with the command and options. Linters read
-       their configuration as normally, and aren't affected by ``-c`` / ``--config``.
-       Linter output is syntax highlighted when the ``pygments`` package is available if
-       run on a terminal and or enabled by explicitly (see ``--color``).
+       Show information about baseline linting using the Graylint package.
 -S, --skip-string-normalization
        Don't normalize string quotes or prefixes
 --no-skip-string-normalization
@@ -410,9 +398,6 @@ An example ``pyproject.toml`` configuration file:
    check = true
    isort = true
    flynt = true
-   lint = [
-       "pylint",
-   ]
    line-length = 80                  # Passed to isort and Black, override their config
    target-version = ["py312"]        # Passed to Black, overriding its config
    log_level = "INFO"
@@ -433,10 +418,6 @@ An example ``pyproject.toml`` configuration file:
    known_third_party = ["pytest"]
    line_length = 88                  # Overridden by [tool.darker] above
 
-Be careful to not use options which generate output which is unexpected for
-other tools. For example, VSCode only expects the reformat diff, so
-``lint = [ ... ]`` can't be used with it.
-
 *New in version 1.0.0:*
 
 - The ``-c``, ``-S`` and ``-l`` command line options.
@@ -470,6 +451,10 @@ command line options
 *New in version 2.1.1:* In ``[tool.darker]``, deprecate the the Black options
 ``skip_string_normalization`` and ``skip_magic_trailing_comma``
 
+*New in version 3.0.0:* Removed the ``-L`` / ``--lint`` functionality and moved it into
+the Graylint_ package. Also removed ``lint =``, ``skip_string_normalization =`` and
+``skip_magic_trailing_comma =`` from ``[tool.darker]``.
+
 .. _Black documentation about pyproject.toml: https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file
 .. _isort documentation about config files: https://timothycrosley.github.io/isort/docs/configuration/config_files/
 .. _public GitHub repositories which install and run Darker: https://github.com/search?q=%2Fpip+install+.*darker%2F+path%3A%2F%5E.github%5C%2Fworkflows%5C%2F.*%2F&type=code
@@ -581,8 +566,6 @@ __ https://github.com/microsoft/vscode-black-formatter
 VSCode will always add ``--diff --quiet`` as arguments to Darker,
 but you can also pass additional arguments in the ``black-formatter.args`` option
 (e.g. ``["-d", "--isort", "--revision=master..."]``).
-Be sure to *not* enable any linters here or in ``pyproject.toml``
-since VSCode won't be able to understand output from them.
 
 Note that VSCode first copies the file to reformat into a temporary
 ``<filename>.py.<hash>.tmp`` file, then calls Black (or Darker in this case) on that
@@ -676,7 +659,7 @@ introduced by Black updates. See `Guarding against Black compatibility breakage`
 more information.
 
 If you'd prefer to not update but keep a stable pre-commit setup, you can pin Black and
-other reformatter/linter tools you use to known compatible versions, for example:
+other reformatter tools you use to known compatible versions, for example:
 
 .. code-block:: yaml
 
@@ -686,18 +669,9 @@ other reformatter/linter tools you use to known compatible versions, for example
        - id: darker
          args:
            - --isort
-           - --lint
-           - mypy
-           - --lint
-           - flake8
-           - --lint
-           - pylint
          additional_dependencies:
            - black==22.12.0
            - isort==5.11.4
-           - mypy==0.990
-           - flake8==5.0.4
-           - pylint==2.15.5
 
 .. _pre-commit: https://pre-commit.com/
 .. _pre-commit Installation: https://pre-commit.com/#installation
@@ -745,12 +719,12 @@ Create a file named ``.github/workflows/darker.yml`` inside your repository with
 
 .. code-block:: yaml
 
-   name: Lint
+   name: Reformat
 
    on: [push, pull_request]
 
    jobs:
-     lint:
+     reformat:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
@@ -762,7 +736,6 @@ Create a file named ``.github/workflows/darker.yml`` inside your repository with
              options: "--check --diff --isort --color"
              src: "./src"
              version: "~=2.1.1"
-             lint: "flake8,pylint==2.13.1"
 
 There needs to be a working Python environment, set up using ``actions/setup-python``
 in the above example. Darker will be installed in an isolated virtualenv to prevent
@@ -789,11 +762,6 @@ You can also configure other arguments passed to Darker via ``"options:"``.
 It defaults to ``"--check --diff --color"``.
 You can e.g. add ``"--isort"`` to sort imports, or ``"--verbose"`` for debug logging.
 
-To run linters through Darker, you can provide a comma separated list of linters using
-the ``lint:`` option. Only ``flake8``, ``pylint`` and ``mypy`` are supported. Other
-linters may or may not work with Darker, depending on their message output format.
-Versions can be constrained using ``pip`` syntax, e.g. ``"flake8>=3.9.2"``.
-
 *New in version 1.1.0:*
 GitHub Actions integration. Modeled after how Black_ does it,
 thanks to Black authors for the example!
@@ -804,76 +772,16 @@ The ``revision:`` option, with smart default value if omitted.
 *New in version 1.5.0:*
 The ``lint:`` option.
 
-
-.. _Using linters:
-
-Using linters
-=============
-
-One way to use Darker is to filter linter output to only those linter messages
-which appeared after the modifications to source code files,
-as well as old messages which concern modified lines.
-Darker supports any linter with output in one of the following formats::
-
-    <file>:<linenum>: <description>
-    <file>:<linenum>:<col>: <description>
-
-Most notably, the following linters/checkers have been verified to work with Darker:
-
-- Mypy_ for static type checking
-- Pylint_ for generic static checking of code
-- Flake8_ for style guide enforcement
-- `cov_to_lint.py`_ for test coverage
-
-*New in version 1.1.0:* Support for Mypy_, Pylint_, Flake8_ and compatible linters.
-
-*New in version 1.2.0:* Support for test coverage output using `cov_to_lint.py`_.
-
-To run a linter, use the ``--lint`` / ``-L`` command line option with the linter
-command or a full command line to pass to a linter. Some examples:
-
-- ``-L flake8``: enforce the Python style guide using Flake8_
-- ``-L "mypy --strict"``: do static type checking using Mypy_
-- ``--lint="pylint --ignore='setup.py'"``: analyze code using Pylint_
-- ``-L cov_to_lint.py``: read ``.coverage`` and list non-covered modified lines
-
-**Note:** Full command lines aren't fully tested on Windows. See issue `#456`_ for a
-possible bug.
-
-Darker also groups linter output into blocks of consecutive lines
-separated by blank lines.
-Here's an example of `cov_to_lint.py`_ output::
-
-    $ darker --revision 0.1.0.. --check --lint cov_to_lint.py src
-    src/darker/__main__.py:94:  no coverage:             logger.debug("No changes in %s after isort", src)
-    src/darker/__main__.py:95:  no coverage:             break
-
-    src/darker/__main__.py:125: no coverage:         except NotEquivalentError:
-
-    src/darker/__main__.py:130: no coverage:             if context_lines == max_context_lines:
-    src/darker/__main__.py:131: no coverage:                 raise
-    src/darker/__main__.py:132: no coverage:             logger.debug(
-
-+-----------------------------------------------------------------------+
-|                               ⚠ NOTE ⚠                                |
-+=======================================================================+
-| Don't enable linting on the command line or in the configuration when |
-| running Darker as a reformatter in VSCode. You will confuse VSCode    |
-| with unexpected output from Darker, as it only expect black's output  |
-+-----------------------------------------------------------------------+
-
-.. _Mypy: https://pypi.org/project/mypy
-.. _Pylint: https://pypi.org/project/pylint
-.. _Flake8: https://pypi.org/project/flake8
-.. _cov_to_lint.py: https://gist.github.com/akaihola/2511fe7d2f29f219cb995649afd3d8d2
-.. _#456: https://github.com/akaihola/darker/issues/456
+*New in version 3.0.0:*
+Removed the ``lint:`` option and moved it into the GitHub action
+of the Graylint_ package.
 
 
 Syntax highlighting
 ===================
 
-Darker automatically enables syntax highlighting for the ``--diff``,
-``-d``/``--stdout`` and ``-L``/``--lint`` options if it's running on a terminal and the
+Darker automatically enables syntax highlighting for the ``--diff`` and
+``-d``/``--stdout`` options if it's running on a terminal and the
 Pygments_ package is installed.
 
 You can force enable syntax highlighting on non-terminal output using
@@ -970,8 +878,6 @@ To sort imports when the ``--isort`` option was specified, Darker proceeds like
 - if all lines between the first and last line modified by isort_ were unchanged between
   the revisions, discard import sorting modifications for that file
 
-For details on how linting support works, see Graylint_ documentation.
-
 
 Limitations and work-arounds
 =============================
diff --git a/action.yml b/action.yml
index 1fc6b8b..cd11968 100644
--- a/action.yml
+++ b/action.yml
@@ -24,9 +24,7 @@ inputs:
     required: false
   lint:
     description: >-
-      Comma-separated list of linters to `pip install` and run from Darker.
-      Optionally, version constraints (using pip syntax) can be specified.
-      Example: flake8,pylint==2.13.1
+      NOTE: Baseline linting has been moved to the Graylint package.
     required: false
     default: ''
 branding:
@@ -56,7 +54,6 @@ runs:
         sys.exit(proc.returncode)
         "
 
-        pip install pip-requirements-parser
         if [ "$RUNNER_OS" == "Windows" ]; then
           echo $entrypoint | python
         else
diff --git a/action/main.py b/action/main.py
index e196079..b903bca 100644
--- a/action/main.py
+++ b/action/main.py
@@ -6,18 +6,20 @@ import sys
 from pathlib import Path
 from subprocess import PIPE, STDOUT, run  # nosec
 
-from pip_requirements_parser import parse_reqparts_from_string
-
-LINTER_WHITELIST = {"flake8", "pylint", "mypy"}
 ACTION_PATH = Path(os.environ["GITHUB_ACTION_PATH"])
 ENV_PATH = ACTION_PATH / ".darker-env"
 ENV_BIN = ENV_PATH / ("Scripts" if sys.platform == "win32" else "bin")
 OPTIONS = os.getenv("INPUT_OPTIONS", default="")
 SRC = os.getenv("INPUT_SRC", default="")
 VERSION = os.getenv("INPUT_VERSION", default="")
-LINT = os.getenv("INPUT_LINT", default="")
 REVISION = os.getenv("INPUT_REVISION") or os.getenv("INPUT_COMMIT_RANGE") or "HEAD^"
 
+if os.getenv("INPUT_LINT", default=""):
+    print(
+        "::notice:: Baseline linting has been moved to the Graylint package."
+        " See https://pypi.org/project/graylint for more information.",
+    )
+
 run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True)  # nosec
 
 req = ["darker[color,isort]"]
@@ -28,18 +30,6 @@ if VERSION:
         req[0] += VERSION
     else:
         req[0] += f"=={VERSION}"
-linter_options = []
-for requirement_string in LINT.split(","):
-    if not requirement_string.strip():
-        continue
-    linter_requirement = parse_reqparts_from_string(requirement_string).requirement
-    linter = linter_requirement.name
-    if linter not in LINTER_WHITELIST:
-        raise RuntimeError(
-            f"{linter!r} is not supported as a linter by the GitHub Action"
-        )
-    req.append(str(linter_requirement))
-    linter_options.extend(["--lint", linter])
 
 pip_proc = run(  # nosec
     [str(ENV_BIN / "python"), "-m", "pip", "install"] + req,
@@ -59,7 +49,6 @@ proc = run(  # nosec
     [
         *base_cmd,
         *shlex.split(OPTIONS),
-        *linter_options,
         "--revision",
         REVISION,
         *shlex.split(SRC),
diff --git a/check-darker.toml b/check-darker.toml
index e29e6fa..85532ab 100644
--- a/check-darker.toml
+++ b/check-darker.toml
@@ -24,6 +24,10 @@ src = [
 ]
 revision = "origin/master..."
 isort = true
+
+[tool.graylint]
+src = ["."]
+revision = "origin/master..."
 lint = [
     "flake8",
     "mypy",
diff --git a/constraints-oldest.txt b/constraints-oldest.txt
index 0f9c615..fc3d5b9 100644
--- a/constraints-oldest.txt
+++ b/constraints-oldest.txt
@@ -10,7 +10,6 @@ flake8-2020==1.6.1
 flake8-bugbear==22.1.11
 flake8-comprehensions==3.7.0
 flynt==0.76
-graylint==2.0.0
 mypy==0.990
 Pygments==2.4.0
 pytest==6.2.0
diff --git a/pyproject.toml b/pyproject.toml
index d19411e..3c30c6d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,6 +27,10 @@ src = [
 ]
 revision = "origin/master..."
 
+[tool.graylint]
+revision = "origin/master..."
+src = ["."]
+
 [tool.pylint."messages control"]
 # Check import order only with isort. Pylint doesn't support a custom list of first
 # party packages. We want to consider "darkgraylib" and "graylint" as first party.
@@ -46,6 +50,15 @@ ignore = [
 ]
 
 [tool.ruff.lint.per-file-ignores]
+"action/*.py" = [
+    "T201",  # `print` found
+]
+"action/tests/*.py" = [
+    "S101",  # Use of `assert` detected
+]
+"src/darker/__main__.py" = [
+    "T201",  # `print` found
+]
 "src/darker/tests/*.py" = [
     "ANN001",  # Missing type annotation for function argument
     "ANN201",  # Missing return type annotation for public function
diff --git a/setup.cfg b/setup.cfg
index f3c842a..191d76a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -30,7 +30,6 @@ install_requires =
     # NOTE: remember to keep `constraints-oldest.txt` in sync with these
     black>=22.3.0
     darkgraylib~=2.0.1
-    graylint~=2.0.0
     toml>=0.10.0
 # NOTE: remember to keep `.github/workflows/python-package.yml` in sync
 #       with the minimum required Python version
@@ -64,7 +63,6 @@ test =
     isort>=5.0.1
     mypy>=0.990
     pathspec  # to test `gen_python_files` in `test_black_diff.py`
-    pip-requirements-parser
     pydocstyle
     pygments
     pylint
diff --git a/src/darker/__main__.py b/src/darker/__main__.py
index 195d5f7..9d59837 100644
--- a/src/darker/__main__.py
+++ b/src/darker/__main__.py
@@ -30,7 +30,7 @@ from darker.git import (
     git_get_modified_python_files,
     git_is_repository,
 )
-from darker.help import get_extra_instruction
+from darker.help import LINTING_GUIDE, get_extra_instruction
 from darker.import_sorting import apply_isort, isort
 from darker.utils import debug_dump, glob_any
 from darker.verification import ASTVerifier, BinarySearch, NotEquivalentError
@@ -53,7 +53,6 @@ from darkgraylib.highlighting import colorize, should_use_color
 from darkgraylib.log import setup_logging
 from darkgraylib.main import resolve_paths
 from darkgraylib.utils import GIT_DATEFORMAT, WINDOWS, DiffChunk, TextDocument
-from graylint.linting import run_linters
 
 logger = logging.getLogger(__name__)
 
@@ -627,20 +626,9 @@ def main(  # noqa: C901,PLR0912,PLR0915
             print_source(new, use_color)
         if write_modified_files:
             modify_file(path, new)
-    linter_failures_on_modified_lines = run_linters(
-        args.lint,
-        common_root,
-        # paths to lint are not limited to modified files or just Python files:
-        {p.resolve().relative_to(common_root) for p in paths},
-        revrange,
-        use_color,
-    )
-    return (
-        1
-        if linter_failures_on_modified_lines
-        or (args.check and formatting_failures_on_modified_lines)
-        else 0
-    )
+    if args.lint:
+        print(LINTING_GUIDE, end="")
+    return 1 if args.check and formatting_failures_on_modified_lines else 0
 
 
 def main_with_error_handling() -> int:
diff --git a/src/darker/command_line.py b/src/darker/command_line.py
index f6bc289..7797266 100644
--- a/src/darker/command_line.py
+++ b/src/darker/command_line.py
@@ -12,7 +12,6 @@ from darker import help as hlp
 from darker.config import DEPRECATED_CONFIG_OPTIONS, DarkerConfig, OutputMode
 from darker.version import __version__
 from darkgraylib.command_line import add_parser_argument
-from graylint.command_line import add_lint_arg
 
 
 def make_argument_parser(require_src: bool) -> ArgumentParser:
@@ -27,8 +26,7 @@ def make_argument_parser(require_src: bool) -> ArgumentParser:
         "Darker",
         hlp.DESCRIPTION,
         "Make `darker`, `black` and `isort` read configuration from `PATH`. Note that"
-        " other tools like `flynt`, `mypy`, `pylint` or `flake8` won't use this"
-        " configuration file.",
+        " other tools like `flynt` won't use this configuration file.",
         __version__,
     )
 
@@ -40,7 +38,7 @@ def make_argument_parser(require_src: bool) -> ArgumentParser:
     add_arg(hlp.FLYNT, "-f", "--flynt", action="store_true")
     add_arg(hlp.ISORT, "-i", "--isort", action="store_true")
     add_arg(hlp.PREVIEW, "--preview", action="store_true")
-    add_lint_arg(parser)
+    add_arg(hlp.LINT, "-L", "--lint", action="append", metavar="CMD", default=[])
     add_arg(
         hlp.SKIP_STRING_NORMALIZATION,
         "-S",
@@ -91,6 +89,13 @@ def show_config_deprecations(config: DarkerConfig) -> None:
             DeprecationWarning,
             stacklevel=2,
         )
+    if "lint" in config:
+        warnings.warn(
+            "Baseline linting has been moved to the Graylint package. Please"
+            " remove the `lint =` option from your configuration file.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
 
 
 def parse_command_line(
diff --git a/src/darker/help.py b/src/darker/help.py
index 96890c6..8376603 100644
--- a/src/darker/help.py
+++ b/src/darker/help.py
@@ -1,5 +1,7 @@
 """Help and usage instruction texts used for the command line parser"""
 
+from textwrap import dedent
+
 from black import TargetVersion
 
 
@@ -100,12 +102,19 @@ if not isort:
     )
 ISORT = "".join(ISORT_PARTS)
 
-LINT = (
-    "Also run a linter on changed files. `CMD` can be a name or path of the linter"
-    " binary, or a full quoted command line with the command and options. Linters read"
-    " their configuration as normally, and aren't affected by `-c` / `--config`. Linter"
-    " output is syntax highlighted when the `pygments` package is available if run on"
-    " a terminal and or enabled by explicitly (see `--color`)."
+LINT = "Show information about baseline linting using the Graylint package."
+LINTING_GUIDE = dedent(
+    """
+    Baseline linting support has been moved to the Graylint package. Please install
+    and run it using:
+
+        pip install graylint
+        graylint -L CMD <directory>
+        graylint --lint CMD <directory>
+
+    See https://pypi.org/project/graylint for more information."
+
+    """,
 )
 
 VERBOSE = "Show steps taken and summarize modifications"


Test patch:
diff --git a/action/tests/test_main.py b/action/tests/test_main.py
index e0d7261..beca446 100644
--- a/action/tests/test_main.py
+++ b/action/tests/test_main.py
@@ -2,18 +2,21 @@
 
 # pylint: disable=use-dict-literal
 
-import re
+from __future__ import annotations
+
 import sys
 from contextlib import contextmanager
-from pathlib import Path
 from runpy import run_module
 from subprocess import PIPE, STDOUT, CompletedProcess  # nosec
 from types import SimpleNamespace
-from typing import Dict, Generator
+from typing import TYPE_CHECKING, Generator
 from unittest.mock import ANY, Mock, call, patch
 
 import pytest
 
+if TYPE_CHECKING:
+    from pathlib import Path
+
 # pylint: disable=redefined-outer-name,unused-argument
 
 
@@ -25,7 +28,7 @@ class SysExitCalled(Exception):
 
 
 @pytest.fixture
-def run_main_env() -> Dict[str, str]:
+def run_main_env() -> dict[str, str]:
     """By default, call `main.py` with just `GITHUB_ACTION_PATH` in the environment"""
     return {}
 
@@ -33,9 +36,9 @@ def run_main_env() -> Dict[str, str]:
 @contextmanager
 def patch_main(
     tmp_path: Path,
-    run_main_env: Dict[str, str],
+    run_main_env: dict[str, str],
     pip_returncode: int = 0,
-) -> Generator[SimpleNamespace, None, None]:
+) -> Generator[SimpleNamespace]:
     """Patch `subprocess.run`, `sys.exit` and environment variables
 
     :param tmp_path: Path to use for the `GITHUB_ACTION_PATH` environment variable
@@ -61,8 +64,9 @@ def patch_main(
 
 @pytest.fixture
 def main_patch(
-    tmp_path: Path, run_main_env: Dict[str, str]
-) -> Generator[SimpleNamespace, None, None]:
+    tmp_path: Path,
+    run_main_env: dict[str, str],
+) -> Generator[SimpleNamespace]:
     """`subprocess.run, `sys.exit` and environment variables patching as Pytest fixture
 
     :param tmp_path: Path to use for the `GITHUB_ACTION_PATH` environment variable
@@ -98,24 +102,12 @@ def test_creates_virtualenv(tmp_path, main_patch):
         ],
     ),
     dict(
-        run_main_env={"INPUT_LINT": "pylint"},
-        expect=["darker[color,isort]", "pylint"],
-    ),
-    dict(
-        run_main_env={"INPUT_LINT": "pylint,flake8"},
-        expect=["darker[color,isort]", "pylint", "flake8"],
-    ),
-    dict(
-        run_main_env={"INPUT_LINT": "  flake8  "},
-        expect=["darker[color,isort]", "flake8"],
-    ),
-    dict(
-        run_main_env={"INPUT_LINT": "  flake8  ,  pylint  "},
-        expect=["darker[color,isort]", "flake8", "pylint"],
+        run_main_env={"INPUT_LINT": "dummy"},
+        expect=["darker[color,isort]"],
     ),
     dict(
-        run_main_env={"INPUT_LINT": "  flake8  >=  3.9.2  ,  pylint  ==  2.13.1  "},
-        expect=["darker[color,isort]", "flake8>=3.9.2", "pylint==2.13.1"],
+        run_main_env={"INPUT_LINT": "dummy,foobar"},
+        expect=["darker[color,isort]"],
     ),
 )
 def test_installs_packages(tmp_path, main_patch, run_main_env, expect):
@@ -139,24 +131,6 @@ def test_installs_packages(tmp_path, main_patch, run_main_env, expect):
     )
 
 
-@pytest.mark.parametrize(
-    "linters", ["foo", "  foo  ", "foo==2.0,bar", "  foo>1.0  ,  bar  ", "pylint,foo"]
-)
-def test_wont_install_unknown_packages(tmp_path, linters):
-    """Non-whitelisted linters raise an exception"""
-    with patch_main(tmp_path, {"INPUT_LINT": linters}) as main_patch, pytest.raises(
-        RuntimeError,
-        match=re.escape("'foo' is not supported as a linter by the GitHub Action"),
-    ):
-
-        run_module("main")
-
-    # only virtualenv `run` called, `pip` and `darker` not called
-    (venv_create,) = main_patch.subprocess.run.call_args_list
-    assert venv_create == call([ANY, "-m", "venv", ANY], check=True)
-    assert not main_patch.sys.exit.called
-
-
 @pytest.mark.kwparametrize(
     dict(env={"INPUT_SRC": "."}, expect=["--revision", "HEAD^", "."]),
     dict(
@@ -184,12 +158,12 @@ def test_wont_install_unknown_packages(tmp_path, linters):
         expect=["--revision", "master...", "."],
     ),
     dict(
-        env={"INPUT_SRC": ".", "INPUT_LINT": "pylint,flake8"},
-        expect=["--lint", "pylint", "--lint", "flake8", "--revision", "HEAD^", "."],
+        env={"INPUT_SRC": ".", "INPUT_LINT": "dummy,foobar"},
+        expect=["--revision", "HEAD^", "."],
     ),
     dict(
-        env={"INPUT_SRC": ".", "INPUT_LINT": "pylint == 2.13.1,flake8>=3.9.2"},
-        expect=["--lint", "pylint", "--lint", "flake8", "--revision", "HEAD^", "."],
+        env={"INPUT_SRC": ".", "INPUT_LINT": "dummy == 2.13.1,foobar>=3.9.2"},
+        expect=["--revision", "HEAD^", "."],
     ),
     dict(
         env={
@@ -197,15 +171,11 @@ def test_wont_install_unknown_packages(tmp_path, linters):
             "INPUT_OPTIONS": "--isort --verbose",
             "INPUT_REVISION": "master...",
             "INPUT_COMMIT_RANGE": "ignored",
-            "INPUT_LINT": "pylint,flake8",
+            "INPUT_LINT": "dummy,foobar",
         },
         expect=[
             "--isort",
             "--verbose",
-            "--lint",
-            "pylint",
-            "--lint",
-            "flake8",
             "--revision",
             "master...",
             "here.py",
@@ -213,7 +183,7 @@ def test_wont_install_unknown_packages(tmp_path, linters):
         ],
     ),
 )
-def test_runs_darker(tmp_path, env, expect):
+def test_runs_darker(tmp_path: Path, env: dict[str, str], expect: list[str]) -> None:
     """Configuration translates correctly into a Darker command line"""
     with patch_main(tmp_path, env) as main_patch, pytest.raises(SysExitCalled):
 
@@ -221,7 +191,12 @@ def test_runs_darker(tmp_path, env, expect):
 
     darker = str(tmp_path / ".darker-env" / BIN / "darker")
     # This gets the first list item of the first positional argument to the `run` call.
-    assert darker in [c.args[0][0] for c in main_patch.subprocess.run.call_args_list]
+    (darker_call,) = (
+        c.args[0]
+        for c in main_patch.subprocess.run.call_args_list
+        if c.args[0][0] == darker
+    )
+    assert darker_call[1:] == expect
 
 
 def test_error_if_pip_fails(tmp_path, capsys):
diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py
index e8940af..698a0ba 100644
--- a/src/darker/tests/test_command_line.py
+++ b/src/darker/tests/test_command_line.py
@@ -118,16 +118,16 @@ def get_darker_help_output(capsys):
         expect_modified=("lint", ...),
     ),
     dict(
-        argv=["-L", "pylint", "."],
-        expect_value=("lint", ["pylint"]),
-        expect_config=("lint", ["pylint"]),
-        expect_modified=("lint", ["pylint"]),
+        argv=["-L", "dummy", "."],
+        expect_value=("lint", ["dummy"]),
+        expect_config=("lint", ["dummy"]),
+        expect_modified=("lint", ["dummy"]),
     ),
     dict(
-        argv=["--lint", "flake8", "-L", "mypy", "."],
-        expect_value=("lint", ["flake8", "mypy"]),
-        expect_config=("lint", ["flake8", "mypy"]),
-        expect_modified=("lint", ["flake8", "mypy"]),
+        argv=["--lint", "dummy", "-L", "foobar", "."],
+        expect_value=("lint", ["dummy", "foobar"]),
+        expect_config=("lint", ["dummy", "foobar"]),
+        expect_modified=("lint", ["dummy", "foobar"]),
     ),
     dict(
         argv=["."],
@@ -255,7 +255,13 @@ def test_parse_command_line(
     dict(config={"stdout": True}, expect_warn=set()),
     dict(config={"check": True}, expect_warn=set()),
     dict(config={"isort": True}, expect_warn=set()),
-    dict(config={"lint": ["pylint"]}, expect_warn=set()),
+    dict(
+        config={"lint": ["dummy"]},
+        expect_warn={
+            "Baseline linting has been moved to the Graylint package. Please remove the"
+            " `lint =` option from your configuration file.",
+        },
+    ),
     dict(
         config={"skip_string_normalization": True},
         expect_warn={
@@ -278,13 +284,15 @@ def test_parse_command_line(
             "stdout": False,
             "check": True,
             "isort": True,
-            "lint": ["pylint"],
+            "lint": ["dummy"],
             "skip_string_normalization": True,
             "skip_magic_trailing_comma": True,
             "line_length": 88,
             "target_version": "py37",
         },
         expect_warn={
+            "Baseline linting has been moved to the Graylint package. Please remove the"
+            " `lint =` option from your configuration file.",
             "The configuration option `skip_magic_trailing_comma` in [tool.darker] is"
             " deprecated and will be removed in Darker 3.0.",
             "The configuration option `skip_string_normalization` in [tool.darker] is"
@@ -717,16 +725,14 @@ def test_options(git_repo, options, expect):
 
 
 @pytest.mark.kwparametrize(
-    dict(check=False, changes=False, lintfail=False),
-    dict(check=False, changes=False, lintfail=True, expect_retval=1),
-    dict(check=False, changes=True, lintfail=False),
-    dict(check=False, changes=True, lintfail=True, expect_retval=1),
-    dict(check=True, changes=False, lintfail=False),
-    dict(check=True, changes=True, lintfail=True, expect_retval=1),
+    dict(arguments=["a.py"], changes=False),
+    dict(arguments=["a.py"], changes=True),
+    dict(arguments=["--check", "a.py"], changes=False),
+    dict(arguments=["--check", "a.py"], changes=True, expect_retval=1),
     expect_retval=0,
 )
-def test_main_retval(git_repo, check, changes, lintfail, expect_retval):
-    """main() return value is correct based on --check, reformatting and linters"""
+def test_main_retval(git_repo, arguments, changes, expect_retval):
+    """``main()`` return value is correct based on ``--check`` and reformatting."""
     git_repo.add({"a.py": ""}, commit="Initial commit")
     format_edited_parts = Mock()
     format_edited_parts.return_value = (
@@ -740,16 +746,13 @@ def test_main_retval(git_repo, check, changes, lintfail, expect_retval):
         if changes
         else []
     )
-    run_linters = Mock(return_value=lintfail)
-    check_arg_maybe = ["--check"] if check else []
     with patch.multiple(
         "darker.__main__",
         format_edited_parts=format_edited_parts,
         modify_file=DEFAULT,
-        run_linters=run_linters,
     ):
 
-        retval = main(check_arg_maybe + ["a.py"])
+        retval = main(arguments)
 
     assert retval == expect_retval
 
diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py
index b074316..dcedaa2 100644
--- a/src/darker/tests/test_main.py
+++ b/src/darker/tests/test_main.py
@@ -10,13 +10,13 @@ import string
 from argparse import ArgumentError
 from pathlib import Path
 from textwrap import dedent
-from unittest.mock import ANY, call, patch
 
 import pytest
 
 import darker.__main__
 import darker.import_sorting
 from darker.git import EditedLinenumsDiffer
+from darker.help import LINTING_GUIDE
 from darker.tests.examples import A_PY, A_PY_BLACK, A_PY_BLACK_FLYNT, A_PY_BLACK_ISORT
 from darker.tests.test_fstring import FLYNTED_SOURCE, MODIFIED_SOURCE, ORIGINAL_SOURCE
 from darkgraylib.git import RevisionRange
@@ -112,17 +112,16 @@ A_PY_DIFF_BLACK_FLYNT = [
         expect_retval=1,
     ),
     dict(
-        arguments=["--check", "--lint", "echo subdir/a.py:1: message"],
+        arguments=["--check", "--lint", "dummy"],
         # Windows compatible path assertion using `pathlib.Path()`
-        expect_stdout=["", f"{Path('subdir/a.py')}:1: message {Path('subdir')} [echo]"],
+        expect_stdout=["", *LINTING_GUIDE.lstrip().splitlines()],
         expect_retval=1,
     ),
     dict(
-        arguments=["--diff", "--lint", "echo subdir/a.py:1: message"],
+        arguments=["--diff", "--lint", "dummy"],
         # Windows compatible path assertion using `pathlib.Path()`
-        expect_stdout=A_PY_DIFF_BLACK
-        + ["", f"{Path('subdir/a.py')}:1: message {Path('subdir')} [echo]"],
-        expect_retval=1,
+        expect_stdout=[*A_PY_DIFF_BLACK, "", *LINTING_GUIDE.lstrip().splitlines()],
+        expect_retval=0,
     ),
     dict(
         arguments=[],
@@ -250,41 +249,35 @@ def test_main_in_plain_directory(tmp_path, capsys):
     (subdir_a / "non-python file.txt").write_text("not  reformatted\n")
     (subdir_a / "python file.py").write_text("import  sys, os\nprint('ok')")
     (subdir_c / "another python file.py").write_text("a  =5")
-    with patch.object(darker.__main__, "run_linters") as run_linters:
 
-        retval = darker.__main__.main(
-            ["--diff", "--check", "--isort", "--lint", "echo", str(tmp_path)]
-        )
+    retval = darker.__main__.main(
+        ["--diff", "--check", "--isort", "--lint", "dummy", str(tmp_path)],
+    )
 
     assert retval == 1
-    assert run_linters.call_args_list == [
-        call(
-            ["echo"],
-            tmp_path,
-            {Path(".")},
-            RevisionRange(rev1="HEAD", rev2=":WORKTREE:"),
-            False,
-        )
-    ]
     output = capsys.readouterr().out
     output = _replace_diff_timestamps(output)
-    assert output == dedent(
-        """\
-        --- subdir_a/python file.py	<timestamp> +0000
-        +++ subdir_a/python file.py	<timestamp> +0000
-        @@ -1,2 +1,4 @@
-        -import  sys, os
-        -print('ok')
-        +import os
-        +import sys
-        +
-        +print("ok")
-        --- subdir_b/subdir_c/another python file.py	<timestamp> +0000
-        +++ subdir_b/subdir_c/another python file.py	<timestamp> +0000
-        @@ -1 +1 @@
-        -a  =5
-        +a = 5
-        """
+    assert (
+        output
+        == dedent(
+            """\
+            --- subdir_a/python file.py	<timestamp> +0000
+            +++ subdir_a/python file.py	<timestamp> +0000
+            @@ -1,2 +1,4 @@
+            -import  sys, os
+            -print('ok')
+            +import os
+            +import sys
+            +
+            +print("ok")
+            --- subdir_b/subdir_c/another python file.py	<timestamp> +0000
+            +++ subdir_b/subdir_c/another python file.py	<timestamp> +0000
+            @@ -1 +1 @@
+            -a  =5
+            +a = 5
+            """,
+        )
+        + LINTING_GUIDE
     )
 
 
@@ -395,20 +388,6 @@ def test_main_vscode_tmpfile(git_repo, capsys):
     ]
 
 
-def test_main_lint_unchanged(git_repo):
-    """Linters are run on all ``src`` command line options, modified or not"""
-    git_repo.add({"src/a.py": "foo\n", "src/subdir/b.py": "bar\n"}, commit="Initial")
-    with patch.object(darker.__main__, "run_linters") as run_linters:
-        run_linters.return_value = 0
-
-        retval = darker.__main__.main(["--check", "--lint=mylint", "src"])
-
-    run_linters.assert_called_once_with(
-        ["mylint"], Path("src").absolute(), {Path(".")}, ANY, ANY
-    )
-    assert retval == 0
-
-
 def test_print_diff(tmp_path, capsys):
     """print_diff() prints Black-style diff output with 5 lines of context"""
     Path(tmp_path / "a.py").write_text("dummy\n", encoding="utf-8")