diff --git a/autoload/ale/assert.vim b/autoload/ale/assert.vim index 7139f05c..ef2dab9e 100644 --- a/autoload/ale/assert.vim +++ b/autoload/ale/assert.vim @@ -26,6 +26,11 @@ function! ale#assert#Linter(expected_executable, expected_command) abort let l:linter = s:GetLinter() 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') 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 endfunction +function! ale#assert#SetUpLinterTestCommands() abort + command! -nargs=+ WithChainResults :call ale#assert#WithChainResults() + command! -nargs=+ AssertLinter :call ale#assert#Linter() + command! -nargs=0 AssertLinterNotExecuted :call ale#assert#LinterNotExecuted() + command! -nargs=+ AssertLSPOptions :call ale#assert#LSPOptions() + command! -nargs=+ AssertLSPConfig :call ale#assert#LSPConfig() + command! -nargs=+ AssertLSPLanguage :call ale#assert#LSPLanguage() + command! -nargs=+ AssertLSPProject :call ale#assert#LSPProject() + command! -nargs=+ AssertLSPProjectFull :call ale#assert#LSPProjectFull() + command! -nargs=+ AssertLSPAddress :call ale#assert#LSPAddress() +endfunction + " A dummy function for making sure this module is loaded. function! ale#assert#SetUpLinterTest(filetype, name) abort " 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') endif - command! -nargs=+ WithChainResults :call ale#assert#WithChainResults() - command! -nargs=+ AssertLinter :call ale#assert#Linter() - command! -nargs=0 AssertLinterNotExecuted :call ale#assert#LinterNotExecuted() - command! -nargs=+ AssertLSPOptions :call ale#assert#LSPOptions() - command! -nargs=+ AssertLSPConfig :call ale#assert#LSPConfig() - command! -nargs=+ AssertLSPLanguage :call ale#assert#LSPLanguage() - command! -nargs=+ AssertLSPProject :call ale#assert#LSPProject() - command! -nargs=+ AssertLSPProjectFull :call ale#assert#LSPProjectFull() - command! -nargs=+ AssertLSPAddress :call ale#assert#LSPAddress() + call ale#assert#SetUpLinterTestCommands() endfunction function! ale#assert#TearDownLinterTest() abort diff --git a/autoload/ale/command.vim b/autoload/ale/command.vim index 89734685..7a66dc77 100644 --- a/autoload/ale/command.vim +++ b/autoload/ale/command.vim @@ -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. 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, \ 'temporary_file': a:data.temporary_file, \}) - if get(a:data, 'result_callback', v:null) isnot v:null - call call(a:data.result_callback, [l:result]) + let l:result = a:data.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 endfunction -function! ale#command#Run(buffer, command, Callback, options) abort - let l:output_stream = get(a:options, 'output_stream', 'stdout') +function! ale#command#Run(buffer, command, Callback, ...) abort + 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:temporary_file, l:command, l:file_created] = ale#command#FormatCommand( \ a:buffer, - \ get(a:options, 'executable', ''), + \ get(l:options, 'executable', ''), \ a:command, - \ get(a:options, 'read_buffer', 0), - \ get(a:options, 'input', v:null), + \ get(l:options, 'read_buffer', 0), + \ get(l:options, 'input', v:null), \) let l:command = ale#job#PrepareCommand(a:buffer, l:command) let l:job_options = { @@ -267,8 +276,8 @@ function! ale#command#Run(buffer, command, Callback, options) abort \ 'job_id': job_id, \ 'exit_code': exit_code, \ 'temporary_file': l:temporary_file, - \ 'log_output': get(a:options, 'log_output', 1), - \ 'result_callback': get(l:result, 'result_callback', v:null), + \ 'log_output': get(l:options, 'log_output', 1), + \ 'result': l:result, \ } \ )}, \ 'mode': 'nl', @@ -344,3 +353,7 @@ function! ale#command#Run(buffer, command, Callback, options) abort return l:result endfunction + +function! ale#command#IsDeferred(value) abort + return type(a:value) is v:t_dict && has_key(a:value, '_deferred_job_id') +endfunction diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 16653dba..c469bf5e 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -590,6 +590,26 @@ function! s:AddProblemsFromOtherBuffers(buffer, linters) abort endif 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. " " Returns 1 if the linter was successfully run. @@ -599,13 +619,7 @@ function! s:RunLinter(buffer, linter) abort else let l:executable = ale#linter#GetExecutable(a:buffer, a:linter) - " 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) - - if ale#engine#IsExecutable(a:buffer, l:executable) - return s:InvokeChain(a:buffer, l:executable, a:linter, 0, []) - endif + return s:RunIfExecutable(a:buffer, a:linter, l:executable) endif return 0 diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index 0b61bad1..86ee506c 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -120,7 +120,8 @@ function! ale#linter#PreProcess(filetype, linter) abort let l:obj.executable = a:linter.executable 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 else 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. function! ale#linter#GetExecutable(buffer, linter) abort - return has_key(a:linter, 'executable_callback') - \ ? ale#util#GetFunction(a:linter.executable_callback)(a:buffer) + let l:Executable = has_key(a:linter, 'executable_callback') + \ ? function(a:linter.executable_callback) \ : a:linter.executable + + return type(l:Executable) is v:t_func + \ ? l:Executable(a:buffer) + \ : l:Executable endfunction " Given a buffer and linter, get the command String for the linter. diff --git a/doc/ale.txt b/doc/ale.txt index 16da5c23..0fb059f9 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2744,7 +2744,11 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* Human-readable |String| error code. `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. Either this or the `executable_callback` argument diff --git a/test/test_deferred_executable_string.vader b/test/test_deferred_executable_string.vader new file mode 100644 index 00000000..65582f1f --- /dev/null +++ b/test/test_deferred_executable_string.vader @@ -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''') diff --git a/test/test_linter_defintion_processing.vader b/test/test_linter_defintion_processing.vader index d967761d..321c6212 100644 --- a/test/test_linter_defintion_processing.vader +++ b/test/test_linter_defintion_processing.vader @@ -48,7 +48,7 @@ Execute (PreProcess should throw when executable is not a string): \ 'executable': 123, \ '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): 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 +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): AssertThrows call ale#linter#PreProcess('testft', { \ 'name': 'foo',