diff --git a/README.md b/README.md index 0eeaf514..61220c45 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ formatting. | OCaml | [merlin](https://github.com/the-lambda-church/merlin) see `:help ale-ocaml-merlin` for configuration instructions, [ols](https://github.com/freebroccolo/ocaml-language-server), [ocamlformat](https://github.com/ocaml-ppx/ocamlformat) | | Pawn | [uncrustify](https://github.com/uncrustify/uncrustify) | | Perl | [perl -c](https://perl.org/), [perl-critic](https://metacpan.org/pod/Perl::Critic), [perltidy](https://metacpan.org/pod/distribution/Perl-Tidy/bin/perltidy) | +| Perl6 | [perl6 -c](https://perl6.org) | | PHP | [langserver](https://github.com/felixfbecker/php-language-server), [phan](https://github.com/phan/phan) see `:help ale-php-phan` to instructions, [php -l](https://secure.php.net/), [phpcs](https://github.com/squizlabs/PHP_CodeSniffer), [phpmd](https://phpmd.org), [phpstan](https://github.com/phpstan/phpstan), [phpcbf](https://github.com/squizlabs/PHP_CodeSniffer), [php-cs-fixer](http://cs.sensiolabs.org/), [psalm](https://getpsalm.org) !! | | 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) | diff --git a/ale_linters/perl6/perl6.vim b/ale_linters/perl6/perl6.vim new file mode 100644 index 00000000..326850a2 --- /dev/null +++ b/ale_linters/perl6/perl6.vim @@ -0,0 +1,147 @@ +" Author:Travis Gibson +" Description: This file adds support for checking perl6 syntax + +let g:ale_perl6_perl6_executable = +\ get(g:, 'ale_perl6_perl6_executable', 'perl6') + +let g:ale_perl6_perl6_options = +\ get(g:, 'ale_perl6_perl6_options', '-c -Ilib') + +let $PERL6_EXCEPTIONS_HANDLER = 'JSON' + +let $RAKUDO_ERROR_COLOR = 0 + +function! ale_linters#perl6#perl6#GetExecutable(buffer) abort + return ale#Var(a:buffer, 'perl6_perl6_executable') +endfunction + +function! ale_linters#perl6#perl6#GetCommand(buffer) abort + return ale_linters#perl6#perl6#GetExecutable(a:buffer) + \ . ' ' . ale#Var(a:buffer, 'perl6_perl6_options') + \ . ' %t' +endfunction + +function! ale_linters#perl6#perl6#ExtractError(dict, item, type) abort + let l:file = '' + let l:line = '' + let l:column = '' + let l:text = '' + let l:pre = '' + let l:counter = 2 + let l:end_line = '' + let l:linepatternmessage = 'at\s\+line\s\+\(\d\+\)' + + if has_key(a:dict[a:item], 'filename') && !empty(a:dict[a:item]['filename']) + let l:file .= a:dict[a:item]['filename'] + endif + + if has_key(a:dict[a:item], 'line') && !empty(a:dict[a:item]['line']) + let l:line .= a:dict[a:item]['line'] + let l:counter -= 1 + endif + + if has_key(a:dict[a:item], 'column') && !empty(a:dict[a:item]['column']) + let l:column .= a:dict[a:item]['column'] + endif + + if has_key(a:dict[a:item], 'message') && !empty(a:dict[a:item]['message']) + let l:text .= substitute(a:dict[a:item]['message'], '\s*\n\s*', ' ', 'g') + let l:counter -= 1 + endif + + if has_key(a:dict[a:item], 'line-real') && !empty(a:dict[a:item]['line-real']) + let l:end_line = l:line + let l:line .= a:dict[a:item]['line-real'] + endif + + for l:match in ale#util#GetMatches(l:text, l:linepatternmessage) + let l:line = l:match[1] + let l:counter -= 1 + endfor + + if l:counter < 1 + return { + \ 'lnum': l:line, + \ 'text': l:text, + \ 'type': a:type, + \ 'col': l:column, + \ 'end_lnum': l:end_line, + \ 'code': a:item, + \} + endif +endfunction + +function! ale_linters#perl6#perl6#Handle(buffer, lines) abort + let l:output = [] + + if empty(a:lines) + return l:output + endif + + if a:lines[0] is# 'Syntax OK' + return l:output + endif + + try + let l:json = json_decode(join(a:lines, '')) + catch /E474/ + call add(l:output, { + \ 'lnum': '1', + \ 'text': 'Received output in the default Perl6 error format. See :ALEDetail for details', + \ 'detail': join(a:lines, "\n"), + \ 'type': 'W', + \ }) + + return l:output + endtry + + if type(l:json) is v:t_dict + for l:key in keys(l:json) + if has_key(l:json[l:key], 'sorrows') && + \ has_key(l:json[l:key], 'worries') + if !empty(l:json[l:key]['sorrows']) + for l:dictionary in get(l:json[l:key], 'sorrows') + for l:item in keys(l:dictionary) + call add(l:output, + \ ale_linters#perl6#perl6#ExtractError( + \ l:dictionary, + \ l:item, + \ 'E' + \ ) + \ ) + endfor + endfor + endif + + if !empty(l:json[l:key]['worries']) + for l:dictionary in get(l:json[l:key], 'worries') + for l:item in keys(l:dictionary) + call add(l:output, + \ ale_linters#perl6#perl6#ExtractError( + \ l:dictionary, + \ l:item, + \ 'W' + \ ) + \ ) + endfor + endfor + endif + else + call add(l:output, + \ ale_linters#perl6#perl6#ExtractError(l:json, l:key, 'E') + \ ) + endif + endfor + endif + + return l:output +endfunction + +call ale#linter#Define('perl6', { +\ 'name': 'perl6', +\ 'executable_callback': 'ale_linters#perl6#perl6#GetExecutable', +\ 'output_stream': 'both', +\ 'command_callback': 'ale_linters#perl6#perl6#GetCommand', +\ 'callback': 'ale_linters#perl6#perl6#Handle', +\}) + diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index 06bc5e80..6c61e3db 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -35,6 +35,7 @@ let s:default_ale_linters = { \ 'hack': ['hack'], \ 'help': [], \ 'perl': ['perlcritic'], +\ 'perl6': [], \ 'python': ['flake8', 'mypy', 'pylint'], \ 'rust': ['cargo'], \ 'spec': [], diff --git a/doc/ale-perl6.txt b/doc/ale-perl6.txt new file mode 100644 index 00000000..94953db5 --- /dev/null +++ b/doc/ale-perl6.txt @@ -0,0 +1,43 @@ +=============================================================================== +ALE Perl6 Integration *ale-perl6-options* + +Checking code with `perl6` is disabled by default, as `perl6` code cannot be +checked without executing it. Specifically, we use the `-c` flag to see if +`perl6` code compiles. This does not execute all of the code in a file, but it +does run `BEGIN` and `CHECK` blocks. See `perl6 --help` + +Full support requires a perl6 implementation that supports the +PERL6_EXCEPTIONS_HANDLER environment variable and JSON error output, +which was specified in 6.d. Rakudo version 2018.08 is the first rakudo release +that supports this. See `perl6 --version` and +https://docs.perl6.org/programs/03-environment-variables. + +Without this variable, errors and warnings will appear at line 1, and can be +viewed with ALEDetail. This also serves as a fallback for errors and warnings +that do not trigger JSON output. + +See |g:ale_linters|. + + +=============================================================================== +perl6 *ale-perl6-perl6* + +g:ale_perl6_perl6_executable *g:ale_perl6_perl6_executable* + *b:ale_perl6_perl6_executable* + Type: |String| + Default: `'perl6'` + + This variable can be changed to modify the executable used for linting + perl6. + + +g:ale_perl6_perl6_options *g:ale_perl6_perl6_options* + *b:ale_perl6_perl6_options* + Type: |String| + Default: `'-c -Ilib'` + + This variable can be changed to alter the command-line arguments to the + perl6 invocation. + +=============================================================================== + vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale.txt b/doc/ale.txt index 3c561e2f..24d69fb2 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -203,6 +203,8 @@ CONTENTS *ale-contents* perl................................|ale-perl-perl| perlcritic..........................|ale-perl-perlcritic| perltidy............................|ale-perl-perltidy| + perl6.................................|ale-perl6-options| + perl6...............................|ale-perl6-perl6| php...................................|ale-php-options| langserver..........................|ale-php-langserver| phan................................|ale-php-phan| @@ -438,6 +440,7 @@ Notes: * OCaml: `merlin` (see |ale-ocaml-merlin|), `ols`, `ocamlformat` * Pawn: `uncrustify` * Perl: `perl -c`, `perl-critic`, `perltidy` +* Perl6: `perl6 -c` * PHP: `langserver`, `phan`, `php -l`, `phpcs`, `phpmd`, `phpstan`, `phpcbf`, `php-cs-fixer`, `psalm`!! * PO: `alex`!!, `msgfmt`, `proselint`, `write-good` * Pod: `alex`!!, `proselint`, `write-good` @@ -1306,6 +1309,7 @@ g:ale_linters *g:ale_linters* \ 'hack': ['hack'], \ 'help': [], \ 'perl': ['perlcritic'], + \ 'perl6': [], \ 'python': ['flake8', 'mypy', 'pylint'], \ 'rust': ['cargo'], \ 'spec': [], diff --git a/test/command_callback/test_perl6_command_callback.vader b/test/command_callback/test_perl6_command_callback.vader new file mode 100644 index 00000000..d3ec6e17 --- /dev/null +++ b/test/command_callback/test_perl6_command_callback.vader @@ -0,0 +1,14 @@ +Before: + call ale#assert#SetUpLinterTest('perl6', 'perl6') + +After: + call ale#assert#TearDownLinterTest() + +Execute(The default Perl6 command callback should be correct): + AssertLinter 'perl6', 'perl6' . ' -c -Ilib %t' + +Execute(Overriding the executable and command should work): + let b:ale_perl6_perl6_executable = 'foobar' + let b:ale_perl6_perl6_options = '-w' + + AssertLinter 'foobar', 'foobar' . ' -w %t' diff --git a/test/handler/test_perl6_handler.vader b/test/handler/test_perl6_handler.vader new file mode 100644 index 00000000..fbc5b12b --- /dev/null +++ b/test/handler/test_perl6_handler.vader @@ -0,0 +1,202 @@ +Before: + call ale#test#SetDirectory('/testplugin/test/handler') + + runtime ale_linters/perl6/perl6.vim + +After: + call ale#test#RestoreDirectory() + call ale#linter#Reset() + +Execute(The Perl6 linter should handle empty output): + call ale#test#SetFilename('bar.pl6') + + AssertEqual [], ale_linters#perl6#perl6#Handle(bufnr(''), []) + +Execute(The Perl6 linter should complain about undeclared variables): + call ale#test#SetFilename('bar.pl6') + + AssertEqual + \ [ + \ { + \ 'lnum': '6', + \ 'text': 'Variable ''$tes'' is not declared. Did you mean any of these? $res $test ', + \ 'type': 'E', + \ 'col': '', + \ 'end_lnum': '', + \ 'code': 'X::Undeclared', + \ } + \ ], + \ ale_linters#perl6#perl6#Handle(bufnr(''), [ + \ '{ + \ "X::Undeclared" : { + \ "highexpect" : [ ], + \ "is-compile-time" : 1, + \ "modules" : [ ], + \ "column" : null, + \ "pos" : 18, + \ "symbol" : "$tes", + \ "filename" : "bar.pl6", + \ "what" : "Variable", + \ "pre" : "my $test = 0; say ", + \ "post" : "$tes", + \ "suggestions" : [ + \ "$res", + \ "$test" + \ ], + \ "line" : 6, + \ "message" : "Variable ''$tes'' is not declared. Did you mean any of these?\n $res\n $test\n" + \ } + \ }' + \ ]) + +Execute(The Perl6 linter should complain about Comp::AdHoc errors): + call ale#test#SetFilename('bar.pl6') + + AssertEqual + \ [ + \ { + \ 'lnum': '3', + \ 'type': 'E', + \ 'text': 'is repr(...) trait needs a parameter', + \ 'col': '', + \ 'end_lnum': '', + \ 'code': 'X::Comp::AdHoc', + \ } + \ ], + \ ale_linters#perl6#perl6#Handle(bufnr(''), [ + \ '{ + \ "X::Comp::AdHoc" : { + \ "pre" : "class test is repr", + \ "message" : "is repr(...) trait needs a parameter", + \ "line" : 3, + \ "post" : " {}", + \ "is-compile-time" : true, + \ "pos" : 19, + \ "highexpect" : [ ], + \ "payload" : "is repr(...) trait needs a parameter", + \ "filename" : "bar.pl6", + \ "column" : null, + \ "modules" : [ ] + \ } + \ }' + \]) + +Execute(The Perl6 linter should be able to extract a line number from an error message): + call ale#test#SetFilename('bar.pl6') + + AssertEqual + \ [ + \ { + \ 'lnum': '3', + \ 'text': 'Could not find Module::Does::not::exist at line 3 in: /usr/share/perl6/site /usr/share/perl6/vendor /usr/share/perl6 CompUnit::Repository::AbsolutePath<94023691448416> CompUnit::Repository::NQP<94023670532736> CompUnit::Repository::Perl5<94023670532776>', + \ 'col': '', + \ 'type': 'E', + \ 'end_lnum': '', + \ 'code': 'X::CompUnit::UnsatisfiedDependency', + \ } + \ ], + \ ale_linters#perl6#perl6#Handle(bufnr(''), [ + \ '{ + \ "X::CompUnit::UnsatisfiedDependency" : { + \ "message" : "Could not find Module::Does::not::exist at line 3 in:\n /usr/share/perl6/site\n /usr/share/perl6/vendor\n /usr/share/perl6\n CompUnit::Repository::AbsolutePath<94023691448416>\n CompUnit::Repository::NQP<94023670532736>\n CompUnit::Repository::Perl5<94023670532776>", + \ "specification" : "Module::Does::not::exist" + \ } + \ }' + \ ]) + +Execute(The Perl6 linter should be able to differentiate between warnings and errors): + call ale#test#SetFilename('bar.pl6') + + AssertEqual + \ [ + \ { + \ 'lnum': '1', + \ 'col': '', + \ 'code': 'X::Syntax::Regex::Unterminated', + \ 'end_lnum': '', + \ 'type': 'E', + \ 'text': 'Regex not terminated.', + \ }, + \ { + \ 'lnum': '1', + \ 'col': '', + \ 'code': 'X::Comp::AdHoc', + \ 'end_lnum': '', + \ 'type': 'W', + \ 'text': 'Space is not significant here; please use quotes or :s (:sigspace) modifier (or, to suppress this warning, omit the space, or otherwise change the spacing)', + \ } + \ ], + \ ale_linters#perl6#perl6#Handle(bufnr(''), [ + \ '{ + \ "X::Comp::Group" : { + \ "message" : "Regex not terminated.\nUnable to parse regex; couldn''t find final ''/''\nSpace is not significant here; please use quotes or :s (:sigspace) modifier (or, to suppress this warning, omit the space, or otherwise change the spacing)", + \ "panic" : "Unable to parse regex; couldn''t find final ''/''", + \ "sorrows" : [ + \ { + \ "X::Syntax::Regex::Unterminated" : { + \ "highexpect" : [ + \ "infix stopper" + \ ], + \ "pos" : 6, + \ "is-compile-time" : 1, + \ "modules" : [ ], + \ "post" : "", + \ "message" : "Regex not terminated.", + \ "line" : 1, + \ "filename" : "bar.pl6", + \ "column" : null, + \ "pre" : "/win 3" + \ } + \ } + \ ], + \ "worries" : [ + \ { + \ "X::Comp::AdHoc" : { + \ "filename" : "bar.pl6", + \ "line" : 1, + \ "column" : null, + \ "pre" : "/win", + \ "highexpect" : [ ], + \ "payload" : "Space is not significant here; please use quotes or :s (:sigspace) modifier (or, to suppress this warning, omit the space, or otherwise change the spacing)", + \ "post" : " 3", + \ "message" : "Space is not significant here; please use quotes or :s (:sigspace) modifier (or, to suppress this warning, omit the space, or otherwise change the spacing)", + \ "modules" : [ ], + \ "is-compile-time" : true, + \ "pos" : 4 + \ } + \ } + \ ] + \ } + \ }' + \]) + +Execute(The Perl6 linter should gracefully handle non-JSON messages): + call ale#test#SetFilename('bar.pl6') + + AssertEqual + \ [ + \ { + \ 'lnum': '1', + \ 'text': 'Received output in the default Perl6 error format. See :ALEDetail for details', + \ 'type': 'W', + \ 'detail': join([ + \ 'Potential difficulties:', + \ ' Redeclaration of symbol ''$_''', + \ ' at /home/travis/perl6-error-fail/insanity-test.pl6:1', + \ ' ------> sub foo($_) {.say}; my $_ = 1; .&foo;', + \ ' Space is not significant here; please use quotes or :s (:sigspace) modifier (or, to suppress this warning, omit the space, or otherwise change the spacing)', + \ ' at /home/travis/perl6-error-fail/insanity-test.pl6:4', + \ ' ------> /win 3/', + \ 'Syntax OK',], "\n") + \ } + \ ], + \ ale_linters#perl6#perl6#Handle(bufnr(''), [ + \ 'Potential difficulties:', + \ ' Redeclaration of symbol ''$_''', + \ ' at /home/travis/perl6-error-fail/insanity-test.pl6:1', + \ ' ------> sub foo($_) {.say}; my $_ = 1; .&foo;', + \ ' Space is not significant here; please use quotes or :s (:sigspace) modifier (or, to suppress this warning, omit the space, or otherwise change the spacing)', + \ ' at /home/travis/perl6-error-fail/insanity-test.pl6:4', + \ ' ------> /win 3/', + \ 'Syntax OK' + \ ])