From 03ab963d1a02b0f4d45fb10915efa9fd9c5deaf7 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 11 Feb 2017 18:14:18 +0000 Subject: [PATCH] Add support for temporary filename substitution, for replacing stdin_wrapper --- autoload/ale/engine.vim | 70 +++++++++++++++++-- doc/ale.txt | 49 +++++++++---- test/test_format_command.vader | 41 +++++++++++ .../test_format_temporary_file_creation.vader | 33 +++++++++ 4 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 test/test_format_command.vader create mode 100644 test/test_format_temporary_file_creation.vader diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 22e8ff93..803fd6dc 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -278,6 +278,66 @@ function! s:FixLocList(buffer, loclist) abort endfor endfunction +" Given part of a command, replace any % with %%, so that no characters in +" the string will be replaced with filenames, etc. +function! ale#engine#EscapeCommandPart(command_part) abort + return substitute(a:command_part, '%', '%%', 'g') +endfunction + +" Given a command string, replace every... +" %s -> with the current filename +" %t -> with the name of an unused file in a temporary directory +" %% -> with a literal % +function! ale#engine#FormatCommand(buffer, command) abort + let l:temporary_file = '' + let l:command = a:command + + " First replace all uses of %%, used for literal percent characters, + " with an ugly string. + let l:command = substitute(l:command, '%%', '<>', 'g') + + " Replace all %s occurences in the string with the name of the current + " file. + if l:command =~# '%s' + let l:filename = fnamemodify(bufname(a:buffer), ':p') + let l:command = substitute(l:command, '%s', fnameescape(l:filename), 'g') + endif + + if l:command =~# '%t' + " Create a temporary filename, / + " The file itself will not be created by this function. + let l:temporary_file = + \ tempname() + \ . (has('win32') ? '\' : '/') + \ . fnamemodify(bufname(a:buffer), ':t') + + let l:command = substitute(l:command, '%t', fnameescape(l:temporary_file), 'g') + endif + + " Finish formatting so %% becomes %. + let l:command = substitute(l:command, '<>', '%', 'g') + + return [l:temporary_file, l:command] +endfunction + +function! s:CreateTemporaryFileForJob(buffer, temporary_file) abort + if empty(a:temporary_file) + " There is no file, so we didn't create anything. + return 0 + endif + + let l:temporary_directory = fnamemodify(a:temporary_file, ':h') + " Create the temporary directory for the file, unreadable by 'other' + " users. + call mkdir(l:temporary_directory, '', 0750) + " Automatically delete the directory later. + call ale#engine#ManageDirectory(a:buffer, l:temporary_directory) + " Write the buffer out to a file. + silent! exec 'write' a:temporary_file + + return 1 +endfunction + function! s:RunJob(options) abort let l:command = a:options.command let l:buffer = a:options.buffer @@ -286,10 +346,12 @@ function! s:RunJob(options) abort let l:next_chain_index = a:options.next_chain_index let l:read_buffer = a:options.read_buffer - if l:command =~# '%s' - " If there is a '%s' in the command string, replace it with the name - " of the file. - let l:command = printf(l:command, shellescape(fnamemodify(bufname(l:buffer), ':p'))) + let [l:temporary_file, l:command] = ale#engine#FormatCommand(l:buffer, l:command) + + if s:CreateTemporaryFileForJob(l:buffer, l:temporary_file) + " If a temporary filename has been formatted in to the command, then + " we do not need to send the Vim buffer to the command. + let l:read_buffer = 0 endif if has('nvim') diff --git a/doc/ale.txt b/doc/ale.txt index d8f8a4c0..835f9803 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -1030,6 +1030,15 @@ ale#Queue(delay) *ale#Queue()* again from the same buffer +ale#engine#EscapeCommandPart(command_part) *ale#engine#EscapeCommandPart()* + + Given a |String|, return a |String| with all `%` characters replaced with + `%%` instead. This function can be used to escape strings which are + dynamically generated for commands before handing them over to ALE, + so that ALE doesn't treat any strings with `%` formatting sequences + specially. + + ale#engine#GetLoclist(buffer) *ale#engine#GetLoclist()* Given a buffer number, this function will rerurn the list of warnings and @@ -1186,16 +1195,35 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* 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 - can be called with the regular arguments for any command to forward data - from stdin to the program, by way of creating a temporary file. The first - argument to the stdin wrapper must be a file extension to save the temporary - file with, and the following arguments are the command as normal. + All command strings will be formatted for special character sequences. + Any substring `%s` will be replaced with the full path to the current file + being edited. This format option can be used to pass the exact filename + being edited to a program. + For example: > - 'command': g:ale#util#stdin_wrapper . ' .hs ghc -fno-code -v0', + 'command': 'eslint -f unix --stdin --stdin-filename %s' < + Any substring `%t` will be replaced with a path to a temporary file. Merely + adding `%t` will cause ALE to create a temporary file containing the + contents of the the buffer being checked. All occurrences of `%t` in command + strings will reference the one temporary file. The temporary file will be + created inside a temporary directory, and the entire temporary directory + will be automatically deleted, following the behaviour of + |ale#engine#ManageDirectory|. This option can be used for some linters which + do not support reading from stdin. + + For example: > + 'command': 'ghc -fno-code -v0 %t', +< + The character sequence `%%` can be used to emit a literal `%` into a + command, so literal character sequences `%s` and `%t` can be escaped by + using `%%s` and `%%t` instead, etc. + + If a callback for a command generates part of a command string which might + possibly contain `%%`, `%s`, or `%t` where the special formatting behaviour + is not desired, the |ale#engine#EscapeCommandPart()| function can be used to + replace those characters to avoid formatting issues. + ale#linter#Get(filetype) *ale#linter#Get()* Return all of linters configured for a given filetype as a |List| of @@ -1217,11 +1245,6 @@ ale#statusline#Status() *ale#statusline#Status()* %{ale#statusline#Status()} -g:ale#util#stdin_wrapper *g:ale#util#stdin_wrapper* - This variable names a wrapper script for sending stdin input to programs - which cannot accept input via stdin. See |ale#linter#Define()| for more. - - ALELint *ALELint* This |User| autocommand is triggered by ALE every time it completes a lint operation. It can be used to update statuslines, send notifications, or diff --git a/test/test_format_command.vader b/test/test_format_command.vader new file mode 100644 index 00000000..d57729b0 --- /dev/null +++ b/test/test_format_command.vader @@ -0,0 +1,41 @@ +Before: + silent! cd /testplugin/test + :e! top/middle/bottom/dummy.txt + +After: + unlet! g:result + unlet! g:match + +Execute(FormatCommand should do nothing to basic command strings): + AssertEqual ['', 'awesome-linter do something'], ale#engine#FormatCommand(bufnr('%'), 'awesome-linter do something') + +Execute(FormatCommand should handle %%, and ignore other percents): + AssertEqual ['', '% %%d %%f %x %'], ale#engine#FormatCommand(bufnr('%'), '%% %%%d %%%f %x %') + +Execute(FormatCommand should convert %s to the current filename): + AssertEqual ['', 'foo ' . fnameescape(expand('%:p')) . ' bar ' . fnameescape(expand('%:p'))], ale#engine#FormatCommand(bufnr('%'), 'foo %s bar %s') + +Execute(FormatCommand should convert %t to a new temporary filename): + let g:result = ale#engine#FormatCommand(bufnr('%'), 'foo %t bar %t') + let g:match = matchlist(g:result[1], '\v^foo (/tmp/.*/dummy.txt) bar (/tmp/.*/dummy.txt)$') + + Assert !empty(g:match), 'No match found! Result was: ' . g:result[1] + " The first item of the result should be a temporary filename, and it should + " be the same as the escaped name in the command string. + AssertEqual g:result[0], fnameescape(g:match[1]) + " The two temporary filenames formatted in should be the same. + AssertEqual g:match[1], g:match[2] + +Execute(FormatCommand should let you combine %s and %t): + let g:result = ale#engine#FormatCommand(bufnr('%'), 'foo %t bar %s') + let g:match = matchlist(g:result[1], '\v^foo (/tmp/.*/dummy.txt) bar (.*/dummy.txt)$') + + Assert !empty(g:match), 'No match found! Result was: ' . g:result[1] + " The first item of the result should be a temporary filename, and it should + " be the same as the escaped name in the command string. + AssertEqual g:result[0], fnameescape(g:match[1]) + " The second item should be equal to the original filename. + AssertEqual fnameescape(expand('%:p')), g:match[2] + +Execute(EscapeCommandPart should escape all percent signs): + AssertEqual '%%s %%t %%%% %%s %%t %%%%', ale#engine#EscapeCommandPart('%s %t %% %s %t %%') diff --git a/test/test_format_temporary_file_creation.vader b/test/test_format_temporary_file_creation.vader new file mode 100644 index 00000000..fa20338c --- /dev/null +++ b/test/test_format_temporary_file_creation.vader @@ -0,0 +1,33 @@ +Before: + let g:output = [] + + function! TestCallback(buffer, output) + let g:output = a:output + + return [] + endfunction + + call ale#linter#Define('foobar', { + \ 'name': 'testlinter', + \ 'callback': 'TestCallback', + \ 'executable': 'cat', + \ 'command': 'cat %t', + \}) + +After: + unlet! g:output + delfunction TestCallback + call ale#linter#Reset() + +Given foobar (Some imaginary filetype): + foo + bar + baz + +Execute(ALE should be able to read the %t file): + AssertEqual 'foobar', &filetype + + call ale#Lint() + call ale#engine#WaitForJobs(2000) + + AssertEqual ['foo', 'bar', 'baz'], g:output