Close #2522 - Check pylint on the fly

Newer versions of pylint will now check your code as you type. Older
versions will still only check the file on disk.

Co-authored-by: Oliver Wiegers <oliver.wiegers@gmail.com>
This commit is contained in:
w0rp 2020-09-09 21:42:27 +01:00
parent 78fa93bd55
commit 4ddf742643
No known key found for this signature in database
GPG key ID: 0FC1ECAA8C81CD83
5 changed files with 119 additions and 54 deletions

View file

@ -17,7 +17,7 @@ function! ale_linters#python#pylint#GetExecutable(buffer) abort
return ale#python#FindExecutable(a:buffer, 'python_pylint', ['pylint']) return ale#python#FindExecutable(a:buffer, 'python_pylint', ['pylint'])
endfunction endfunction
function! ale_linters#python#pylint#GetCommand(buffer) abort function! ale_linters#python#pylint#GetCommand(buffer, version) abort
let l:cd_string = '' let l:cd_string = ''
if ale#Var(a:buffer, 'python_pylint_change_directory') if ale#Var(a:buffer, 'python_pylint_change_directory')
@ -38,17 +38,23 @@ function! ale_linters#python#pylint#GetCommand(buffer) abort
return l:cd_string return l:cd_string
\ . ale#Escape(l:executable) . l:exec_args \ . ale#Escape(l:executable) . l:exec_args
\ . ' ' . ale#Var(a:buffer, 'python_pylint_options') \ . ale#Pad(ale#Var(a:buffer, 'python_pylint_options'))
\ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n' \ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n'
\ . (ale#semver#GTE(a:version, [2, 4, 0]) ? ' --from-stdin' : '')
\ . ' %s' \ . ' %s'
endfunction endfunction
function! ale_linters#python#pylint#Handle(buffer, lines) abort function! ale_linters#python#pylint#Handle(buffer, lines) abort
let l:output = ale#python#HandleTraceback(a:lines, 10)
if !empty(l:output)
return l:output
endif
" Matches patterns like the following: " Matches patterns like the following:
" "
" test.py:4:4: W0101 (unreachable) Unreachable code " test.py:4:4: W0101 (unreachable) Unreachable code
let l:pattern = '\v^[a-zA-Z]?:?[^:]+:(\d+):(\d+): ([[:alnum:]]+) \(([^(]*)\) (.*)$' let l:pattern = '\v^[a-zA-Z]?:?[^:]+:(\d+):(\d+): ([[:alnum:]]+) \(([^(]*)\) (.*)$'
let l:output = []
for l:match in ale#util#GetMatches(a:lines, l:pattern) for l:match in ale#util#GetMatches(a:lines, l:pattern)
"let l:failed = append(0, l:match) "let l:failed = append(0, l:match)
@ -71,13 +77,19 @@ function! ale_linters#python#pylint#Handle(buffer, lines) abort
let l:code_out = l:match[4] let l:code_out = l:match[4]
endif endif
call add(l:output, { let l:item = {
\ 'lnum': l:match[1] + 0, \ 'lnum': l:match[1] + 0,
\ 'col': l:match[2] + 1, \ 'col': l:match[2] + 1,
\ 'text': l:match[5], \ 'text': l:match[5],
\ 'code': l:code_out, \ 'code': l:code_out,
\ 'type': l:code[:0] is# 'E' ? 'E' : 'W', \ 'type': 'W',
\}) \}
if l:code[:0] is# 'E'
let l:item.type = 'E'
endif
call add(l:output, l:item)
endfor endfor
return l:output return l:output
@ -86,7 +98,17 @@ endfunction
call ale#linter#Define('python', { call ale#linter#Define('python', {
\ 'name': 'pylint', \ 'name': 'pylint',
\ 'executable': function('ale_linters#python#pylint#GetExecutable'), \ 'executable': function('ale_linters#python#pylint#GetExecutable'),
\ 'command': function('ale_linters#python#pylint#GetCommand'), \ 'lint_file': {buffer -> ale#semver#RunWithVersionCheck(
\ buffer,
\ ale#Var(buffer, 'python_pylint_executable'),
\ '%e --version',
\ {buffer, version -> !ale#semver#GTE(version, [2, 4, 0])},
\ )},
\ 'command': {buffer -> ale#semver#RunWithVersionCheck(
\ buffer,
\ ale#Var(buffer, 'python_pylint_executable'),
\ '%e --version',
\ function('ale_linters#python#pylint#GetCommand'),
\ )},
\ 'callback': 'ale_linters#python#pylint#Handle', \ 'callback': 'ale_linters#python#pylint#Handle',
\ 'lint_file': 1,
\}) \})

View file

@ -444,7 +444,7 @@ function! s:RunJob(command, options) abort
return 1 return 1
endfunction endfunction
function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort function! s:StopCurrentJobs(buffer, clear_lint_file_jobs, linter_slots) abort
let l:info = get(g:ale_buffer_info, a:buffer, {}) let l:info = get(g:ale_buffer_info, a:buffer, {})
call ale#command#StopJobs(a:buffer, 'linter') call ale#command#StopJobs(a:buffer, 'linter')
@ -453,13 +453,23 @@ function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort
call ale#command#StopJobs(a:buffer, 'file_linter') call ale#command#StopJobs(a:buffer, 'file_linter')
let l:info.active_linter_list = [] let l:info.active_linter_list = []
else else
let l:lint_file_map = {}
" Use a previously computed map of `lint_file` values to find
" linters that are used for linting files.
for [l:lint_file, l:linter] in a:linter_slots
if l:lint_file is 1
let l:lint_file_map[l:linter.name] = 1
endif
endfor
" Keep jobs for linting files when we're only linting buffers. " Keep jobs for linting files when we're only linting buffers.
call filter(l:info.active_linter_list, 'get(v:val, ''lint_file'')') call filter(l:info.active_linter_list, 'get(l:lint_file_map, v:val.name)')
endif endif
endfunction endfunction
function! ale#engine#Stop(buffer) abort function! ale#engine#Stop(buffer) abort
call s:StopCurrentJobs(a:buffer, 1) call s:StopCurrentJobs(a:buffer, 1, [])
endfunction endfunction
function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort
@ -562,6 +572,22 @@ function! s:RunLinter(buffer, linter, lint_file) abort
return 0 return 0
endfunction endfunction
function! s:GetLintFileSlots(buffer, linters) abort
let l:linter_slots = []
for l:linter in a:linters
let l:LintFile = l:linter.lint_file
if type(l:LintFile) is v:t_func
let l:LintFile = l:LintFile(a:buffer)
endif
call add(l:linter_slots, [l:LintFile, l:linter])
endfor
return l:linter_slots
endfunction
function! s:GetLintFileValues(slots, Callback) abort function! s:GetLintFileValues(slots, Callback) abort
let l:deferred_list = [] let l:deferred_list = []
let l:new_slots = [] let l:new_slots = []
@ -595,12 +621,18 @@ endfunction
function! s:RunLinters( function! s:RunLinters(
\ buffer, \ buffer,
\ linters,
\ slots, \ slots,
\ should_lint_file, \ should_lint_file,
\ new_buffer, \ new_buffer,
\ can_clear_results
\) abort \) abort
let l:can_clear_results = a:can_clear_results call s:StopCurrentJobs(a:buffer, a:should_lint_file, a:slots)
call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters)
" We can only clear the results if we aren't checking the buffer.
let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer)
silent doautocmd <nomodeline> User ALELintPre
for [l:lint_file, l:linter] in a:slots for [l:lint_file, l:linter] in a:slots
" Only run lint_file linters if we should. " Only run lint_file linters if we should.
@ -631,36 +663,19 @@ endfunction
function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort
" Initialise the buffer information if needed. " Initialise the buffer information if needed.
let l:new_buffer = ale#engine#InitBufferInfo(a:buffer) let l:new_buffer = ale#engine#InitBufferInfo(a:buffer)
call s:StopCurrentJobs(a:buffer, a:should_lint_file)
call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters)
" We can only clear the results if we aren't checking the buffer. call s:GetLintFileValues(
let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer) \ s:GetLintFileSlots(a:buffer, a:linters),
\ {
silent doautocmd <nomodeline> User ALELintPre \ slots -> s:RunLinters(
\ a:buffer,
" Handle `lint_file` callbacks first. \ a:linters,
let l:linter_slots = [] \ slots,
\ a:should_lint_file,
for l:linter in a:linters \ l:new_buffer,
let l:LintFile = l:linter.lint_file \ )
\ }
if type(l:LintFile) is v:t_func \)
let l:LintFile = l:LintFile(a:buffer)
endif
call add(l:linter_slots, [l:LintFile, l:linter])
endfor
call s:GetLintFileValues(l:linter_slots, {
\ new_slots -> s:RunLinters(
\ a:buffer,
\ new_slots,
\ a:should_lint_file,
\ l:new_buffer,
\ l:can_clear_results,
\ )
\})
endfunction endfunction
" Clean up a buffer. " Clean up a buffer.

View file

@ -179,8 +179,11 @@ script like so. >
#!/usr/bin/env bash #!/usr/bin/env bash
exec docker run --rm -v "$(pwd):/data" cytopia/pylint "$@" exec docker run -i --rm -v "$(pwd):/data" cytopia/pylint "$@"
< <
You will run to run Docker commands with `-i` in order to read from stdin.
With the above script in mind, you might configure ALE to lint your Python With the above script in mind, you might configure ALE to lint your Python
project with `pylint` by providing the path to the script to execute, and project with `pylint` by providing the path to the script to execute, and
mappings which describe how to between the two file systems in your mappings which describe how to between the two file systems in your

View file

@ -8,6 +8,8 @@ Before:
let b:bin_dir = has('win32') ? 'Scripts' : 'bin' let b:bin_dir = has('win32') ? 'Scripts' : 'bin'
let b:command_tail = ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s' let b:command_tail = ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s'
GivenCommandOutput ['pylint 2.3.0']
After: After:
unlet! b:bin_dir unlet! b:bin_dir
unlet! b:executable unlet! b:executable
@ -17,26 +19,33 @@ After:
Execute(The pylint callbacks should return the correct default values): Execute(The pylint callbacks should return the correct default values):
AssertLinter 'pylint', AssertLinter 'pylint',
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) \ ale#path#CdString(expand('%:p:h'))
\ . ale#Escape('pylint') . ' ' . b:command_tail \ . ale#Escape('pylint') . b:command_tail
Execute(Pylint should run with the --from-stdin in new enough versions):
GivenCommandOutput ['pylint 2.4.0']
AssertLinter 'pylint',
\ ale#path#CdString(expand('%:p:h'))
\ . ale#Escape('pylint') . b:command_tail[:-3] . '--from-stdin %s'
Execute(The option for disabling changing directories should work): Execute(The option for disabling changing directories should work):
let g:ale_python_pylint_change_directory = 0 let g:ale_python_pylint_change_directory = 0
AssertLinter 'pylint', ale#Escape('pylint') . ' ' . b:command_tail AssertLinter 'pylint', ale#Escape('pylint') . b:command_tail
Execute(The pylint executable should be configurable, and escaped properly): Execute(The pylint executable should be configurable, and escaped properly):
let g:ale_python_pylint_executable = 'executable with spaces' let g:ale_python_pylint_executable = 'executable with spaces'
AssertLinter 'executable with spaces', AssertLinter 'executable with spaces',
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) \ ale#path#CdString(expand('%:p:h'))
\ . ale#Escape('executable with spaces') . ' ' . b:command_tail \ . ale#Escape('executable with spaces') . b:command_tail
Execute(The pylint command callback should let you set options): Execute(The pylint command callback should let you set options):
let g:ale_python_pylint_options = '--some-option' let g:ale_python_pylint_options = '--some-option'
AssertLinter 'pylint', AssertLinter 'pylint',
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) \ ale#path#CdString(expand('%:p:h'))
\ . ale#Escape('pylint') . ' --some-option' . b:command_tail \ . ale#Escape('pylint') . ' --some-option' . b:command_tail
Execute(The pylint callbacks shouldn't detect virtualenv directories where they don't exist): Execute(The pylint callbacks shouldn't detect virtualenv directories where they don't exist):
@ -44,7 +53,7 @@ Execute(The pylint callbacks shouldn't detect virtualenv directories where they
AssertLinter 'pylint', AssertLinter 'pylint',
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/no_virtualenv/subdir')) \ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/no_virtualenv/subdir'))
\ . ale#Escape('pylint') . ' ' . b:command_tail \ . ale#Escape('pylint') . b:command_tail
Execute(The pylint callbacks should detect virtualenv directories): Execute(The pylint callbacks should detect virtualenv directories):
silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py') silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py')
@ -55,7 +64,7 @@ Execute(The pylint callbacks should detect virtualenv directories):
AssertLinter b:executable, AssertLinter b:executable,
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir')) \ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir'))
\ . ale#Escape(b:executable) . ' ' . b:command_tail \ . ale#Escape(b:executable) . b:command_tail
Execute(You should able able to use the global pylint instead): Execute(You should able able to use the global pylint instead):
silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py') silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py')
@ -63,7 +72,7 @@ Execute(You should able able to use the global pylint instead):
AssertLinter 'pylint', AssertLinter 'pylint',
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir')) \ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir'))
\ . ale#Escape('pylint') . ' ' . b:command_tail \ . ale#Escape('pylint') . b:command_tail
Execute(Setting executable to 'pipenv' appends 'run pylint'): Execute(Setting executable to 'pipenv' appends 'run pylint'):
let g:ale_python_pylint_executable = 'path/to/pipenv' let g:ale_python_pylint_executable = 'path/to/pipenv'
@ -71,7 +80,7 @@ Execute(Setting executable to 'pipenv' appends 'run pylint'):
AssertLinter 'path/to/pipenv', AssertLinter 'path/to/pipenv',
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) \ ale#path#CdString(expand('#' . bufnr('') . ':p:h'))
\ . ale#Escape('path/to/pipenv') . ' run pylint' \ . ale#Escape('path/to/pipenv') . ' run pylint'
\ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s' \ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s'
Execute(Pipenv is detected when python_pylint_auto_pipenv is set): Execute(Pipenv is detected when python_pylint_auto_pipenv is set):
let g:ale_python_pylint_auto_pipenv = 1 let g:ale_python_pylint_auto_pipenv = 1
@ -80,4 +89,4 @@ Execute(Pipenv is detected when python_pylint_auto_pipenv is set):
AssertLinter 'pipenv', AssertLinter 'pipenv',
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) \ ale#path#CdString(expand('#' . bufnr('') . ':p:h'))
\ . ale#Escape('pipenv') . ' run pylint' \ . ale#Escape('pipenv') . ' run pylint'
\ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s' \ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s'

View file

@ -132,3 +132,19 @@ Execute(Linters where lint_file eventually evaluates to 1 shouldn't be run if we
call ale#test#FlushJobs() call ale#test#FlushJobs()
AssertEqual [], ale#test#GetLoclistWithoutModule() AssertEqual [], ale#test#GetLoclistWithoutModule()
Execute(Keeping computed lint_file jobs running should work):
AssertEqual 'testlinter2', ale#linter#Get('foobar')[1].name
call ale#engine#InitBufferInfo(bufnr(''))
call ale#engine#MarkLinterActive(
\ g:ale_buffer_info[bufnr('')],
\ ale#linter#Get('foobar')[1]
\)
call ale#engine#RunLinters(bufnr(''), ale#linter#Get('foobar'), 0)
Assert !empty(g:ale_buffer_info[bufnr('')].active_linter_list),
\ 'The active linter list was empty'
Assert ale#engine#IsCheckingBuffer(bufnr('')),
\ 'The IsCheckingBuffer function returned 0'