mirror of
https://github.com/dense-analysis/ale.git
synced 2025-12-08 05:24:46 +08:00
`lint_file` can now be computed dynamically with a callback function, which can return a deferred result, as per `ale#command#Run`. This allows linters to dynamically switch between checking files on disk, or checking code on the fly. Some tests have been fixed on Windows.
425 lines
13 KiB
VimL
425 lines
13 KiB
VimL
" Author: w0rp <devw0rp@gmail.com>
|
|
" Description: Linter registration and lazy-loading
|
|
" Retrieves linters as requested by the engine, loading them if needed.
|
|
|
|
let s:runtime_loaded_map = {}
|
|
let s:linters = {}
|
|
|
|
" Default filetype aliases.
|
|
" The user defined aliases will be merged with this Dictionary.
|
|
"
|
|
" NOTE: Update the g:ale_linter_aliases documentation when modifying this.
|
|
let s:default_ale_linter_aliases = {
|
|
\ 'Dockerfile': 'dockerfile',
|
|
\ 'csh': 'sh',
|
|
\ 'javascriptreact': ['javascript', 'jsx'],
|
|
\ 'plaintex': 'tex',
|
|
\ 'ps1': 'powershell',
|
|
\ 'rmarkdown': 'r',
|
|
\ 'rmd': 'r',
|
|
\ 'systemverilog': 'verilog',
|
|
\ 'typescriptreact': ['typescript', 'tsx'],
|
|
\ 'verilog_systemverilog': ['verilog_systemverilog', 'verilog'],
|
|
\ 'vimwiki': 'markdown',
|
|
\ 'vue': ['vue', 'javascript'],
|
|
\ 'xsd': ['xsd', 'xml'],
|
|
\ 'xslt': ['xslt', 'xml'],
|
|
\ 'zsh': 'sh',
|
|
\}
|
|
|
|
" Default linters to run for particular filetypes.
|
|
" The user defined linter selections will be merged with this Dictionary.
|
|
"
|
|
" No linters are used for plaintext files by default.
|
|
"
|
|
" Only cargo and rls are enabled for Rust by default.
|
|
" rpmlint is disabled by default because it can result in code execution.
|
|
" hhast is disabled by default because it executes code in the project root.
|
|
"
|
|
" NOTE: Update the g:ale_linters documentation when modifying this.
|
|
let s:default_ale_linters = {
|
|
\ 'csh': ['shell'],
|
|
\ 'elixir': ['credo', 'dialyxir', 'dogma'],
|
|
\ 'go': ['gofmt', 'golint', 'go vet'],
|
|
\ 'hack': ['hack'],
|
|
\ 'help': [],
|
|
\ 'perl': ['perlcritic'],
|
|
\ 'perl6': [],
|
|
\ 'python': ['flake8', 'mypy', 'pylint', 'pyright'],
|
|
\ 'rust': ['cargo', 'rls'],
|
|
\ 'spec': [],
|
|
\ 'text': [],
|
|
\ 'vue': ['eslint', 'vls'],
|
|
\ 'zsh': ['shell'],
|
|
\}
|
|
|
|
" Testing/debugging helper to unload all linters.
|
|
function! ale#linter#Reset() abort
|
|
let s:runtime_loaded_map = {}
|
|
let s:linters = {}
|
|
endfunction
|
|
|
|
" Return a reference to the linters loaded.
|
|
" This is only for tests.
|
|
" Do not call this function.
|
|
function! ale#linter#GetLintersLoaded() abort
|
|
" This command will throw from the sandbox.
|
|
let &l:equalprg=&l:equalprg
|
|
|
|
return s:linters
|
|
endfunction
|
|
|
|
function! s:IsCallback(value) abort
|
|
return type(a:value) is v:t_string || type(a:value) is v:t_func
|
|
endfunction
|
|
|
|
function! s:IsBoolean(value) abort
|
|
return type(a:value) is v:t_number && (a:value == 0 || a:value == 1)
|
|
endfunction
|
|
|
|
function! ale#linter#PreProcess(filetype, linter) abort
|
|
if type(a:linter) isnot v:t_dict
|
|
throw 'The linter object must be a Dictionary'
|
|
endif
|
|
|
|
let l:obj = {
|
|
\ 'name': get(a:linter, 'name'),
|
|
\ 'lsp': get(a:linter, 'lsp', ''),
|
|
\}
|
|
|
|
if type(l:obj.name) isnot v:t_string
|
|
throw '`name` must be defined to name the linter'
|
|
endif
|
|
|
|
let l:needs_address = l:obj.lsp is# 'socket'
|
|
let l:needs_executable = l:obj.lsp isnot# 'socket'
|
|
let l:needs_command = l:obj.lsp isnot# 'socket'
|
|
let l:needs_lsp_details = !empty(l:obj.lsp)
|
|
|
|
if empty(l:obj.lsp)
|
|
let l:obj.callback = get(a:linter, 'callback')
|
|
|
|
if !s:IsCallback(l:obj.callback)
|
|
throw '`callback` must be defined with a callback to accept output'
|
|
endif
|
|
endif
|
|
|
|
if index(['', 'socket', 'stdio', 'tsserver'], l:obj.lsp) < 0
|
|
throw '`lsp` must be either `''lsp''`, `''stdio''`, `''socket''` or `''tsserver''` if defined'
|
|
endif
|
|
|
|
if !l:needs_executable
|
|
if has_key(a:linter, 'executable')
|
|
throw '`executable` cannot be used when lsp == ''socket'''
|
|
endif
|
|
elseif has_key(a:linter, 'executable')
|
|
let l:obj.executable = a:linter.executable
|
|
|
|
if type(l:obj.executable) isnot v:t_string
|
|
\&& type(l:obj.executable) isnot v:t_func
|
|
throw '`executable` must be a String or Function if defined'
|
|
endif
|
|
else
|
|
throw '`executable` must be defined'
|
|
endif
|
|
|
|
if !l:needs_command
|
|
if has_key(a:linter, 'command')
|
|
throw '`command` cannot be used when lsp == ''socket'''
|
|
endif
|
|
elseif has_key(a:linter, 'command')
|
|
let l:obj.command = a:linter.command
|
|
|
|
if type(l:obj.command) isnot v:t_string
|
|
\&& type(l:obj.command) isnot v:t_func
|
|
throw '`command` must be a String or Function if defined'
|
|
endif
|
|
else
|
|
throw '`command` must be defined'
|
|
endif
|
|
|
|
if !l:needs_address
|
|
if has_key(a:linter, 'address')
|
|
throw '`address` cannot be used when lsp != ''socket'''
|
|
endif
|
|
elseif has_key(a:linter, 'address')
|
|
if type(a:linter.address) isnot v:t_string
|
|
\&& type(a:linter.address) isnot v:t_func
|
|
throw '`address` must be a String or Function if defined'
|
|
endif
|
|
|
|
let l:obj.address = a:linter.address
|
|
else
|
|
throw '`address` must be defined for getting the LSP address'
|
|
endif
|
|
|
|
if l:needs_lsp_details
|
|
" Default to using the filetype as the language.
|
|
let l:obj.language = get(a:linter, 'language', a:filetype)
|
|
|
|
if type(l:obj.language) isnot v:t_string
|
|
\&& type(l:obj.language) isnot v:t_func
|
|
throw '`language` must be a String or Funcref if defined'
|
|
endif
|
|
|
|
if has_key(a:linter, 'project_root')
|
|
let l:obj.project_root = a:linter.project_root
|
|
|
|
if type(l:obj.project_root) isnot v:t_string
|
|
\&& type(l:obj.project_root) isnot v:t_func
|
|
throw '`project_root` must be a String or Function'
|
|
endif
|
|
else
|
|
throw '`project_root` must be defined for LSP linters'
|
|
endif
|
|
|
|
if has_key(a:linter, 'completion_filter')
|
|
let l:obj.completion_filter = a:linter.completion_filter
|
|
|
|
if !s:IsCallback(l:obj.completion_filter)
|
|
throw '`completion_filter` must be a callback'
|
|
endif
|
|
endif
|
|
|
|
if has_key(a:linter, 'initialization_options')
|
|
let l:obj.initialization_options = a:linter.initialization_options
|
|
|
|
if type(l:obj.initialization_options) isnot v:t_dict
|
|
\&& type(l:obj.initialization_options) isnot v:t_func
|
|
throw '`initialization_options` must be a Dictionary or Function if defined'
|
|
endif
|
|
endif
|
|
|
|
if has_key(a:linter, 'lsp_config')
|
|
if type(a:linter.lsp_config) isnot v:t_dict
|
|
\&& type(a:linter.lsp_config) isnot v:t_func
|
|
throw '`lsp_config` must be a Dictionary or Function if defined'
|
|
endif
|
|
|
|
let l:obj.lsp_config = a:linter.lsp_config
|
|
endif
|
|
endif
|
|
|
|
let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout')
|
|
|
|
if type(l:obj.output_stream) isnot v:t_string
|
|
\|| index(['stdout', 'stderr', 'both'], l:obj.output_stream) < 0
|
|
throw "`output_stream` must be 'stdout', 'stderr', or 'both'"
|
|
endif
|
|
|
|
" An option indicating that this linter should only be run against the
|
|
" file on disk.
|
|
let l:obj.lint_file = get(a:linter, 'lint_file', 0)
|
|
|
|
if !s:IsBoolean(l:obj.lint_file) && type(l:obj.lint_file) isnot v:t_func
|
|
throw '`lint_file` must be `0`, `1`, or a Function'
|
|
endif
|
|
|
|
" An option indicating that the buffer should be read.
|
|
let l:obj.read_buffer = get(a:linter, 'read_buffer', 1)
|
|
|
|
if !s:IsBoolean(l:obj.read_buffer)
|
|
throw '`read_buffer` must be `0` or `1`'
|
|
endif
|
|
|
|
let l:obj.aliases = get(a:linter, 'aliases', [])
|
|
|
|
if type(l:obj.aliases) isnot v:t_list
|
|
\|| len(filter(copy(l:obj.aliases), 'type(v:val) isnot v:t_string')) > 0
|
|
throw '`aliases` must be a List of String values'
|
|
endif
|
|
|
|
return l:obj
|
|
endfunction
|
|
|
|
function! ale#linter#Define(filetype, linter) abort
|
|
" This command will throw from the sandbox.
|
|
let &l:equalprg=&l:equalprg
|
|
|
|
let l:new_linter = ale#linter#PreProcess(a:filetype, a:linter)
|
|
|
|
if !has_key(s:linters, a:filetype)
|
|
let s:linters[a:filetype] = []
|
|
endif
|
|
|
|
" Remove previously defined linters with the same name.
|
|
call filter(s:linters[a:filetype], 'v:val.name isnot# a:linter.name')
|
|
call add(s:linters[a:filetype], l:new_linter)
|
|
endfunction
|
|
|
|
" Prevent any linters from being loaded for a given filetype.
|
|
function! ale#linter#PreventLoading(filetype) abort
|
|
let s:runtime_loaded_map[a:filetype] = 1
|
|
endfunction
|
|
|
|
function! ale#linter#GetAll(filetypes) abort
|
|
" Don't return linters in the sandbox.
|
|
" Otherwise a sandboxed script could modify them.
|
|
if ale#util#InSandbox()
|
|
return []
|
|
endif
|
|
|
|
let l:combined_linters = []
|
|
|
|
for l:filetype in a:filetypes
|
|
" Load linters from runtimepath if we haven't done that yet.
|
|
if !has_key(s:runtime_loaded_map, l:filetype)
|
|
execute 'silent! runtime! ale_linters/' . l:filetype . '/*.vim'
|
|
|
|
let s:runtime_loaded_map[l:filetype] = 1
|
|
endif
|
|
|
|
call extend(l:combined_linters, get(s:linters, l:filetype, []))
|
|
endfor
|
|
|
|
return l:combined_linters
|
|
endfunction
|
|
|
|
function! s:GetAliasedFiletype(original_filetype) abort
|
|
let l:buffer_aliases = get(b:, 'ale_linter_aliases', {})
|
|
|
|
" b:ale_linter_aliases can be set to a List or String.
|
|
if type(l:buffer_aliases) is v:t_list
|
|
\|| type(l:buffer_aliases) is v:t_string
|
|
return l:buffer_aliases
|
|
endif
|
|
|
|
" Check for aliased filetypes first in a buffer variable,
|
|
" then the global variable,
|
|
" then in the default mapping,
|
|
" otherwise use the original filetype.
|
|
for l:dict in [
|
|
\ l:buffer_aliases,
|
|
\ g:ale_linter_aliases,
|
|
\ s:default_ale_linter_aliases,
|
|
\]
|
|
if has_key(l:dict, a:original_filetype)
|
|
return l:dict[a:original_filetype]
|
|
endif
|
|
endfor
|
|
|
|
return a:original_filetype
|
|
endfunction
|
|
|
|
function! ale#linter#ResolveFiletype(original_filetype) abort
|
|
let l:filetype = s:GetAliasedFiletype(a:original_filetype)
|
|
|
|
if type(l:filetype) isnot v:t_list
|
|
return [l:filetype]
|
|
endif
|
|
|
|
return l:filetype
|
|
endfunction
|
|
|
|
function! s:GetLinterNames(original_filetype) abort
|
|
let l:buffer_ale_linters = get(b:, 'ale_linters', {})
|
|
|
|
" b:ale_linters can be set to 'all'
|
|
if l:buffer_ale_linters is# 'all'
|
|
return 'all'
|
|
endif
|
|
|
|
" b:ale_linters can be set to a List.
|
|
if type(l:buffer_ale_linters) is v:t_list
|
|
return l:buffer_ale_linters
|
|
endif
|
|
|
|
" Try to get a buffer-local setting for the filetype
|
|
if has_key(l:buffer_ale_linters, a:original_filetype)
|
|
return l:buffer_ale_linters[a:original_filetype]
|
|
endif
|
|
|
|
" Try to get a global setting for the filetype
|
|
if has_key(g:ale_linters, a:original_filetype)
|
|
return g:ale_linters[a:original_filetype]
|
|
endif
|
|
|
|
" If the user has configured ALE to only enable linters explicitly, then
|
|
" don't enable any linters by default.
|
|
if g:ale_linters_explicit
|
|
return []
|
|
endif
|
|
|
|
" Try to get a default setting for the filetype
|
|
if has_key(s:default_ale_linters, a:original_filetype)
|
|
return s:default_ale_linters[a:original_filetype]
|
|
endif
|
|
|
|
return 'all'
|
|
endfunction
|
|
|
|
function! ale#linter#Get(original_filetypes) abort
|
|
let l:possibly_duplicated_linters = []
|
|
|
|
" Handle dot-separated filetypes.
|
|
for l:original_filetype in split(a:original_filetypes, '\.')
|
|
let l:filetype = ale#linter#ResolveFiletype(l:original_filetype)
|
|
let l:linter_names = s:GetLinterNames(l:original_filetype)
|
|
let l:all_linters = ale#linter#GetAll(l:filetype)
|
|
let l:filetype_linters = []
|
|
|
|
if type(l:linter_names) is v:t_string && l:linter_names is# 'all'
|
|
let l:filetype_linters = l:all_linters
|
|
elseif type(l:linter_names) is v:t_list
|
|
" Select only the linters we or the user has specified.
|
|
for l:linter in l:all_linters
|
|
let l:name_list = [l:linter.name] + l:linter.aliases
|
|
|
|
for l:name in l:name_list
|
|
if index(l:linter_names, l:name) >= 0
|
|
call add(l:filetype_linters, l:linter)
|
|
break
|
|
endif
|
|
endfor
|
|
endfor
|
|
endif
|
|
|
|
call extend(l:possibly_duplicated_linters, l:filetype_linters)
|
|
endfor
|
|
|
|
let l:name_list = []
|
|
let l:combined_linters = []
|
|
|
|
" Make sure we override linters so we don't get two with the same name,
|
|
" like 'eslint' for both 'javascript' and 'typescript'
|
|
"
|
|
" Note that the reverse calls here modify the List variables.
|
|
for l:linter in reverse(l:possibly_duplicated_linters)
|
|
if index(l:name_list, l:linter.name) < 0
|
|
call add(l:name_list, l:linter.name)
|
|
call add(l:combined_linters, l:linter)
|
|
endif
|
|
endfor
|
|
|
|
return reverse(l:combined_linters)
|
|
endfunction
|
|
|
|
" Given a buffer and linter, get the executable String for the linter.
|
|
function! ale#linter#GetExecutable(buffer, linter) abort
|
|
let l:Executable = a:linter.executable
|
|
|
|
return type(l:Executable) is v:t_func
|
|
\ ? l:Executable(a:buffer)
|
|
\ : l:Executable
|
|
endfunction
|
|
|
|
" Given a buffer and linter, get the command String for the linter.
|
|
function! ale#linter#GetCommand(buffer, linter) abort
|
|
let l:Command = a:linter.command
|
|
|
|
return type(l:Command) is v:t_func ? l:Command(a:buffer) : l:Command
|
|
endfunction
|
|
|
|
" Given a buffer and linter, get the address for connecting to the server.
|
|
function! ale#linter#GetAddress(buffer, linter) abort
|
|
let l:Address = a:linter.address
|
|
|
|
return type(l:Address) is v:t_func ? l:Address(a:buffer) : l:Address
|
|
endfunction
|
|
|
|
function! ale#linter#GetLanguage(buffer, linter) abort
|
|
let l:Language = a:linter.language
|
|
|
|
return type(l:Language) is v:t_func ? l:Language(a:buffer) : l:Language
|
|
endfunction
|