Adding new linter (F#) to ALE
ALE is a linting engine for vim, it works like a charm and I prefer it over Syntastic because it’s asynchronous which means it doesn’t block while you’re typing. Syntastic may have support for async now but I’m still on ALE.
ALE has support for a lot of languages, the only exceptions that I faced were F# and Rust (Rust seems to work now), both can work with Syntastic but since I like ALE so much I decided to try to add a FSharpLint support to it. And it happens that was not difficult to get it working.
Yeah nice, but how to do it!?
Basically you need to call ale#linter#Define
with a file type and some
options. It works in two ways, by executing a command and using a vimscript
function to parse the output, or using LSP.
I choose using the command option because since FSharpLint was available it seemed the natural way to me. Let’s go in parts so my future self can follow
Lintint a file with FSharpLint
I can use dotnet fsharplint lint <file>
to lint a file from command line. By
the way to install fsharplint
just run dotnet tool install -g dotnet-fsharplint
.
Here is the output of the above command:
$ dotnet fsharplint lint Program.fs
========== Linting Program.fs ==========
Consider changing `path` to PascalCase.
Error on line 5 starting at column 5
type path = string
^
--------------------------------------------------------------------------------
========== Finished: 1 warnings ==========
========== Summary: 1 warnings ==========
The problem with the above command is that the output is not very parse
friendly, so looking at --help
I found --format msbuild
$ dotnet fsharplint --format msbuild lint Program.fs
========== Linting Program.fs ==========
Program.fs(5,5,5,9):FSharpLint warning FL0038: Consider changing `path` to PascalCase.
========== Finished: 1 warnings ==========
========== Summary: 1 warnings ==========
Much better. Now … the Program.fs(...
line is the matter, I just need to
parse it and return on a dictionary. But first let me introduce
ale#linter#Define(ftype, options)
.
`ale#linter#Define(ftype, options)
This function will be called, usually by a plugin, to define a linter. Here is the call that I’m using right now
call ale#linter#Define('fsharp', {
\ 'name': 'FSharpLint',
\ 'alias': ['fsharplint'],
\ 'executable': 'dotnet',
\ 'command': '%e fsharplint --format msbuild lint %t',
\ 'callback': 'Fsharp_callback',
\ 'lint_file': 1,
\ })
The first argument is the file type, you can get it by opening a file and then
running :set ft?
. Now the dictionary with the options:
name
: Is the linter name, will be used ing:ale_linters
to enable this linter.alias
: Just aliases for using instead ofname
executable
: And executable, ALE will call this to check if it’s installed or not. If not installed you will receive an error.command
: This is the command that will be called to lint the file.%e
: Will be replaced by theexacutable
name%t
: Will be replace by a temporary file created by ALE during the lint, more on this latter too.
callback
: This is a function that will be called with the output of thecommand
and should return a well formated dictionary, more on this next too.lint_file
: This is a “boolean” to inform that ALE should write file to disk and pass it as argument (the%t
) before running the command. The default behavior is to use stdio communication which is better because no disk is touched butdotnet fsharplint
has not option for it.
Now basically the main things to pay attention are the command and the callback.
Just as side note, if you’re implementing an LSP linter you must not pass
command
and use lsp
option instead, you can check more info in
ale#linters#Define
docs.
Now, with linter defined it will call the command and pass the output to the
callback but how this callback looks like and what it needs to return. It has
the signature like this function! Fsharp_callback(bufnr, lines) abort
(is just
an example). bufnr
is the vim buffer number for the file being linted, and
lines is a list with the linter output lines.
It must return a list of dics with this keys
text
: The error messagedetail
: Detailed error messagelnum
: Error line number startcol
: Error column startend_lnum
: Error line number endend_col
: Error column numberfilename
: The absolute path of the filename, you can get the absolute filename withfnamemodify(relative_filename, ':p')
type
: ‘E’ for error ‘W’ for warning
So we all we need to do is parse each line and create a dict like this, not that hard uh? Here is how I did it. Some nodes
matchlist
return a list of regex groups matches, like \1 \2 …add
is the list append of vimscript, it would be better to use map here but I’m not very good on vimscript so this was easier to Google :P
The remaining of the function should be self explanatory, I ignore first and last two lines since they are header/footer and not error messages
function! Fsharp_callback(bufnr, lines) abort
" output example: Program.fs(7,5,7,9):FSharpLint warning FL0038: Consider changing `path` to PascalCase.
let pattern = '\([^(]\+\)(\(\d\+\),\(\d\+\),\(\d\+\),\(\d\+\)):FSharpLint \(\w\+\) \(.*\)$'
let lines = a:lines[1:-2]
let result = []
for line in lines
let matches = matchlist(line, pattern)
if len(matches) >= 7
if matches[6] == 'error'
let type = 'E'
else
let type = 'W'
end
let element = {
\ 'text': matches[7],
\ 'detail': matches[7],
\ 'lnum': matches[2],
\ 'col': matches[3],
\ 'end_lnum': matches[4],
\ 'end_col': matches[5],
\ 'filename': fnamemodify(matches[1], ':p'),
\ 'type': type
\ }
call add(result, element)
endif
endfor
return result
endfun
That’s it! This was not exhaustively tested so there are bugs for sure!
Next I will try to add an LSP version of this and then open a PR for Ionide-vim
Cheers, drink water!