]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/linter.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 / linter.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Linter registration and lazy-loading
3 "   Retrieves linters as requested by the engine, loading them if needed.
4
5 let s:runtime_loaded_map = {}
6 let s:linters = {}
7
8 " Default filetype aliases.
9 " The user defined aliases will be merged with this Dictionary.
10 "
11 " NOTE: Update the g:ale_linter_aliases documentation when modifying this.
12 let s:default_ale_linter_aliases = {
13 \   'Dockerfile': 'dockerfile',
14 \   'csh': 'sh',
15 \   'javascriptreact': ['javascript', 'jsx'],
16 \   'plaintex': 'tex',
17 \   'ps1': 'powershell',
18 \   'rmarkdown': 'r',
19 \   'rmd': 'r',
20 \   'systemverilog': 'verilog',
21 \   'typescriptreact': ['typescript', 'tsx'],
22 \   'vader': ['vim', 'vader'],
23 \   'verilog_systemverilog': ['verilog_systemverilog', 'verilog'],
24 \   'vimwiki': 'markdown',
25 \   'vue': ['vue', 'javascript'],
26 \   'xsd': ['xsd', 'xml'],
27 \   'xslt': ['xslt', 'xml'],
28 \   'zsh': 'sh',
29 \}
30
31 " Default linters to run for particular filetypes.
32 " The user defined linter selections will be merged with this Dictionary.
33 "
34 " No linters are used for plaintext files by default.
35 "
36 " Only cargo and rls are enabled for Rust by default.
37 " rpmlint is disabled by default because it can result in code execution.
38 " hhast is disabled by default because it executes code in the project root.
39 "
40 " NOTE: Update the g:ale_linters documentation when modifying this.
41 let s:default_ale_linters = {
42 \   'apkbuild': ['apkbuild_lint', 'secfixes_check'],
43 \   'astro': ['eslint'],
44 \   'csh': ['shell'],
45 \   'elixir': ['credo', 'dialyxir', 'dogma'],
46 \   'go': ['gofmt', 'golangci-lint', 'gopls', 'govet'],
47 \   'groovy': ['npm-groovy-lint'],
48 \   'hack': ['hack'],
49 \   'help': [],
50 \   'inko': ['inko'],
51 \   'json': ['biome', 'jsonlint', 'spectral', 'vscodejson'],
52 \   'json5': [],
53 \   'jsonc': ['biome'],
54 \   'perl': ['perlcritic'],
55 \   'perl6': [],
56 \   'python': ['flake8', 'mypy', 'pylint', 'pyright', 'ruff'],
57 \   'rust': ['analyzer', 'cargo'],
58 \   'spec': [],
59 \   'text': [],
60 \   'vader': ['vimls'],
61 \   'vue': ['eslint', 'vls'],
62 \   'zsh': ['shell'],
63 \   'v': ['v'],
64 \   'yaml': ['actionlint', 'spectral', 'yaml-language-server', 'yamllint'],
65 \}
66
67 " Testing/debugging helper to unload all linters.
68 function! ale#linter#Reset() abort
69     let s:runtime_loaded_map = {}
70     let s:linters = {}
71 endfunction
72
73 " Return a reference to the linters loaded.
74 " This is only for tests.
75 " Do not call this function.
76 function! ale#linter#GetLintersLoaded() abort
77     " This command will throw from the sandbox.
78     let &l:equalprg=&l:equalprg
79
80     return s:linters
81 endfunction
82
83 function! s:IsCallback(value) abort
84     return type(a:value) is v:t_string || type(a:value) is v:t_func
85 endfunction
86
87 function! s:IsBoolean(value) abort
88     return type(a:value) is v:t_number && (a:value == 0 || a:value == 1)
89 endfunction
90
91 function! ale#linter#PreProcess(filetype, linter) abort
92     if type(a:linter) isnot v:t_dict
93         throw 'The linter object must be a Dictionary'
94     endif
95
96     let l:obj = {
97     \   'name': get(a:linter, 'name'),
98     \   'lsp': get(a:linter, 'lsp', ''),
99     \}
100
101     if type(l:obj.name) isnot v:t_string
102         throw '`name` must be defined to name the linter'
103     endif
104
105     let l:needs_address = l:obj.lsp is# 'socket'
106     let l:needs_executable = l:obj.lsp isnot# 'socket'
107     let l:needs_command = l:obj.lsp isnot# 'socket'
108     let l:needs_lsp_details = !empty(l:obj.lsp)
109
110     if empty(l:obj.lsp)
111         let l:obj.callback = get(a:linter, 'callback')
112
113         if !s:IsCallback(l:obj.callback)
114             throw '`callback` must be defined with a callback to accept output'
115         endif
116     endif
117
118     if index(['', 'socket', 'stdio', 'tsserver'], l:obj.lsp) < 0
119         throw '`lsp` must be either `''lsp''`, `''stdio''`, `''socket''` or `''tsserver''` if defined'
120     endif
121
122     if !l:needs_executable
123         if has_key(a:linter, 'executable')
124             throw '`executable` cannot be used when lsp == ''socket'''
125         endif
126     elseif has_key(a:linter, 'executable')
127         let l:obj.executable = a:linter.executable
128
129         if type(l:obj.executable) isnot v:t_string
130         \&& type(l:obj.executable) isnot v:t_func
131             throw '`executable` must be a String or Function if defined'
132         endif
133     else
134         throw '`executable` must be defined'
135     endif
136
137     if !l:needs_command
138         if has_key(a:linter, 'command')
139             throw '`command` cannot be used when lsp == ''socket'''
140         endif
141     elseif has_key(a:linter, 'command')
142         let l:obj.command = a:linter.command
143
144         if type(l:obj.command) isnot v:t_string
145         \&& type(l:obj.command) isnot v:t_func
146             throw '`command` must be a String or Function if defined'
147         endif
148     else
149         throw '`command` must be defined'
150     endif
151
152     if !l:needs_address
153         if has_key(a:linter, 'address')
154             throw '`address` cannot be used when lsp != ''socket'''
155         endif
156     elseif has_key(a:linter, 'address')
157         if type(a:linter.address) isnot v:t_string
158         \&& type(a:linter.address) isnot v:t_func
159             throw '`address` must be a String or Function if defined'
160         endif
161
162         let l:obj.address = a:linter.address
163
164         if has_key(a:linter, 'cwd')
165             throw '`cwd` makes no sense for socket LSP connections'
166         endif
167     else
168         throw '`address` must be defined for getting the LSP address'
169     endif
170
171     if has_key(a:linter, 'cwd')
172         let l:obj.cwd = a:linter.cwd
173
174         if type(l:obj.cwd) isnot v:t_string
175         \&& type(l:obj.cwd) isnot v:t_func
176             throw '`cwd` must be a String or Function if defined'
177         endif
178     endif
179
180     if l:needs_lsp_details
181         " Default to using the filetype as the language.
182         let l:obj.language = get(a:linter, 'language', a:filetype)
183
184         if type(l:obj.language) isnot v:t_string
185         \&& type(l:obj.language) isnot v:t_func
186             throw '`language` must be a String or Function if defined'
187         endif
188
189         if has_key(a:linter, 'project_root')
190             let l:obj.project_root = a:linter.project_root
191
192             if type(l:obj.project_root) isnot v:t_string
193             \&& type(l:obj.project_root) isnot v:t_func
194                 throw '`project_root` must be a String or Function'
195             endif
196         else
197             throw '`project_root` must be defined for LSP linters'
198         endif
199
200         if has_key(a:linter, 'completion_filter')
201             let l:obj.completion_filter = a:linter.completion_filter
202
203             if !s:IsCallback(l:obj.completion_filter)
204                 throw '`completion_filter` must be a callback'
205             endif
206         endif
207
208         if has_key(a:linter, 'initialization_options')
209             let l:obj.initialization_options = a:linter.initialization_options
210
211             if type(l:obj.initialization_options) isnot v:t_dict
212             \&& type(l:obj.initialization_options) isnot v:t_func
213                 throw '`initialization_options` must be a Dictionary or Function if defined'
214             endif
215         endif
216
217         if has_key(a:linter, 'lsp_config')
218             if type(a:linter.lsp_config) isnot v:t_dict
219             \&& type(a:linter.lsp_config) isnot v:t_func
220                 throw '`lsp_config` must be a Dictionary or Function if defined'
221             endif
222
223             let l:obj.lsp_config = a:linter.lsp_config
224         endif
225     endif
226
227     let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout')
228
229     if type(l:obj.output_stream) isnot v:t_string
230     \|| index(['stdout', 'stderr', 'both'], l:obj.output_stream) < 0
231         throw "`output_stream` must be 'stdout', 'stderr', or 'both'"
232     endif
233
234     " An option indicating that this linter should only be run against the
235     " file on disk.
236     let l:obj.lint_file = get(a:linter, 'lint_file', 0)
237
238     if !s:IsBoolean(l:obj.lint_file) && type(l:obj.lint_file) isnot v:t_func
239         throw '`lint_file` must be `0`, `1`, or a Function'
240     endif
241
242     " An option indicating that the buffer should be read.
243     let l:obj.read_buffer = get(a:linter, 'read_buffer', 1)
244
245     if !s:IsBoolean(l:obj.read_buffer)
246         throw '`read_buffer` must be `0` or `1`'
247     endif
248
249     let l:obj.aliases = get(a:linter, 'aliases', [])
250
251     if type(l:obj.aliases) isnot v:t_list
252     \|| len(filter(copy(l:obj.aliases), 'type(v:val) isnot v:t_string')) > 0
253         throw '`aliases` must be a List of String values'
254     endif
255
256     return l:obj
257 endfunction
258
259 function! ale#linter#Define(filetype, linter) abort
260     " This command will throw from the sandbox.
261     let &l:equalprg=&l:equalprg
262
263     let l:new_linter = ale#linter#PreProcess(a:filetype, a:linter)
264
265     if !has_key(s:linters, a:filetype)
266         let s:linters[a:filetype] = []
267     endif
268
269     " Remove previously defined linters with the same name.
270     call filter(s:linters[a:filetype], 'v:val.name isnot# a:linter.name')
271     call add(s:linters[a:filetype], l:new_linter)
272 endfunction
273
274 " Prevent any linters from being loaded for a given filetype.
275 function! ale#linter#PreventLoading(filetype) abort
276     let s:runtime_loaded_map[a:filetype] = 1
277 endfunction
278
279 function! ale#linter#GetAll(filetypes) abort
280     " Don't return linters in the sandbox.
281     " Otherwise a sandboxed script could modify them.
282     if ale#util#InSandbox()
283         return []
284     endif
285
286     let l:combined_linters = []
287
288     for l:filetype in a:filetypes
289         " Load linters from runtimepath if we haven't done that yet.
290         if !has_key(s:runtime_loaded_map, l:filetype)
291             execute 'silent! runtime! ale_linters/' . l:filetype . '/*.vim'
292
293             let s:runtime_loaded_map[l:filetype] = 1
294         endif
295
296         call extend(l:combined_linters, get(s:linters, l:filetype, []))
297     endfor
298
299     return l:combined_linters
300 endfunction
301
302 function! s:GetAliasedFiletype(original_filetype) abort
303     let l:buffer_aliases = get(b:, 'ale_linter_aliases', {})
304
305     " b:ale_linter_aliases can be set to a List or String.
306     if type(l:buffer_aliases) is v:t_list
307     \|| type(l:buffer_aliases) is v:t_string
308         return l:buffer_aliases
309     endif
310
311     " Check for aliased filetypes first in a buffer variable,
312     " then the global variable,
313     " then in the default mapping,
314     " otherwise use the original filetype.
315     for l:dict in [
316     \   l:buffer_aliases,
317     \   g:ale_linter_aliases,
318     \   s:default_ale_linter_aliases,
319     \]
320         if has_key(l:dict, a:original_filetype)
321             return l:dict[a:original_filetype]
322         endif
323     endfor
324
325     return a:original_filetype
326 endfunction
327
328 function! ale#linter#ResolveFiletype(original_filetype) abort
329     let l:filetype = s:GetAliasedFiletype(a:original_filetype)
330
331     if type(l:filetype) isnot v:t_list
332         return [l:filetype]
333     endif
334
335     return l:filetype
336 endfunction
337
338 function! s:GetLinterNames(original_filetype) abort
339     let l:buffer_ale_linters = get(b:, 'ale_linters', {})
340
341     " b:ale_linters can be set to 'all'
342     if l:buffer_ale_linters is# 'all'
343         return 'all'
344     endif
345
346     " b:ale_linters can be set to a List.
347     if type(l:buffer_ale_linters) is v:t_list
348         return l:buffer_ale_linters
349     endif
350
351     " Try to get a buffer-local setting for the filetype
352     if has_key(l:buffer_ale_linters, a:original_filetype)
353         return l:buffer_ale_linters[a:original_filetype]
354     endif
355
356     " Try to get a global setting for the filetype
357     if has_key(g:ale_linters, a:original_filetype)
358         return g:ale_linters[a:original_filetype]
359     endif
360
361     " If the user has configured ALE to only enable linters explicitly, then
362     " don't enable any linters by default.
363     if g:ale_linters_explicit
364         return []
365     endif
366
367     " Try to get a default setting for the filetype
368     if has_key(s:default_ale_linters, a:original_filetype)
369         return s:default_ale_linters[a:original_filetype]
370     endif
371
372     return 'all'
373 endfunction
374
375 function! ale#linter#Get(original_filetypes) abort
376     let l:possibly_duplicated_linters = []
377
378     " Handle dot-separated filetypes.
379     for l:original_filetype in split(a:original_filetypes, '\.')
380         let l:filetype = ale#linter#ResolveFiletype(l:original_filetype)
381         let l:linter_names = s:GetLinterNames(l:original_filetype)
382         let l:all_linters = ale#linter#GetAll(l:filetype)
383         let l:filetype_linters = []
384
385         if type(l:linter_names) is v:t_string && l:linter_names is# 'all'
386             let l:filetype_linters = l:all_linters
387         elseif type(l:linter_names) is v:t_list
388             " Select only the linters we or the user has specified.
389             for l:linter in l:all_linters
390                 let l:name_list = [l:linter.name] + l:linter.aliases
391
392                 for l:name in l:name_list
393                     if index(l:linter_names, l:name) >= 0
394                         call add(l:filetype_linters, l:linter)
395                         break
396                     endif
397                 endfor
398             endfor
399         endif
400
401         call extend(l:possibly_duplicated_linters, l:filetype_linters)
402     endfor
403
404     let l:name_list = []
405     let l:combined_linters = []
406
407     " Make sure we override linters so we don't get two with the same name,
408     " like 'eslint' for both 'javascript' and 'typescript'
409     "
410     " Note that the reverse calls here modify the List variables.
411     for l:linter in reverse(l:possibly_duplicated_linters)
412         if index(l:name_list, l:linter.name) < 0
413             call add(l:name_list, l:linter.name)
414             call add(l:combined_linters, l:linter)
415         endif
416     endfor
417
418     return reverse(l:combined_linters)
419 endfunction
420
421 " Given a buffer and linter, get the executable String for the linter.
422 function! ale#linter#GetExecutable(buffer, linter) abort
423     let l:Executable = a:linter.executable
424
425     return type(l:Executable) is v:t_func
426     \   ? l:Executable(a:buffer)
427     \   : l:Executable
428 endfunction
429
430 function! ale#linter#GetCwd(buffer, linter) abort
431     let l:Cwd = get(a:linter, 'cwd', v:null)
432
433     return type(l:Cwd) is v:t_func ? l:Cwd(a:buffer) : l:Cwd
434 endfunction
435
436 " Given a buffer and linter, get the command String for the linter.
437 function! ale#linter#GetCommand(buffer, linter) abort
438     let l:Command = a:linter.command
439
440     return type(l:Command) is v:t_func ? l:Command(a:buffer) : l:Command
441 endfunction
442
443 " Given a buffer and linter, get the address for connecting to the server.
444 function! ale#linter#GetAddress(buffer, linter) abort
445     let l:Address = a:linter.address
446
447     return type(l:Address) is v:t_func ? l:Address(a:buffer) : l:Address
448 endfunction