From ba3dd0d02735e7b23918200ae58410b0deed2f62 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sun, 23 Aug 2020 19:55:42 +0100 Subject: [PATCH] Close #2556 - Support filename mapping ALE now supports mapping files between different systems for running linters and fixers with Docker, in virtual machines, in servers, etc. --- README.md | 12 ++ autoload/ale.vim | 20 +++ autoload/ale/command.vim | 26 +++- autoload/ale/engine.vim | 20 ++- autoload/ale/filename_mapping.vim | 22 ++++ autoload/ale/fix.vim | 40 +++--- autoload/ale/fix/registry.vim | 4 +- autoload/ale/lsp_linter.vim | 9 +- autoload/ale/path.vim | 2 +- doc/ale.txt | 197 +++++++++++++++++++++++++--- plugin/ale.vim | 4 + test/fix/test_ale_fix.vader | 24 ++++ test/test_filename_mapping.vader | 62 +++++++++ test/test_format_command.vader | 46 ++++--- test/test_loclist_corrections.vader | 69 ++++++++-- 15 files changed, 483 insertions(+), 74 deletions(-) create mode 100644 autoload/ale/filename_mapping.vim create mode 100644 test/test_filename_mapping.vader diff --git a/README.md b/README.md index ffc6dd19..0f1c613b 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ other content at [w0rp.com](https://w0rp.com). 17. [How can I configure my C or C++ project?](#faq-c-configuration) 18. [How can I configure ALE differently for different buffers?](#faq-buffer-configuration) 19. [How can I configure the height of the list in which ALE displays errors?](#faq-list-window-height) + 20. [How can I run linters or fixers via Docker or a VM?](#faq-vm) @@ -877,3 +878,14 @@ To set a default height for the error list, use the `g:ale_list_window_size` var " Show 5 lines of errors (default: 10) let g:ale_list_window_size = 5 ``` + + + +### 5.xx. How can I run linters or fixers via Docker or a VM? + +ALE supports running linters or fixers via Docker, virtual machines, or in +combination with any remote machine with a different file system, so long as the +tools are well-integrated with ALE, and ALE is properly configured to run the +correct commands and map filename paths between different file systems. See +`:help ale-lint-other-machines` for the full documentation on how to configure +ALE to support this. diff --git a/autoload/ale.vim b/autoload/ale.vim index 6251b47b..b75c9fc9 100644 --- a/autoload/ale.vim +++ b/autoload/ale.vim @@ -266,3 +266,23 @@ function! ale#GetLocItemMessage(item, format_string) abort return l:msg endfunction + +" Given a buffer and a linter or fixer name, return an Array of two-item +" Arrays describing how to map filenames to and from the local to foreign file +" systems. +function! ale#GetFilenameMappings(buffer, name) abort + let l:linter_mappings = ale#Var(a:buffer, 'filename_mappings') + + if type(l:linter_mappings) is v:t_list + return l:linter_mappings + endif + + let l:name = a:name + + if !has_key(l:linter_mappings, l:name) + " Use * as a default setting for all tools. + let l:name = '*' + endif + + return get(l:linter_mappings, l:name, []) +endfunction diff --git a/autoload/ale/command.vim b/autoload/ale/command.vim index 1bbc4f4c..ec264a36 100644 --- a/autoload/ale/command.vim +++ b/autoload/ale/command.vim @@ -133,11 +133,30 @@ function! ale#command#EscapeCommandPart(command_part) abort return substitute(a:command_part, '%', '%%', 'g') endfunction +" Format a filename, converting it with filename mappings, if non-empty, +" and escaping it for putting into a command string. +function! s:FormatFilename(filename, mappings) abort + let l:filename = a:filename + + if !empty(a:mappings) + let l:filename = ale#filename_mapping#Map(l:filename, a:mappings) + endif + + return ale#Escape(l:filename) +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#command#FormatCommand(buffer, executable, command, pipe_file_if_needed, input) abort +function! ale#command#FormatCommand( +\ buffer, +\ executable, +\ command, +\ pipe_file_if_needed, +\ input, +\ filename_mappings, +\) abort let l:temporary_file = '' let l:command = a:command @@ -154,14 +173,14 @@ function! ale#command#FormatCommand(buffer, executable, command, pipe_file_if_ne " file. if l:command =~# '%s' let l:filename = fnamemodify(bufname(a:buffer), ':p') - let l:command = substitute(l:command, '%s', '\=ale#Escape(l:filename)', 'g') + let l:command = substitute(l:command, '%s', '\=s:FormatFilename(l:filename, a:filename_mappings)', 'g') endif if a:input isnot v:false && l:command =~# '%t' " Create a temporary filename, / " The file itself will not be created by this function. let l:temporary_file = s:TemporaryFilename(a:buffer) - let l:command = substitute(l:command, '%t', '\=ale#Escape(l:temporary_file)', 'g') + let l:command = substitute(l:command, '%t', '\=s:FormatFilename(l:temporary_file, a:filename_mappings)', 'g') endif " Finish formatting so %% becomes %. @@ -265,6 +284,7 @@ function! ale#command#Run(buffer, command, Callback, ...) abort \ a:command, \ get(l:options, 'read_buffer', 0), \ get(l:options, 'input', v:null), + \ get(l:options, 'filename_mappings', []), \) let l:command = ale#job#PrepareCommand(a:buffer, l:command) let l:job_options = { diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index cfc1e5d7..3f4f9338 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -256,6 +256,13 @@ function! s:RemapItemTypes(type_map, loclist) abort endfunction function! ale#engine#FixLocList(buffer, linter_name, from_other_source, loclist) abort + let l:mappings = ale#GetFilenameMappings(a:buffer, a:linter_name) + + if !empty(l:mappings) + " We need to apply reverse filename mapping here. + let l:mappings = ale#filename_mapping#Invert(l:mappings) + endif + let l:bufnr_map = {} let l:new_loclist = [] @@ -296,13 +303,19 @@ function! ale#engine#FixLocList(buffer, linter_name, from_other_source, loclist) let l:item.code = l:old_item.code endif - if has_key(l:old_item, 'filename') - \&& !ale#path#IsTempName(l:old_item.filename) + let l:old_name = get(l:old_item, 'filename', '') + + " Map parsed from output to local filesystem files. + if !empty(l:old_name) && !empty(l:mappings) + let l:old_name = ale#filename_mapping#Map(l:old_name, l:mappings) + endif + + if !empty(l:old_name) && !ale#path#IsTempName(l:old_name) " Use the filename given. " Temporary files are assumed to be for this buffer, " and the filename is not included then, because it looks bad " in the loclist window. - let l:filename = l:old_item.filename + let l:filename = l:old_name let l:item.filename = l:filename if has_key(l:old_item, 'bufnr') @@ -415,6 +428,7 @@ function! s:RunJob(command, options) abort \ 'executable': l:executable, \ 'read_buffer': l:read_buffer, \ 'log_output': 1, + \ 'filename_mappings': ale#GetFilenameMappings(l:buffer, l:linter.name), \}) " Only proceed if the job is being run. diff --git a/autoload/ale/filename_mapping.vim b/autoload/ale/filename_mapping.vim new file mode 100644 index 00000000..76d47acc --- /dev/null +++ b/autoload/ale/filename_mapping.vim @@ -0,0 +1,22 @@ +" Author: w0rp +" Description: Logic for handling mappings between files + +" Invert filesystem mappings so they can be mapped in reverse. +function! ale#filename_mapping#Invert(filename_mappings) abort + return map(copy(a:filename_mappings), '[v:val[1], v:val[0]]') +endfunction + +" Given a filename and some filename_mappings, map a filename. +function! ale#filename_mapping#Map(filename, filename_mappings) abort + let l:simplified_filename = ale#path#Simplify(a:filename) + + for [l:mapping_from, l:mapping_to] in a:filename_mappings + let l:mapping_from = ale#path#Simplify(l:mapping_from) + + if l:simplified_filename[:len(l:mapping_from) - 1] is# l:mapping_from + return l:mapping_to . l:simplified_filename[len(l:mapping_from):] + endif + endfor + + return a:filename +endfunction diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index b2a39444..a53f8626 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -1,4 +1,8 @@ +" Author: w0rp +" Description: Functions for fixing code with programs, or other means. + call ale#Set('fix_on_save_ignore', {}) +call ale#Set('filename_mappings', {}) " Apply fixes queued up for buffers which may be hidden. " Vim doesn't let you modify hidden buffers. @@ -110,7 +114,6 @@ function! s:HandleExit(job_info, buffer, job_output, data) abort call s:RunFixer({ \ 'buffer': a:buffer, \ 'input': l:input, - \ 'output': l:output, \ 'callback_list': a:job_info.callback_list, \ 'callback_index': a:job_info.callback_index + 1, \}) @@ -125,6 +128,7 @@ function! s:RunJob(result, options) abort let l:buffer = a:options.buffer let l:input = a:options.input + let l:fixer_name = a:options.fixer_name if a:result is 0 || type(a:result) is v:t_list if type(a:result) is v:t_list @@ -150,7 +154,6 @@ function! s:RunJob(result, options) abort \ 'input': l:input, \ 'callback_index': a:options.callback_index, \ 'callback_list': a:options.callback_list, - \ 'output': [], \}) return @@ -177,6 +180,7 @@ function! s:RunJob(result, options) abort \ 'read_buffer': l:read_buffer, \ 'input': l:input, \ 'log_output': 0, + \ 'filename_mappings': ale#GetFilenameMappings(l:buffer, l:fixer_name), \}) if empty(l:run_result) @@ -200,32 +204,22 @@ function! s:RunFixer(options) abort return endif - let l:ChainCallback = get(a:options, 'chain_callback', v:null) - - let l:Function = l:ChainCallback isnot v:null - \ ? ale#util#GetFunction(l:ChainCallback) - \ : a:options.callback_list[l:index] + let [l:fixer_name, l:Function] = a:options.callback_list[l:index] " Record new jobs started as fixer jobs. call setbufvar(l:buffer, 'ale_job_type', 'fixer') - if l:ChainCallback isnot v:null - " Chained commands accept (buffer, output, [input]) - let l:result = ale#util#FunctionArgCount(l:Function) == 2 - \ ? call(l:Function, [l:buffer, a:options.output]) - \ : call(l:Function, [l:buffer, a:options.output, copy(l:input)]) - else - " Regular fixer commands accept (buffer, [input]) - let l:result = ale#util#FunctionArgCount(l:Function) == 1 - \ ? call(l:Function, [l:buffer]) - \ : call(l:Function, [l:buffer, copy(l:input)]) - endif + " Regular fixer commands accept (buffer, [input]) + let l:result = ale#util#FunctionArgCount(l:Function) == 1 + \ ? call(l:Function, [l:buffer]) + \ : call(l:Function, [l:buffer, copy(l:input)]) call s:RunJob(l:result, { \ 'buffer': l:buffer, \ 'input': l:input, \ 'callback_list': a:options.callback_list, \ 'callback_index': l:index, + \ 'fixer_name': l:fixer_name, \}) endfunction @@ -293,16 +287,24 @@ function! s:GetCallbacks(buffer, fixing_flag, fixers) abort " Variables with capital characters are needed, or Vim will complain about " funcref variables. for l:Item in l:callback_list + " Try to capture the names of registered fixer names, so we can use + " them for filename mapping or other purposes later. + let l:fixer_name = v:null + if type(l:Item) is v:t_string let l:Func = ale#fix#registry#GetFunc(l:Item) if !empty(l:Func) + let l:fixer_name = l:Item let l:Item = l:Func endif endif try - call add(l:corrected_list, ale#util#GetFunction(l:Item)) + call add(l:corrected_list, [ + \ l:fixer_name, + \ ale#util#GetFunction(l:Item) + \]) catch /E475/ " Rethrow exceptions for failing to get a function so we can print " a friendly message about it. diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index 94476ca9..2a38945c 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -160,11 +160,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['php'], \ 'description': 'Fix PHP files with php-cs-fixer.', \ }, -\ 'astyle': { +\ 'astyle': { \ 'function': 'ale#fixers#astyle#Fix', \ 'suggested_filetypes': ['c', 'cpp'], \ 'description': 'Fix C/C++ with astyle.', -\ }, +\ }, \ 'clangtidy': { \ 'function': 'ale#fixers#clangtidy#Fix', \ 'suggested_filetypes': ['c', 'cpp', 'objc'], diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index f4a42bf4..db640654 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -265,7 +265,14 @@ function! s:StartLSP(options, address, executable, command) abort call ale#lsp#MarkConnectionAsTsserver(l:conn_id) endif - let l:command = ale#command#FormatCommand(l:buffer, a:executable, a:command, 0, v:false)[1] + let l:command = ale#command#FormatCommand( + \ l:buffer, + \ a:executable, + \ a:command, + \ 0, + \ v:false, + \ [], + \)[1] let l:command = ale#job#PrepareCommand(l:buffer, l:command) let l:ready = ale#lsp#StartProgram(l:conn_id, a:executable, l:command) endif diff --git a/autoload/ale/path.vim b/autoload/ale/path.vim index 30550503..f18a9733 100644 --- a/autoload/ale/path.vim +++ b/autoload/ale/path.vim @@ -95,7 +95,7 @@ function! ale#path#IsAbsolute(filename) abort return a:filename[:0] is# '/' || a:filename[1:2] is# ':\' endfunction -let s:temp_dir = ale#path#Simplify(fnamemodify(ale#util#Tempname(), ':h')) +let s:temp_dir = ale#path#Simplify(fnamemodify(ale#util#Tempname(), ':h:h')) " Given a filename, return 1 if the file represents some temporary file " created by Vim. diff --git a/doc/ale.txt b/doc/ale.txt index 81271105..7327959b 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -9,8 +9,9 @@ CONTENTS *ale-contents* 1. Introduction.........................|ale-introduction| 2. Supported Languages & Tools..........|ale-support| 3. Linting..............................|ale-lint| - 3.1 Adding Language Servers...........|ale-lint-language-servers| - 3.2 Other Sources.....................|ale-lint-other-sources| + 3.1 Linting On Other Machines.........|ale-lint-other-machines| + 3.2 Adding Language Servers...........|ale-lint-language-servers| + 3.3 Other Sources.....................|ale-lint-other-sources| 4. Fixing Problems......................|ale-fix| 5. Language Server Protocol Support.....|ale-lsp| 5.1 Completion........................|ale-completion| @@ -148,7 +149,61 @@ ALE offers several options for controlling which linters are run. ------------------------------------------------------------------------------- -3.1 Adding Language Servers *ale-lint-language-servers* +3.1 Linting On Other Machines *ale-lint-other-machines* + +ALE offers support for running linters or fixers on files you are editing +locally on other machines, so long as the other machine has access the file +you are editing. This could be a linter or fixer run inside of a Docker image, +running in a virtual machine, running on a remote server, etc. + +In order to run tools on other machines, you will need to configure your tools +to run via scripts that execute commands on those machines, such as by setting +the ALE `_executable` options for those tools to a path for a script to run, +or by using |g:ale_command_wrapper| to specify a script to wrap all commands +that are run by ALE, before they are executed. For tools that ALE runs where +ALE looks for locally installed executables first, you may need to set the +`_use_global` options for those tools to `1`, or you can set +|g:ale_use_global_executables| to `1` before ALE is loaded to only use global +executables for all tools. + +In order for ALE to properly lint or fix files which are running on another +file system, you must provide ALE with |List|s of strings for mapping paths to +and from your local file system and the remote file system, such as the file +system of your Docker container. See |g:ale_filename_mappings| for all of the +different ways these filename mappings can be configured. + +For example, you might configure `pylint` to run via Docker by creating a +script like so. > + + #!/usr/bin/env bash + + exec docker run --rm -v "$(pwd):/data" cytopia/pylint "$@" +< +With the above script in mind, you might configure ALE to lint your Python +project with `pylint` by providing the path to the script to execute, and +mappings which describe how to between the two file systems in your +`python.vim` |ftplugin| file, like so: > + + if expand('%:p') =~# '^/home/w0rp/git/test-pylint/' + let b:ale_linters = ['pylint'] + let b:ale_python_pylint_use_global = 1 + " This is the path to the script above. + let b:ale_python_pylint_executable = '/home/w0rp/git/test-pylint/pylint.sh' + " /data matches the path in Docker. + let b:ale_filename_mappings = { + \ 'pylint': [ + \ ['/home/w0rp/git/test-pylint', '/data'], + \ ], + \} + endif +< + +You might consider using a Vim plugin for loading Vim configuration files +specific to each project, if you have a lot of projects to manage. + + +------------------------------------------------------------------------------- +3.2 Adding Language Servers *ale-lint-language-servers* ALE comes with many default configurations for language servers, so they can be detected and run automatically. ALE can connect to other language servers @@ -189,7 +244,7 @@ address to connect to instead. > ------------------------------------------------------------------------------- -3.2 Other Sources *ale-lint-other-sources* +3.3 Other Sources *ale-lint-other-sources* Problems for a buffer can be taken from other sources and rendered by ALE. This allows ALE to be used in combination with other plugins which also want @@ -356,6 +411,10 @@ by default. Fixers can be disabled on save with |g:ale_fix_on_save_ignore|. They will still be run when you manually run |ALEFix|. +Fixers can be run on another machines, just like linters, such as fixers run +from a Docker container, running in a virtual machine, running a remote +server, etc. See |ale-lint-other-machines|. + =============================================================================== 5. Language Server Protocol Support *ale-lsp* @@ -1286,6 +1345,90 @@ g:ale_linter_aliases *g:ale_linter_aliases* < No linters will be loaded when the buffer's filetype is empty. + +g:ale_filename_mappings *g:ale_filename_mappings* + *b:ale_filename_mappings* + + Type: |Dictionary| or |List| + Default: `{}` + + Either a |Dictionary| mapping a linter or fixer name, as displayed in + |:ALEInfo|, to a |List| of two-item |List|s for filename mappings, or just a + |List| of two-item |List|s. When given some paths to files, the value of + this setting will be used to convert filenames on a local file system to + filenames on some remote file system, such as paths in a Docker image, + virtual machine, or network drive. + + For example: > + + let g:ale_filename_mappings = { + \ 'pylint': [ + \ ['/home/john/proj', '/data'], + \ ], + \} +< + With the above configuration, a filename such as `/home/john/proj/foo.py` + will be provided to the linter/fixer as `/data/foo.py`, and paths parsed + from linter results such as `/data/foo.py` will be converted back to + `/home/john/proj/foo.py`. + + You can use `*` as to apply a |List| of filename mappings to all other + linters or fixers not otherwise matched. > + + " Use one List of paths for pylint. + " Use another List of paths for everything else. + let g:ale_filename_mappings = { + \ 'pylint': [ + \ ['/home/john/proj', '/data'], + \ ], + \ '*': [ + \ ['/home/john/proj', '/other-data'], + \ ], + \} +< + If you just want every single linter or fixer to use the same filename + mapping, you can just use a |List|. > + + " Same as above, but for ALL linters and fixers. + let g:ale_filename_mappings = [ + \ ['/home/john/proj', '/data'], + \] +< + You can provide many such filename paths for multiple projects. Paths are + matched by checking if the start of a file path matches the given strings, + in a case-sensitive manner. Earlier entries in the |List| will be tried + before later entries when mapping to a given file system. + + Buffer-local options can be set to the same values to override the global + options, such as in |ftplugin| files. + + NOTE: Only fixers registered with a short name can support filename mapping + by their fixer names. See |ale-fix|. Filename mappings set for all tools by + using only a |List| for the setting will also be applied to fixers not in + the registry. + + NOTE: In order for this filename mapping to work correctly, linters and + fixers must exclusively determine paths to files to lint or fix via ALE + command formatting as per |ale-command-format-strings|, and paths parsed + from linter files must be provided in `filename` keys if a linter returns + results for more than one file at a time, as per |ale-loclist-format|. If + you discover a linter or fixer which does not behave properly, please report + it as an issue. + + If you are running a linter or fixer through Docker or another remote file + system, you may have to mount your temporary directory, which you can + discover with the following command: > + + :echo fnamemodify(tempname(), ':h:h') +< + You should provide a mapping from this temporary directory to whatever you + mount this directory to in Docker, or whatever remote file system you are + working with. + + You can inspect the filename mappings ALE will use with the + |ale#GetFilenameMappings()| function. + + g:ale_linters *g:ale_linters* *b:ale_linters* Type: |Dictionary| @@ -3073,6 +3216,15 @@ ale#Env(variable_name, value) *ale#Env()* 'set VAR="some value" && command' # On Windows +ale#GetFilenameMappings(buffer, name) *ale#GetFilenameMappings()* + + Given a `buffer` and the `name` of either a linter for fixer, return a + |List| of two-item |List|s that describe mapping to and from the local and + foreign file systems for running a particular linter or fixer. + + See |g:ale_filename_mappings| for details on filename mapping. + + ale#Has(feature) *ale#Has()* Return `1` if ALE supports a given feature, like |has()| for Vim features. @@ -3187,23 +3339,36 @@ ale#command#Run(buffer, command, callback, [options]) *ale#command#Run()* < The following `options` can be provided. - `output_stream` - Either `'stdout'`, `'stderr'`, `'both'`, or `'none`' for - selecting which output streams to read lines from. + `output_stream` - Either `'stdout'`, `'stderr'`, `'both'`, or + `'none`' for selecting which output streams to read + lines from. - The default is `'stdout'` + The default is `'stdout'` - `executable` - An executable for formatting into `%e` in the command. - If this option is not provided, formatting commands with - `%e` will not work. + `executable` - An executable for formatting into `%e` in the + command. If this option is not provided, formatting + commands with `%e` will not work. - `read_buffer` - If set to `1`, the buffer will be piped into the - command. + `read_buffer` - If set to `1`, the buffer will be piped into the + command. - The default is `0`. + The default is `0`. + + `input` - When creating temporary files with `%t` or piping + text into a command `input` can be set to a |List| of + text to use instead of the buffer's text. + + `filename_mappings` - A |List| of two-item |List|s describing filename + mappings to apply for formatted filenames in the + command string, as per |g:ale_filename_mappings|. + + If the call to this function is being used for a + linter or fixer, the mappings should be provided with + this option, and can be retrieved easily with + |ale#GetFilenameMappings()|. + + The default is `[]`. - `input` - When creating temporary files with `%t` or piping text - into a command `input` can be set to a |List| of text to - use instead of the buffer's text. ale#command#EscapeCommandPart(command_part) *ale#command#EscapeCommandPart()* diff --git a/plugin/ale.vim b/plugin/ale.vim index 0651dd4b..6a947ea7 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -97,6 +97,10 @@ let g:ale_fix_on_save = get(g:, 'ale_fix_on_save', 0) " should be used instead. let g:ale_enabled = get(g:, 'ale_enabled', 1) +" A Dictionary mapping linter or fixer names to Arrays of two-item Arrays +" mapping filename paths from one system to another. +let g:ale_filename_mappings = get(g:, 'ale_filename_mappings', {}) + " These flags dictates if ale uses the quickfix or the loclist (loclist is the " default, quickfix overrides loclist). let g:ale_set_loclist = get(g:, 'ale_set_loclist', 1) diff --git a/test/fix/test_ale_fix.vader b/test/fix/test_ale_fix.vader index 53079d16..ee96f0ff 100644 --- a/test/fix/test_ale_fix.vader +++ b/test/fix/test_ale_fix.vader @@ -6,6 +6,7 @@ Before: Save g:ale_lint_on_save Save g:ale_echo_cursor Save g:ale_command_wrapper + Save g:ale_filename_mappings silent! cd /testplugin/test/fix @@ -19,6 +20,7 @@ Before: let g:ale_fixers = { \ 'testft': [], \} + let g:ale_filename_mappings = {} let g:pre_success = 0 let g:post_success = 0 @@ -72,6 +74,10 @@ Before: return {'command': 'cat %t <(echo d)'} endfunction + function EchoFilename(buffer, lines) abort + return {'command': 'echo %s'} + endfunction + function RemoveLastLine(buffer, lines) abort return ['a', 'b'] endfunction @@ -155,6 +161,7 @@ After: delfunction CatLineDeferred delfunction ReplaceWithTempFile delfunction CatWithTempFile + delfunction EchoFilename delfunction RemoveLastLine delfunction RemoveLastLineOneArg delfunction TestCallback @@ -209,6 +216,23 @@ Expect(The first function should be used): ^b ^c +Execute(Should apply filename mpapings): + " The command echos %s, and we'll map the current path so we can check + " that ALEFix applies filename mappings, end-to-end. + let g:ale_filename_mappings = { + \ 'echo_filename': [ + \ [expand('%:p:h'), '/some/fake/path'], + \ ], + \} + + call ale#fix#registry#Add('echo_filename', 'EchoFilename', [], 'echo filename') + let g:ale_fixers.testft = ['echo_filename'] + ALEFix + call ale#test#FlushJobs() + +Expect(The mapped filename should be printed): + /some/fake/path/test.txt + Execute(ALEFix should apply simple functions in a chain): let g:ale_fixers.testft = ['AddCarets', 'Capitalize'] ALEFix diff --git a/test/test_filename_mapping.vader b/test/test_filename_mapping.vader new file mode 100644 index 00000000..5b6e4f12 --- /dev/null +++ b/test/test_filename_mapping.vader @@ -0,0 +1,62 @@ +Before: + Save g:ale_filename_mappings + Save b:ale_filename_mappings + + let g:ale_filename_mappings = {} + unlet! b:ale_filename_mappings + +After: + Restore + +Execute(ale#GetFilenameMappings should return the correct mappings for given linters/fixers): + let g:ale_filename_mappings = {'a': [['foo', 'bar']], 'b': [['baz', 'foo']]} + + AssertEqual [['foo', 'bar']], ale#GetFilenameMappings(bufnr(''), 'a') + AssertEqual [['baz', 'foo']], ale#GetFilenameMappings(bufnr(''), 'b') + AssertEqual [], ale#GetFilenameMappings(bufnr(''), 'c') + + let b:ale_filename_mappings = {'b': [['abc', 'xyz']]} + + AssertEqual [], ale#GetFilenameMappings(bufnr(''), 'a') + AssertEqual [['abc', 'xyz']], ale#GetFilenameMappings(bufnr(''), 'b') + AssertEqual [], ale#GetFilenameMappings(bufnr(''), 'c') + +Execute(ale#GetFilenameMappings should return Lists set for use with all tools): + let g:ale_filename_mappings = [['foo', 'bar']] + + AssertEqual [['foo', 'bar']], ale#GetFilenameMappings(bufnr(''), 'a') + AssertEqual [['foo', 'bar']], ale#GetFilenameMappings(bufnr(''), '') + AssertEqual [['foo', 'bar']], ale#GetFilenameMappings(bufnr(''), v:null) + + let b:ale_filename_mappings = [['abc', 'xyz']] + + AssertEqual [['abc', 'xyz']], ale#GetFilenameMappings(bufnr(''), 'a') + AssertEqual [['abc', 'xyz']], ale#GetFilenameMappings(bufnr(''), '') + AssertEqual [['abc', 'xyz']], ale#GetFilenameMappings(bufnr(''), v:null) + +Execute(ale#GetFilenameMappings should let you use * as a fallback): + let g:ale_filename_mappings = {'a': [['foo', 'bar']], '*': [['abc', 'xyz']]} + + AssertEqual [['foo', 'bar']], ale#GetFilenameMappings(bufnr(''), 'a') + AssertEqual [['abc', 'xyz']], ale#GetFilenameMappings(bufnr(''), 'b') + AssertEqual [['abc', 'xyz']], ale#GetFilenameMappings(bufnr(''), '') + AssertEqual [['abc', 'xyz']], ale#GetFilenameMappings(bufnr(''), v:null) + +Execute(ale#filename_mapping#Invert should invert filename mappings): + AssertEqual + \ [['b', 'a'], ['y', 'x']], + \ ale#filename_mapping#Invert([['a', 'b'], ['x', 'y']]) + \ +Execute(ale#filename_mapping#Map return the filename as-is if there are no mappings): + AssertEqual + \ '/foo//bar', + \ ale#filename_mapping#Map('/foo//bar', [['/bar', '/data/']]) + +Execute(ale#filename_mapping#Map should map filenames): + AssertEqual + \ '/data/bar', + \ ale#filename_mapping#Map('/foo//bar', [ + \ ['/data', '/baz'], + \ ['/foo', '/data'], + \ ['/foo', '/xyz'], + \ ]) diff --git a/test/test_format_command.vader b/test/test_format_command.vader index 15435326..f8e2a66e 100644 --- a/test/test_format_command.vader +++ b/test/test_format_command.vader @@ -25,12 +25,12 @@ After: Execute(FormatCommand should do nothing to basic command strings): AssertEqual \ ['', 'awesome-linter do something', 0], - \ ale#command#FormatCommand(bufnr('%'), '', 'awesome-linter do something', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), '', 'awesome-linter do something', 0, v:null, []) Execute(FormatCommand should handle %%, and ignore other percents): AssertEqual \ ['', '% %%d %%f %x %', 0], - \ ale#command#FormatCommand(bufnr('%'), '', '%% %%%d %%%f %x %', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), '', '%% %%%d %%%f %x %', 0, v:null, []) Execute(FormatCommand should convert %s to the current filename): AssertEqual @@ -39,10 +39,10 @@ Execute(FormatCommand should convert %s to the current filename): \ 'foo ' . ale#Escape(expand('%:p')) . ' bar ' . ale#Escape(expand('%:p')), \ 0, \ ], - \ ale#command#FormatCommand(bufnr('%'), '', 'foo %s bar %s', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), '', 'foo %s bar %s', 0, v:null, []) Execute(FormatCommand should convert %t to a new temporary filename): - let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo %t bar %t', 0, v:null) + let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo %t bar %t', 0, v:null, []) call CheckTempFile(g:result[0]) @@ -56,21 +56,21 @@ Execute(FormatCommand should convert %t to a new temporary filename): AssertEqual g:match[1], g:match[2] Execute(FormatCommand should not convert %t to a new temporary filename when the input is given as v:false): - let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo %t bar %t', 0, v:false) + let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo %t bar %t', 0, v:false, []) AssertEqual ['', 'foo %t bar %t', 0], g:result Execute(FormatCommand should signal that files are created when temporary files are needed): AssertEqual \ 1, - \ ale#command#FormatCommand(bufnr('%'), '', 'foo %t', 0, v:null)[2] + \ ale#command#FormatCommand(bufnr('%'), '', 'foo %t', 0, v:null, [])[2] AssertEqual \ 0, - \ ale#command#FormatCommand(bufnr('%'), '', 'foo %s', 0, v:null)[2] + \ ale#command#FormatCommand(bufnr('%'), '', 'foo %s', 0, v:null, [])[2] Execute(FormatCommand should let you combine %s and %t): - let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo %t bar %s', 0, v:null) + let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo %t bar %s', 0, v:null, []) call CheckTempFile(g:result[0]) @@ -87,30 +87,30 @@ Execute(FormatCommand should replace %e with the escaped executable): if has('win32') AssertEqual \ ['', 'foo foo', 0], - \ ale#command#FormatCommand(bufnr('%'), 'foo', '%e %e', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), 'foo', '%e %e', 0, v:null, []) AssertEqual \ ['', '"foo bar"', 0], - \ ale#command#FormatCommand(bufnr('%'), 'foo bar', '%e', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), 'foo bar', '%e', 0, v:null, []) AssertEqual \ ['', '%e %e', 0], - \ ale#command#FormatCommand(bufnr('%'), '', '%e %e', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), '', '%e %e', 0, v:null, []) else AssertEqual \ ['', '''foo'' ''foo''', 0], - \ ale#command#FormatCommand(bufnr('%'), 'foo', '%e %e', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), 'foo', '%e %e', 0, v:null, []) AssertEqual \ ['', '''foo bar''', 0], - \ ale#command#FormatCommand(bufnr('%'), 'foo bar', '%e', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), 'foo bar', '%e', 0, v:null, []) AssertEqual \ ['', '%e %e', 0], - \ ale#command#FormatCommand(bufnr('%'), '', '%e %e', 0, v:null) + \ ale#command#FormatCommand(bufnr('%'), '', '%e %e', 0, v:null, []) endif Execute(EscapeCommandPart should escape all percent signs): AssertEqual '%%s %%t %%%% %%s %%t %%%%', ale#engine#EscapeCommandPart('%s %t %% %s %t %%') Execute(EscapeCommandPart should pipe in temporary files appropriately): - let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo bar', 1, v:null) + let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo bar', 1, v:null, []) call CheckTempFile(g:result[0]) @@ -118,10 +118,24 @@ Execute(EscapeCommandPart should pipe in temporary files appropriately): Assert !empty(g:match), 'No match found! Result was: ' . g:result[1] AssertEqual ale#Escape(g:result[0]), g:match[1] - let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo bar %t', 1, v:null) + let g:result = ale#command#FormatCommand(bufnr('%'), '', 'foo bar %t', 1, v:null, []) call CheckTempFile(g:result[0]) let g:match = matchlist(g:result[1], '\v^foo bar (.*)$') Assert !empty(g:match), 'No match found! Result was: ' . g:result[1] AssertEqual ale#Escape(g:result[0]), g:match[1] + +Execute(FormatCommand should apply filename mappings the current file): + let g:result = ale#command#FormatCommand(bufnr('%'), '', '%s', 1, v:null, [ + \ [expand('%:p:h'), '/foo/bar'], + \]) + + Assert g:result[1] =~# '/foo/bar' + +Execute(FormatCommand should apply filename mappings to temporary files): + let g:result = ale#command#FormatCommand(bufnr('%'), '', '%t', 1, v:null, [ + \ [fnamemodify(tempname(), ':h:h'), '/foo/bar'] + \]) + + Assert g:result[1] =~# '/foo/bar' diff --git a/test/test_loclist_corrections.vader b/test/test_loclist_corrections.vader index 343620a5..2dde9c63 100644 --- a/test/test_loclist_corrections.vader +++ b/test/test_loclist_corrections.vader @@ -1,7 +1,50 @@ +Before: + Save g:ale_filename_mappings + + let g:ale_filename_mappings = {} + After: unlet! b:temp_name unlet! b:other_bufnr + Restore + + +Execute(FixLocList should map filenames): + " Paths converted back into temporary filenames shouldn't be included. + let g:ale_filename_mappings = { + \ 'linter2': [['/xxx', '/data']], + \ 'linter1': [ + \ ['/bar', '/data/special'], + \ ['/foo', '/data'], + \ [ + \ ale#path#Simplify(fnamemodify(ale#util#Tempname(), ':h:h')), + \ '/x-tmp', + \ ], + \ ], + \} + + AssertEqual + \ [ + \ '/foo/file.txt', + \ v:null, + \ '/bar/file.txt', + \ ], + \ map( + \ ale#engine#FixLocList( + \ bufnr('%'), + \ 'linter1', + \ 0, + \ [ + \ {'text': 'x', 'lnum': 1, 'filename': '/data/file.txt'}, + \ {'text': 'x', 'lnum': 1, 'filename': '/x-tmp/file.txt'}, + \ {'text': 'x', 'lnum': 1, 'filename': '/data/special/file.txt'}, + \ ], + \ ), + \ 'get(v:val, ''filename'', v:null)', + \ ) + + Given foo (Some file with lines to count): foo12345678 bar12345678 @@ -37,7 +80,7 @@ Execute(FixLocList should set all the default values correctly): \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -58,7 +101,7 @@ Execute(FixLocList should use the values we supply): \ 'nr': 42, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -87,7 +130,7 @@ Execute(FixLocList should set items with lines beyond the end to the last line): \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -108,7 +151,7 @@ Execute(FixLocList should move line 0 to line 1): \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -130,7 +173,7 @@ Execute(FixLocList should convert line and column numbers correctly): \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -164,7 +207,7 @@ Execute(FixLocList should pass on end_col values): \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -202,7 +245,7 @@ Execute(FixLocList should pass on end_lnum values): \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -227,7 +270,7 @@ Execute(FixLocList should allow subtypes to be set): \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -289,7 +332,7 @@ Execute(FixLocList should accept filenames): \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -327,7 +370,7 @@ Execute(FixLocList should interpret temporary filenames as being the current buf \ 'nr': -1, \ 'linter_name': 'foobar', \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr(''), \ 'foobar', @@ -352,7 +395,7 @@ Execute(The error code should be passed on): \ 'linter_name': 'foobar', \ 'code': 'some-code' \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -385,7 +428,7 @@ Execute(FixLocList should mark problems as coming from other sources if requeste \ 'linter_name': 'foobar', \ 'from_other_source': 1, \ }, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar', @@ -407,7 +450,7 @@ Execute(character positions should be converted to byte positions): \ {'lnum': 1, 'bufnr': bufnr(''), 'col': 7, 'end_col': 13, 'end_lnum': 1, 'linter_name': 'foobar', 'nr': -1, 'type': 'E', 'vcol': 0, 'text': 'a'}, \ {'lnum': 1, 'bufnr': bufnr(''), 'col': 7, 'end_col': 17, 'end_lnum': 2, 'linter_name': 'foobar', 'nr': -1, 'type': 'E', 'vcol': 0, 'text': 'a'}, \ {'lnum': 2, 'bufnr': bufnr(''), 'col': 17, 'linter_name': 'foobar', 'nr': -1, 'type': 'E', 'vcol': 0, 'text': 'a'}, - \], + \ ], \ ale#engine#FixLocList( \ bufnr('%'), \ 'foobar',