summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--ale_linters/coffee/coffee.vim17
-rw-r--r--ale_linters/coffee/coffeelint.vim17
-rw-r--r--ale_linters/go/gobuild.vim213
-rw-r--r--ale_linters/javascript/xo.vim41
-rw-r--r--ale_linters/php/phpmd.vim41
-rw-r--r--ale_linters/python/flake8.vim58
-rw-r--r--ale_linters/ruby/rubocop.vim14
-rw-r--r--ale_linters/rust/cargo.vim7
-rw-r--r--ale_linters/rust/rustc.vim74
-rw-r--r--autoload/ale/engine.vim66
-rw-r--r--autoload/ale/handlers/rust.vim90
-rw-r--r--autoload/ale/loclist_jumping.vim16
-rw-r--r--autoload/ale/semver.vim29
-rw-r--r--autoload/ale/statusline.vim4
-rw-r--r--autoload/ale/util.vim2
-rw-r--r--doc/ale.txt89
-rw-r--r--plugin/ale.vim81
-rw-r--r--stdin-wrapper.bat22
-rw-r--r--stdin-wrapper.exebin0 -> 534016 bytes
-rw-r--r--stdin_wrapper.d84
-rw-r--r--test/test_rust_handler.vader28
-rw-r--r--test/test_semver_utils.vader16
-rw-r--r--test/test_sign_placement.vader68
24 files changed, 902 insertions, 182 deletions
diff --git a/README.md b/README.md
index 56a7486d..8daf2979 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ name. That seems to be the fairest way to arrange this table.
| CoffeeScript | [coffee](http://coffeescript.org/), [coffeelint](https://www.npmjs.com/package/coffeelint) |
| CSS | [csslint](http://csslint.net/), [stylelint](https://github.com/stylelint/stylelint) |
| Cython (pyrex filetype) | [cython](http://cython.org/) |
-| D | [dmd](https://dlang.org/dmd-linux.html)^ |
+| D | [dmd](https://dlang.org/dmd-linux.html) |
| Dockerfile | [hadolint](https://github.com/lukasmartinelli/hadolint) |
| Elixir | [credo](https://github.com/rrrene/credo) |
| Elm | [elm-make](https://github.com/elm-lang/elm-make) |
@@ -76,7 +76,7 @@ name. That seems to be the fairest way to arrange this table.
| MATLAB | [mlint](https://www.mathworks.com/help/matlab/ref/mlint.html) |
| OCaml | [merlin](https://github.com/the-lambda-church/merlin) see `:help ale-integration-ocaml-merlin` for configuration instructions
| Perl | [perl -c](https://perl.org/), [perl-critic](https://metacpan.org/pod/Perl::Critic) |
-| PHP | [hack](http://hacklang.org/), [php -l](https://secure.php.net/), [phpcs](https://github.com/squizlabs/PHP_CodeSniffer) |
+| PHP | [hack](http://hacklang.org/), [php -l](https://secure.php.net/), [phpcs](https://github.com/squizlabs/PHP_CodeSniffer), [phpmd](https://phpmd.org) |
| Pug | [pug-lint](https://github.com/pugjs/pug-lint) |
| Puppet | [puppet](https://puppet.com), [puppet-lint](https://puppet-lint.com) |
| Python | [flake8](http://flake8.pycqa.org/en/latest/), [mypy](http://mypy-lang.org/), [pylint](https://www.pylint.org/) |
@@ -93,14 +93,13 @@ name. That seems to be the fairest way to arrange this table.
| Vim | [vint](https://github.com/Kuniwak/vint) |
| YAML | [yamllint](https://yamllint.readthedocs.io/) |
-* *^ Supported only on Unix machines via a wrapper script.*
* *^^ No text linters are enabled by default.*
If you would like to see support for more languages and tools, please
[create an issue](https://github.com/w0rp/ale/issues)
or [create a pull request](https://github.com/w0rp/ale/pulls).
If your tool can read from stdin or you have code to suggest which is good,
-support can be happily added for more tools.
+support can be happily added for it.
<a name="usage"></a>
diff --git a/ale_linters/coffee/coffee.vim b/ale_linters/coffee/coffee.vim
index 243fec6b..ac9ef79e 100644
--- a/ale_linters/coffee/coffee.vim
+++ b/ale_linters/coffee/coffee.vim
@@ -1,10 +1,23 @@
" Author: KabbAmine - https://github.com/KabbAmine
" Description: Coffee for checking coffee files
+function! ale_linters#coffee#coffee#GetExecutable(buffer) abort
+ return ale#util#ResolveLocalPath(
+ \ a:buffer,
+ \ 'node_modules/.bin/coffee',
+ \ 'coffee'
+ \)
+endfunction
+
+function! ale_linters#coffee#coffee#GetCommand(buffer) abort
+ return ale_linters#coffee#coffee#GetExecutable(a:buffer)
+ \ . ' -cp -s'
+endfunction
+
call ale#linter#Define('coffee', {
\ 'name': 'coffee',
-\ 'executable': 'coffee',
-\ 'command': 'coffee -cp -s',
+\ 'executable_callback': 'ale_linters#coffee#coffee#GetExecutable',
+\ 'command_callback': 'ale_linters#coffee#coffee#GetCommand',
\ 'output_stream': 'stderr',
\ 'callback': 'ale#handlers#HandleGCCFormat',
\})
diff --git a/ale_linters/coffee/coffeelint.vim b/ale_linters/coffee/coffeelint.vim
index 96b8c59d..8b1c713e 100644
--- a/ale_linters/coffee/coffeelint.vim
+++ b/ale_linters/coffee/coffeelint.vim
@@ -1,6 +1,19 @@
" Author: Prashanth Chandra https://github.com/prashcr
" Description: coffeelint linter for coffeescript files
+function! ale_linters#coffee#coffeelint#GetExecutable(buffer) abort
+ return ale#util#ResolveLocalPath(
+ \ a:buffer,
+ \ 'node_modules/.bin/coffeelint',
+ \ 'coffeelint'
+ \)
+endfunction
+
+function! ale_linters#coffee#coffeelint#GetCommand(buffer) abort
+ return ale_linters#coffee#coffeelint#GetExecutable(a:buffer)
+ \ . ' --stdin --reporter csv'
+endfunction
+
function! ale_linters#coffee#coffeelint#Handle(buffer, lines) abort
" Matches patterns like the following:
"
@@ -40,7 +53,7 @@ endfunction
call ale#linter#Define('coffee', {
\ 'name': 'coffeelint',
-\ 'executable': 'coffeelint',
-\ 'command': 'coffeelint --stdin --reporter csv',
+\ 'executable_callback': 'ale_linters#coffee#coffeelint#GetExecutable',
+\ 'command_callback': 'ale_linters#coffee#coffeelint#GetCommand',
\ 'callback': 'ale_linters#coffee#coffeelint#Handle',
\})
diff --git a/ale_linters/go/gobuild.vim b/ale_linters/go/gobuild.vim
index 4abf1987..832669f2 100644
--- a/ale_linters/go/gobuild.vim
+++ b/ale_linters/go/gobuild.vim
@@ -1,17 +1,214 @@
-" Author: dzhou121 <dzhou121@gmail.com>
+" Author: Joshua Rubin <joshua@rubixconsulting.com>
" Description: go build for Go files
-function! s:FindGobuildScript() abort
- return g:ale#util#stdin_wrapper . ' .go go build -o /dev/null'
+" inspired by work from dzhou121 <dzhou121@gmail.com>
+
+function! ale_linters#go#gobuild#GoEnv(buffer) abort
+ if exists('s:go_env')
+ return ''
+ endif
+
+ return 'go env GOPATH GOROOT'
+endfunction
+
+let s:SplitChar = has('unix') ? ':' : ':'
+
+" get a list of all source directories from $GOPATH and $GOROOT
+function! s:SrcDirs() abort
+ let l:paths = split(s:go_env.GOPATH, s:SplitChar)
+ call add(l:paths, s:go_env.GOROOT)
+
+ return l:paths
+endfunction
+
+" figure out from a directory like `/home/user/go/src/some/package` that the
+" import for that path is simply `some/package`
+function! s:PackageImportPath(buffer) abort
+ let l:bufname = resolve(bufname(a:buffer))
+ let l:pkgdir = fnamemodify(l:bufname, ':p:h')
+
+ for l:path in s:SrcDirs()
+ let l:path = l:path . '/src/'
+
+ if stridx(l:pkgdir, l:path) == 0
+ return l:pkgdir[strlen(l:path):]
+ endif
+ endfor
+
+ return ''
+endfunction
+
+" get the package info data structure using `go list`
+function! ale_linters#go#gobuild#GoList(buffer, goenv_output) abort
+ if !empty(a:goenv_output)
+ let s:go_env = {
+ \ 'GOPATH': a:goenv_output[0],
+ \ 'GOROOT': a:goenv_output[1],
+ \}
+ endif
+
+ return 'go list -json ' . shellescape(s:PackageImportPath(a:buffer))
+endfunction
+
+let s:filekeys = [
+\ 'GoFiles',
+\ 'CgoFiles',
+\ 'CFiles',
+\ 'CXXFiles',
+\ 'MFiles',
+\ 'HFiles',
+\ 'FFiles',
+\ 'SFiles',
+\ 'SwigFiles',
+\ 'SwigCXXFiles',
+\ 'SysoFiles',
+\ 'TestGoFiles',
+\ 'XTestGoFiles',
+\]
+
+" get the go and test go files from the package
+" will return empty list if the package has any cgo or other invalid files
+function! s:PkgFiles(pkginfo) abort
+ let l:files = []
+
+ for l:key in s:filekeys
+ if has_key(a:pkginfo, l:key)
+ call extend(l:files, a:pkginfo[l:key])
+ endif
+ endfor
+
+ " resolve the path of the file relative to the window directory
+ return map(l:files, 'shellescape(fnamemodify(resolve(a:pkginfo.Dir . ''/'' . v:val), '':p''))')
+endfunction
+
+function! ale_linters#go#gobuild#CopyFiles(buffer, golist_output) abort
+ let l:tempdir = tempname()
+ let l:temppkgdir = l:tempdir . '/src/' . s:PackageImportPath(a:buffer)
+ call mkdir(l:temppkgdir, 'p', 0700)
+
+ if empty(a:golist_output)
+ return 'echo ' . shellescape(l:tempdir)
+ endif
+
+ " parse the output
+ let l:pkginfo = json_decode(join(a:golist_output, "\n"))
+
+ " get all files for the package
+ let l:files = s:PkgFiles(l:pkginfo)
+
+ " copy the files to a temp directory with $GOPATH structure
+ return 'cp ' . join(l:files, ' ') . ' ' . shellescape(l:temppkgdir) . ' && echo ' . shellescape(l:tempdir)
+endfunction
+
+function! ale_linters#go#gobuild#GetCommand(buffer, copy_output) abort
+ let l:tempdir = a:copy_output[0]
+ let l:importpath = s:PackageImportPath(a:buffer)
+
+ " write the a:buffer and any modified buffers from the package to the tempdir
+ for l:bufnum in range(1, bufnr('$'))
+ " ignore unloaded buffers (can't be a:buffer or a modified buffer)
+ if !bufloaded(l:bufnum)
+ continue
+ endif
+
+ " ignore non-Go buffers
+ if getbufvar(l:bufnum, '&ft') !=# 'go'
+ continue
+ endif
+
+ " only consider buffers other than a:buffer if they have the same import
+ " path as a:buffer and are modified
+ if l:bufnum != a:buffer
+ if s:PackageImportPath(l:bufnum) !=# l:importpath
+ continue
+ endif
+
+ if !getbufvar(l:bufnum, '&mod')
+ continue
+ endif
+ endif
+
+ call writefile(getbufline(l:bufnum, 1, '$'), l:tempdir . '/src/' . s:PkgFile(l:bufnum))
+ endfor
+
+ let l:gopaths = [ l:tempdir ]
+ call extend(l:gopaths, split(s:go_env.GOPATH, s:SplitChar))
+
+ return 'GOPATH=' . shellescape(join(l:gopaths, s:SplitChar)) . ' go test -c -o /dev/null ' . shellescape(l:importpath)
+endfunction
+
+function! s:PkgFile(buffer) abort
+ let l:bufname = resolve(bufname(a:buffer))
+ let l:importpath = s:PackageImportPath(a:buffer)
+ let l:fname = fnamemodify(l:bufname, ':t')
+
+ return l:importpath . '/' . l:fname
+endfunction
+
+function! s:FindBuffer(file) abort
+ for l:buffer in range(1, bufnr('$'))
+ if !buflisted(l:buffer)
+ continue
+ endif
+
+ let l:pkgfile = s:PkgFile(l:buffer)
+
+ if a:file =~ '/' . l:pkgfile . '$'
+ return l:buffer
+ endif
+ endfor
+
+ return -1
endfunction
-let g:ale#util#gobuild_script =
-\ get(g:, 'ale_go_gobuild_script', s:FindGobuildScript())
+let s:path_pattern = '[a-zA-Z]\?\\\?:\?[[:alnum:]/\.\-_]\+'
+let s:handler_pattern = '^\(' . s:path_pattern . '\):\(\d\+\):\?\(\d\+\)\?: \(.\+\)$'
+
+let s:multibuffer = 0
+
+function! ale_linters#go#gobuild#Handler(buffer, lines) abort
+ let l:output = []
+
+ for l:line in a:lines
+ let l:match = matchlist(l:line, s:handler_pattern)
+
+ if len(l:match) == 0
+ continue
+ endif
+
+ let l:buffer = s:FindBuffer(l:match[1])
+
+ if l:buffer == -1
+ continue
+ endif
+
+ if !s:multibuffer && l:buffer != a:buffer
+ " strip lines from other buffers
+ continue
+ endif
+
+ call add(l:output, {
+ \ 'bufnr': l:buffer,
+ \ 'lnum': l:match[2] + 0,
+ \ 'vcol': 0,
+ \ 'col': l:match[3] + 0,
+ \ 'text': l:match[4],
+ \ 'type': 'E',
+ \ 'nr': -1,
+ \})
+ endfor
+
+ return l:output
+endfunction
call ale#linter#Define('go', {
\ 'name': 'go build',
-\ 'output_stream': 'stderr',
\ 'executable': 'go',
-\ 'command': g:ale#util#gobuild_script,
-\ 'callback': 'ale#handlers#HandleUnixFormatAsError',
+\ 'command_chain': [
+\ {'callback': 'ale_linters#go#gobuild#GoEnv', 'output_stream': 'stdout'},
+\ {'callback': 'ale_linters#go#gobuild#GoList', 'output_stream': 'stdout'},
+\ {'callback': 'ale_linters#go#gobuild#CopyFiles', 'output_stream': 'stdout'},
+\ {'callback': 'ale_linters#go#gobuild#GetCommand', 'output_stream': 'stderr'},
+\ ],
+\ 'callback': 'ale_linters#go#gobuild#Handler',
\})
diff --git a/ale_linters/javascript/xo.vim b/ale_linters/javascript/xo.vim
new file mode 100644
index 00000000..e27f6f33
--- /dev/null
+++ b/ale_linters/javascript/xo.vim
@@ -0,0 +1,41 @@
+" Author: Daniel Lupu <lupu.daniel.f@gmail.com>
+" Description: xo for JavaScript files
+
+let g:ale_javascript_xo_executable =
+\ get(g:, 'ale_javascript_xo_executable', 'xo')
+
+let g:ale_javascript_xo_options =
+\ get(g:, 'ale_javascript_xo_options', '')
+
+let g:ale_javascript_xo_use_global =
+\ get(g:, 'ale_javascript_xo_use_global', 0)
+
+function! ale_linters#javascript#xo#GetExecutable(buffer) abort
+ if g:ale_javascript_xo_use_global
+ return g:ale_javascript_xo_executable
+ endif
+
+ return ale#util#ResolveLocalPath(
+ \ a:buffer,
+ \ 'node_modules/.bin/xo',
+ \ g:ale_javascript_xo_executable
+ \)
+endfunction
+
+function! ale_linters#javascript#xo#GetCommand(buffer) abort
+ return ale_linters#javascript#xo#GetExecutable(a:buffer)
+ \ . ' ' . g:ale_javascript_xo_options
+ \ . ' --reporter unix --stdin --stdin-filename %s'
+endfunction
+
+function! ale_linters#javascript#xo#Handle(buffer, lines) abort
+ " xo uses eslint and the output format is the same
+ return ale_linters#javascript#eslint#Handle(a:buffer, a:lines)
+endfunction
+
+call ale#linter#Define('javascript', {
+\ 'name': 'xo',
+\ 'executable_callback': 'ale_linters#javascript#xo#GetExecutable',
+\ 'command_callback': 'ale_linters#javascript#xo#GetCommand',
+\ 'callback': 'ale_linters#javascript#xo#Handle',
+\})
diff --git a/ale_linters/php/phpmd.vim b/ale_linters/php/phpmd.vim
new file mode 100644
index 00000000..73432538
--- /dev/null
+++ b/ale_linters/php/phpmd.vim
@@ -0,0 +1,41 @@
+" Author: medains <https://github.com/medains>
+" Description: phpmd for PHP files
+
+" Set to change the ruleset
+let g:ale_php_phpmd_ruleset = get(g:, 'ale_php_phpmd_ruleset', 'cleancode,codesize,controversial,design,naming,unusedcode')
+
+function! ale_linters#php#phpmd#Handle(buffer, lines) abort
+ " Matches against lines like the following:
+ "
+ " /path/to/some-filename.php:18 message
+ let l:pattern = '^.*:\(\d\+\)\t\(.\+\)$'
+ let l:output = []
+
+ for l:line in a:lines
+ let l:match = matchlist(l:line, l:pattern)
+
+ if len(l:match) == 0
+ continue
+ endif
+
+ " vcol is Needed to indicate that the column is a character.
+ call add(l:output, {
+ \ 'bufnr': a:buffer,
+ \ 'lnum': l:match[1] + 0,
+ \ 'vcol': 0,
+ \ 'col': 0,
+ \ 'text': l:match[2],
+ \ 'type': 'W',
+ \ 'nr': -1,
+ \})
+ endfor
+
+ return l:output
+endfunction
+
+call ale#linter#Define('php', {
+\ 'name': 'phpmd',
+\ 'executable': 'phpmd',
+\ 'command': g:ale#util#stdin_wrapper . ' .php phpmd %s text ' . g:ale_php_phpmd_ruleset . ' --ignore-violations-on-exit',
+\ 'callback': 'ale_linters#php#phpmd#Handle',
+\})
diff --git a/ale_linters/python/flake8.vim b/ale_linters/python/flake8.vim
index 42b8ca78..bd136b27 100644
--- a/ale_linters/python/flake8.vim
+++ b/ale_linters/python/flake8.vim
@@ -7,18 +7,70 @@ let g:ale_python_flake8_executable =
let g:ale_python_flake8_args =
\ get(g:, 'ale_python_flake8_args', '')
+" A map from Python executable paths to semver strings parsed for those
+" executables, so we don't have to look up the version number constantly.
+let s:version_cache = {}
+
function! ale_linters#python#flake8#GetExecutable(buffer) abort
return g:ale_python_flake8_executable
endfunction
-function! ale_linters#python#flake8#GetCommand(buffer) abort
+function! ale_linters#python#flake8#VersionCheck(buffer) abort
+ let l:executable = ale_linters#python#flake8#GetExecutable(a:buffer)
+
+ " If we have previously stored the version number in a cache, then
+ " don't look it up again.
+ if has_key(s:version_cache, l:executable)
+ " Returning an empty string skips this command.
+ return ''
+ endif
+
+ return ale_linters#python#flake8#GetExecutable(a:buffer) . ' --version'
+endfunction
+
+" Get the flake8 version from the output, or the cache.
+function! s:GetVersion(buffer, version_output) abort
+ let l:executable = ale_linters#python#flake8#GetExecutable(a:buffer)
+ let l:version = []
+
+ " Get the version from the cache.
+ if has_key(s:version_cache, l:executable)
+ return s:version_cache[l:executable]
+ endif
+
+ if !empty(a:version_output)
+ " Parse the version string, and store it in the cache.
+ let l:version = ale#semver#Parse(a:version_output[0])
+ let s:version_cache[l:executable] = l:version
+ endif
+
+ return l:version
+endfunction
+
+" flake8 versions 3 and up support the --stdin-display-name argument.
+function! s:SupportsDisplayName(version) abort
+ return !empty(a:version) && ale#semver#GreaterOrEqual(a:version, [3, 0, 0])
+endfunction
+
+function! ale_linters#python#flake8#GetCommand(buffer, version_output) abort
+ let l:version = s:GetVersion(a:buffer, a:version_output)
+
+ " Only include the --stdin-display-name argument if we can parse the
+ " flake8 version, and it is recent enough to support it.
+ let l:display_name_args = s:SupportsDisplayName(l:version)
+ \ ? '--stdin-display-name %s'
+ \ : ''
+
return ale_linters#python#flake8#GetExecutable(a:buffer)
- \ . ' ' . g:ale_python_flake8_args . ' --stdin-display-name %s -'
+ \ . ' ' . g:ale_python_flake8_args . ' ' . l:display_name_args . ' -'
endfunction
call ale#linter#Define('python', {
\ 'name': 'flake8',
\ 'executable_callback': 'ale_linters#python#flake8#GetExecutable',
-\ 'command_callback': 'ale_linters#python#flake8#GetCommand',
+\ 'command_chain': [
+\ {'callback': 'ale_linters#python#flake8#VersionCheck'},
+\ {'callback': 'ale_linters#python#flake8#GetCommand'},
+\ ],
\ 'callback': 'ale#handlers#HandlePEP8Format',
\})
diff --git a/ale_linters/ruby/rubocop.vim b/ale_linters/ruby/rubocop.vim
index 69d26d33..87fc7b79 100644
--- a/ale_linters/ruby/rubocop.vim
+++ b/ale_linters/ruby/rubocop.vim
@@ -4,9 +4,9 @@
function! ale_linters#ruby#rubocop#Handle(buffer, lines) abort
" Matches patterns line the following:
"
- " <path>/_:47:14: 83:29: C: Prefer single-quoted strings when you don't
+ " <path>:83:29: C: Prefer single-quoted strings when you don't
" need string interpolation or special symbols.
- let l:pattern = '\v_:(\d+):(\d+): (.): (.+)'
+ let l:pattern = '\v:(\d+):(\d+): (.): (.+)'
let l:output = []
for l:line in a:lines
@@ -34,6 +34,12 @@ function! ale_linters#ruby#rubocop#Handle(buffer, lines) abort
return l:output
endfunction
+function! ale_linters#ruby#rubocop#GetCommand(buffer) abort
+ return 'rubocop --format emacs --force-exclusion ' .
+ \ g:ale_ruby_rubocop_options .
+ \ ' --stdin ' . bufname(a:buffer)
+endfunction
+
" Set this option to change Rubocop options.
if !exists('g:ale_ruby_rubocop_options')
" let g:ale_ruby_rubocop_options = '--lint'
@@ -43,8 +49,6 @@ endif
call ale#linter#Define('ruby', {
\ 'name': 'rubocop',
\ 'executable': 'rubocop',
-\ 'command': 'rubocop --format emacs --force-exclusion --stdin '
-\ . g:ale_ruby_rubocop_options
-\ . ' %s',
+\ 'command_callback': 'ale_linters#ruby#rubocop#GetCommand',
\ 'callback': 'ale_linters#ruby#rubocop#Handle',
\})
diff --git a/ale_linters/rust/cargo.vim b/ale_linters/rust/cargo.vim
index 738821d7..7f821d2c 100644
--- a/ale_linters/rust/cargo.vim
+++ b/ale_linters/rust/cargo.vim
@@ -1,7 +1,6 @@
" Author: Daniel Schemala <istjanichtzufassen@gmail.com>
" Description: rustc invoked by cargo for rust files
-
function! ale_linters#rust#cargo#GetCargoExecutable(bufnr) abort
if ale#util#FindNearestFile(a:bufnr, 'Cargo.toml') !=# ''
return 'cargo'
@@ -15,7 +14,7 @@ endfunction
call ale#linter#Define('rust', {
\ 'name': 'cargo',
\ 'executable_callback': 'ale_linters#rust#cargo#GetCargoExecutable',
-\ 'command': 'cargo rustc -- --error-format=json -Z no-trans',
-\ 'callback': 'ale_linters#rust#rustc#HandleRustcErrors',
-\ 'output_stream': 'stderr',
+\ 'command': 'cargo build --message-format=json -q',
+\ 'callback': 'ale#handlers#rust#HandleRustErrors',
+\ 'output_stream': 'stdout',
\})
diff --git a/ale_linters/rust/rustc.vim b/ale_linters/rust/rustc.vim
index 3eeede66..1d080b98 100644
--- a/ale_linters/rust/rustc.vim
+++ b/ale_linters/rust/rustc.vim
@@ -1,77 +1,6 @@
" Author: Daniel Schemala <istjanichtzufassen@gmail.com>
" Description: rustc for rust files
-if !exists('g:ale_rust_ignore_error_codes')
- let g:ale_rust_ignore_error_codes = []
-endif
-
-
-function! ale_linters#rust#rustc#HandleRustcErrors(buffer_number, errorlines) abort
- let l:file_name = fnamemodify(bufname(a:buffer_number), ':t')
- let l:output = []
-
- for l:errorline in a:errorlines
- " ignore everything that is not Json
- if l:errorline !~# '^{'
- continue
- endif
-
- let l:error = json_decode(l:errorline)
-
- if !empty(l:error.code) && index(g:ale_rust_ignore_error_codes, l:error.code.code) > -1
- continue
- endif
-
- for l:span in l:error.spans
- if l:span.is_primary &&
- \ (l:span.file_name ==# l:file_name || l:span.file_name ==# '<anon>')
- call add(l:output, {
- \ 'bufnr': a:buffer_number,
- \ 'lnum': l:span.line_start,
- \ 'vcol': 0,
- \ 'col': l:span.byte_start,
- \ 'nr': -1,
- \ 'text': l:error.message,
- \ 'type': toupper(l:error.level[0]),
- \})
- else
- " when the error is caused in the expansion of a macro, we have
- " to bury deeper
- let l:root_cause = s:FindErrorInExpansion(l:span, l:file_name)
-
- if !empty(l:root_cause)
- call add(l:output, {
- \ 'bufnr': a:buffer_number,
- \ 'lnum': l:root_cause[0],
- \ 'vcol': 0,
- \ 'col': l:root_cause[1],
- \ 'nr': -1,
- \ 'text': l:error.message,
- \ 'type': toupper(l:error.level[0]),
- \})
- endif
- endif
- endfor
- endfor
-
- return l:output
-endfunction
-
-
-" returns: a list [lnum, col] with the location of the error or []
-function! s:FindErrorInExpansion(span, file_name) abort
- if a:span.file_name ==# a:file_name
- return [a:span.line_start, a:span.byte_start]
- endif
-
- if !empty(a:span.expansion)
- return s:FindErrorInExpansion(a:span.expansion.span, a:file_name)
- endif
-
- return []
-endfunction
-
-
function! ale_linters#rust#rustc#RustcCommand(buffer_number) abort
" Try to guess the library search path. If the project is managed by cargo,
" it's usually <project root>/target/debug/deps/ or
@@ -89,11 +18,10 @@ function! ale_linters#rust#rustc#RustcCommand(buffer_number) abort
return 'rustc --error-format=json -Z no-trans ' . l:dependencies . ' -'
endfunction
-
call ale#linter#Define('rust', {
\ 'name': 'rustc',
\ 'executable': 'rustc',
\ 'command_callback': 'ale_linters#rust#rustc#RustcCommand',
-\ 'callback': 'ale_linters#rust#rustc#HandleRustcErrors',
+\ 'callback': 'ale#handlers#rust#HandleRustErrors',
\ 'output_stream': 'stderr',
\})
diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim
index 25be0457..34e5ad95 100644
--- a/autoload/ale/engine.vim
+++ b/autoload/ale/engine.vim
@@ -166,24 +166,28 @@ function! s:HandleExit(job) abort
let g:ale_buffer_info[l:buffer].loclist = g:ale_buffer_info[l:buffer].new_loclist
let g:ale_buffer_info[l:buffer].new_loclist = []
+ call ale#engine#SetResults(l:buffer, g:ale_buffer_info[l:buffer].loclist)
+
+ " Call user autocommands. This allows users to hook into ALE's lint cycle.
+ silent doautocmd User ALELint
+
+ " Mark line 200, column 17 with a squiggly line or something
+ " matchadd('ALEError', '\%200l\%17v')
+endfunction
+
+function! ale#engine#SetResults(buffer, loclist) abort
if g:ale_set_quickfix || g:ale_set_loclist
- call ale#list#SetLists(g:ale_buffer_info[l:buffer].loclist)
+ call ale#list#SetLists(a:loclist)
endif
if g:ale_set_signs
- call ale#sign#SetSigns(l:buffer, g:ale_buffer_info[l:buffer].loclist)
+ call ale#sign#SetSigns(a:buffer, a:loclist)
endif
if exists('*ale#statusline#Update')
" Don't load/run if not already loaded.
- call ale#statusline#Update(l:buffer, g:ale_buffer_info[l:buffer].loclist)
+ call ale#statusline#Update(a:buffer, a:loclist)
endif
-
- " Call user autocommands. This allows users to hook into ALE's lint cycle.
- silent doautocmd User ALELint
-
- " Mark line 200, column 17 with a squiggly line or something
- " matchadd('ALEError', '\%200l\%17v')
endfunction
function! s:HandleExitNeoVim(job, data, event) abort
@@ -215,6 +219,7 @@ function! s:RunJob(command, generic_job_options) abort
let l:linter = a:generic_job_options.linter
let l:output_stream = a:generic_job_options.output_stream
let l:next_chain_index = a:generic_job_options.next_chain_index
+ let l:read_buffer = a:generic_job_options.read_buffer
let l:command = a:command
if l:command =~# '%s'
@@ -270,10 +275,12 @@ function! s:RunJob(command, generic_job_options) abort
" Execute the command with the shell, to fix escaping issues.
let l:command = split(&shell) + split(&shellcmdflag) + [l:command]
- " On Unix machines, we can send the Vim buffer directly.
- " This is faster than reading the lines ourselves.
- let l:job_options.in_io = 'buffer'
- let l:job_options.in_buf = l:buffer
+ if l:read_buffer
+ " On Unix machines, we can send the Vim buffer directly.
+ " This is faster than reading the lines ourselves.
+ let l:job_options.in_io = 'buffer'
+ let l:job_options.in_buf = l:buffer
+ endif
endif
" Vim 8 will read the stdin from the file's buffer.
@@ -293,20 +300,22 @@ function! s:RunJob(command, generic_job_options) abort
\ 'next_chain_index': l:next_chain_index,
\}
- if has('nvim')
- " In NeoVim, we have to send the buffer lines ourselves.
- let l:input = join(getbufline(l:buffer, 1, '$'), "\n") . "\n"
-
- call jobsend(l:job, l:input)
- call jobclose(l:job, 'stdin')
- elseif has('win32')
- " On some Vim versions, we have to send the buffer data ourselves.
- let l:input = join(getbufline(l:buffer, 1, '$'), "\n") . "\n"
- let l:channel = job_getchannel(l:job)
-
- if ch_status(l:channel) ==# 'open'
- call ch_sendraw(l:channel, l:input)
- call ch_close_in(l:channel)
+ if l:read_buffer
+ if has('nvim')
+ " In NeoVim, we have to send the buffer lines ourselves.
+ let l:input = join(getbufline(l:buffer, 1, '$'), "\n") . "\n"
+
+ call jobsend(l:job, l:input)
+ call jobclose(l:job, 'stdin')
+ elseif has('win32')
+ " On some Vim versions, we have to send the buffer data ourselves.
+ let l:input = join(getbufline(l:buffer, 1, '$'), "\n") . "\n"
+ let l:channel = job_getchannel(l:job)
+
+ if ch_status(l:channel) ==# 'open'
+ call ch_sendraw(l:channel, l:input)
+ call ch_close_in(l:channel)
+ endif
endif
endif
endif
@@ -364,11 +373,14 @@ function! s:InvokeChain(buffer, linter, chain_index, input) abort
let l:command = a:linter.command
endif
+ let l:is_last_job = l:chain_index >= len(get(a:linter, 'command_chain', [])) - 1
+
call s:RunJob(l:command, {
\ 'buffer': a:buffer,
\ 'linter': a:linter,
\ 'output_stream': l:output_stream,
\ 'next_chain_index': l:chain_index + 1,
+ \ 'read_buffer': l:is_last_job,
\})
endfunction
diff --git a/autoload/ale/handlers/rust.vim b/autoload/ale/handlers/rust.vim
new file mode 100644
index 00000000..c00c2276
--- /dev/null
+++ b/autoload/ale/handlers/rust.vim
@@ -0,0 +1,90 @@
+" Author: Daniel Schemala <istjanichtzufassen@gmail.com>,
+" w0rp <devw0rp@gmail.com>
+"
+" Description: This file implements handlers specific to Rust.
+
+if !exists('g:ale_rust_ignore_error_codes')
+ let g:ale_rust_ignore_error_codes = []
+endif
+
+" returns: a list [lnum, col] with the location of the error or []
+function! s:FindErrorInExpansion(span, file_name) abort
+ if a:span.file_name ==# a:file_name
+ return [a:span.line_start, a:span.byte_start]
+ endif
+
+ if !empty(a:span.expansion)
+ return s:FindErrorInExpansion(a:span.expansion.span, a:file_name)
+ endif
+
+ return []
+endfunction
+
+" A handler function which accepts a file name, to make unit testing easier.
+function! ale#handlers#rust#HandleRustErrorsForFile(buffer, full_filename, lines) abort
+ let l:filename = fnamemodify(a:full_filename, ':t')
+ let l:output = []
+
+ for l:errorline in a:lines
+ " ignore everything that is not Json
+ if l:errorline !~# '^{'
+ continue
+ endif
+
+ let l:error = json_decode(l:errorline)
+
+ if has_key(l:error, 'message') && type(l:error.message) == type({})
+ let l:error = l:error.message
+ endif
+
+ if !has_key(l:error, 'code')
+ continue
+ endif
+
+ if !empty(l:error.code) && index(g:ale_rust_ignore_error_codes, l:error.code.code) > -1
+ continue
+ endif
+
+ for l:span in l:error.spans
+ let l:span_filename = fnamemodify(l:span.file_name, ':t')
+
+ if (
+ \ l:span.is_primary
+ \ && (l:span_filename ==# l:filename || l:span_filename ==# '<anon>')
+ \)
+ call add(l:output, {
+ \ 'bufnr': a:buffer,
+ \ 'lnum': l:span.line_start,
+ \ 'vcol': 0,
+ \ 'col': l:span.byte_start,
+ \ 'nr': -1,
+ \ 'text': l:error.message,
+ \ 'type': toupper(l:error.level[0]),
+ \})
+ else
+ " when the error is caused in the expansion of a macro, we have
+ " to bury deeper
+ let l:root_cause = s:FindErrorInExpansion(l:span, l:filename)
+
+ if !empty(l:root_cause)
+ call add(l:output, {
+ \ 'bufnr': a:buffer,
+ \ 'lnum': l:root_cause[0],
+ \ 'vcol': 0,
+ \ 'col': l:root_cause[1],
+ \ 'nr': -1,
+ \ 'text': l:error.message,
+ \ 'type': toupper(l:error.level[0]),
+ \})
+ endif
+ endif
+ endfor
+ endfor
+
+ return l:output
+endfunction
+
+" A handler for output for Rust linters.
+function! ale#handlers#rust#HandleRustErrors(buffer, lines) abort
+ return ale#handlers#rust#HandleRustErrorsForFile(a:buffer, bufname(a:buffer), a:lines)
+endfunction
diff --git a/autoload/ale/loclist_jumping.vim b/autoload/ale/loclist_jumping.vim
index b9ea9d13..b4d1993f 100644
--- a/autoload/ale/loclist_jumping.vim
+++ b/autoload/ale/loclist_jumping.vim
@@ -1,11 +1,23 @@
" Author: w0rp <devw0rp@gmail.com>
" Description: This file implements functions for jumping around in a file
-" based on errors and warnings in the loclist.
+" based on errors and warnings in the loclist or quickfix list.
+
+function! s:GetCurrentList() abort
+ if g:ale_set_loclist
+ return getloclist(winnr())
+ elseif g:ale_set_quickfix
+ let l:buffer = bufnr('%')
+
+ return filter(getqflist(), 'get(v:val, ''bufnr'', -1) == ' . l:buffer)
+ endif
+
+ return []
+endfunction
function! s:GetSortedLoclist() abort
let l:loclist = []
- for l:item in getloclist(winnr())
+ for l:item in s:GetCurrentList()
if l:item.lnum < 1
" Remove items we can't even jump to.
continue
diff --git a/autoload/ale/semver.vim b/autoload/ale/semver.vim
new file mode 100644
index 00000000..b153dd1d
--- /dev/null
+++ b/autoload/ale/semver.vim
@@ -0,0 +1,29 @@
+" Given some text, parse a semantic versioning string from the text
+" into a triple of integeers [major, minor, patch].
+"
+" If no match can be performed, then an empty List will be returned instead.
+function! ale#semver#Parse(text) abort
+ let l:match = matchlist(a:text, '^ *\(\d\+\)\.\(\d\+\)\.\(\d\+\)')
+
+ if empty(l:match)
+ return []
+ endif
+
+ return [l:match[1] + 0, l:match[2] + 0, l:match[3] + 0]
+endfunction
+
+" Given two triples of integers [major, minor, patch], compare the triples
+" and return 1 if the lhs is greater than or equal to the rhs.
+function! ale#semver#GreaterOrEqual(lhs, rhs) abort
+ if a:lhs[0] > a:rhs[0]
+ return 1
+ elseif a:lhs[0] == a:rhs[0]
+ if a:lhs[1] > a:rhs[1]
+ return 1
+ elseif a:lhs[1] == a:rhs[1]
+ return a:lhs[2] >= a:rhs[2]
+ endif
+ endif
+
+ return 0
+endfunction
diff --git a/autoload/ale/statusline.vim b/autoload/ale/statusline.vim
index c01dd34d..7269ddca 100644
--- a/autoload/ale/statusline.vim
+++ b/autoload/ale/statusline.vim
@@ -3,6 +3,10 @@
" Update the buffer error/warning count with data from loclist.
function! ale#statusline#Update(buffer, loclist) abort
+ if !has_key(g:ale_buffer_info, a:buffer)
+ return
+ endif
+
let l:errors = 0
let l:warnings = 0
diff --git a/autoload/ale/util.vim b/autoload/ale/util.vim
index 27517ef7..6c6f20b0 100644
--- a/autoload/ale/util.vim
+++ b/autoload/ale/util.vim
@@ -8,7 +8,7 @@ function! s:FindWrapperScript() abort
if filereadable(l:path)
if has('win32')
- return l:path . '.bat'
+ return l:path . '.exe'
endif
return l:path
diff --git a/doc/ale.txt b/doc/ale.txt
index 4bed2e02..348be809 100644
--- a/doc/ale.txt
+++ b/doc/ale.txt
@@ -35,6 +35,8 @@ CONTENTS *ale-contents*
4.23. python-mypy.....................|ale-linter-options-python-mypy|
4.24. python-pylint...................|ale-linter-options-python-pylint|
4.25. erlang..........................|ale-linter-options-erlang|
+ 4.26. phpmd...........................|ale-linter-options-phpmd|
+ 4.27. xo..............................|ale-linter-options-xo|
5. Linter Integration Notes.............|ale-linter-integration|
5.1. merlin..........................|ale-linter-integration-ocaml-merlin|
5.2. rust.............................|ale-integration-rust|
@@ -85,7 +87,7 @@ The following languages and tools are supported.
* Go: 'gofmt -e', 'go vet', 'golint', 'go build'
* Haskell: 'ghc', 'hlint'
* HTML: 'HTMLHint', 'tidy'
-* JavaScript: 'eslint', 'jscs', 'jshint', 'flow'
+* JavaScript: 'eslint', 'jscs', 'jshint', 'flow', 'xo'
* JSON: 'jsonlint'
* LaTeX: 'chktex', 'lacheck'
* Lua: 'luacheck'
@@ -93,7 +95,7 @@ The following languages and tools are supported.
* MATLAB: 'mlint'
* OCaml: 'merlin' (see |ale-linter-integration-ocaml-merlin|)
* Perl: 'perl' (-c flag), 'perlcritic'
-* PHP: 'hack', 'php' (-l flag), 'phpcs'
+* PHP: 'hack', 'php' (-l flag), 'phpcs', 'phpmd'
* Pug: 'pug-lint'
* Puppet: 'puppet', 'puppet-lint'
* Python: 'flake8', 'mypy', 'pylint'
@@ -880,6 +882,52 @@ g:ale_erlang_erlc_options *g:ale_erlang_erlc_options*
This variable controls additional parameters passed to `erlc`, such as `-I`
or `-pa`.
+------------------------------------------------------------------------------
+4.26. phpmd *ale-linter-options-phpmd*
+
+g:ale_php_phpmd_ruleset *g:ale_php_phpmd_ruleset*
+
+ Type: |String|
+ Default: 'cleancode,codesize,controversial,design,naming,unusedcode'
+
+ This variable controls the ruleset used by phpmd. Default is to use all of
+ the available phpmd rulesets
+
+------------------------------------------------------------------------------
+4.27. xo *ale-linter-options-xo*
+
+g:ale_javascript_xo_executable *g:ale_javascript_xo_executable*
+
+ Type: |String|
+ Default: `'xo'`
+
+ ALE will first discover the xo path in an ancestor node_modules
+ directory. If no such path exists, this variable will be used instead.
+
+ This variable can be set to change the path to xo.
+
+ If you wish to use only a globally installed version of xo, set
+ |g:ale_javascript_xo_use_global| to `1`.
+
+
+g:ale_javascript_xo_options *g:ale_javascript_xo_options*
+
+ Type: |String|
+ Default: `''`
+
+ This variable can be set to pass additional options to xo.
+
+
+g:ale_javascript_xo_use_global *g:ale_javascript_xo_use_global*
+
+ Type: |String|
+ Default: `0`
+
+ This variable controls whether or not ALE will search for a local path for
+ xo first. If this variable is set to `1`, then ALE will always use the
+ global version of xo, in preference to locally installed versions of
+ xo in node_modules.
+
===============================================================================
5. Linter Integration Notes *ale-linter-integration*
@@ -1005,15 +1053,40 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()*
check, and will produce the lines of output given to
the `callback`.
- Either this or the `command_callback` argument must
- be provided.
-
`command_callback` A |String| or |Funcref| for a callback function
accepting a buffer number. A |String| should be
returned for a command to run. This can be used in
place of `command` when more complicated processing
is needed.
+ `command_chain` A |List| of |Dictionary| items defining a series
+ of commands to be run. At least one |Dictionary|
+ should be provided. Each Dictionary must contain the
+ key `callback`, defining a |String| or |Funcref| for
+ a function returning a |String| for a command to run.
+
+ The callback functions for each command after the
+ first command in in the chain should accept two
+ arguments `(buffer, output)`, a buffer number and a
+ |List| of lines of output from the previous command
+ in the chain.
+
+ The first callback function in a chain accepts only
+ a `(buffer)` argument, as there are no previous
+ commands to run which return `output`.
+
+ Commands in the chain will all use the
+ `output_stream` value provided in the root
+ |Dictionary|. Each command in the chain can also
+ provide an `output_stream` key to override this value.
+ See the `output_stream` description for more
+ information.
+
+ The Vim buffer being checked for linter will only
+ be sent to the final command in the chain. Previous
+ commands in the chain will receive no input from
+ stdin.
+
`output_stream` A |String| for the output stream the lines of output
should be read from for the command which is run. The
accepted values are `'stdout'`, `'stderr'`, and
@@ -1023,6 +1096,12 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()*
instead of stdout. The option `'both'` will read
from both stder and stdout at the same time.
+ Only one of `command`, `command_callback`, or `command_chain` should be
+ specified. `command_callback` is generally recommended when a command string
+ needs to be generated dynamically, or any global options are used.
+ `command_chain` is recommended where any system calls need to be made to
+ retrieve some kind of information before running the final command.
+
Some programs for checking for errors are not capable of receiving input
from stdin, as is required by ALE. To remedy this, a wrapper script is
provided named in the variable |g:ale#util#stdin_wrapper|. This variable
diff --git a/plugin/ale.vim b/plugin/ale.vim
index 5c319870..780d37e0 100644
--- a/plugin/ale.vim
+++ b/plugin/ale.vim
@@ -49,30 +49,16 @@ let g:ale_lint_delay = get(g:, 'ale_lint_delay', 200)
" This flag can be set to 0 to disable linting when text is changed.
let g:ale_lint_on_text_changed = get(g:, 'ale_lint_on_text_changed', 1)
-if g:ale_lint_on_text_changed
- augroup ALERunOnTextChangedGroup
- autocmd!
- autocmd TextChanged,TextChangedI * call ale#Queue(g:ale_lint_delay)
- augroup END
-endif
" This flag can be set to 0 to disable linting when the buffer is entered.
let g:ale_lint_on_enter = get(g:, 'ale_lint_on_enter', 1)
-if g:ale_lint_on_enter
- augroup ALERunOnEnterGroup
- autocmd!
- autocmd BufEnter,BufRead * call ale#Queue(300)
- augroup END
-endif
" This flag can be set to 1 to enable linting when a buffer is written.
let g:ale_lint_on_save = get(g:, 'ale_lint_on_save', 0)
-if g:ale_lint_on_save
- augroup ALERunOnSaveGroup
- autocmd!
- autocmd BufWrite * call ale#Queue(0)
- augroup END
-endif
+
+" This flag may be set to 0 to disable ale. After ale is loaded, :ALEToggle
+" should be used instead.
+let g:ale_enabled = get(g:, 'ale_enabled', 1)
" These flags dictates if ale uses the quickfix or the loclist (loclist is the
" default, quickfix overrides loclist).
@@ -112,12 +98,6 @@ let g:ale_echo_msg_warning_str = get(g:, 'ale_echo_msg_warning_str', 'Warning')
" This flag can be set to 0 to disable echoing when the cursor moves.
let g:ale_echo_cursor = get(g:, 'ale_echo_cursor', 1)
-if g:ale_echo_cursor
- augroup ALECursorGroup
- autocmd!
- autocmd CursorMoved,CursorHold * call ale#cursor#EchoCursorWarningWithDelay()
- augroup END
-endif
" String format for statusline
" Its a list where:
@@ -132,12 +112,64 @@ let g:ale_statusline_format = get(g:, 'ale_statusline_format',
let g:ale_warn_about_trailing_whitespace =
\ get(g:, 'ale_warn_about_trailing_whitespace', 1)
+function! s:ALEInitAuGroups() abort
+ augroup ALERunOnTextChangedGroup
+ autocmd!
+ if g:ale_enabled && g:ale_lint_on_text_changed
+ autocmd TextChanged,TextChangedI * call ale#Queue(g:ale_lint_delay)
+ endif
+ augroup END
+
+ augroup ALERunOnEnterGroup
+ autocmd!
+ if g:ale_enabled && g:ale_lint_on_enter
+ autocmd BufEnter,BufRead * call ale#Queue(300)
+ endif
+ augroup END
+
+ augroup ALERunOnSaveGroup
+ autocmd!
+ if g:ale_enabled && g:ale_lint_on_save
+ autocmd BufWrite * call ale#Queue(0)
+ endif
+ augroup END
+
+ augroup ALECursorGroup
+ autocmd!
+ if g:ale_enabled && g:ale_echo_cursor
+ autocmd CursorMoved,CursorHold * call ale#cursor#EchoCursorWarningWithDelay()
+ endif
+ augroup END
+endfunction
+
+function! s:ALEToggle() abort
+ let g:ale_enabled = !get(g:, 'ale_enabled')
+
+ if g:ale_enabled
+ " Lint immediately
+ call ale#Queue(0)
+ else
+ for l:buffer in keys(g:ale_buffer_info)
+ " Stop jobs and delete stored buffer data
+ call ale#cleanup#Buffer(l:buffer)
+ " Clear signs, loclist, quicklist
+ call ale#engine#SetResults(l:buffer, [])
+ endfor
+ endif
+
+ call s:ALEInitAuGroups()
+endfunction
+
+call s:ALEInitAuGroups()
+
" Define commands for moving through warnings and errors.
command! ALEPrevious :call ale#loclist_jumping#Jump('before', 0)
command! ALEPreviousWrap :call ale#loclist_jumping#Jump('before', 1)
command! ALENext :call ale#loclist_jumping#Jump('after', 0)
command! ALENextWrap :call ale#loclist_jumping#Jump('after', 1)
+command! ALEToggle :call s:ALEToggle()
+
" Define command to get information about current filetype.
command! ALEInfo :call ale#linter#Info()
@@ -146,6 +178,7 @@ nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<Return>
nnoremap <silent> <Plug>(ale_previous_wrap) :ALEPreviousWrap<Return>
nnoremap <silent> <Plug>(ale_next) :ALENext<Return>
nnoremap <silent> <Plug>(ale_next_wrap) :ALENextWrap<Return>
+nnoremap <silent> <Plug>(ale_toggle) :ALEToggle<Return>
" Housekeeping
diff --git a/stdin-wrapper.bat b/stdin-wrapper.bat
deleted file mode 100644
index eca8d717..00000000
--- a/stdin-wrapper.bat
+++ /dev/null
@@ -1,22 +0,0 @@
-@echo off
-
-REM Get a unique directory name in the temporary directory
-:loop
-set "directory=%tmp%\ale_%RANDOM%"
-if exist "%directory%" goto :loop
-
-REM Use a filename with the same file extension
-mkdir "%directory%"
-set filename="%directory%\file%1"
-
-REM Get all arguments after the first to run as a command
-for /f "tokens=1,* delims= " %%a in ("%*") do set command_args=%%b
-
-REM Read all stdin data to the filename
-more > "%filename%"
-
-REM Run the command on the file
-%command_args% "%filename%"
-
-REM Delete the temporary directory
-rmdir "%directory%" /s /q
diff --git a/stdin-wrapper.exe b/stdin-wrapper.exe
new file mode 100644
index 00000000..d79f6785
--- /dev/null
+++ b/stdin-wrapper.exe
Binary files differ
diff --git a/stdin_wrapper.d b/stdin_wrapper.d
new file mode 100644
index 00000000..8714bc2a
--- /dev/null
+++ b/stdin_wrapper.d
@@ -0,0 +1,84 @@
+// Author: w0rp <devw0rp@gmail.com>
+// Description: This file provides a D program for implementing
+// the stdin-wrapper on Windows.
+
+import std.algorithm;
+import std.array;
+import std.file;
+import std.process;
+import std.stdio;
+import std.path;
+
+@safe
+auto createTemporaryFilename(string fileExtension) {
+ import std.uuid;
+
+ string filename;
+
+ do {
+ const randomPart = randomUUID().toString.replace("-", "_");
+
+ filename = buildPath(tempDir(), "ale_" ~ randomPart ~ fileExtension);
+ } while (exists(filename));
+
+ return filename;
+}
+
+@trusted
+void readStdinToFile(ref File tempFile) {
+ stdin.byChunk(4096).copy(tempFile.lockingTextWriter());
+}
+
+// Expand program names like "csslint" to "csslint.cmd"
+// D is not able to perform this kind of expanstion in spawnProcess
+@safe
+string expandedProgramName(string name) {
+ auto extArray = environment["PATHEXT"].split(";");
+
+ foreach(pathDir; environment["PATH"].split(";")) {
+ foreach(extension; extArray) {
+ const candidate = buildPath(pathDir, name ~ extension);
+
+ if (exists(candidate)) {
+ return candidate;
+ }
+ }
+ }
+
+ // We were given a full path for a program name, so use that.
+ if (exists(name)) {
+ return name;
+ }
+
+ return "";
+}
+
+@trusted
+int runLinterProgram(string[] args) {
+ const expandedName = expandedProgramName(args[0]);
+
+ writeln(expandedName);
+
+ if (expandedName) {
+ return wait(spawnProcess([expandedName] ~ args[1..$]));
+ }
+
+ return 1;
+}
+
+@safe
+int main(string[] args) {
+ const filename = createTemporaryFilename(args[1]);
+
+ auto tempFile = File(filename, "w");
+
+ scope(exit) {
+ tempFile.close();
+ remove(filename);
+ }
+
+ readStdinToFile(tempFile);
+ tempFile.close();
+
+ return runLinterProgram(args[2..$] ~ [filename]);
+}
diff --git a/test/test_rust_handler.vader b/test/test_rust_handler.vader
new file mode 100644
index 00000000..d4d54d37
--- /dev/null
+++ b/test/test_rust_handler.vader
@@ -0,0 +1,28 @@
+Execute(The Rust handler should handle rustc output):
+ AssertEqual
+ \ [
+ \ {'lnum': 15, 'bufnr': 347, 'vcol': 0, 'nr': -1, 'type': 'E', 'col': 418, 'text': 'expected one of `.`, `;`, `?`, `}`, or an operator, found `for`'},
+ \ {'lnum': 13, 'bufnr': 347, 'vcol': 0, 'nr': -1, 'type': 'E', 'col': 407, 'text': 'no method named `wat` found for type `std::string::String` in the current scope'},
+ \ ],
+ \ ale#handlers#rust#HandleRustErrorsForFile(347, 'src/playpen.rs', [
+ \ '',
+ \ 'ignore this',
+ \ '{"message":"expected one of `.`, `;`, `?`, `}`, or an operator, found `for`","code":null,"level":"error","spans":[{"file_name":"<anon>","byte_start":418,"byte_end":421,"line_start":15,"line_end":15,"column_start":5,"column_end":8,"is_primary":true,"text":[{"text":" for chr in source.trim().chars() {","highlight_start":5,"highlight_end":8}],"label":null,"suggested_replacement":null,"expansion":null}],"children":[],"rendered":null}',
+ \ '{"message":"main function not found","code":null,"level":"error","spans":[],"children":[],"rendered":null}',
+ \ '{"message":"no method named `wat` found for type `std::string::String` in the current scope","code":null,"level":"error","spans":[{"file_name":"<anon>","byte_start":407,"byte_end":410,"line_start":13,"line_end":13,"column_start":7,"column_end":10,"is_primary":true,"text":[{"text":" s.wat()","highlight_start":7,"highlight_end":10}],"label":null,"suggested_replacement":null,"expansion":null}],"children":[],"rendered":null}',
+ \ '{"message":"aborting due to previous error","code":null,"level":"error","spans":[],"children":[],"rendered":null}',
+ \ ])
+
+Execute(The Rust handler should handle cargo output):
+ AssertEqual
+ \ [
+ \ {'lnum': 15, 'bufnr': 347, 'vcol': 0, 'nr': -1, 'type': 'E', 'col': 11505, 'text': 'expected one of `.`, `;`, `?`, `}`, or an operator, found `for`'},
+ \ {'lnum': 13, 'bufnr': 347, 'vcol': 0, 'nr': -1, 'type': 'E', 'col': 11494, 'text': 'no method named `wat` found for type `std::string::String` in the current scope'},
+ \ ],
+ \ ale#handlers#rust#HandleRustErrorsForFile(347, 'src/playpen.rs', [
+ \ '',
+ \ 'ignore this',
+ \ '{"message":{"children":[],"code":null,"level":"error","message":"expected one of `.`, `;`, `?`, `}`, or an operator, found `for`","rendered":null,"spans":[{"byte_end":11508,"byte_start":11505,"column_end":8,"column_start":5,"expansion":null,"file_name":"src/playpen.rs","is_primary":true,"label":null,"line_end":15,"line_start":15,"suggested_replacement":null,"text":[{"highlight_end":8,"highlight_start":5,"text":" for chr in source.trim().chars() {"}]}]},"package_id":"update 0.0.1 (path+file:///home/w0rp/Downloads/rust-by-example)","reason":"compiler-message","target":{"kind":["bin"],"name":"update","src_path":"/home/w0rp/Downloads/rust-by-example/src/main.rs"}}',
+ \ '{"message":{"children":[],"code":null,"level":"error","message":"no method named `wat` found for type `std::string::String` in the current scope","rendered":null,"spans":[{"byte_end":11497,"byte_start":11494,"column_end":10,"column_start":7,"expansion":null,"file_name":"src/playpen.rs","is_primary":true,"label":null,"line_end":13,"line_start":13,"suggested_replacement":null,"text":[{"highlight_end":10,"highlight_start":7,"text":" s.wat()"}]}]},"package_id":"update 0.0.1 (path+file:///home/w0rp/Downloads/rust-by-example)","reason":"compiler-message","target":{"kind":["bin"],"name":"update","src_path":"/home/w0rp/Downloads/rust-by-example/src/main.rs"}}',
+ \ '{"message":{"children":[],"code":null,"level":"error","message":"aborting due to previous error","rendered":null,"spans":[]},"package_id":"update 0.0.1 (path+file:///home/w0rp/Downloads/rust-by-example)","reason":"compiler-message","target":{"kind":["bin"],"name":"update","src_path":"/home/w0rp/Downloads/rust-by-example/src/main.rs"}}',
+ \ ])
diff --git a/test/test_semver_utils.vader b/test/test_semver_utils.vader
new file mode 100644
index 00000000..9730b74b
--- /dev/null
+++ b/test/test_semver_utils.vader
@@ -0,0 +1,16 @@
+Execute(ParseSemver should return the correct results):
+ " We should be able to parse the semver string from flake8
+ AssertEqual [3, 0, 4], ale#semver#Parse('3.0.4 (mccabe: 0.5.2, pyflakes: 1.2.3, pycodestyle: 2.0.0) CPython 2.7.12 on Linux')
+
+Execute(GreaterOrEqual should compare triples correctly):
+ Assert ale#semver#GreaterOrEqual([3, 0, 4], [3, 0, 0])
+ Assert ale#semver#GreaterOrEqual([3, 0, 0], [3, 0, 0])
+ Assert ale#semver#GreaterOrEqual([3, 0, 0], [2, 0, 0])
+ Assert ale#semver#GreaterOrEqual([3, 1, 0], [3, 1, 0])
+ Assert ale#semver#GreaterOrEqual([3, 2, 0], [3, 1, 0])
+ Assert ale#semver#GreaterOrEqual([3, 2, 2], [3, 1, 6])
+ Assert ale#semver#GreaterOrEqual([3, 2, 5], [3, 2, 5])
+ Assert ale#semver#GreaterOrEqual([3, 2, 6], [3, 2, 5])
+ Assert !ale#semver#GreaterOrEqual([2, 9, 1], [3, 0, 0])
+ Assert !ale#semver#GreaterOrEqual([3, 2, 3], [3, 3, 3])
+ Assert !ale#semver#GreaterOrEqual([3, 3, 2], [3, 3, 3])
diff --git a/test/test_sign_placement.vader b/test/test_sign_placement.vader
new file mode 100644
index 00000000..dbec27ef
--- /dev/null
+++ b/test/test_sign_placement.vader
@@ -0,0 +1,68 @@
+Before:
+ function! GenerateResults(buffer, output)
+ return [
+ \ {
+ \ 'lnum': 1,
+ \ 'col': 1,
+ \ 'bufnr': bufnr('%'),
+ \ 'vcol': 0,
+ \ 'nr': -1,
+ \ 'type': 'E',
+ \ 'text': 'foo',
+ \ },
+ \ {
+ \ 'lnum': 2,
+ \ 'col': 1,
+ \ 'bufnr': bufnr('%'),
+ \ 'vcol': 0,
+ \ 'nr': -1,
+ \ 'type': 'W',
+ \ 'text': 'bar',
+ \ },
+ \ {
+ \ 'lnum': 3,
+ \ 'col': 1,
+ \ 'bufnr': bufnr('%'),
+ \ 'vcol': 0,
+ \ 'nr': -1,
+ \ 'type': 'E',
+ \ 'text': 'baz',
+ \ },
+ \]
+ endfunction
+
+ call ale#linter#Define('testft', {
+ \ 'name': 'x',
+ \ 'executable': 'echo',
+ \ 'command': 'echo',
+ \ 'callback': 'GenerateResults',
+ \})
+
+After:
+ call ale#linter#Reset()
+ delfunction GenerateResults
+ unlet! g:output
+
+Given testft(A Javscript file with warnings/errors):
+ foo
+ bar
+ baz
+
+Execute:
+ call ale#Lint()
+ call ale#engine#WaitForJobs(2000)
+
+ redir => g:output
+ :sign place
+ redir END
+
+ AssertEqual
+ \ [
+ \ ['1', '1000001', 'ALEErrorSign'],
+ \ ['2', '1000002', 'ALEWarningSign'],
+ \ ['3', '1000003', 'ALEErrorSign'],
+ \ ],
+ \ map(
+ \ split(g:output, '\n')[2:],
+ \ 'matchlist(v:val, "[^=]*=\\(\\d\\+\\)[^=]*=\\(\\d\\+\\).*\\(ALE.*\\)$")[1:3]'
+ \ )