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:
Jake Zimmerman 2017-09-03 11:56:14 -07:00 committed by w0rp
parent c7fbcb3c02
commit 63e8946fc8
11 changed files with 205 additions and 49 deletions

View File

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

View 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

View 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
View 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:

View File

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

View File

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

View File

0
test/smlnj/cm/sources.cm Normal file
View File

0
test/smlnj/file/qux.sml Normal file
View File

View 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('%'))