diff --git a/README.md b/README.md index aa35e91..36d35b7 100644 --- a/README.md +++ b/README.md @@ -61,5 +61,8 @@ - [ ] [shellcheck](https://github.com/koalaman/shellcheck) - [ ] [stylelint](https://stylelint.io/) - [x] [ruff](https://github.com/astral-sh/ruff) +- [x] [mypy](https://mypy.readthedocs.io/en/stable/index.html) +- [x] [mypyc](https://mypyc.readthedocs.io/en/latest/index.html) +- [x] [dmypy](https://mypy.readthedocs.io/en/stable/mypy_daemon.html) ## License MIT diff --git a/lua/guard-collection/linter/init.lua b/lua/guard-collection/linter/init.lua index 110a595..6e5c33f 100644 --- a/lua/guard-collection/linter/init.lua +++ b/lua/guard-collection/linter/init.lua @@ -15,4 +15,7 @@ return { shellcheck = require('guard-collection.linter.shellcheck'), stylelint = require('guard-collection.linter.stylelint'), ruff = require('guard-collection.linter.ruff'), + mypy = require('guard-collection.linter.mypy').mypy, + mypyc = require('guard-collection.linter.mypy').mypyc, + dmypy = require('guard-collection.linter.mypy').dmypy, } diff --git a/lua/guard-collection/linter/mypy/base.lua b/lua/guard-collection/linter/mypy/base.lua new file mode 100644 index 0000000..ba0b513 --- /dev/null +++ b/lua/guard-collection/linter/mypy/base.lua @@ -0,0 +1,78 @@ +local lint = require('guard.lint') + +return function(cmd, args) + local opts = { + offset = 1, + severities = { + error = lint.severities.error, + warning = lint.severities.warning, + note = lint.severities.info, + }, + source = 'mypy', + } + + -- see spec for pattern examples + local full = { + regex = '([^:]+):(%d+):(%d+):(%d+):(%d+): (%a+): (.*) %[([%a-]+)%]', + groups = { 'filename', 'lnum', 'col', 'end_lnum', 'end_col', 'severity', 'message', 'code' }, + } + + -- no err code + local no_err = { + regex = '([^:]+):(%d+):(%d+):(%d+):(%d+): (%a+): (.*)', + groups = { 'filename', 'lnum', 'col', 'end_lnum', 'end_col', 'severity', 'message' }, + } + + return { + cmd = cmd, + args = args, + fname = true, + parse = function(result, bufnr) + local diags, offences = {}, {} + + local lines = vim.split(result, '\n', { trimempty = true }) + + for _, line in ipairs(lines) do + local offence = {} + + local groups = full.groups + local matches = { line:match(full.regex) } + + if #matches ~= #groups then + matches = { line:match(no_err.regex) } + groups = no_err.groups + end + + -- regex matched + if #matches == #groups then + for i = 1, #groups do + offence[groups[i]] = matches[i] + end + + offences[#offences + 1] = offence + end + end + + vim.tbl_map(function(mes) + local code = mes.code + if not mes.code then + code = '' + end + local diag = lint.diag_fmt( + bufnr, + tonumber(mes.lnum) - opts.offset, + tonumber(mes.col) - opts.offset, + ('%s [%s]'):format(mes.message, code), + opts.severities[mes.severity], + opts.source + ) + + diag.end_col = tonumber(mes.end_col) - opts.offset + diag.end_lnum = tonumber(mes.end_lnum) - opts.offset + diags[#diags + 1] = diag + end, offences) + + return diags + end, + } +end diff --git a/lua/guard-collection/linter/mypy/init.lua b/lua/guard-collection/linter/mypy/init.lua new file mode 100644 index 0000000..73b8c2a --- /dev/null +++ b/lua/guard-collection/linter/mypy/init.lua @@ -0,0 +1,20 @@ +local base = require('guard-collection.linter.mypy.base') + +local args = { + '--hide-error-codes', + '--hide-error-context', + '--no-color-output', + '--show-absolute-path', + '--show-column-numbers', + '--show-error-codes', + '--show-error-end', + '--no-error-summary', + '--no-pretty', +} + +local unpack = table.unpack or unpack +return { + mypy = base('mypy', args), + mypyc = base('mypyc', args), + dmypy = base('dmypy', { 'run', '--', unpack(args) }), +} diff --git a/test/linter/mypy_spec.lua b/test/linter/mypy_spec.lua new file mode 100644 index 0000000..6c1cebd --- /dev/null +++ b/test/linter/mypy_spec.lua @@ -0,0 +1,84 @@ +describe('mypy', function() + it('can lint', function() + local helper = require('test.linter.helper') + local ns = helper.namespace + local ft = require('guard.filetype') + ft('python'):lint('mypy') + require('guard').setup() + + local diagnostics = helper.test_with('python', { + [[def f(i: int) -> int:]], + [[ i += "123"]], + [[ return i]], + [[sum()]], + }) + assert.are.same({ + { + bufnr = 3, + col = 9, + end_col = 13, + end_lnum = 1, + lnum = 1, + message = 'Unsupported operand types for + ("int" and "str") [operator]', + namespace = 33, + severity = 1, + source = 'mypy', + }, + { + bufnr = 3, + col = 0, + end_col = 4, + end_lnum = 3, + lnum = 3, + message = 'All overload variants of "sum" require at least one argument [call-overload]', + namespace = 33, + severity = 1, + source = 'mypy', + }, + { + bufnr = 3, + col = 0, + end_col = 4, + end_lnum = 3, + lnum = 3, + message = 'Possible overload variants: []', + namespace = 33, + severity = 3, + source = 'mypy', + }, + { + bufnr = 3, + col = 0, + end_col = 4, + end_lnum = 3, + lnum = 3, + message = ' def sum(Iterable[bool], /, start: int = ...) -> int []', + namespace = 33, + severity = 3, + source = 'mypy', + }, + { + bufnr = 3, + col = 0, + end_col = 4, + end_lnum = 3, + lnum = 3, + message = ' def [_SupportsSumNoDefaultT <: _SupportsSumWithNoDefaultGiven] sum(Iterable[_SupportsSumNoDefaultT], /) -> _SupportsSumNoDefaultT | Literal[0] []', + namespace = 33, + severity = 3, + source = 'mypy', + }, + { + bufnr = 3, + col = 0, + end_col = 4, + end_lnum = 3, + lnum = 3, + message = ' def [_AddableT1 <: SupportsAdd[Any, Any], _AddableT2 <: SupportsAdd[Any, Any]] sum(Iterable[_AddableT1], /, start: _AddableT2) -> _AddableT1 | _AddableT2 []', + namespace = 33, + severity = 3, + source = 'mypy', + }, + }, diagnostics) + end) +end) diff --git a/test/setup.sh b/test/setup.sh index 59e8f9e..a2da7e1 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -8,13 +8,14 @@ sudo apt-get install -qqq \ clang-format clang-tidy fish elixir & luarocks install luacheck & go install github.com/segmentio/golines@latest & -pip -qqq install autopep8 black djhtml flake8 isort pylint yapf codespell ruff sqlfluff & +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.0 & +pip -qqq install autopep8 black djhtml flake8 isort pylint yapf codespell ruff sqlfluff mypy & npm install -g --silent \ prettier @fsouza/prettierd sql-formatter shellcheck shfmt @taplo/cli & gem install -q rubocop & # Block, homebrew takes the longest time brew install \ - swiftformat swift-format hadolint google-java-format pgformatter fnlfmt ormolu golangci-lint + swiftformat swift-format hadolint google-java-format pgformatter fnlfmt ormolu # Install standalone binary packages bin="/home/runner/.local/bin"