From 2ed53108c4f000e0a36a79c4317dad4fdfb545fe Mon Sep 17 00:00:00 2001 From: Jesse Harris Date: Sat, 13 Apr 2019 21:24:56 +1000 Subject: [PATCH] Linter for powershell syntax errors (#2413) * Linter for powershell syntax errors --- ale_linters/powershell/powershell.vim | 91 +++++++++++++++++++++ ale_linters/powershell/psscriptanalyzer.vim | 37 +-------- autoload/ale/powershell.vim | 32 ++++++++ doc/ale-powershell.txt | 15 ++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + test/handler/test_powershell_handler.vader | 62 ++++++++++++++ 8 files changed, 207 insertions(+), 33 deletions(-) create mode 100755 ale_linters/powershell/powershell.vim create mode 100644 autoload/ale/powershell.vim create mode 100755 test/handler/test_powershell_handler.vader diff --git a/ale_linters/powershell/powershell.vim b/ale_linters/powershell/powershell.vim new file mode 100755 index 00000000..51ded71d --- /dev/null +++ b/ale_linters/powershell/powershell.vim @@ -0,0 +1,91 @@ +" Author: Jesse Harris - https://github.com/zigford +" Description: This file adds support for powershell scripts synatax errors + +call ale#Set('powershell_powershell_executable', 'pwsh') + +function! ale_linters#powershell#powershell#GetExecutable(buffer) abort + return ale#Var(a:buffer, 'powershell_powershell_executable') +endfunction + +" Some powershell magic to show syntax errors without executing the script +" thanks to keith hill: +" https://rkeithhill.wordpress.com/2007/10/30/powershell-quicktip-preparsing-scripts-to-check-for-syntax-errors/ +function! ale_linters#powershell#powershell#GetCommand(buffer) abort + let l:script = ['Param($Script); + \ trap {$_;continue} & { + \ $Contents = Get-Content -Path $Script; + \ $Contents = [string]::Join([Environment]::NewLine, $Contents); + \ [void]$ExecutionContext.InvokeCommand.NewScriptBlock($Contents); + \ };'] + + return ale#powershell#RunPowerShell( + \ a:buffer, 'powershell_powershell', l:script) +endfunction + +" Parse powershell error output using regex into a list of dicts +function! ale_linters#powershell#powershell#Handle(buffer, lines) abort + let l:output = [] + " Our 3 patterns we need to scrape the data for the dicts + let l:patterns = [ + \ '\v^At line:(\d+) char:(\d+)', + \ '\v^(At|\+| )@!.*', + \ '\vFullyQualifiedErrorId : (\w+)', + \] + + let l:matchcount = 0 + + for l:match in ale#util#GetMatches(a:lines, l:patterns) + " We want to work with 3 matches per syntax error + let l:matchcount = l:matchcount + 1 + + if l:matchcount == 1 || str2nr(l:match[1]) + " First match consists of 2 capture groups, and + " can capture the line and col + if exists('l:item') + " We may be here because the last syntax + " didn't emit a code, and so only had 2 + " matches + call add(l:output, l:item) + let l:matchcount = 1 + endif + + let l:item = { + \ 'lnum': str2nr(l:match[1]), + \ 'col': str2nr(l:match[2]), + \ 'type': 'E', + \} + elseif l:matchcount == 2 + " Second match[0] grabs the full line in order + " to handles the text + let l:item['text'] = l:match[0] + else + " Final match handles the code, however + " powershell only emits 1 code for all errors + " so, we get the final code on the last error + " and loop over the previously added items to + " append the code we now know + call add(l:output, l:item) + unlet l:item + + if len(l:match[1]) > 0 + for l:i in l:output + let l:i['code'] = l:match[1] + endfor + endif + + " Reset the matchcount so we can begin gathering + " matches for the next syntax error + let l:matchcount = 0 + endif + endfor + + return l:output +endfunction + +call ale#linter#Define('powershell', { +\ 'name': 'powershell', +\ 'executable_callback': 'ale_linters#powershell#powershell#GetExecutable', +\ 'command_callback': 'ale_linters#powershell#powershell#GetCommand', +\ 'output_stream': 'stdout', +\ 'callback': 'ale_linters#powershell#powershell#Handle', +\}) diff --git a/ale_linters/powershell/psscriptanalyzer.vim b/ale_linters/powershell/psscriptanalyzer.vim index 8440d771..4794d9d8 100644 --- a/ale_linters/powershell/psscriptanalyzer.vim +++ b/ale_linters/powershell/psscriptanalyzer.vim @@ -13,37 +13,6 @@ function! ale_linters#powershell#psscriptanalyzer#GetExecutable(buffer) abort return ale#Var(a:buffer, 'powershell_psscriptanalyzer_executable') endfunction -" Write a powershell script to a temp file for execution -" return the command used to execute it -function! s:TemporaryPSScript(buffer, input) abort - let l:filename = 'script.ps1' - " Create a temp dir to house our temp .ps1 script - " a temp dir is needed as powershell needs the .ps1 - " extension - let l:tempdir = ale#util#Tempname() . (has('win32') ? '\' : '/') - let l:tempscript = l:tempdir . l:filename - " Create the temporary directory for the file, unreadable by 'other' - " users. - call mkdir(l:tempdir, '', 0750) - " Automatically delete the directory later. - call ale#command#ManageDirectory(a:buffer, l:tempdir) - " Write the script input out to a file. - call ale#util#Writefile(a:buffer, a:input, l:tempscript) - - return l:tempscript -endfunction - -function! ale_linters#powershell#psscriptanalyzer#RunPowerShell(buffer, command) abort - let l:executable = ale_linters#powershell#psscriptanalyzer#GetExecutable( - \ a:buffer) - let l:tempscript = s:TemporaryPSScript(a:buffer, a:command) - - return ale#Escape(l:executable) - \ . ' -Exe Bypass -NoProfile -File ' - \ . ale#Escape(l:tempscript) - \ . ' %t' -endfunction - " Run Invoke-ScriptAnalyzer and output each linting message as 4 seperate lines " for each parsing function! ale_linters#powershell#psscriptanalyzer#GetCommand(buffer) abort @@ -60,8 +29,10 @@ function! ale_linters#powershell#psscriptanalyzer#GetCommand(buffer) abort \ $_.Message; \ $_.RuleName}'] - return ale_linters#powershell#psscriptanalyzer#RunPowerShell( - \ a:buffer, l:script) + return ale#powershell#RunPowerShell( + \ a:buffer, + \ 'powershell_psscriptanalyzer', + \ l:script) endfunction " add every 4 lines to an item(Dict) and every item to a list diff --git a/autoload/ale/powershell.vim b/autoload/ale/powershell.vim new file mode 100644 index 00000000..8c163206 --- /dev/null +++ b/autoload/ale/powershell.vim @@ -0,0 +1,32 @@ +" Author: zigford +" Description: Functions for integrating with Powershell linters. + +" Write a powershell script to a temp file for execution +" return the command used to execute it +function! s:TemporaryPSScript(buffer, input) abort + let l:filename = 'script.ps1' + " Create a temp dir to house our temp .ps1 script + " a temp dir is needed as powershell needs the .ps1 + " extension + let l:tempdir = ale#util#Tempname() . (has('win32') ? '\' : '/') + let l:tempscript = l:tempdir . l:filename + " Create the temporary directory for the file, unreadable by 'other' + " users. + call mkdir(l:tempdir, '', 0750) + " Automatically delete the directory later. + call ale#command#ManageDirectory(a:buffer, l:tempdir) + " Write the script input out to a file. + call ale#util#Writefile(a:buffer, a:input, l:tempscript) + + return l:tempscript +endfunction + +function! ale#powershell#RunPowerShell(buffer, base_var_name, command) abort + let l:executable = ale#Var(a:buffer, a:base_var_name . '_executable') + let l:tempscript = s:TemporaryPSScript(a:buffer, a:command) + + return ale#Escape(l:executable) + \ . ' -Exe Bypass -NoProfile -File ' + \ . ale#Escape(l:tempscript) + \ . ' %t' +endfunction diff --git a/doc/ale-powershell.txt b/doc/ale-powershell.txt index 743b5ae3..c28ef9ea 100644 --- a/doc/ale-powershell.txt +++ b/doc/ale-powershell.txt @@ -2,6 +2,21 @@ ALE PowerShell Integration *ale-powershell-options* +=============================================================================== +powershell *ale-powershell-powershell* + +g:ale_powershell_powershell_executable *g:ale_powershell_powershell_executable* + *b:ale_powershell_powershell_executable* + Type: String + Default: `'pwsh'` + + This variable can be changed to use a different executable for powershell. + +> + " Use powershell.exe rather than the default pwsh + let g:ale_powershell_powershell_executable = 'powershell.exe' +> + =============================================================================== psscriptanalyzer *ale-powershell-psscriptanalyzer* diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index eff2e607..70b86a03 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -319,6 +319,7 @@ Notes: * Pony * `ponyc` * PowerShell + * `powershell` * `psscriptanalyzer` * Prolog * `swipl` diff --git a/doc/ale.txt b/doc/ale.txt index 6d78fd89..54e3c455 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2070,6 +2070,7 @@ documented in additional help files. pony....................................|ale-pony-options| ponyc.................................|ale-pony-ponyc| powershell............................|ale-powershell-options| + powershell..........................|ale-powershell-powershell| psscriptanalyzer....................|ale-powershell-psscriptanalyzer| prolog..................................|ale-prolog-options| swipl.................................|ale-prolog-swipl| diff --git a/supported-tools.md b/supported-tools.md index 74cea65c..108e08f2 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -328,6 +328,7 @@ formatting. * Pony * [ponyc](https://github.com/ponylang/ponyc) * PowerShell + * [powershell](https://github.com/PowerShell/PowerShell) :floppy_disk * [psscriptanalyzer](https://github.com/PowerShell/PSScriptAnalyzer) :floppy_disk * Prolog * [swipl](https://github.com/SWI-Prolog/swipl-devel) diff --git a/test/handler/test_powershell_handler.vader b/test/handler/test_powershell_handler.vader new file mode 100755 index 00000000..635bcd20 --- /dev/null +++ b/test/handler/test_powershell_handler.vader @@ -0,0 +1,62 @@ +Before: + runtime ale_linters/powershell/powershell.vim + +After: + call ale#linter#Reset() + +Execute(The powershell handler should process syntax errors from parsing a powershell script): + AssertEqual + \ [ + \ { + \ 'lnum': 8, + \ 'col': 29, + \ 'type': 'E', + \ 'text': 'Missing closing ''}'' in statement block or type definition.', + \ 'code': 'ParseException', + \ }, + \ ], + \ ale_linters#powershell#powershell#Handle(bufnr(''), [ + \ "At line:8 char:29", + \ "+ Invoke-Command -ScriptBlock {", + \ "+ ~", + \ "Missing closing '}' in statement block or type definition.", + \ "At /home/harrisj/tester.ps1:5 char:5", + \ "+ [void]$ExecutionContext.InvokeCommand.NewScriptBlock($Contents);", + \ "+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + \ "+ CategoryInfo : NotSpecified: (:) [], ParseException", + \ "+ FullyQualifiedErrorId : ParseException" + \ ]) + +Execute(The powershell handler should process multiple syntax errors from parsing a powershell script): + AssertEqual + \ [ + \ { + \ 'lnum': 11, + \ 'col': 31, + \ 'type': 'E', + \ 'text': 'The string is missing the terminator: ".', + \ 'code': 'ParseException' + \ }, + \ { + \ 'lnum': 3, + \ 'col': 16, + \ 'type': 'E', + \ 'text': 'Missing closing ''}'' in statement block or type definition.', + \ 'code': 'ParseException' + \ }, + \ ], + \ ale_linters#powershell#powershell#Handle(bufnr(''), [ + \ 'At line:11 char:31', + \ '+ write-verbose ''deleted''', + \ '+ ~', + \ 'The string is missing the terminator: ".', + \ 'At line:3 char:16', + \ '+ invoke-command {', + \ '+ ~', + \ 'Missing closing ''}'' in statement block or type definition.', + \ 'At /var/folders/qv/15ybvt050v9cgwrm7c95x4r4zc4qsg/T/vwhzIc8/1/script.ps1:1 char:150', + \ '+ ... ontents); [void]$ExecutionContext.InvokeCommand.NewScriptBlock($Con ...', + \ '+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', + \ '+ CategoryInfo : NotSpecified: (:) [], ParseException', + \ '+ FullyQualifiedErrorId : ParseException' + \ ])