]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/handlers/shellcheck.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/ale/' content from commit 22185c4c
[etc/vim.git] / autoload / ale / handlers / shellcheck.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: This file adds support for using the shellcheck linter
3
4 " Shellcheck supports shell directives to define the shell dialect for scripts
5 " that do not have a shebang for some reason.
6 " https://github.com/koalaman/shellcheck/wiki/Directive#shell
7 function! ale#handlers#shellcheck#GetShellcheckDialectDirective(buffer) abort
8     let l:linenr = 0
9     let l:pattern = '\s\{-}#\s\{-}shellcheck\s\{-}shell=\(.*\)'
10     let l:possible_shell = ['bash', 'dash', 'ash', 'tcsh', 'csh', 'zsh', 'ksh', 'sh']
11
12     while l:linenr < min([50, line('$')])
13         let l:linenr += 1
14         let l:match = matchlist(getline(l:linenr), l:pattern)
15
16         if len(l:match) > 1 && index(l:possible_shell, l:match[1]) >= 0
17             return l:match[1]
18         endif
19     endwhile
20
21     return ''
22 endfunction
23
24 function! ale#handlers#shellcheck#GetDialectArgument(buffer) abort
25     let l:shell_type = ale#handlers#shellcheck#GetShellcheckDialectDirective(a:buffer)
26
27     if empty(l:shell_type)
28         let l:shell_type = ale#handlers#sh#GetShellType(a:buffer)
29     endif
30
31     if !empty(l:shell_type)
32         " Use the dash dialect for /bin/ash, etc.
33         if l:shell_type is# 'ash'
34             return 'dash'
35         endif
36
37         return l:shell_type
38     endif
39
40     return ''
41 endfunction
42
43 function! ale#handlers#shellcheck#GetCwd(buffer) abort
44     return ale#Var(a:buffer, 'sh_shellcheck_change_directory') ? '%s:h' : ''
45 endfunction
46
47 function! ale#handlers#shellcheck#GetCommand(buffer, version) abort
48     let l:options = ale#Var(a:buffer, 'sh_shellcheck_options')
49     let l:exclude_option = ale#Var(a:buffer, 'sh_shellcheck_exclusions')
50     let l:dialect = ale#Var(a:buffer, 'sh_shellcheck_dialect')
51     let l:external_option = ale#semver#GTE(a:version, [0, 4, 0]) ? ' -x' : ''
52     let l:format = ale#semver#GTE(a:version, [0, 7, 0]) ? 'json1' : 'gcc'
53
54     if l:dialect is# 'auto'
55         let l:dialect = ale#handlers#shellcheck#GetDialectArgument(a:buffer)
56     endif
57
58     return '%e'
59     \   . (!empty(l:dialect) ? ' -s ' . l:dialect : '')
60     \   . (!empty(l:options) ? ' ' . l:options : '')
61     \   . (!empty(l:exclude_option) ? ' -e ' . l:exclude_option : '')
62     \   . l:external_option
63     \   . ' -f ' . l:format . ' -'
64 endfunction
65
66 function! s:HandleShellcheckJSON(buffer, lines) abort
67     try
68         let l:errors = json_decode(a:lines[0])
69     catch
70         return []
71     endtry
72
73     if !has_key(l:errors, 'comments')
74         return []
75     endif
76
77     let l:output = []
78
79     for l:error in l:errors['comments']
80         if l:error['level'] is# 'error'
81             let l:type = 'E'
82         elseif l:error['level'] is# 'info'
83             let l:type = 'I'
84         elseif l:error['level'] is# 'style'
85             let l:type = 'I'
86         else
87             let l:type = 'W'
88         endif
89
90         let l:item = {
91         \   'lnum': l:error['line'],
92         \   'type': l:type,
93         \   'text': l:error['message'],
94         \   'code': 'SC' . l:error['code'],
95         \   'detail': l:error['message'] . "\n\nFor more information:\n  https://www.shellcheck.net/wiki/SC" . l:error['code'],
96         \}
97
98         if has_key(l:error, 'column')
99             let l:item.col = l:error['column']
100         endif
101
102         if has_key(l:error, 'endColumn')
103             let l:item.end_col = l:error['endColumn'] - 1
104         endif
105
106         if has_key(l:error, 'endLine')
107             let l:item.end_lnum = l:error['endLine']
108         endif
109
110
111         " If the filename is something like <stdin>, <nofile> or -, then
112         " this is an error for the file we checked.
113         if has_key(l:error, 'file')
114             if l:error['file'] isnot# '-' && l:error['file'][0] isnot# '<'
115                 let l:item['filename'] = l:error['file']
116             endif
117         endif
118
119         call add(l:output, l:item)
120     endfor
121
122     return l:output
123 endfunction
124
125 function! s:HandleShellcheckGCC(buffer, lines) abort
126     let l:pattern = '\v^([a-zA-Z]?:?[^:]+):(\d+):(\d+)?:? ([^:]+): (.+) \[([^\]]+)\]$'
127     let l:output = []
128
129     for l:match in ale#util#GetMatches(a:lines, l:pattern)
130         if l:match[4] is# 'error'
131             let l:type = 'E'
132         elseif l:match[4] is# 'note'
133             let l:type = 'I'
134         else
135             let l:type = 'W'
136         endif
137
138         let l:item = {
139         \   'lnum': str2nr(l:match[2]),
140         \   'type': l:type,
141         \   'text': l:match[5],
142         \   'code': l:match[6],
143         \   'detail': l:match[5] . "\n\nFor more information:\n  https://www.shellcheck.net/wiki/" . l:match[6],
144         \}
145
146         if !empty(l:match[3])
147             let l:item.col = str2nr(l:match[3])
148         endif
149
150         " If the filename is something like <stdin>, <nofile> or -, then
151         " this is an error for the file we checked.
152         if l:match[1] isnot# '-' && l:match[1][0] isnot# '<'
153             let l:item['filename'] = l:match[1]
154         endif
155
156         call add(l:output, l:item)
157     endfor
158
159     return l:output
160 endfunction
161
162 function! ale#handlers#shellcheck#Handle(buffer, version, lines) abort
163     return ale#semver#GTE(a:version, [0, 7, 0])
164     \   ? s:HandleShellcheckJSON(a:buffer, a:lines)
165     \   : s:HandleShellcheckGCC(a:buffer, a:lines)
166 endfunction
167
168 function! ale#handlers#shellcheck#DefineLinter(filetype) abort
169     " This global variable can be set with a string of comma-separated error
170     " codes to exclude from shellcheck. For example:
171     " let g:ale_sh_shellcheck_exclusions = 'SC2002,SC2004'
172     call ale#Set('sh_shellcheck_exclusions', '')
173     call ale#Set('sh_shellcheck_executable', 'shellcheck')
174     call ale#Set('sh_shellcheck_dialect', 'auto')
175     call ale#Set('sh_shellcheck_options', '')
176     call ale#Set('sh_shellcheck_change_directory', 1)
177
178     call ale#linter#Define(a:filetype, {
179     \   'name': 'shellcheck',
180     \   'executable': {buffer -> ale#Var(buffer, 'sh_shellcheck_executable')},
181     \   'cwd': function('ale#handlers#shellcheck#GetCwd'),
182     \   'command': {buffer -> ale#semver#RunWithVersionCheck(
183     \       buffer,
184     \       ale#Var(buffer, 'sh_shellcheck_executable'),
185     \       '%e --version',
186     \       function('ale#handlers#shellcheck#GetCommand'),
187     \   )},
188     \   'callback': {buffer, lines -> ale#semver#RunWithVersionCheck(
189     \       buffer,
190     \       ale#Var(buffer, 'sh_shellcheck_executable'),
191     \       '%e --version',
192     \       {buffer, version -> ale#handlers#shellcheck#Handle(
193     \           buffer,
194     \           l:version,
195     \           lines)},
196     \   )},
197     \})
198 endfunction