From 8b1ea33cc0cb18bec3bf19d1131b322b313cea4e Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Mon, 7 Feb 2022 22:54:25 -0800 Subject: Add a unimport linter for Python files (#4058) Unimport (https://github.com/hakancelik96/unimport/) is a linter, formatter for finding and removing unused import statements. This introduces linting support, although fixer support could come later. --- ale_linters/python/unimport.vim | 75 ++++++++++++++++++++++ doc/ale-python.txt | 52 +++++++++++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + test/handler/test_unimport_handler.vader | 18 ++++++ test/linter/test_unimport.vader | 71 ++++++++++++++++++++ .../with_virtualenv/env/Scripts/unimport.exe | 0 .../python/with_virtualenv/env/bin/unimport | 0 9 files changed, 219 insertions(+) create mode 100644 ale_linters/python/unimport.vim create mode 100644 test/handler/test_unimport_handler.vader create mode 100644 test/linter/test_unimport.vader create mode 100755 test/test-files/python/with_virtualenv/env/Scripts/unimport.exe create mode 100755 test/test-files/python/with_virtualenv/env/bin/unimport diff --git a/ale_linters/python/unimport.vim b/ale_linters/python/unimport.vim new file mode 100644 index 00000000..71fd80f0 --- /dev/null +++ b/ale_linters/python/unimport.vim @@ -0,0 +1,75 @@ +" Author: Author: Jon Parise + +call ale#Set('python_unimport_executable', 'unimport') +call ale#Set('python_unimport_options', '') +call ale#Set('python_unimport_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('python_unimport_auto_pipenv', 0) +call ale#Set('python_unimport_auto_poetry', 0) + +function! ale_linters#python#unimport#GetExecutable(buffer) abort + if (ale#Var(a:buffer, 'python_auto_pipenv') || ale#Var(a:buffer, 'python_unimport_auto_pipenv')) + \ && ale#python#PipenvPresent(a:buffer) + return 'pipenv' + endif + + if (ale#Var(a:buffer, 'python_auto_poetry') || ale#Var(a:buffer, 'python_unimport_auto_poetry')) + \ && ale#python#PoetryPresent(a:buffer) + return 'poetry' + endif + + return ale#python#FindExecutable(a:buffer, 'python_unimport', ['unimport']) +endfunction + +function! ale_linters#python#unimport#GetCommand(buffer) abort + let l:executable = ale_linters#python#unimport#GetExecutable(a:buffer) + let l:exec_args = l:executable =~? 'pipenv\|poetry$' + \ ? ' run unimport' + \ : '' + + return '%e' . l:exec_args + \ . ale#Pad(ale#Var(a:buffer, 'python_unimport_options')) + \ . ' --check' + \ . ' %t' +endfunction + + +function! ale_linters#python#unimport#GetCwd(buffer) abort + let l:project_root = ale#python#FindProjectRoot(a:buffer) + + return !empty(l:project_root) + \ ? l:project_root + \ : expand('#' . a:buffer . ':p:h') +endfunction + + +function! ale_linters#python#unimport#Handle(buffer, lines) abort + let l:output = ale#python#HandleTraceback(a:lines, 10) + + if !empty(l:output) + return l:output + endif + + " Matches lines like: + " + " urllib.parse at path/to/file.py:9 + let l:pattern = '\v(.+) at [^:]+:(\d+)$' + + for l:match in ale#util#GetMatches(a:lines, l:pattern) + call add(l:output, { + \ 'lnum': l:match[2] + 0, + \ 'type': 'W', + \ 'text': 'unused: ' . l:match[1], + \}) + endfor + + return l:output +endfunction + + +call ale#linter#Define('python', { +\ 'name': 'unimport', +\ 'executable': function('ale_linters#python#unimport#GetExecutable'), +\ 'cwd': function('ale_linters#python#unimport#GetCwd'), +\ 'command': function('ale_linters#python#unimport#GetCommand'), +\ 'callback': 'ale_linters#python#unimport#Handle', +\}) diff --git a/doc/ale-python.txt b/doc/ale-python.txt index 7486d0b4..2f9f5d29 100644 --- a/doc/ale-python.txt +++ b/doc/ale-python.txt @@ -1066,6 +1066,58 @@ g:ale_python_reorder_python_imports_use_global See |ale-integrations-local-executables| +=============================================================================== +unimport *ale-python-unimport* + +`unimport` will be run from a detected project root, per |ale-python-root|. + + +g:ale_python_unimport_auto_pipenv *g:ale_python_unimport_auto_pipenv* + *b:ale_python_unimport_auto_pipenv* + Type: |Number| + Default: `0` + + Detect whether the file is inside a pipenv, and set the executable to `pipenv` + if true. This is overridden by a manually-set executable. + + +g:ale_python_unimport_auto_poetry *g:ale_python_unimport_auto_poetry* + *b:ale_python_unimport_auto_poetry* + Type: |Number| + Default: `0` + + Detect whether the file is inside a poetry, and set the executable to `poetry` + if true. This is overridden by a manually-set executable. + + +g:ale_python_unimport_executable *g:ale_python_mypy_executable* + *b:ale_python_unimport_executable* + Type: |String| + Default: `'unimport'` + + See |ale-integrations-local-executables| + + Set this to `'pipenv'` to invoke `'pipenv` `run` `unimport'`. + Set this to `'poetry'` to invoke `'poetry` `run` `unimport'`. + + +g:ale_python_unimport_options *g:ale_python_mypy_options* + *b:ale_python_unimport_options* + Type: |String| + Default: `''` + + This variable can be changed to add command-line arguments to the unimport + invocation. + + +g:ale_python_unimport_use_global *g:ale_python_mypy_use_global* + *b:ale_python_unimport_use_global* + Type: |Number| + Default: `get(g:, 'ale_use_global_executables', 0)` + + See |ale-integrations-local-executables| + + =============================================================================== vulture *ale-python-vulture* diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index ee950592..67efc6ec 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -463,6 +463,7 @@ Notes: * `pyre` * `pyright` * `reorder-python-imports` + * `unimport` * `vulture`!! * `yapf` * QML diff --git a/doc/ale.txt b/doc/ale.txt index 9ce54f12..6dbfb6d0 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3081,6 +3081,7 @@ documented in additional help files. pyre..................................|ale-python-pyre| pyright...............................|ale-python-pyright| reorder-python-imports................|ale-python-reorder_python_imports| + unimport..............................|ale-python-unimport| vulture...............................|ale-python-vulture| yapf..................................|ale-python-yapf| qml.....................................|ale-qml-options| diff --git a/supported-tools.md b/supported-tools.md index 123df579..491e7b96 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -472,6 +472,7 @@ formatting. * [pyre](https://github.com/facebook/pyre-check) :warning: * [pyright](https://github.com/microsoft/pyright) * [reorder-python-imports](https://github.com/asottile/reorder_python_imports) + * [unimport](https://github.com/hakancelik96/unimport) * [vulture](https://github.com/jendrikseipp/vulture) :warning: :floppy_disk: * [yapf](https://github.com/google/yapf) * QML diff --git a/test/handler/test_unimport_handler.vader b/test/handler/test_unimport_handler.vader new file mode 100644 index 00000000..5acdcbb9 --- /dev/null +++ b/test/handler/test_unimport_handler.vader @@ -0,0 +1,18 @@ +Before: + runtime ale_linters/python/unimport.vim + +After: + call ale#linter#Reset() + +Execute(The unimport handler should handle import warnings): + AssertEqual + \ [ + \ { + \ 'lnum': 9, + \ 'type': 'W', + \ 'text': 'unused: urllib.parse', + \ }, + \ ], + \ ale_linters#python#unimport#Handle(1, [ + \ 'urllib.parse at path/to/file.py:9', + \ ]) diff --git a/test/linter/test_unimport.vader b/test/linter/test_unimport.vader new file mode 100644 index 00000000..a5607ce9 --- /dev/null +++ b/test/linter/test_unimport.vader @@ -0,0 +1,71 @@ +Before: + call ale#assert#SetUpLinterTest('python', 'unimport') + call ale#test#SetFilename('test.py') + + let b:bin_dir = has('win32') ? 'Scripts' : 'bin' + +After: + unlet! b:executable + unlet! b:bin_dir + + call ale#assert#TearDownLinterTest() + +Execute(The unimport callbacks should return the correct default values): + AssertLinter 'unimport', ale#Escape('unimport') . ' --check %t' + +Execute(The unimport executable should be configurable, and escaped properly): + let b:ale_python_unimport_executable = 'foobar' + + AssertLinter 'foobar', ale#Escape('foobar') . ' --check %t' + +Execute(The unimport command callback should let you set options): + let b:ale_python_unimport_options = '--gitignore' + + AssertLinter 'unimport', ale#Escape('unimport') . ' --gitignore --check %t' + +Execute(The unimport command should switch directories to the detected project root): + call ale#test#SetFilename('../test-files/python/no_virtualenv/subdir/foo/bar.py') + + AssertLinterCwd ale#path#Simplify(g:dir . '/../test-files/python/no_virtualenv/subdir') + AssertLinter 'unimport', ale#Escape('unimport') . ' --check %t' + +Execute(The unimport callbacks should detect virtualenv directories and switch to the project root): + call ale#test#SetFilename('../test-files/python/with_virtualenv/subdir/foo/bar.py') + + let b:executable = ale#path#Simplify( + \ g:dir . '/../test-files/python/with_virtualenv/env/' . b:bin_dir . '/unimport' + \) + + AssertLinter b:executable, ale#Escape(b:executable) . ' --check %t' + +Execute(You should able able to use the global unimport instead): + call ale#test#SetFilename('../test-files/python/with_virtualenv/subdir/foo/bar.py') + let g:ale_python_unimport_use_global = 1 + + AssertLinter 'unimport', ale#Escape('unimport') . ' --check %t' + +Execute(Setting executable to 'pipenv' appends 'run unimport'): + let g:ale_python_unimport_executable = 'path/to/pipenv' + + AssertLinterCwd expand('#' . bufnr('') . ':p:h') + AssertLinter 'path/to/pipenv', ale#Escape('path/to/pipenv') . ' run unimport --check %t' + +Execute(Pipenv is detected when python_unimport_auto_pipenv is set): + call ale#test#SetFilename('../test-files/python/pipenv/whatever.py') + let g:ale_python_unimport_auto_pipenv = 1 + + AssertLinterCwd expand('#' . bufnr('') . ':p:h') + AssertLinter 'pipenv', ale#Escape('pipenv') . ' run unimport --check %t' + +Execute(Setting executable to 'poetry' appends 'run unimport'): + let g:ale_python_unimport_executable = 'path/to/poetry' + + AssertLinterCwd expand('#' . bufnr('') . ':p:h') + AssertLinter 'path/to/poetry', ale#Escape('path/to/poetry') . ' run unimport --check %t' + +Execute(Poetry is detected when python_unimport_auto_poetry is set): + call ale#test#SetFilename('../test-files/python/poetry/whatever.py') + let g:ale_python_unimport_auto_poetry = 1 + + AssertLinterCwd expand('#' . bufnr('') . ':p:h') + AssertLinter 'poetry', ale#Escape('poetry') . ' run unimport --check %t' diff --git a/test/test-files/python/with_virtualenv/env/Scripts/unimport.exe b/test/test-files/python/with_virtualenv/env/Scripts/unimport.exe new file mode 100755 index 00000000..e69de29b diff --git a/test/test-files/python/with_virtualenv/env/bin/unimport b/test/test-files/python/with_virtualenv/env/bin/unimport new file mode 100755 index 00000000..e69de29b -- cgit v1.2.3