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.
This commit is contained in:
w0rp 2020-08-23 19:55:42 +01:00
parent 2b785688ea
commit ba3dd0d027
No known key found for this signature in database
GPG Key ID: 0FC1ECAA8C81CD83
15 changed files with 483 additions and 74 deletions

View File

@ -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.

View File

@ -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

View File

@ -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 = {

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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'],

View File

@ -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

View File

@ -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.

View File

@ -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()*

View File

@ -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)

View File

@ -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

View File

@ -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'],
\ ])

View File

@ -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'

View File

@ -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',