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:
parent
78fa93bd55
commit
4ddf742643
5 changed files with 119 additions and 54 deletions
|
@ -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,
|
|
||||||
\})
|
\})
|
||||||
|
|
|
@ -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(
|
||||||
|
|
||||||
" Handle `lint_file` callbacks first.
|
|
||||||
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
|
|
||||||
|
|
||||||
call s:GetLintFileValues(l:linter_slots, {
|
|
||||||
\ new_slots -> s:RunLinters(
|
|
||||||
\ a:buffer,
|
\ a:buffer,
|
||||||
\ new_slots,
|
\ a:linters,
|
||||||
|
\ slots,
|
||||||
\ a:should_lint_file,
|
\ a:should_lint_file,
|
||||||
\ l:new_buffer,
|
\ l:new_buffer,
|
||||||
\ l:can_clear_results,
|
|
||||||
\ )
|
\ )
|
||||||
\})
|
\ }
|
||||||
|
\)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" Clean up a buffer.
|
" Clean up a buffer.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
Reference in a new issue