Detect and use CM files for smlnj (#884)
* Detect and use CM files for smlnj * Split into two checkers - one for CM projects - one for single SML files * Fix some typos * Fix error caught by writing tests We want to actually use `glob` to search in paths upwards from us. (Previously we were just searching in the current directory every time!) * Fix errors from former test run * Write tests for GetCmFile and GetExecutableSmlnj * Typo in 'smlnj/' fixture filenames
This commit is contained in:
parent
c7fbcb3c02
commit
63e8946fc8
11 changed files with 205 additions and 49 deletions
|
@ -1,47 +1,9 @@
|
||||||
" Author: Paulo Alem <paulo.alem@gmail.com>
|
" Author: Paulo Alem <paulo.alem@gmail.com>, Jake Zimmerman <jake@zimmerman.io>
|
||||||
" Description: Rudimentary SML checking with smlnj compiler
|
" Description: Single-file SML checking with SML/NJ compiler
|
||||||
|
|
||||||
function! ale_linters#sml#smlnj#Handle(buffer, lines) abort
|
|
||||||
" Try to match basic sml errors
|
|
||||||
|
|
||||||
let l:out = []
|
|
||||||
let l:pattern = '^.*\:\([0-9\.]\+\)\ \(\w\+\)\:\ \(.*\)'
|
|
||||||
let l:pattern2 = '^.*\:\([0-9]\+\)\.\?\([0-9]\+\).* \(\(Warning\|Error\): .*\)'
|
|
||||||
|
|
||||||
for l:line in a:lines
|
|
||||||
let l:match2 = matchlist(l:line, l:pattern2)
|
|
||||||
|
|
||||||
if len(l:match2) != 0
|
|
||||||
call add(l:out, {
|
|
||||||
\ 'bufnr': a:buffer,
|
|
||||||
\ 'lnum': l:match2[1] + 0,
|
|
||||||
\ 'col' : l:match2[2] - 1,
|
|
||||||
\ 'text': l:match2[3],
|
|
||||||
\ 'type': l:match2[3] =~# '^Warning' ? 'W' : 'E',
|
|
||||||
\})
|
|
||||||
continue
|
|
||||||
endif
|
|
||||||
|
|
||||||
let l:match = matchlist(l:line, l:pattern)
|
|
||||||
|
|
||||||
if len(l:match) != 0
|
|
||||||
call add(l:out, {
|
|
||||||
\ 'bufnr': a:buffer,
|
|
||||||
\ 'lnum': l:match[1] + 0,
|
|
||||||
\ 'text': l:match[2] . ': ' . l:match[3],
|
|
||||||
\ 'type': l:match[2] is# 'error' ? 'E' : 'W',
|
|
||||||
\})
|
|
||||||
continue
|
|
||||||
endif
|
|
||||||
|
|
||||||
endfor
|
|
||||||
|
|
||||||
return l:out
|
|
||||||
endfunction
|
|
||||||
|
|
||||||
call ale#linter#Define('sml', {
|
call ale#linter#Define('sml', {
|
||||||
\ 'name': 'smlnj',
|
\ 'name': 'smlnj',
|
||||||
\ 'executable': 'sml',
|
\ 'executable_callback': 'ale#handlers#sml#GetExecutableSmlnjFile',
|
||||||
\ 'command': 'sml',
|
\ 'command': 'sml',
|
||||||
\ 'callback': 'ale_linters#sml#smlnj#Handle',
|
\ 'callback': 'ale#handlers#sml#Handle',
|
||||||
\})
|
\})
|
||||||
|
|
25
ale_linters/sml/smlnj_cm.vim
Normal file
25
ale_linters/sml/smlnj_cm.vim
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
" Author: Jake Zimmerman <jake@zimmerman.io>
|
||||||
|
" Description: SML checking with SML/NJ Compilation Manager
|
||||||
|
|
||||||
|
" Let user manually set the CM file (in case our search for a CM file is
|
||||||
|
" ambiguous and picks the wrong one)
|
||||||
|
"
|
||||||
|
" See :help ale-sml-smlnj for more information.
|
||||||
|
call ale#Set('sml_smlnj_cm_file', '*.cm')
|
||||||
|
|
||||||
|
function! ale_linters#sml#smlnj_cm#GetCommand(buffer) abort
|
||||||
|
let l:cmfile = ale#handlers#sml#GetCmFile(a:buffer)
|
||||||
|
return 'sml -m ' . l:cmfile . ' < /dev/null'
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Using CM requires that we set "lint_file: 1", since it reads the files
|
||||||
|
" from the disk itself.
|
||||||
|
call ale#linter#Define('sml', {
|
||||||
|
\ 'name': 'smlnj-cm',
|
||||||
|
\ 'executable_callback': 'ale#handlers#sml#GetExecutableSmlnjCm',
|
||||||
|
\ 'lint_file': 1,
|
||||||
|
\ 'command_callback': 'ale_linters#sml#smlnj_cm#GetCommand',
|
||||||
|
\ 'callback': 'ale#handlers#sml#Handle',
|
||||||
|
\})
|
||||||
|
|
||||||
|
" vim:ts=4:sts=4:sw=4
|
87
autoload/ale/handlers/sml.vim
Normal file
87
autoload/ale/handlers/sml.vim
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
" Author: Jake Zimmerman <jake@zimmerman.io>
|
||||||
|
" Description: Shared functions for SML linters
|
||||||
|
|
||||||
|
function! ale#handlers#sml#GetCmFile(buffer) abort
|
||||||
|
let l:pattern = ale#Var(a:buffer, 'sml_smlnj_cm_file')
|
||||||
|
let l:as_list = 1
|
||||||
|
|
||||||
|
let l:cmfile = ''
|
||||||
|
for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h'))
|
||||||
|
let l:results = glob(l:path . '/' . l:pattern, 0, l:as_list)
|
||||||
|
if len(l:results) > 0
|
||||||
|
" If there is more than one CM file, we take the first one
|
||||||
|
" See :help ale-sml-smlnj for how to configure this.
|
||||||
|
let l:cmfile = l:results[0]
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
|
||||||
|
return l:cmfile
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Only one of smlnj or smlnj-cm can be enabled at a time.
|
||||||
|
" executable_callback is called before *every* lint attempt
|
||||||
|
function! s:GetExecutable(buffer, source) abort
|
||||||
|
if ale#handlers#sml#GetCmFile(a:buffer) is# ''
|
||||||
|
" No CM file found; only allow single-file mode to be enabled
|
||||||
|
if a:source is# 'smlnj-file'
|
||||||
|
return 'sml'
|
||||||
|
elseif a:source is# 'smlnj-cm'
|
||||||
|
return ''
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
" Found a CM file; only allow cm-file mode to be enabled
|
||||||
|
if a:source is# 'smlnj-file'
|
||||||
|
return ''
|
||||||
|
elseif a:source is# 'smlnj-cm'
|
||||||
|
return 'sml'
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! ale#handlers#sml#GetExecutableSmlnjCm(buffer) abort
|
||||||
|
return s:GetExecutable(a:buffer, 'smlnj-cm')
|
||||||
|
endfunction
|
||||||
|
function! ale#handlers#sml#GetExecutableSmlnjFile(buffer) abort
|
||||||
|
return s:GetExecutable(a:buffer, 'smlnj-file')
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! ale#handlers#sml#Handle(buffer, lines) abort
|
||||||
|
" Try to match basic sml errors
|
||||||
|
" TODO(jez) We can get better errorfmt strings from Syntastic
|
||||||
|
|
||||||
|
let l:out = []
|
||||||
|
let l:pattern = '^.*\:\([0-9\.]\+\)\ \(\w\+\)\:\ \(.*\)'
|
||||||
|
let l:pattern2 = '^.*\:\([0-9]\+\)\.\?\([0-9]\+\).* \(\(Warning\|Error\): .*\)'
|
||||||
|
|
||||||
|
for l:line in a:lines
|
||||||
|
let l:match2 = matchlist(l:line, l:pattern2)
|
||||||
|
|
||||||
|
if len(l:match2) != 0
|
||||||
|
call add(l:out, {
|
||||||
|
\ 'bufnr': a:buffer,
|
||||||
|
\ 'lnum': l:match2[1] + 0,
|
||||||
|
\ 'col' : l:match2[2] - 1,
|
||||||
|
\ 'text': l:match2[3],
|
||||||
|
\ 'type': l:match2[3] =~# '^Warning' ? 'W' : 'E',
|
||||||
|
\})
|
||||||
|
continue
|
||||||
|
endif
|
||||||
|
|
||||||
|
let l:match = matchlist(l:line, l:pattern)
|
||||||
|
|
||||||
|
if len(l:match) != 0
|
||||||
|
call add(l:out, {
|
||||||
|
\ 'bufnr': a:buffer,
|
||||||
|
\ 'lnum': l:match[1] + 0,
|
||||||
|
\ 'text': l:match[2] . ': ' . l:match[3],
|
||||||
|
\ 'type': l:match[2] is# 'error' ? 'E' : 'W',
|
||||||
|
\})
|
||||||
|
continue
|
||||||
|
endif
|
||||||
|
|
||||||
|
endfor
|
||||||
|
|
||||||
|
return l:out
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" vim:ts=4:sts=4:sw=4
|
36
doc/ale-sml.txt
Normal file
36
doc/ale-sml.txt
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
===============================================================================
|
||||||
|
ALE SML Integration *ale-sml-options*
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
smlnj *ale-sml-smlnj*
|
||||||
|
*ale-sml-smlnj-cm*
|
||||||
|
|
||||||
|
There are two SML/NJ powered checkers:
|
||||||
|
|
||||||
|
- one using Compilation Manager that works on whole projects, but requires you
|
||||||
|
to save before errors show up
|
||||||
|
- one using the SML/NJ REPL that works as you change the text, but might fail
|
||||||
|
if your project can only be built with CM.
|
||||||
|
|
||||||
|
We dynamically select which one to use based whether we find a `*.cm` file at
|
||||||
|
or above the directory of the file being checked. Only one checker (`smlnj`,
|
||||||
|
`smlnj-cm`) will be enabled at a time.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
g:ale_sml_smlnj_cm_file *g:ale_sml_smlnj_cm_file*
|
||||||
|
*b:ale_sml_smlnj_cm_file*
|
||||||
|
Type: |String|
|
||||||
|
Default: `'*.cm'`
|
||||||
|
|
||||||
|
By default, ALE will look for a `*.cm` file in your current directory,
|
||||||
|
searching upwards. It stops when it finds at least one `*.cm` file (taking
|
||||||
|
the first file if there are more than one).
|
||||||
|
|
||||||
|
Change this option (in the buffer or global scope) to control how ALE finds
|
||||||
|
CM files. For example, to always search for a CM file named `sandbox.cm`:
|
||||||
|
>
|
||||||
|
let g:ale_sml_smlnj_cm_file = 'sandbox.cm'
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl:
|
|
@ -119,6 +119,8 @@ CONTENTS *ale-contents*
|
||||||
sh....................................|ale-sh-options|
|
sh....................................|ale-sh-options|
|
||||||
shell...............................|ale-sh-shell|
|
shell...............................|ale-sh-shell|
|
||||||
shellcheck..........................|ale-sh-shellcheck|
|
shellcheck..........................|ale-sh-shellcheck|
|
||||||
|
sml...................................|ale-sml-options|
|
||||||
|
smlnj...............................|ale-sml-smlnj|
|
||||||
spec..................................|ale-spec-options|
|
spec..................................|ale-spec-options|
|
||||||
rpmlint.............................|ale-spec-rpmlint|
|
rpmlint.............................|ale-spec-rpmlint|
|
||||||
stylus................................|ale-stylus-options|
|
stylus................................|ale-stylus-options|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
Before:
|
|
||||||
runtime ale_linters/sml/smlnj.vim
|
|
||||||
|
|
||||||
Execute (Testing on EOF error):
|
Execute (Testing on EOF error):
|
||||||
AssertEqual [
|
AssertEqual [
|
||||||
\ {
|
\ {
|
||||||
|
@ -11,7 +8,7 @@ Execute (Testing on EOF error):
|
||||||
\ 'text': 'Error: syntax error found at EOF',
|
\ 'text': 'Error: syntax error found at EOF',
|
||||||
\ },
|
\ },
|
||||||
\],
|
\],
|
||||||
\ ale_linters#sml#smlnj#Handle(42, [
|
\ ale#handlers#sml#Handle(42, [
|
||||||
\ "Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]",
|
\ "Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]",
|
||||||
\ "[opening a.sml]",
|
\ "[opening a.sml]",
|
||||||
\ "a.sml:2.16 Error: syntax error found at EOF",
|
\ "a.sml:2.16 Error: syntax error found at EOF",
|
||||||
|
@ -35,7 +32,7 @@ Execute (Testing if the handler can handle multiple errors on the same line):
|
||||||
\ 'text': 'Error: unbound variable or constructor: wow',
|
\ 'text': 'Error: unbound variable or constructor: wow',
|
||||||
\ },
|
\ },
|
||||||
\],
|
\],
|
||||||
\ ale_linters#sml#smlnj#Handle(42, [
|
\ ale#handlers#sml#Handle(42, [
|
||||||
\ "Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]",
|
\ "Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]",
|
||||||
\ "[opening test.sml]",
|
\ "[opening test.sml]",
|
||||||
\ "a.sml:1.6-1.10 Error: can't find function arguments in clause",
|
\ "a.sml:1.6-1.10 Error: can't find function arguments in clause",
|
||||||
|
@ -61,7 +58,7 @@ Execute (Testing rarer errors):
|
||||||
\ 'text': "Error: value type in structure doesn't match signature spec",
|
\ 'text': "Error: value type in structure doesn't match signature spec",
|
||||||
\ },
|
\ },
|
||||||
\],
|
\],
|
||||||
\ ale_linters#sml#smlnj#Handle(42, [
|
\ ale#handlers#sml#Handle(42, [
|
||||||
\ "Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]",
|
\ "Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]",
|
||||||
\ "[opening test.sml]",
|
\ "[opening test.sml]",
|
||||||
\ "a.sml:5.19 Error: syntax error found at ID",
|
\ "a.sml:5.19 Error: syntax error found at ID",
|
||||||
|
@ -80,7 +77,7 @@ Execute (Testing a warning):
|
||||||
\ 'text': "Warning: match nonexhaustive",
|
\ 'text': "Warning: match nonexhaustive",
|
||||||
\ },
|
\ },
|
||||||
\],
|
\],
|
||||||
\ ale_linters#sml#smlnj#Handle(42, [
|
\ ale#handlers#sml#Handle(42, [
|
||||||
\ "Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]",
|
\ "Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]",
|
||||||
\ "[opening a.sml]",
|
\ "[opening a.sml]",
|
||||||
\ "a.sml:4.5-4.12 Warning: match nonexhaustive",
|
\ "a.sml:4.5-4.12 Warning: match nonexhaustive",
|
||||||
|
|
0
test/smlnj/cm/foo.sml
Normal file
0
test/smlnj/cm/foo.sml
Normal file
0
test/smlnj/cm/path/to/bar.sml
Normal file
0
test/smlnj/cm/path/to/bar.sml
Normal file
0
test/smlnj/cm/sources.cm
Normal file
0
test/smlnj/cm/sources.cm
Normal file
0
test/smlnj/file/qux.sml
Normal file
0
test/smlnj/file/qux.sml
Normal file
47
test/test_sml_command.vader
Normal file
47
test/test_sml_command.vader
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
Before:
|
||||||
|
runtime ale_linters/sml/sml.vim
|
||||||
|
runtime ale_linters/sml/smlnj.vim
|
||||||
|
call ale#test#SetDirectory('/testplugin/test')
|
||||||
|
|
||||||
|
After:
|
||||||
|
call ale#test#RestoreDirectory()
|
||||||
|
call ale#linter#Reset()
|
||||||
|
|
||||||
|
# ----- GetCmFile -----
|
||||||
|
|
||||||
|
Execute(smlnj finds CM file if it exists):
|
||||||
|
call ale#test#SetFilename('smlnj/cm/foo.sml')
|
||||||
|
|
||||||
|
AssertEqual '/testplugin/test/smlnj/cm/sources.cm', ale#handlers#sml#GetCmFile(bufnr('%'))
|
||||||
|
|
||||||
|
Execute(smlnj finds CM file by searching upwards):
|
||||||
|
call ale#test#SetFilename('smlnj/cm/path/to/bar.sml')
|
||||||
|
|
||||||
|
AssertEqual '/testplugin/test/smlnj/cm/sources.cm', ale#handlers#sml#GetCmFile(bufnr('%'))
|
||||||
|
|
||||||
|
Execute(smlnj returns '' when no CM file found):
|
||||||
|
call ale#test#SetFilename('smlnj/file/qux.sml')
|
||||||
|
|
||||||
|
AssertEqual '', ale#handlers#sml#GetCmFile(bufnr('%'))
|
||||||
|
|
||||||
|
# ----- GetExecutableSmlnjCm & GetExecutableSmlnjFile -----
|
||||||
|
|
||||||
|
Execute(CM-project mode enabled when CM file found):
|
||||||
|
call ale#test#SetFilename('smlnj/cm/foo.sml')
|
||||||
|
|
||||||
|
AssertEqual 'sml', ale#handlers#sml#GetExecutableSmlnjCm(bufnr('%'))
|
||||||
|
|
||||||
|
Execute(single-file mode disabled when CM file found):
|
||||||
|
call ale#test#SetFilename('smlnj/cm/foo.sml')
|
||||||
|
|
||||||
|
AssertEqual '', ale#handlers#sml#GetExecutableSmlnjFile(bufnr('%'))
|
||||||
|
|
||||||
|
Execute(CM-project mode disabled when CM file not found):
|
||||||
|
call ale#test#SetFilename('smlnj/file/qux.sml')
|
||||||
|
|
||||||
|
AssertEqual '', ale#handlers#sml#GetExecutableSmlnjCm(bufnr('%'))
|
||||||
|
|
||||||
|
Execute(single-file mode enabled when CM file found):
|
||||||
|
call ale#test#SetFilename('smlnj/file/qux.sml')
|
||||||
|
|
||||||
|
AssertEqual 'sml', ale#handlers#sml#GetExecutableSmlnjFile(bufnr('%'))
|
Reference in a new issue