From 34318aedf4ffaf4558a102db6c03ae3b13d5ff08 Mon Sep 17 00:00:00 2001 From: Takuya Fujiwara Date: Sat, 27 Oct 2018 01:29:17 +0900 Subject: [PATCH] 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 --- README.md | 1 + ale_linters/prolog/swipl.vim | 100 ++++++++++++++++++++++++++ doc/ale-prolog.txt | 56 +++++++++++++++ doc/ale.txt | 3 + test/handler/test_swipl_handler.vader | 95 ++++++++++++++++++++++++ 5 files changed, 255 insertions(+) create mode 100644 ale_linters/prolog/swipl.vim create mode 100644 doc/ale-prolog.txt create mode 100644 test/handler/test_swipl_handler.vader diff --git a/README.md b/README.md index f3d616f3..4b6d7663 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/ale_linters/prolog/swipl.vim b/ale_linters/prolog/swipl.vim new file mode 100644 index 00000000..401e52b6 --- /dev/null +++ b/ale_linters/prolog/swipl.vim @@ -0,0 +1,100 @@ +" Author: Takuya Fujiwara +" 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 [, ] +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', +\}) diff --git a/doc/ale-prolog.txt b/doc/ale-prolog.txt new file mode 100644 index 00000000..14062a5a --- /dev/null +++ b/doc/ale-prolog.txt @@ -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: diff --git a/doc/ale.txt b/doc/ale.txt index 66ce8ab1..95110431 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -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` diff --git a/test/handler/test_swipl_handler.vader b/test/handler/test_swipl_handler.vader new file mode 100644 index 00000000..9e425cf6 --- /dev/null +++ b/test/handler/test_swipl_handler.vader @@ -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', + \ ])