#2132 - Implement deferred executable string handling for linters

This commit is contained in:
w0rp 2019-02-12 18:05:33 +00:00
parent bf196ba17c
commit 926ad47a49
No known key found for this signature in database
GPG Key ID: 0FC1ECAA8C81CD83
7 changed files with 116 additions and 31 deletions

View File

@ -26,6 +26,11 @@ function! ale#assert#Linter(expected_executable, expected_command) abort
let l:linter = s:GetLinter() let l:linter = s:GetLinter()
let l:executable = ale#linter#GetExecutable(l:buffer, l:linter) let l:executable = ale#linter#GetExecutable(l:buffer, l:linter)
while ale#command#IsDeferred(l:executable)
call ale#test#FlushJobs()
let l:executable = l:executable.value
endwhile
if has_key(l:linter, 'command_chain') if has_key(l:linter, 'command_chain')
let l:callbacks = map(copy(l:linter.command_chain), 'v:val.callback') let l:callbacks = map(copy(l:linter.command_chain), 'v:val.callback')
@ -125,6 +130,18 @@ function! ale#assert#LSPAddress(expected_address) abort
AssertEqual a:expected_address, l:address AssertEqual a:expected_address, l:address
endfunction endfunction
function! ale#assert#SetUpLinterTestCommands() abort
command! -nargs=+ WithChainResults :call ale#assert#WithChainResults(<args>)
command! -nargs=+ AssertLinter :call ale#assert#Linter(<args>)
command! -nargs=0 AssertLinterNotExecuted :call ale#assert#LinterNotExecuted()
command! -nargs=+ AssertLSPOptions :call ale#assert#LSPOptions(<args>)
command! -nargs=+ AssertLSPConfig :call ale#assert#LSPConfig(<args>)
command! -nargs=+ AssertLSPLanguage :call ale#assert#LSPLanguage(<args>)
command! -nargs=+ AssertLSPProject :call ale#assert#LSPProject(<args>)
command! -nargs=+ AssertLSPProjectFull :call ale#assert#LSPProjectFull(<args>)
command! -nargs=+ AssertLSPAddress :call ale#assert#LSPAddress(<args>)
endfunction
" A dummy function for making sure this module is loaded. " A dummy function for making sure this module is loaded.
function! ale#assert#SetUpLinterTest(filetype, name) abort function! ale#assert#SetUpLinterTest(filetype, name) abort
" Set up a marker so ALE doesn't create real random temporary filenames. " Set up a marker so ALE doesn't create real random temporary filenames.
@ -159,15 +176,7 @@ function! ale#assert#SetUpLinterTest(filetype, name) abort
call ale#test#SetDirectory('/testplugin/test/command_callback') call ale#test#SetDirectory('/testplugin/test/command_callback')
endif endif
command! -nargs=+ WithChainResults :call ale#assert#WithChainResults(<args>) call ale#assert#SetUpLinterTestCommands()
command! -nargs=+ AssertLinter :call ale#assert#Linter(<args>)
command! -nargs=0 AssertLinterNotExecuted :call ale#assert#LinterNotExecuted()
command! -nargs=+ AssertLSPOptions :call ale#assert#LSPOptions(<args>)
command! -nargs=+ AssertLSPConfig :call ale#assert#LSPConfig(<args>)
command! -nargs=+ AssertLSPLanguage :call ale#assert#LSPLanguage(<args>)
command! -nargs=+ AssertLSPProject :call ale#assert#LSPProject(<args>)
command! -nargs=+ AssertLSPProjectFull :call ale#assert#LSPProjectFull(<args>)
command! -nargs=+ AssertLSPAddress :call ale#assert#LSPAddress(<args>)
endfunction endfunction
function! ale#assert#TearDownLinterTest() abort function! ale#assert#TearDownLinterTest() abort

View File

@ -236,26 +236,35 @@ function! s:ExitCallback(buffer, line_list, Callback, data) abort
" If the callback starts any new jobs, use the same job type for them. " If the callback starts any new jobs, use the same job type for them.
call setbufvar(a:buffer, 'ale_job_type', l:job_type) call setbufvar(a:buffer, 'ale_job_type', l:job_type)
let l:result = a:Callback(a:buffer, a:line_list, { let l:value = a:Callback(a:buffer, a:line_list, {
\ 'exit_code': a:data.exit_code, \ 'exit_code': a:data.exit_code,
\ 'temporary_file': a:data.temporary_file, \ 'temporary_file': a:data.temporary_file,
\}) \})
if get(a:data, 'result_callback', v:null) isnot v:null let l:result = a:data.result
call call(a:data.result_callback, [l:result]) let l:result.value = l:value
if get(l:result, 'result_callback', v:null) isnot v:null
call call(l:result.result_callback, [l:value])
endif endif
endfunction endfunction
function! ale#command#Run(buffer, command, Callback, options) abort function! ale#command#Run(buffer, command, Callback, ...) abort
let l:output_stream = get(a:options, 'output_stream', 'stdout') let l:options = get(a:000, 0, {})
if len(a:000) > 1
throw 'Too many arguments!'
endif
let l:output_stream = get(l:options, 'output_stream', 'stdout')
let l:line_list = [] let l:line_list = []
let [l:temporary_file, l:command, l:file_created] = ale#command#FormatCommand( let [l:temporary_file, l:command, l:file_created] = ale#command#FormatCommand(
\ a:buffer, \ a:buffer,
\ get(a:options, 'executable', ''), \ get(l:options, 'executable', ''),
\ a:command, \ a:command,
\ get(a:options, 'read_buffer', 0), \ get(l:options, 'read_buffer', 0),
\ get(a:options, 'input', v:null), \ get(l:options, 'input', v:null),
\) \)
let l:command = ale#job#PrepareCommand(a:buffer, l:command) let l:command = ale#job#PrepareCommand(a:buffer, l:command)
let l:job_options = { let l:job_options = {
@ -267,8 +276,8 @@ function! ale#command#Run(buffer, command, Callback, options) abort
\ 'job_id': job_id, \ 'job_id': job_id,
\ 'exit_code': exit_code, \ 'exit_code': exit_code,
\ 'temporary_file': l:temporary_file, \ 'temporary_file': l:temporary_file,
\ 'log_output': get(a:options, 'log_output', 1), \ 'log_output': get(l:options, 'log_output', 1),
\ 'result_callback': get(l:result, 'result_callback', v:null), \ 'result': l:result,
\ } \ }
\ )}, \ )},
\ 'mode': 'nl', \ 'mode': 'nl',
@ -344,3 +353,7 @@ function! ale#command#Run(buffer, command, Callback, options) abort
return l:result return l:result
endfunction endfunction
function! ale#command#IsDeferred(value) abort
return type(a:value) is v:t_dict && has_key(a:value, '_deferred_job_id')
endfunction

View File

@ -590,6 +590,26 @@ function! s:AddProblemsFromOtherBuffers(buffer, linters) abort
endif endif
endfunction endfunction
function! s:RunIfExecutable(buffer, linter, executable) abort
if ale#command#IsDeferred(a:executable)
let a:executable.result_callback = {
\ executable -> s:RunIfExecutable(a:buffer, a:linter, executable)
\}
return 1
endif
if ale#engine#IsExecutable(a:buffer, a:executable)
" Use different job types for file or linter jobs.
let l:job_type = a:linter.lint_file ? 'file_linter' : 'linter'
call setbufvar(a:buffer, 'ale_job_type', l:job_type)
return s:InvokeChain(a:buffer, a:executable, a:linter, 0, [])
endif
return 0
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.
@ -599,13 +619,7 @@ function! s:RunLinter(buffer, linter) abort
else else
let l:executable = ale#linter#GetExecutable(a:buffer, a:linter) let l:executable = ale#linter#GetExecutable(a:buffer, a:linter)
" Use different job types for file or linter jobs. return s:RunIfExecutable(a:buffer, a:linter, l:executable)
let l:job_type = a:linter.lint_file ? 'file_linter' : 'linter'
call setbufvar(a:buffer, 'ale_job_type', l:job_type)
if ale#engine#IsExecutable(a:buffer, l:executable)
return s:InvokeChain(a:buffer, l:executable, a:linter, 0, [])
endif
endif endif
return 0 return 0

View File

@ -120,7 +120,8 @@ function! ale#linter#PreProcess(filetype, linter) abort
let l:obj.executable = a:linter.executable let l:obj.executable = a:linter.executable
if type(l:obj.executable) isnot v:t_string if type(l:obj.executable) isnot v:t_string
throw '`executable` must be a string if defined' \&& type(l:obj.executable) isnot v:t_func
throw '`executable` must be a String or Function if defined'
endif endif
else else
throw 'Either `executable` or `executable_callback` must be defined' throw 'Either `executable` or `executable_callback` must be defined'
@ -476,9 +477,13 @@ endfunction
" Given a buffer and linter, get the executable String for the linter. " Given a buffer and linter, get the executable String for the linter.
function! ale#linter#GetExecutable(buffer, linter) abort function! ale#linter#GetExecutable(buffer, linter) abort
return has_key(a:linter, 'executable_callback') let l:Executable = has_key(a:linter, 'executable_callback')
\ ? ale#util#GetFunction(a:linter.executable_callback)(a:buffer) \ ? function(a:linter.executable_callback)
\ : a:linter.executable \ : a:linter.executable
return type(l:Executable) is v:t_func
\ ? l:Executable(a:buffer)
\ : l:Executable
endfunction endfunction
" Given a buffer and linter, get the command String for the linter. " Given a buffer and linter, get the command String for the linter.

View File

@ -2744,7 +2744,11 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()*
Human-readable |String| error code. Human-readable |String| error code.
`executable` A |String| naming the executable itself which `executable` A |String| naming the executable itself which
will be run. This value will be used to check if the will be run, or a |Funcref| for a function to call
for computing the executable, accepting a buffer
number.
This value will be used to check if the
program requested is installed or not. program requested is installed or not.
Either this or the `executable_callback` argument Either this or the `executable_callback` argument

View File

@ -0,0 +1,32 @@
Before:
Save g:ale_run_synchronously
let g:ale_run_synchronously = 1
call ale#linter#Reset()
call ale#assert#SetUpLinterTestCommands()
call ale#linter#Define('foobar', {
\ 'name': 'lint_file_linter',
\ 'callback': 'LintFileCallback',
\ 'executable': {b -> ale#command#Run(b, 'echo', {-> ale#command#Run(b, 'echo', {-> 'foo'})})},
\ 'command': 'echo',
\ 'read_buffer': 0,
\})
After:
Restore
call ale#assert#TearDownLinterTest()
Given foobar (Some imaginary filetype):
Execute(It should be possible to compute an executable to check based on the result of commands):
let b:ale_history = []
AssertLinter 'foo', 'echo'
ALELint
call ale#test#FlushJobs()
AssertEqual
\ [{'status': 0, 'job_id': 'executable', 'command': 'foo'}],
\ filter(copy(b:ale_history), 'v:val.job_id is# ''executable''')

View File

@ -48,7 +48,7 @@ Execute (PreProcess should throw when executable is not a string):
\ 'executable': 123, \ 'executable': 123,
\ 'command': 'echo', \ 'command': 'echo',
\}) \})
AssertEqual '`executable` must be a string if defined', g:vader_exception AssertEqual '`executable` must be a String or Function if defined', g:vader_exception
Execute (PreProcess should throw when executable_callback is not a callback): Execute (PreProcess should throw when executable_callback is not a callback):
AssertThrows call ale#linter#PreProcess('testft', { AssertThrows call ale#linter#PreProcess('testft', {
@ -59,6 +59,14 @@ Execute (PreProcess should throw when executable_callback is not a callback):
\}) \})
AssertEqual '`executable_callback` must be a callback if defined', g:vader_exception AssertEqual '`executable_callback` must be a callback if defined', g:vader_exception
Execute (PreProcess should allow executable to be a callback):
call ale#linter#PreProcess('testft', {
\ 'name': 'foo',
\ 'callback': 'SomeFunction',
\ 'executable': function('type'),
\ 'command': 'echo',
\})
Execute (PreProcess should throw when there is no command): Execute (PreProcess should throw when there is no command):
AssertThrows call ale#linter#PreProcess('testft', { AssertThrows call ale#linter#PreProcess('testft', {
\ 'name': 'foo', \ 'name': 'foo',