diff --git a/autoload/ale/cleanup.vim b/autoload/ale/cleanup.vim index 0a1cd39..d720cc9 100644 --- a/autoload/ale/cleanup.vim +++ b/autoload/ale/cleanup.vim @@ -3,6 +3,8 @@ function! ale#cleanup#Buffer(buffer) abort if has_key(g:ale_buffer_info, a:buffer) + call ale#engine#RemoveManagedFiles(a:buffer) + " When buffers are removed, clear all of the jobs. for l:job in get(g:ale_buffer_info[a:buffer], 'job_list', []) call ale#engine#ClearJob(l:job) diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 70cd052..22e8ff9 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -30,10 +30,14 @@ function! ale#engine#InitBufferInfo(buffer) abort " job_list will hold the list of jobs " loclist holds the loclist items after all jobs have completed. " new_loclist holds loclist items while jobs are being run. + " temporary_file_list holds temporary files to be cleaned up + " temporary_directory_list holds temporary directories to be cleaned up let g:ale_buffer_info[a:buffer] = { \ 'job_list': [], \ 'loclist': [], \ 'new_loclist': [], + \ 'temporary_file_list': [], + \ 'temporary_directory_list': [], \} endif endfunction @@ -134,6 +138,40 @@ function! ale#engine#JoinNeovimOutput(output, data) abort endif endfunction +" Register a temporary file to be managed with the ALE engine for +" a current job run. +function! ale#engine#ManageFile(buffer, filename) abort + call add(g:ale_buffer_info[a:buffer].temporary_file_list, a:filename) +endfunction + +" Same as the above, but manage an entire directory. +function! ale#engine#ManageDirectory(buffer, directory) abort + call add(g:ale_buffer_info[a:buffer].temporary_directory_list, a:directory) +endfunction + +function! ale#engine#RemoveManagedFiles(buffer) abort + if !has_key(g:ale_buffer_info, a:buffer) + return + endif + + " Delete files with a call akin to a plan `rm` command. + for l:filename in g:ale_buffer_info[a:buffer].temporary_file_list + call delete(l:filename) + endfor + + let g:ale_buffer_info[a:buffer].temporary_file_list = [] + + " Delete directories like `rm -rf`. + " Directories are handled differently from files, so paths that are + " intended to be single files can be set up for automatic deletion without + " accidentally deleting entire directories. + for l:directory in g:ale_buffer_info[a:buffer].temporary_directory_list + call delete(l:directory, 'rf') + endfor + + let g:ale_buffer_info[a:buffer].temporary_directory_list = [] +endfunction + function! s:HandleExit(job) abort if a:job ==# 'no process' " Stop right away when the job is not valid in Vim 8. @@ -178,6 +216,10 @@ function! s:HandleExit(job) abort return endif + " Automatically remove all managed temporary files and directories + " now that all jobs have completed. + call ale#engine#RemoveManagedFiles(l:buffer) + " Sort the loclist again. " We need a sorted list so we can run a binary search against it " for efficient lookup of the messages in the cursor handler. @@ -424,6 +466,10 @@ function! s:InvokeChain(buffer, linter, chain_index, input) abort if !empty(l:options) call s:RunJob(l:options) + elseif empty(g:ale_buffer_info[a:buffer].job_list) + " If we cancelled running a command, and we have no jobs in progress, + " then delete the managed temporary files now. + call ale#engine#RemoveManagedFiles(a:buffer) endif endfunction diff --git a/doc/ale.txt b/doc/ale.txt index 3ec54d4..d8f8a4c 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -1037,6 +1037,35 @@ ale#engine#GetLoclist(buffer) *ale#engine#GetLoclist()* |setqflist()|. +ale#engine#ManageFile(buffer, filename) *ale#engine#ManageFile()* + + Given a buffer number for a buffer currently running some linting tasks + and a filename, register a filename with ALE for automatic deletion after + linting is complete, or when Vim exits. + + If Vim exits suddenly, ALE will try its best to remove temporary files, but + ALE cannot guarantee with absolute certainty that the files will be removed. + It is advised to create temporary files in the operating system's managed + temporary file directory, such as with |tempname()|. + + Directory names should not be given to this function. ALE will only delete + files and symlinks given to this function. This is to prevent entire + directories from being accidentally deleted, say in cases of writing + `dir . '/' . filename` where `filename` is actually `''`, etc. ALE instead + manages directories separetly with the |ale#engine#ManageDirectory| function. + + +ale#engine#ManageDirectory(buffer, directory) *ale#engine#ManageDirectory()* + + Like |ale#engine#ManageFile()|, but directories and all of their contents + will be deleted, akin to `rm -rf directory`, which could lead to loss of + data if mistakes are made. This command will also delete any temporary + filenames given to it. + + It is advised to use |ale#engine#ManageFile()| instead for deleting single + files. + + ale#linter#Define(filetype, linter) *ale#linter#Define()* Given a |String| for a filetype and a |Dictionary| Describing a linter configuration, add a linter for the given filetype. The dictionaries each @@ -1151,6 +1180,12 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* `command_chain` is recommended where any system calls need to be made to retrieve some kind of information before running the final command. + If temporary files or directories are created for commands run with + `command_callback` or `command_chain`, then these tempoary files or + directories can be managed by ALE, for automatic deletion. + See |ale#engine#ManageFile()| and |ale#engine#ManageDirectory| for more + information. + Some programs for checking for errors are not capable of receiving input from stdin, as is required by ALE. To remedy this, a wrapper script is provided named in the variable |g:ale#util#stdin_wrapper|. This variable diff --git a/test/test_cleanup.vader b/test/test_cleanup.vader index ec2b38a..23e5bcf 100644 --- a/test/test_cleanup.vader +++ b/test/test_cleanup.vader @@ -2,8 +2,8 @@ Before: let g:buffer = bufnr('%') let g:ale_buffer_info = { - \ g:buffer : {}, - \ 10347: {}, + \ g:buffer : {'temporary_file_list': [], 'temporary_directory_list': []}, + \ 10347: {'temporary_file_list': [], 'temporary_directory_list': []}, \} After: @@ -12,4 +12,4 @@ After: Execute('ALE globals should be cleared when the buffer is closed.'): :q! - AssertEqual {10347: {}}, g:ale_buffer_info + AssertEqual {10347: {'temporary_file_list': [], 'temporary_directory_list': []}}, g:ale_buffer_info diff --git a/test/test_temporary_file_management.vader b/test/test_temporary_file_management.vader new file mode 100644 index 0000000..17a375e --- /dev/null +++ b/test/test_temporary_file_management.vader @@ -0,0 +1,83 @@ +Before: + let g:command = 'echo test' + let g:filename = tempname() + let g:directory = tempname() + let g:preserved_directory = tempname() + + function! TestCommandCallback(buffer) abort + " We are registering a temporary file, so we should delete it. + call writefile(['foo'], g:filename) + call ale#engine#ManageFile(a:buffer, g:filename) + + " We are registering this directory appropriately, so we should delete + " the whole thing. + call mkdir(g:directory) + call writefile(['foo'], g:directory . '/bar') + call ale#engine#ManageDirectory(a:buffer, g:directory) + + " We are registering this directory as temporary file, so we + " shouldn't delete it. + call mkdir(g:preserved_directory) + call writefile(['foo'], g:preserved_directory . '/bar') + call ale#engine#ManageFile(a:buffer, g:preserved_directory) + + return g:command + endfunction + + function! TestCallback(buffer, output) abort + return [] + endfunction + + call ale#linter#Define('foobar', { + \ 'name': 'testlinter', + \ 'executable': 'echo', + \ 'callback': 'TestCallback', + \ 'command_callback': 'TestCommandCallback', + \}) + +After: + call delete(g:preserved_directory, 'rf') + + unlet! g:command + unlet! g:filename + unlet! g:directory + unlet! g:preserved_directory + delfunction TestCommandCallback + delfunction TestCallback + call ale#linter#Reset() + +Given foobar (Some imaginary filetype): + foo + bar + baz + +Execute(ALE should delete managed files/directories appropriately after linting): + AssertEqual 'foobar', &filetype + + call ale#Lint() + call ale#engine#WaitForJobs(2000) + + Assert !filereadable(g:filename), 'The tempoary file was not deleted' + Assert !isdirectory(g:directory), 'The tempoary directory was not deleted' + Assert isdirectory(g:preserved_directory), 'The tempoary directory was not kept' + +Execute(ALE should delete managed files even if no command is run): + AssertEqual 'foobar', &filetype + + let g:command = '' + + call ale#Lint() + call ale#engine#WaitForJobs(2000) + + Assert !filereadable(g:filename), 'The tempoary file was not deleted' + Assert !isdirectory(g:directory), 'The tempoary directory was not deleted' + Assert isdirectory(g:preserved_directory), 'The tempoary directory was not kept' + +Execute(ALE should delete managed files when the buffer is removed): + call ale#engine#InitBufferInfo(bufnr('%')) + call TestCommandCallback(bufnr('%')) + call ale#cleanup#Buffer(bufnr('%')) + + Assert !filereadable(g:filename), 'The tempoary file was not deleted' + Assert !isdirectory(g:directory), 'The tempoary directory was not deleted' + Assert isdirectory(g:preserved_directory), 'The tempoary directory was not kept'