Skip to content

Commit 4ddf742

Browse files
w0rpoliverwiegers
andcommitted
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 <[email protected]>
1 parent 78fa93b commit 4ddf742

File tree

5 files changed

+119
-54
lines changed

5 files changed

+119
-54
lines changed

ale_linters/python/pylint.vim

+30-8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function! ale_linters#python#pylint#GetExecutable(buffer) abort
1717
return ale#python#FindExecutable(a:buffer, 'python_pylint', ['pylint'])
1818
endfunction
1919

20-
function! ale_linters#python#pylint#GetCommand(buffer) abort
20+
function! ale_linters#python#pylint#GetCommand(buffer, version) abort
2121
let l:cd_string = ''
2222

2323
if ale#Var(a:buffer, 'python_pylint_change_directory')
@@ -38,17 +38,23 @@ function! ale_linters#python#pylint#GetCommand(buffer) abort
3838

3939
return l:cd_string
4040
\ . ale#Escape(l:executable) . l:exec_args
41-
\ . ' ' . ale#Var(a:buffer, 'python_pylint_options')
41+
\ . ale#Pad(ale#Var(a:buffer, 'python_pylint_options'))
4242
\ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n'
43+
\ . (ale#semver#GTE(a:version, [2, 4, 0]) ? ' --from-stdin' : '')
4344
\ . ' %s'
4445
endfunction
4546

4647
function! ale_linters#python#pylint#Handle(buffer, lines) abort
48+
let l:output = ale#python#HandleTraceback(a:lines, 10)
49+
50+
if !empty(l:output)
51+
return l:output
52+
endif
53+
4754
" Matches patterns like the following:
4855
"
4956
" test.py:4:4: W0101 (unreachable) Unreachable code
5057
let l:pattern = '\v^[a-zA-Z]?:?[^:]+:(\d+):(\d+): ([[:alnum:]]+) \(([^(]*)\) (.*)$'
51-
let l:output = []
5258

5359
for l:match in ale#util#GetMatches(a:lines, l:pattern)
5460
"let l:failed = append(0, l:match)
@@ -71,13 +77,19 @@ function! ale_linters#python#pylint#Handle(buffer, lines) abort
7177
let l:code_out = l:match[4]
7278
endif
7379

74-
call add(l:output, {
80+
let l:item = {
7581
\ 'lnum': l:match[1] + 0,
7682
\ 'col': l:match[2] + 1,
7783
\ 'text': l:match[5],
7884
\ 'code': l:code_out,
79-
\ 'type': l:code[:0] is# 'E' ? 'E' : 'W',
80-
\})
85+
\ 'type': 'W',
86+
\}
87+
88+
if l:code[:0] is# 'E'
89+
let l:item.type = 'E'
90+
endif
91+
92+
call add(l:output, l:item)
8193
endfor
8294

8395
return l:output
@@ -86,7 +98,17 @@ endfunction
8698
call ale#linter#Define('python', {
8799
\ 'name': 'pylint',
88100
\ 'executable': function('ale_linters#python#pylint#GetExecutable'),
89-
\ 'command': function('ale_linters#python#pylint#GetCommand'),
101+
\ 'lint_file': {buffer -> ale#semver#RunWithVersionCheck(
102+
\ buffer,
103+
\ ale#Var(buffer, 'python_pylint_executable'),
104+
\ '%e --version',
105+
\ {buffer, version -> !ale#semver#GTE(version, [2, 4, 0])},
106+
\ )},
107+
\ 'command': {buffer -> ale#semver#RunWithVersionCheck(
108+
\ buffer,
109+
\ ale#Var(buffer, 'python_pylint_executable'),
110+
\ '%e --version',
111+
\ function('ale_linters#python#pylint#GetCommand'),
112+
\ )},
90113
\ 'callback': 'ale_linters#python#pylint#Handle',
91-
\ 'lint_file': 1,
92114
\})

autoload/ale/engine.vim

+49-34
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ function! s:RunJob(command, options) abort
444444
return 1
445445
endfunction
446446

447-
function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort
447+
function! s:StopCurrentJobs(buffer, clear_lint_file_jobs, linter_slots) abort
448448
let l:info = get(g:ale_buffer_info, a:buffer, {})
449449
call ale#command#StopJobs(a:buffer, 'linter')
450450

@@ -453,13 +453,23 @@ function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort
453453
call ale#command#StopJobs(a:buffer, 'file_linter')
454454
let l:info.active_linter_list = []
455455
else
456+
let l:lint_file_map = {}
457+
458+
" Use a previously computed map of `lint_file` values to find
459+
" linters that are used for linting files.
460+
for [l:lint_file, l:linter] in a:linter_slots
461+
if l:lint_file is 1
462+
let l:lint_file_map[l:linter.name] = 1
463+
endif
464+
endfor
465+
456466
" Keep jobs for linting files when we're only linting buffers.
457-
call filter(l:info.active_linter_list, 'get(v:val, ''lint_file'')')
467+
call filter(l:info.active_linter_list, 'get(l:lint_file_map, v:val.name)')
458468
endif
459469
endfunction
460470

461471
function! ale#engine#Stop(buffer) abort
462-
call s:StopCurrentJobs(a:buffer, 1)
472+
call s:StopCurrentJobs(a:buffer, 1, [])
463473
endfunction
464474

465475
function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort
@@ -562,6 +572,22 @@ function! s:RunLinter(buffer, linter, lint_file) abort
562572
return 0
563573
endfunction
564574

575+
function! s:GetLintFileSlots(buffer, linters) abort
576+
let l:linter_slots = []
577+
578+
for l:linter in a:linters
579+
let l:LintFile = l:linter.lint_file
580+
581+
if type(l:LintFile) is v:t_func
582+
let l:LintFile = l:LintFile(a:buffer)
583+
endif
584+
585+
call add(l:linter_slots, [l:LintFile, l:linter])
586+
endfor
587+
588+
return l:linter_slots
589+
endfunction
590+
565591
function! s:GetLintFileValues(slots, Callback) abort
566592
let l:deferred_list = []
567593
let l:new_slots = []
@@ -595,12 +621,18 @@ endfunction
595621

596622
function! s:RunLinters(
597623
\ buffer,
624+
\ linters,
598625
\ slots,
599626
\ should_lint_file,
600627
\ new_buffer,
601-
\ can_clear_results
602628
\) abort
603-
let l:can_clear_results = a:can_clear_results
629+
call s:StopCurrentJobs(a:buffer, a:should_lint_file, a:slots)
630+
call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters)
631+
632+
" We can only clear the results if we aren't checking the buffer.
633+
let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer)
634+
635+
silent doautocmd <nomodeline> User ALELintPre
604636

605637
for [l:lint_file, l:linter] in a:slots
606638
" Only run lint_file linters if we should.
@@ -631,36 +663,19 @@ endfunction
631663
function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort
632664
" Initialise the buffer information if needed.
633665
let l:new_buffer = ale#engine#InitBufferInfo(a:buffer)
634-
call s:StopCurrentJobs(a:buffer, a:should_lint_file)
635-
call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters)
636-
637-
" We can only clear the results if we aren't checking the buffer.
638-
let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer)
639-
640-
silent doautocmd <nomodeline> User ALELintPre
641-
642-
" Handle `lint_file` callbacks first.
643-
let l:linter_slots = []
644-
645-
for l:linter in a:linters
646-
let l:LintFile = l:linter.lint_file
647-
648-
if type(l:LintFile) is v:t_func
649-
let l:LintFile = l:LintFile(a:buffer)
650-
endif
651666

652-
call add(l:linter_slots, [l:LintFile, l:linter])
653-
endfor
654-
655-
call s:GetLintFileValues(l:linter_slots, {
656-
\ new_slots -> s:RunLinters(
657-
\ a:buffer,
658-
\ new_slots,
659-
\ a:should_lint_file,
660-
\ l:new_buffer,
661-
\ l:can_clear_results,
662-
\ )
663-
\})
667+
call s:GetLintFileValues(
668+
\ s:GetLintFileSlots(a:buffer, a:linters),
669+
\ {
670+
\ slots -> s:RunLinters(
671+
\ a:buffer,
672+
\ a:linters,
673+
\ slots,
674+
\ a:should_lint_file,
675+
\ l:new_buffer,
676+
\ )
677+
\ }
678+
\)
664679
endfunction
665680

666681
" Clean up a buffer.

doc/ale.txt

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

test/command_callback/test_pylint_command_callback.vader

+20-11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Before:
88
let b:bin_dir = has('win32') ? 'Scripts' : 'bin'
99
let b:command_tail = ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s'
1010

11+
GivenCommandOutput ['pylint 2.3.0']
12+
1113
After:
1214
unlet! b:bin_dir
1315
unlet! b:executable
@@ -17,34 +19,41 @@ After:
1719

1820
Execute(The pylint callbacks should return the correct default values):
1921
AssertLinter 'pylint',
20-
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h'))
21-
\ . ale#Escape('pylint') . ' ' . b:command_tail
22+
\ ale#path#CdString(expand('%:p:h'))
23+
\ . ale#Escape('pylint') . b:command_tail
24+
25+
Execute(Pylint should run with the --from-stdin in new enough versions):
26+
GivenCommandOutput ['pylint 2.4.0']
27+
28+
AssertLinter 'pylint',
29+
\ ale#path#CdString(expand('%:p:h'))
30+
\ . ale#Escape('pylint') . b:command_tail[:-3] . '--from-stdin %s'
2231

2332
Execute(The option for disabling changing directories should work):
2433
let g:ale_python_pylint_change_directory = 0
2534

26-
AssertLinter 'pylint', ale#Escape('pylint') . ' ' . b:command_tail
35+
AssertLinter 'pylint', ale#Escape('pylint') . b:command_tail
2736

2837
Execute(The pylint executable should be configurable, and escaped properly):
2938
let g:ale_python_pylint_executable = 'executable with spaces'
3039

3140
AssertLinter 'executable with spaces',
32-
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h'))
33-
\ . ale#Escape('executable with spaces') . ' ' . b:command_tail
41+
\ ale#path#CdString(expand('%:p:h'))
42+
\ . ale#Escape('executable with spaces') . b:command_tail
3443

3544
Execute(The pylint command callback should let you set options):
3645
let g:ale_python_pylint_options = '--some-option'
3746

3847
AssertLinter 'pylint',
39-
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h'))
48+
\ ale#path#CdString(expand('%:p:h'))
4049
\ . ale#Escape('pylint') . ' --some-option' . b:command_tail
4150

4251
Execute(The pylint callbacks shouldn't detect virtualenv directories where they don't exist):
4352
silent execute 'file ' . fnameescape(g:dir . '/python_paths/no_virtualenv/subdir/foo/bar.py')
4453

4554
AssertLinter 'pylint',
4655
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/no_virtualenv/subdir'))
47-
\ . ale#Escape('pylint') . ' ' . b:command_tail
56+
\ . ale#Escape('pylint') . b:command_tail
4857

4958
Execute(The pylint callbacks should detect virtualenv directories):
5059
silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py')
@@ -55,23 +64,23 @@ Execute(The pylint callbacks should detect virtualenv directories):
5564

5665
AssertLinter b:executable,
5766
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir'))
58-
\ . ale#Escape(b:executable) . ' ' . b:command_tail
67+
\ . ale#Escape(b:executable) . b:command_tail
5968

6069
Execute(You should able able to use the global pylint instead):
6170
silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py')
6271
let g:ale_python_pylint_use_global = 1
6372

6473
AssertLinter 'pylint',
6574
\ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir'))
66-
\ . ale#Escape('pylint') . ' ' . b:command_tail
75+
\ . ale#Escape('pylint') . b:command_tail
6776

6877
Execute(Setting executable to 'pipenv' appends 'run pylint'):
6978
let g:ale_python_pylint_executable = 'path/to/pipenv'
7079

7180
AssertLinter 'path/to/pipenv',
7281
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h'))
7382
\ . ale#Escape('path/to/pipenv') . ' run pylint'
74-
\ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s'
83+
\ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s'
7584

7685
Execute(Pipenv is detected when python_pylint_auto_pipenv is set):
7786
let g:ale_python_pylint_auto_pipenv = 1
@@ -80,4 +89,4 @@ Execute(Pipenv is detected when python_pylint_auto_pipenv is set):
8089
AssertLinter 'pipenv',
8190
\ ale#path#CdString(expand('#' . bufnr('') . ':p:h'))
8291
\ . ale#Escape('pipenv') . ' run pylint'
83-
\ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s'
92+
\ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s'

test/test_computed_lint_file_values.vader

+16
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,19 @@ Execute(Linters where lint_file eventually evaluates to 1 shouldn't be run if we
132132
call ale#test#FlushJobs()
133133

134134
AssertEqual [], ale#test#GetLoclistWithoutModule()
135+
136+
Execute(Keeping computed lint_file jobs running should work):
137+
AssertEqual 'testlinter2', ale#linter#Get('foobar')[1].name
138+
139+
call ale#engine#InitBufferInfo(bufnr(''))
140+
141+
call ale#engine#MarkLinterActive(
142+
\ g:ale_buffer_info[bufnr('')],
143+
\ ale#linter#Get('foobar')[1]
144+
\)
145+
call ale#engine#RunLinters(bufnr(''), ale#linter#Get('foobar'), 0)
146+
147+
Assert !empty(g:ale_buffer_info[bufnr('')].active_linter_list),
148+
\ 'The active linter list was empty'
149+
Assert ale#engine#IsCheckingBuffer(bufnr('')),
150+
\ 'The IsCheckingBuffer function returned 0'

0 commit comments

Comments
 (0)