Multiple, optional linters for a single filetype

Python has a number of linters and static analysis tools available, some of them are meta-tools that run multiple different kinds of checks over your code at once. Unfortunately the check that the meta-tools always seem to leave out is static type checking, using a tool like mypy. And so I want to run two linters on my Python code, mypy for type-checking and flake8 for everything else.

Kakoune has only one lintcmd option, which contains a shell-command that will be given the path to a file to check. That means we can run multiple linters by using a shell function to run both of them:

set-option window lintcmd %{ runlints() {
        mypy --show-column-numbers "$1"
        flake8 --max-complexity 10 "$1"
}; runlints }

The lintcmd will be given to the shell with a filename added on; this causes the runlints function to be defined, and then immediately called with the filename. Inside the function, each linter is called with $1, the filename that was passed to the runlints function.

But that’s not enough. I use my Kakoune config on different systems, and they don’t always have the same tools available. I’d like to gracefully ignore tools that don’t exist, perhaps with a warning if there’s no linters available at all.

This is what I came up with:

hook global WinSetOption filetype=python %{
    evaluate-commands %sh{
        # Clear existing arguments
        # so we can use it as an array.
        set --

        # Add these linters to the array, if we have them.
        for linter in \
                "mypy --strict --show-column-numbers" \
                "flake8 --max-complexity 10"
        do
            # The first whitespace-delimited word of the linter
            tool_name=${linter%% *}
            # If we have that tool...
            if command -V "$tool_name" >/dev/null 2>/dev/null; then
                # ...add it to the list
                set -- "$@" "$linter"
            fi
        done

        # If we found any linters...
        if [ $# -gt 0 ]; then
            # ...add them all to the lintcmd option,
            # using a shell function to run each linter sequentially
            # on the given file.
            echo "set-option window lintcmd %{ l() {"
            printf '%s "$1"\n' "$@"
            echo "}; l }"

            # ...and automatically lint the buffer after writing it.
            echo hook window -group python-auto-lint BufWritePost '.*' lint-buffer
            echo hook window WinSetOption filetype=.* %{ rmhooks window python-auto-lint; unset-option window lintcmd }
        else
            # Otherwise, put a warning in the *debug* buffer
            echo "Could not find any supported linters" 1>&2
        fi
    }
}
3 Likes