summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md12
-rw-r--r--autoload/ale.vim20
-rw-r--r--autoload/ale/command.vim26
-rw-r--r--autoload/ale/engine.vim20
-rw-r--r--autoload/ale/filename_mapping.vim22
-rw-r--r--autoload/ale/fix.vim40
-rw-r--r--autoload/ale/fix/registry.vim4
-rw-r--r--autoload/ale/lsp_linter.vim9
-rw-r--r--autoload/ale/path.vim2
-rw-r--r--doc/ale.txt197
-rw-r--r--plugin/ale.vim4
-rw-r--r--test/fix/test_ale_fix.vader26
-rw-r--r--test/test_filename_mapping.vader62
-rw-r--r--test/test_format_command.vader46
-rw-r--r--test/test_loclist_corrections.vader69
15 files changed, 485 insertions, 74 deletions
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)
<a name="supported-languages"></a>
@@ -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
```
+
+<a name="faq-vm"></a>
+
+### 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, <temp_dir>/<original_basename>
" 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 <devw0rp@gmail.com>
+" 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 <devw0rp@gmail.com>
+" 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'`
+
+ `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.
+
+ The default is `0`.
- The default is `'stdout'`
+ `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.
- `executable` - An executable for formatting into `%e` in the command.
- If this option is not provided, formatting commands with
- `%e` will not work.
+ `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|.
- `read_buffer` - If set to `1`, the buffer will be piped into the
- command.
+ 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 `0`.
+ 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..e8a9dd7a 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,25 @@ 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()
+ " Remote trailing whitespace from the line.
+ call setline(1, substitute(getline(1), '[ \r]\+$', '', ''))
+
+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..e9af539a
--- /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..d53b1411 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',