Add prolog swipl linter (#1979)

* add prolog/swipl linter

* use load_files/2 instead of read_term/2

Because it also checks some semantic warnings / errors
not only syntactic warnings / errors.

e.g.:
* singleton warning
* discontiguous warning
* ...

cf. http://www.swi-prolog.org/pldoc/doc_for?object=style_check/1

* support error messages with no line number

    :- module(module_name, [pred/0]).

causes

    ERROR: Exported procedure module_name:pred/0 is not defined

* add test for prolog/swipl handler

* cosmetic fixes

* detect timeout using SIGALRM

* rename g:prolog_swipl_goals to g:prolog_swipl_load

* write doc for prolog/swipl linter

* update toc and README

* fix ignore patterns
This commit is contained in:
Takuya Fujiwara 2018-10-27 01:29:17 +09:00 committed by w0rp
parent 3d74a4f8a6
commit 34318aedf4
5 changed files with 255 additions and 0 deletions

View file

@ -165,6 +165,7 @@ formatting.
| PO | [alex](https://github.com/wooorm/alex) !!, [msgfmt](https://www.gnu.org/software/gettext/manual/html_node/msgfmt-Invocation.html), [proselint](http://proselint.com/), [write-good](https://github.com/btford/write-good) |
| Pod | [alex](https://github.com/wooorm/alex) !!, [proselint](http://proselint.com/), [write-good](https://github.com/btford/write-good) |
| Pony | [ponyc](https://github.com/ponylang/ponyc) |
| Prolog | [swipl](https://github.com/SWI-Prolog/swipl-devel) |
| proto | [protoc-gen-lint](https://github.com/ckaznocha/protoc-gen-lint) |
| Pug | [pug-lint](https://github.com/pugjs/pug-lint) |
| Puppet | [languageserver](https://github.com/lingua-pupuli/puppet-editor-services), [puppet](https://puppet.com), [puppet-lint](https://puppet-lint.com) |

View file

@ -0,0 +1,100 @@
" Author: Takuya Fujiwara <tyru.exe@gmail.com>
" Description: swipl syntax / semantic check for Prolog files
call ale#Set('prolog_swipl_executable', 'swipl')
call ale#Set('prolog_swipl_load', 'current_prolog_flag(argv, [File]), load_files(File, [sandboxed(true)]), halt.')
call ale#Set('prolog_swipl_timeout', 3)
call ale#Set('prolog_swipl_alarm', 'alarm(%t, (%h), _, [])')
call ale#Set('prolog_swipl_alarm_handler', 'writeln(user_error, "ERROR: Exceeded %t seconds, Please change g:prolog_swipl_timeout to modify the limit."), halt(1)')
function! ale_linters#prolog#swipl#GetCommand(buffer) abort
let l:goals = ale#Var(a:buffer, 'prolog_swipl_load')
let l:goals = l:goals =~# '^\s*$' ? 'halt' : l:goals
let l:timeout = ale#Var(a:buffer, 'prolog_swipl_timeout') + 0
if l:timeout > 0
let l:goals = s:GetAlarm(a:buffer, l:timeout) . ', ' . l:goals
endif
return '%e -g ' . ale#Escape(l:goals) . ' -- %s'
endfunction
function! s:GetAlarm(buffer, timeout) abort
let l:handler = ale#Var(a:buffer, 'prolog_swipl_alarm_handler')
let l:handler = s:Subst(l:handler, {'t': a:timeout})
let l:alarm = ale#Var(a:buffer, 'prolog_swipl_alarm')
let l:alarm = s:Subst(l:alarm, {'t': a:timeout, 'h': l:handler})
return l:alarm
endfunction
function! s:Subst(format, vars) abort
let l:vars = extend(copy(a:vars), {'%': '%'})
return substitute(a:format, '%\(.\)', '\=get(l:vars, submatch(1), "")', 'g')
endfunction
function! ale_linters#prolog#swipl#Handle(buffer, lines) abort
let l:pattern = '\v^(ERROR|Warning)+%(:\s*[^:]+:(\d+)%(:(\d+))?)?:\s*(.*)$'
let l:output = []
let l:i = 0
while l:i < len(a:lines)
let l:match = matchlist(a:lines[l:i], l:pattern)
if empty(l:match)
let l:i += 1
continue
endif
let [l:i, l:text] = s:GetErrMsg(l:i, a:lines, l:match[4])
let l:item = {
\ 'lnum': (l:match[2] + 0 ? l:match[2] + 0 : 1),
\ 'col': l:match[3] + 0,
\ 'text': l:text,
\ 'type': (l:match[1] is# 'ERROR' ? 'E' : 'W'),
\}
if !s:Ignore(l:item)
call add(l:output, l:item)
endif
endwhile
return l:output
endfunction
" This returns [<next line number>, <error message string>]
function! s:GetErrMsg(i, lines, text) abort
if a:text !~# '^\s*$'
return [a:i + 1, a:text]
endif
let l:i = a:i + 1
let l:text = []
while l:i < len(a:lines) && a:lines[l:i] =~# '^\s'
call add(l:text, s:Trim(a:lines[l:i]))
let l:i += 1
endwhile
return [l:i, join(l:text, '. ')]
endfunction
function! s:Trim(str) abort
return substitute(a:str, '\v^\s+|\s+$', '', 'g')
endfunction
" Skip sandbox error which is caused by directives
" because what we want is syntactic or semantic check.
function! s:Ignore(item) abort
return a:item.type is# 'E' &&
\ a:item.text =~# '\vNo permission to (call|directive|assert) sandboxed'
endfunction
call ale#linter#Define('prolog', {
\ 'name': 'swipl',
\ 'output_stream': 'stderr',
\ 'executable_callback': ale#VarFunc('prolog_swipl_executable'),
\ 'command_callback': 'ale_linters#prolog#swipl#GetCommand',
\ 'callback': 'ale_linters#prolog#swipl#Handle',
\})

56
doc/ale-prolog.txt Normal file
View file

@ -0,0 +1,56 @@
===============================================================================
ALE Prolog Integration *ale-prolog-options*
===============================================================================
swipl *ale-prolog-swipl*
g:ale_prolog_swipl_executable *g:ale_prolog_swipl_executable*
*b:ale_prolog_swipl_executable*
Type: |String|
Default: `'swipl'`
The executable that will be run for the `swipl` linter.
g:ale_prolog_swipl_load *g:ale_prolog_swipl_load*
*b:ale_prolog_swipl_load*
Type: |String|
Default: `'current_prolog_flag(argv, [File]), load_files(File, [sandboxed(true)]), halt.'`
The prolog goals that will be passed to |g:ale_prolog_swipl_executable| with `-g` option.
It does:
1. Takes the first command argument (current file path)
2. Checks (syntactic / semantic) problems and output to stderr
NOTE: `sandboxed(true)` prohibits executing some directives such as 'initialization main'.
g:ale_prolog_swipl_timeout *g:ale_prolog_swipl_timeout*
*b:ale_prolog_swipl_timeout*
Type: |Number|
Default: `3`
Timeout seconds to detect long-running linter.
It is done by setting SIGALRM.
See |g:ale_prolog_swipl_alarm| and |g:ale_prolog_swipl_alarm_handler|.
g:ale_prolog_swipl_alarm *g:ale_prolog_swipl_alarm*
*b:ale_prolog_swipl_alarm*
Type: |String|
Default: `'alarm(%t, (%h), _, [])'`
The prolog goals to be expected to set SIGALRM.
`%t` is replaced by |g:ale_prolog_swipl_timeout|.
`%h` is replaced by |g:ale_prolog_swipl_alarm_handler|.
g:ale_prolog_swipl_alarm_handler *g:ale_prolog_swipl_alarm_handler*
*b:ale_prolog_swipl_alarm_handler*
Type: |String|
Default: `'writeln(user_error, "ERROR: Exceeded %t seconds, Please change g:prolog_swipl_timeout to modify the limit."), halt(1)'`
The prolog goals to be expected that will be run on SIGALRM.
`%t` is replaced by |g:ale_prolog_swipl_timeout|.
===============================================================================
vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl:

View file

@ -226,6 +226,8 @@ CONTENTS *ale-contents*
write-good..........................|ale-pod-write-good|
pony..................................|ale-pony-options|
ponyc...............................|ale-pony-ponyc|
prolog................................|ale-prolog-options|
swipl...............................|ale-prolog-swipl|
proto.................................|ale-proto-options|
protoc-gen-lint.....................|ale-proto-protoc-gen-lint|
pug...................................|ale-pug-options|
@ -453,6 +455,7 @@ Notes:
* PO: `alex`!!, `msgfmt`, `proselint`, `write-good`
* Pod: `alex`!!, `proselint`, `write-good`
* Pony: `ponyc`
* Prolog: `swipl`
* proto: `protoc-gen-lint`
* Pug: `pug-lint`
* Puppet: `languageserver`, `puppet`, `puppet-lint`

View file

@ -0,0 +1,95 @@
Before:
runtime ale_linters/prolog/swipl.vim
After:
call ale#linter#Reset()
Execute (The swipl handler should handle oneline warning / error):
call ale#test#SetFilename('test.pl')
AssertEqual
\ [
\ {
\ 'lnum': 5,
\ 'col': 1,
\ 'text': 'Syntax error: Operator expected',
\ 'type': 'E',
\ },
\ ],
\ ale_linters#prolog#swipl#Handle(bufnr(''), [
\ 'ERROR: /path/to/test.pl:5:1: Syntax error: Operator expected',
\ ])
Execute (The swipl handler should handle a warning / error of two lines):
call ale#test#SetFilename('test.pl')
AssertEqual
\ [
\ {
\ 'lnum': 9,
\ 'col': 0,
\ 'text': 'Singleton variables: [M]',
\ 'type': 'W',
\ },
\ ],
\ ale_linters#prolog#swipl#Handle(bufnr(''), [
\ 'Warning: /path/to/test.pl:9:',
\ ' Singleton variables: [M]',
\ ])
Execute (The swipl handler should join three or more lines with '. '):
call ale#test#SetFilename('test.pl')
AssertEqual
\ [
\ {
\ 'lnum': 10,
\ 'col': 0,
\ 'text': 'Clauses of fib/2 are not together in the source-file. Earlier definition at /path/to/test.pl:7. Current predicate: f/0. Use :- discontiguous fib/2. to suppress this message',
\ 'type': 'W',
\ },
\ ],
\ ale_linters#prolog#swipl#Handle(bufnr(''), [
\ 'Warning: /path/to/test.pl:10:',
\ ' Clauses of fib/2 are not together in the source-file',
\ ' Earlier definition at /path/to/test.pl:7',
\ ' Current predicate: f/0',
\ ' Use :- discontiguous fib/2. to suppress this message',
\ ])
Execute (The swipl handler should ignore warnings / errors 'No permission to call sandboxed ...'):
call ale#test#SetFilename('test.pl')
AssertEqual
\ [],
\ ale_linters#prolog#swipl#Handle(bufnr(''), [
\ 'ERROR: /path/to/test.pl:11:',
\ ' No permission to call sandboxed `''$set_predicate_attribute''(_G3416:_G3417,_G3413,_G3414)''',
\ ' Reachable from:',
\ ' system:''$set_pattr''(A,B,C,D)',
\ ' system:''$set_pattr''(vimscript:A,B,C)',
\ ' vimscript: (multifile A)',
\ 'ERROR: /path/to/test.pl:12:',
\ ' No permission to call sandboxed `''$set_predicate_attribute''(_G205:_G206,_G202,_G203)''',
\ ' Reachable from:',
\ ' system:''$set_pattr''(A,B,C,D)',
\ ' system:''$set_pattr''(vimscript:A,B,C)',
\ ' vimscript: (multifile A)',
\ 'ERROR: /path/to/test.pl:13:',
\ ' No permission to call sandboxed `''$set_predicate_attribute''(_G1808:_G1809,_G1805,_G1806)''',
\ ' Reachable from:',
\ ' system:''$set_pattr''(A,B,C,D)',
\ ' system:''$set_pattr''(vimscript:A,B,C)',
\ ' vimscript: (multifile A)',
\ ])
Execute (The swipl handler should handle a warning / error with no line number):
call ale#test#SetFilename('test.pl')
AssertEqual
\ [
\ {
\ 'lnum': 1,
\ 'col': 0,
\ 'text': 'Exported procedure module_name:pred/0 is not defined',
\ 'type': 'E',
\ },
\ ],
\ ale_linters#prolog#swipl#Handle(bufnr(''), [
\ 'ERROR: Exported procedure module_name:pred/0 is not defined',
\ ])