Close #3285 - lint_file is now dynamic

`lint_file` can now be computed dynamically with a callback function,
which can return a deferred result, as per `ale#command#Run`. This
allows linters to dynamically switch between checking files on disk,
or checking code on the fly.

Some tests have been fixed on Windows.
This commit is contained in:
w0rp 2020-08-28 14:02:05 +01:00
parent b8c0ac2e61
commit 34e409ea21
No known key found for this signature in database
GPG key ID: 0FC1ECAA8C81CD83
9 changed files with 276 additions and 62 deletions

View file

@ -417,7 +417,7 @@ function! s:RunJob(command, options) abort
let l:buffer = a:options.buffer let l:buffer = a:options.buffer
let l:linter = a:options.linter let l:linter = a:options.linter
let l:output_stream = a:options.output_stream let l:output_stream = a:options.output_stream
let l:read_buffer = a:options.read_buffer let l:read_buffer = a:options.read_buffer && !a:options.lint_file
let l:info = g:ale_buffer_info[l:buffer] let l:info = g:ale_buffer_info[l:buffer]
let l:Callback = function('s:HandleExit', [{ let l:Callback = function('s:HandleExit', [{
@ -508,10 +508,15 @@ function! s:AddProblemsFromOtherBuffers(buffer, linters) abort
endif endif
endfunction endfunction
function! s:RunIfExecutable(buffer, linter, executable) abort function! s:RunIfExecutable(buffer, linter, lint_file, executable) abort
if ale#command#IsDeferred(a:executable) if ale#command#IsDeferred(a:executable)
let a:executable.result_callback = { let a:executable.result_callback = {
\ executable -> s:RunIfExecutable(a:buffer, a:linter, executable) \ executable -> s:RunIfExecutable(
\ a:buffer,
\ a:linter,
\ a:lint_file,
\ executable
\ )
\} \}
return 1 return 1
@ -519,7 +524,7 @@ function! s:RunIfExecutable(buffer, linter, executable) abort
if ale#engine#IsExecutable(a:buffer, a:executable) if ale#engine#IsExecutable(a:buffer, a:executable)
" Use different job types for file or linter jobs. " Use different job types for file or linter jobs.
let l:job_type = a:linter.lint_file ? 'file_linter' : 'linter' let l:job_type = a:lint_file ? 'file_linter' : 'linter'
call setbufvar(a:buffer, 'ale_job_type', l:job_type) call setbufvar(a:buffer, 'ale_job_type', l:job_type)
let l:command = ale#linter#GetCommand(a:buffer, a:linter) let l:command = ale#linter#GetCommand(a:buffer, a:linter)
@ -529,6 +534,7 @@ function! s:RunIfExecutable(buffer, linter, executable) abort
\ 'linter': a:linter, \ 'linter': a:linter,
\ 'output_stream': get(a:linter, 'output_stream', 'stdout'), \ 'output_stream': get(a:linter, 'output_stream', 'stdout'),
\ 'read_buffer': a:linter.read_buffer, \ 'read_buffer': a:linter.read_buffer,
\ 'lint_file': a:lint_file,
\} \}
return s:RunJob(l:command, l:options) return s:RunJob(l:command, l:options)
@ -540,33 +546,62 @@ endfunction
" Run a linter for a buffer. " Run a linter for a buffer.
" "
" Returns 1 if the linter was successfully run. " Returns 1 if the linter was successfully run.
function! s:RunLinter(buffer, linter) abort function! s:RunLinter(buffer, linter, lint_file) abort
if !empty(a:linter.lsp) if !empty(a:linter.lsp)
return ale#lsp_linter#CheckWithLSP(a:buffer, a:linter) return ale#lsp_linter#CheckWithLSP(a:buffer, a:linter)
else else
let l:executable = ale#linter#GetExecutable(a:buffer, a:linter) let l:executable = ale#linter#GetExecutable(a:buffer, a:linter)
return s:RunIfExecutable(a:buffer, a:linter, l:executable) return s:RunIfExecutable(a:buffer, a:linter, a:lint_file, l:executable)
endif endif
return 0 return 0
endfunction endfunction
function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort function! s:GetLintFileValues(slots, Callback) abort
" Initialise the buffer information if needed. let l:deferred_list = []
let l:new_buffer = ale#engine#InitBufferInfo(a:buffer) let l:new_slots = []
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. for [l:lint_file, l:linter] in a:slots
let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer) while ale#command#IsDeferred(l:lint_file) && has_key(l:lint_file, 'value')
" If we've already computed the return value, use it.
let l:lint_file = l:lint_file.value
endwhile
silent doautocmd <nomodeline> User ALELintPre if ale#command#IsDeferred(l:lint_file)
" If we are going to return the result later, wait for it.
call add(l:deferred_list, l:lint_file)
else
" If we have the value now, coerce it to 0 or 1.
let l:lint_file = l:lint_file is 1
endif
for l:linter in a:linters call add(l:new_slots, [l:lint_file, l:linter])
endfor
if !empty(l:deferred_list)
for l:deferred in l:deferred_list
let l:deferred.result_callback =
\ {-> s:GetLintFileValues(l:new_slots, a:Callback)}
endfor
else
call a:Callback(l:new_slots)
endif
endfunction
function! s:RunLinters(
\ buffer,
\ slots,
\ should_lint_file,
\ new_buffer,
\ can_clear_results
\) abort
let l:can_clear_results = a:can_clear_results
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.
if !l:linter.lint_file || a:should_lint_file if !l:lint_file || a:should_lint_file
if s:RunLinter(a:buffer, l:linter) if s:RunLinter(a:buffer, l:linter, l:lint_file)
" If a single linter ran, we shouldn't clear everything. " If a single linter ran, we shouldn't clear everything.
let l:can_clear_results = 0 let l:can_clear_results = 0
endif endif
@ -581,11 +616,49 @@ function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort
" disabled, or ALE itself is disabled. " disabled, or ALE itself is disabled.
if l:can_clear_results if l:can_clear_results
call ale#engine#SetResults(a:buffer, []) call ale#engine#SetResults(a:buffer, [])
elseif l:new_buffer elseif a:new_buffer
call s:AddProblemsFromOtherBuffers(a:buffer, a:linters) call s:AddProblemsFromOtherBuffers(
\ a:buffer,
\ map(copy(a:slots), 'v:val[1]')
\)
endif endif
endfunction endfunction
function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort
" Initialise the buffer information if needed.
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.
let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer)
silent doautocmd <nomodeline> User ALELintPre
" 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,
\ new_slots,
\ a:should_lint_file,
\ l:new_buffer,
\ l:can_clear_results,
\ )
\})
endfunction
" Clean up a buffer. " Clean up a buffer.
" "
" This function will stop all current jobs for the buffer, " This function will stop all current jobs for the buffer,

View file

@ -211,21 +211,17 @@ function! ale#linter#PreProcess(filetype, linter) abort
" file on disk. " file on disk.
let l:obj.lint_file = get(a:linter, 'lint_file', 0) let l:obj.lint_file = get(a:linter, 'lint_file', 0)
if !s:IsBoolean(l:obj.lint_file) if !s:IsBoolean(l:obj.lint_file) && type(l:obj.lint_file) isnot v:t_func
throw '`lint_file` must be `0` or `1`' throw '`lint_file` must be `0`, `1`, or a Function'
endif endif
" An option indicating that the buffer should be read. " An option indicating that the buffer should be read.
let l:obj.read_buffer = get(a:linter, 'read_buffer', !l:obj.lint_file) let l:obj.read_buffer = get(a:linter, 'read_buffer', 1)
if !s:IsBoolean(l:obj.read_buffer) if !s:IsBoolean(l:obj.read_buffer)
throw '`read_buffer` must be `0` or `1`' throw '`read_buffer` must be `0` or `1`'
endif endif
if l:obj.lint_file && l:obj.read_buffer
throw 'Only one of `lint_file` or `read_buffer` can be `1`'
endif
let l:obj.aliases = get(a:linter, 'aliases', []) let l:obj.aliases = get(a:linter, 'aliases', [])
if type(l:obj.aliases) isnot v:t_list if type(l:obj.aliases) isnot v:t_list

View file

@ -3038,8 +3038,8 @@ ALELint *ALELint*
Run ALE once for the current buffer. This command can be used to run ALE Run ALE once for the current buffer. This command can be used to run ALE
manually, instead of automatically, if desired. manually, instead of automatically, if desired.
This command will also run linters where `lint_file` is set to `1`, or in This command will also run linters where `lint_file` is evaluates to `1`,
other words linters which check the file instead of the Vim buffer. meaning linters which check the file instead of the Vim buffer.
A plug mapping `<Plug>(ale_lint)` is defined for this command. A plug mapping `<Plug>(ale_lint)` is defined for this command.
@ -3252,9 +3252,9 @@ ale#Queue(delay, [linting_flag, buffer_number]) *ale#Queue()*
The linters will always be run in the background. Calling this function The linters will always be run in the background. Calling this function
again from the same buffer again from the same buffer
An optional `linting_flag` argument can be given. If `linting_flag` An optional `linting_flag` argument can be given. If `linting_flag` is
is `'lint_file'`, then linters where the `lint_file` option is set to `1` will be `'lint_file'`, then linters where the `lint_file` option evaluates to `1`
run. Linters with `lint_file` set to `1` are not run by default. will be run. Otherwise, those linters will not be run.
An optional `buffer_number` argument can be given for specifying the buffer An optional `buffer_number` argument can be given for specifying the buffer
to check. The active buffer (`bufnr('')`) will be checked by default. to check. The active buffer (`bufnr('')`) will be checked by default.
@ -3588,24 +3588,30 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()*
if a command manually reads from a temporary file if a command manually reads from a temporary file
instead, etc. instead, etc.
This option behaves as if it was set to `0` when the
`lint_file` option evaluates to `1`.
*ale-lint-file* *ale-lint-file*
`lint_file` A |Number| (`0` or `1`) indicating whether a command `lint_file` A |Number| (`0` or `1`), or a |Funcref| for a function
should read the file instead of the Vim buffer. This accepting a buffer number for computing either `0` or
option can be used for linters which must check the `1`, indicating whether a command should read the file
file on disk, and which cannot check a Vim buffer instead of the Vim buffer. This option can be used
instead. for linters which must check the file on disk, and
which cannot check a Vim buffer instead.
Linters set with this option will not be run as a The result can be computed with |ale#command#Run()|.
user types, per |g:ale_lint_on_text_changed|. Linters
will instead be run only when events occur against
the file on disk, including |g:ale_lint_on_enter|
and |g:ale_lint_on_save|. Linters with this option
set to `1` will also be run when linters are run
manually, per |ALELintPost-autocmd|.
When this option is set to `1`, `read_buffer` will Linters where the eventual value of this option
be set automatically to `0`. The two options cannot evaluates to `1` will not be run as a user types, per
be used together. |g:ale_lint_on_text_changed|. Linters will instead be
run only when events occur against the file on disk,
including |g:ale_lint_on_enter| and
|g:ale_lint_on_save|. Linters where this option
evaluates to `1` will also be run when the |ALELint|
command is run.
When this option is evaluates to `1`, ALE will behave
as if `read_buffer` was set to `0`.
*ale-lsp-linters* *ale-lsp-linters*
`lsp` A |String| for defining LSP (Language Server Protocol) `lsp` A |String| for defining LSP (Language Server Protocol)

View file

@ -16,7 +16,7 @@ formatting.
| Key | Definition | | Key | Definition |
| ------------- | -------------------------------- | | ------------- | -------------------------------- |
| :floppy_disk: | Only checked when saved to disk | | :floppy_disk: | May only run on files on disk |
| :warning: | Disabled by default | | :warning: | Disabled by default |
--- ---

View file

@ -1,8 +1,10 @@
Before: Before:
Save g:ale_enabled
Save g:ale_set_lists_synchronously Save g:ale_set_lists_synchronously
Save g:ale_buffer_info Save g:ale_buffer_info
Save &shell Save &shell
let g:ale_enabled = 1
let g:ale_buffer_info = {} let g:ale_buffer_info = {}
let g:ale_set_lists_synchronously = 1 let g:ale_set_lists_synchronously = 1

View file

@ -0,0 +1,134 @@
Before:
Save g:ale_enabled
Save g:ale_run_synchronously
Save g:ale_set_lists_synchronously
Save g:ale_buffer_info
let g:ale_enabled = 1
let g:ale_buffer_info = {}
let g:ale_run_synchronously = 1
let g:ale_set_lists_synchronously = 1
function! TestCallback(buffer, output)
" Windows adds extra spaces to the text from echo.
return [{
\ 'lnum': 2,
\ 'col': 3,
\ 'text': 'testlinter1',
\}]
endfunction
function! TestCallback2(buffer, output)
" Windows adds extra spaces to the text from echo.
return [{
\ 'lnum': 1,
\ 'col': 3,
\ 'text': 'testlinter2',
\}]
endfunction
function! TestCallback3(buffer, output)
" Windows adds extra spaces to the text from echo.
return [{
\ 'lnum': 3,
\ 'col': 3,
\ 'text': 'testlinter3',
\}]
endfunction
" These two linters computer their lint_file values after running commands.
call ale#linter#Define('foobar', {
\ 'name': 'testlinter1',
\ 'callback': 'TestCallback',
\ 'executable': has('win32') ? 'cmd' : 'echo',
\ 'command': has('win32') ? 'echo foo bar' : '/bin/sh -c ''echo foo bar''',
\ 'lint_file': {b -> ale#command#Run(b, 'echo', {-> 1})},
\})
call ale#linter#Define('foobar', {
\ 'name': 'testlinter2',
\ 'callback': 'TestCallback2',
\ 'executable': has('win32') ? 'cmd' : 'echo',
\ 'command': has('win32') ? 'echo foo bar' : '/bin/sh -c ''echo foo bar''',
\ 'lint_file': {b -> ale#command#Run(b, 'echo', {-> ale#command#Run(b, 'echo', {-> 1})})},
\})
" This one directly computes the result.
call ale#linter#Define('foobar', {
\ 'name': 'testlinter3',
\ 'callback': 'TestCallback3',
\ 'executable': has('win32') ? 'cmd' : 'echo',
\ 'command': has('win32') ? 'echo foo bar' : '/bin/sh -c ''echo foo bar''',
\ 'lint_file': {b -> 1},
\})
let g:filename = tempname()
call writefile([], g:filename)
call ale#test#SetFilename(g:filename)
After:
delfunction TestCallback
call ale#engine#Cleanup(bufnr(''))
Restore
call ale#linter#Reset()
" Items and markers, etc.
call setloclist(0, [])
call clearmatches()
call ale#sign#Clear()
if filereadable(g:filename)
call delete(g:filename)
endif
unlet g:filename
Given foobar(A file with some lines):
foo
bar
baz
Execute(lint_file results where the result is eventually computed should be run):
call ale#Queue(0, 'lint_file')
call ale#test#FlushJobs()
AssertEqual
\ [
\ {
\ 'bufnr': bufnr('%'),
\ 'lnum': 1,
\ 'vcol': 0,
\ 'col': 3,
\ 'text': 'testlinter2',
\ 'type': 'E',
\ 'nr': -1,
\ 'pattern': '',
\ 'valid': 1,
\ },
\ {
\ 'bufnr': bufnr('%'),
\ 'lnum': 2,
\ 'vcol': 0,
\ 'col': 3,
\ 'text': 'testlinter1',
\ 'type': 'E',
\ 'nr': -1,
\ 'pattern': '',
\ 'valid': 1,
\ },
\ {
\ 'bufnr': bufnr('%'),
\ 'lnum': 3,
\ 'vcol': 0,
\ 'col': 3,
\ 'text': 'testlinter3',
\ 'type': 'E',
\ 'nr': -1,
\ 'pattern': '',
\ 'valid': 1,
\ },
\ ],
\ ale#test#GetLoclistWithoutModule()
Execute(Linters where lint_file eventually evaluates to 1 shouldn't be run if we don't want to run them):
call ale#Queue(0, '')
call ale#test#FlushJobs()
AssertEqual [], ale#test#GetLoclistWithoutModule()

View file

@ -12,7 +12,7 @@ Before:
call ale#linter#Define('foobar', { call ale#linter#Define('foobar', {
\ 'name': 'lint_file_linter', \ 'name': 'lint_file_linter',
\ 'callback': 'LintFileCallback', \ 'callback': 'LintFileCallback',
\ 'executable': 'echo', \ 'executable': has('win32') ? 'cmd' : 'echo',
\ 'command': {b -> ale#command#Run(b, 'echo', {-> ale#command#Run(b, 'echo', {-> 'foo'})})}, \ 'command': {b -> ale#command#Run(b, 'echo', {-> ale#command#Run(b, 'echo', {-> 'foo'})})},
\ 'read_buffer': 0, \ 'read_buffer': 0,
\}) \})
@ -28,7 +28,7 @@ After:
Given foobar (Some imaginary filetype): Given foobar (Some imaginary filetype):
Execute(It should be possible to compute an executable to check based on the result of commands): Execute(It should be possible to compute an executable to check based on the result of commands):
AssertLinter 'echo', 'foo' AssertLinter has('win32') ? 'cmd' : 'echo', 'foo'
ALELint ALELint
call ale#test#FlushJobs() call ale#test#FlushJobs()
@ -40,7 +40,7 @@ Execute(It should be possible to compute an executable to check based on the res
Execute(It handle the deferred command failing): Execute(It handle the deferred command failing):
let g:ale_emulate_job_failure = 1 let g:ale_emulate_job_failure = 1
AssertLinter 'echo', 0 AssertLinter has('win32') ? 'cmd' : 'echo', 0
ALELint ALELint
call ale#test#FlushJobs() call ale#test#FlushJobs()

View file

@ -174,7 +174,7 @@ Execute(PreProcess should process the lint_file option correctly):
\} \}
AssertThrows call ale#linter#PreProcess('testft', g:linter) AssertThrows call ale#linter#PreProcess('testft', g:linter)
AssertEqual '`lint_file` must be `0` or `1`', g:vader_exception AssertEqual '`lint_file` must be `0`, `1`, or a Function', g:vader_exception
let g:linter.lint_file = 0 let g:linter.lint_file = 0
@ -185,14 +185,17 @@ Execute(PreProcess should process the lint_file option correctly):
let g:linter.lint_file = 1 let g:linter.lint_file = 1
AssertEqual 1, ale#linter#PreProcess('testft', g:linter).lint_file AssertEqual 1, ale#linter#PreProcess('testft', g:linter).lint_file
" The default for read_buffer should change to 0 when lint_file is 1. " The default for read_buffer should still be 1
AssertEqual 0, ale#linter#PreProcess('testft', g:linter).read_buffer AssertEqual 1, ale#linter#PreProcess('testft', g:linter).read_buffer
let g:linter.read_buffer = 1 let g:linter.read_buffer = 1
" We shouldn't be able to set both options to 1 at the same time. " We should be able to set `read_buffer` and `lint_file` at the same time.
AssertThrows call ale#linter#PreProcess('testft', g:linter) AssertEqual 1, ale#linter#PreProcess('testft', g:linter).read_buffer
AssertEqual 'Only one of `lint_file` or `read_buffer` can be `1`', g:vader_exception
let g:linter.lint_file = function('type')
Assert type(ale#linter#PreProcess('testft', g:linter).lint_file) is v:t_func
Execute(PreProcess should set a default value for lint_file): Execute(PreProcess should set a default value for lint_file):
let g:linter = { let g:linter = {

View file

@ -23,22 +23,22 @@ Execute(React Native apps using CocoaPods should take precedence over the defaul
call ale#test#SetFilename('swiftlint-test-files/react-native/testfile.swift') call ale#test#SetFilename('swiftlint-test-files/react-native/testfile.swift')
AssertEqual AssertEqual
\ ale#path#Simplify(g:dir . '/swiftlint-test-files/react-native/ios/Pods/SwiftLint/swiftlint'), \ tolower(ale#path#Simplify(g:dir . '/swiftlint-test-files/react-native/ios/Pods/SwiftLint/swiftlint')),
\ ale_linters#swift#swiftlint#GetExecutable(bufnr('')) \ tolower(ale_linters#swift#swiftlint#GetExecutable(bufnr('')))
Execute(CocoaPods installation should take precedence over the default executable): Execute(CocoaPods installation should take precedence over the default executable):
call ale#test#SetFilename('swiftlint-test-files/cocoapods/testfile.swift') call ale#test#SetFilename('swiftlint-test-files/cocoapods/testfile.swift')
AssertEqual AssertEqual
\ ale#path#Simplify(g:dir . '/swiftlint-test-files/cocoapods/Pods/SwiftLint/swiftlint'), \ tolower(ale#path#Simplify(g:dir . '/swiftlint-test-files/cocoapods/Pods/SwiftLint/swiftlint')),
\ ale_linters#swift#swiftlint#GetExecutable(bufnr('')) \ tolower(ale_linters#swift#swiftlint#GetExecutable(bufnr('')))
Execute(Top level CocoaPods installation should take precedence over React Native installation): Execute(Top level CocoaPods installation should take precedence over React Native installation):
call ale#test#SetFilename('swiftlint-test-files/cocoapods-and-react-native/testfile.swift') call ale#test#SetFilename('swiftlint-test-files/cocoapods-and-react-native/testfile.swift')
AssertEqual AssertEqual
\ ale#path#Simplify(g:dir . '/swiftlint-test-files/cocoapods-and-react-native/Pods/SwiftLint/swiftlint'), \ tolower(ale#path#Simplify(g:dir . '/swiftlint-test-files/cocoapods-and-react-native/Pods/SwiftLint/swiftlint')),
\ ale_linters#swift#swiftlint#GetExecutable(bufnr('')) \ tolower(ale_linters#swift#swiftlint#GetExecutable(bufnr('')))
Execute(use-global should override other versions): Execute(use-global should override other versions):
let g:ale_swift_swiftlint_use_global = 1 let g:ale_swift_swiftlint_use_global = 1