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.
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.
5 let s:runtime_loaded_map = {}
8 " Default filetype aliases.
9 " The user defined aliases will be merged with this Dictionary.
11 " NOTE: Update the g:ale_linter_aliases documentation when modifying this.
12 let s:default_ale_linter_aliases = {
13 \ 'Dockerfile': 'dockerfile',
15 \ 'javascriptreact': ['javascript', 'jsx'],
17 \ 'ps1': 'powershell',
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'],
31 " Default linters to run for particular filetypes.
32 " The user defined linter selections will be merged with this Dictionary.
34 " No linters are used for plaintext files by default.
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.
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'],
45 \ 'elixir': ['credo', 'dialyxir', 'dogma'],
46 \ 'go': ['gofmt', 'golangci-lint', 'gopls', 'govet'],
47 \ 'groovy': ['npm-groovy-lint'],
51 \ 'json': ['biome', 'jsonlint', 'spectral', 'vscodejson'],
54 \ 'perl': ['perlcritic'],
56 \ 'python': ['flake8', 'mypy', 'pylint', 'pyright', 'ruff'],
57 \ 'rust': ['analyzer', 'cargo'],
61 \ 'vue': ['eslint', 'vls'],
64 \ 'yaml': ['actionlint', 'spectral', 'yaml-language-server', 'yamllint'],
67 " Testing/debugging helper to unload all linters.
68 function! ale#linter#Reset() abort
69 let s:runtime_loaded_map = {}
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
83 function! s:IsCallback(value) abort
84 return type(a:value) is v:t_string || type(a:value) is v:t_func
87 function! s:IsBoolean(value) abort
88 return type(a:value) is v:t_number && (a:value == 0 || a:value == 1)
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'
97 \ 'name': get(a:linter, 'name'),
98 \ 'lsp': get(a:linter, 'lsp', ''),
101 if type(l:obj.name) isnot v:t_string
102 throw '`name` must be defined to name the linter'
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)
111 let l:obj.callback = get(a:linter, 'callback')
113 if !s:IsCallback(l:obj.callback)
114 throw '`callback` must be defined with a callback to accept output'
118 if index(['', 'socket', 'stdio', 'tsserver'], l:obj.lsp) < 0
119 throw '`lsp` must be either `''lsp''`, `''stdio''`, `''socket''` or `''tsserver''` if defined'
122 if !l:needs_executable
123 if has_key(a:linter, 'executable')
124 throw '`executable` cannot be used when lsp == ''socket'''
126 elseif has_key(a:linter, 'executable')
127 let l:obj.executable = a:linter.executable
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'
134 throw '`executable` must be defined'
138 if has_key(a:linter, 'command')
139 throw '`command` cannot be used when lsp == ''socket'''
141 elseif has_key(a:linter, 'command')
142 let l:obj.command = a:linter.command
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'
149 throw '`command` must be defined'
153 if has_key(a:linter, 'address')
154 throw '`address` cannot be used when lsp != ''socket'''
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'
162 let l:obj.address = a:linter.address
164 if has_key(a:linter, 'cwd')
165 throw '`cwd` makes no sense for socket LSP connections'
168 throw '`address` must be defined for getting the LSP address'
171 if has_key(a:linter, 'cwd')
172 let l:obj.cwd = a:linter.cwd
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'
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)
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'
189 if has_key(a:linter, 'project_root')
190 let l:obj.project_root = a:linter.project_root
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'
197 throw '`project_root` must be defined for LSP linters'
200 if has_key(a:linter, 'completion_filter')
201 let l:obj.completion_filter = a:linter.completion_filter
203 if !s:IsCallback(l:obj.completion_filter)
204 throw '`completion_filter` must be a callback'
208 if has_key(a:linter, 'initialization_options')
209 let l:obj.initialization_options = a:linter.initialization_options
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'
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'
223 let l:obj.lsp_config = a:linter.lsp_config
227 let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout')
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'"
234 " An option indicating that this linter should only be run against the
236 let l:obj.lint_file = get(a:linter, 'lint_file', 0)
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'
242 " An option indicating that the buffer should be read.
243 let l:obj.read_buffer = get(a:linter, 'read_buffer', 1)
245 if !s:IsBoolean(l:obj.read_buffer)
246 throw '`read_buffer` must be `0` or `1`'
249 let l:obj.aliases = get(a:linter, 'aliases', [])
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'
259 function! ale#linter#Define(filetype, linter) abort
260 " This command will throw from the sandbox.
261 let &l:equalprg=&l:equalprg
263 let l:new_linter = ale#linter#PreProcess(a:filetype, a:linter)
265 if !has_key(s:linters, a:filetype)
266 let s:linters[a:filetype] = []
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)
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
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()
286 let l:combined_linters = []
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'
293 let s:runtime_loaded_map[l:filetype] = 1
296 call extend(l:combined_linters, get(s:linters, l:filetype, []))
299 return l:combined_linters
302 function! s:GetAliasedFiletype(original_filetype) abort
303 let l:buffer_aliases = get(b:, 'ale_linter_aliases', {})
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
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.
317 \ g:ale_linter_aliases,
318 \ s:default_ale_linter_aliases,
320 if has_key(l:dict, a:original_filetype)
321 return l:dict[a:original_filetype]
325 return a:original_filetype
328 function! ale#linter#ResolveFiletype(original_filetype) abort
329 let l:filetype = s:GetAliasedFiletype(a:original_filetype)
331 if type(l:filetype) isnot v:t_list
338 function! s:GetLinterNames(original_filetype) abort
339 let l:buffer_ale_linters = get(b:, 'ale_linters', {})
341 " b:ale_linters can be set to 'all'
342 if l:buffer_ale_linters is# 'all'
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
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]
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]
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
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]
375 function! ale#linter#Get(original_filetypes) abort
376 let l:possibly_duplicated_linters = []
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 = []
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
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)
401 call extend(l:possibly_duplicated_linters, l:filetype_linters)
405 let l:combined_linters = []
407 " Make sure we override linters so we don't get two with the same name,
408 " like 'eslint' for both 'javascript' and 'typescript'
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)
418 return reverse(l:combined_linters)
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
425 return type(l:Executable) is v:t_func
426 \ ? l:Executable(a:buffer)
430 function! ale#linter#GetCwd(buffer, linter) abort
431 let l:Cwd = get(a:linter, 'cwd', v:null)
433 return type(l:Cwd) is v:t_func ? l:Cwd(a:buffer) : l:Cwd
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
440 return type(l:Command) is v:t_func ? l:Command(a:buffer) : l:Command
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
447 return type(l:Address) is v:t_func ? l:Address(a:buffer) : l:Address