Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ale_linters/python/pylsp.vim
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function! ale_linters#python#pylsp#GetCwd(buffer) abort
\ 'name': 'pylsp',
\ 'project_root': function('ale#python#FindProjectRoot'),
\}
let l:root = ale#lsp_linter#FindProjectRoot(a:buffer, l:fake_linter)
let l:root = ale#linter#GetRoot(a:buffer, l:fake_linter)

return !empty(l:root) ? l:root : v:null
endfunction
Expand Down
2 changes: 1 addition & 1 deletion ale_linters/python/pyright.vim
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function! ale_linters#python#pyright#GetCwd(buffer) abort
\ 'name': 'pyright',
\ 'project_root': function('ale#python#FindProjectRoot'),
\}
let l:root = ale#lsp_linter#FindProjectRoot(a:buffer, l:fake_linter)
let l:root = ale#linter#GetRoot(a:buffer, l:fake_linter)

return !empty(l:root) ? l:root : v:null
endfunction
Expand Down
2 changes: 1 addition & 1 deletion autoload/ale/assert.vim
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ endfunction
function! ale#assert#LSPProject(expected_root) abort
let l:buffer = bufnr('')
let l:linter = s:GetLinter()
let l:root = ale#lsp_linter#FindProjectRoot(l:buffer, l:linter)
let l:root = ale#linter#GetRoot(l:buffer, l:linter)

AssertEqual a:expected_root, l:root
endfunction
Expand Down
29 changes: 29 additions & 0 deletions autoload/ale/linter.vim
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,32 @@ function! ale#linter#GetAddress(buffer, linter) abort

return type(l:Address) is v:t_func ? l:Address(a:buffer) : l:Address
endfunction

" Get the project root for a linter.
" If |b:ale_root| or |g:ale_root| is set to either a String or a Dict mapping
" linter names to roots or callbacks, return that value immediately. When no
" value is available, fall back to the linter-specific configuration.
function! ale#linter#GetRoot(buffer, linter) abort
let l:buffer_ale_root = getbufvar(a:buffer, 'ale_root', {})

if type(l:buffer_ale_root) is v:t_string
return l:buffer_ale_root
endif

if has_key(l:buffer_ale_root, a:linter.name)
let l:Root = l:buffer_ale_root[a:linter.name]
return type(l:Root) is v:t_func ? l:Root(a:buffer) : l:Root
endif

if has_key(g:ale_root, a:linter.name)
let l:Root = g:ale_root[a:linter.name]
return type(l:Root) is v:t_func ? l:Root(a:buffer) : l:Root
endif

if has_key(a:linter, 'project_root')
let l:Root = a:linter.project_root
return type(l:Root) is v:t_func ? l:Root(a:buffer) : l:Root
endif

Copy link

Copilot AI Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetRoot currently returns an empty string by default when no root is found, which drops the fallback to a linter's existing project_root_callback. Consider invoking a:linter.project_root_callback (via ale#util#GetFunction) when present to maintain the original behavior for linters that define callbacks.

Suggested change
if has_key(a:linter, 'project_root_callback')
let l:Callback = ale#util#GetFunction(a:linter.project_root_callback)
if type(l:Callback) is v:t_func
return l:Callback(a:buffer)
endif
endif

Copilot uses AI. Check for mistakes.
return ''
endfunction
40 changes: 1 addition & 39 deletions autoload/ale/lsp_linter.vim
Original file line number Diff line number Diff line change
Expand Up @@ -296,44 +296,6 @@ function! ale#lsp_linter#GetConfig(buffer, linter) abort
return {}
endfunction

function! ale#lsp_linter#FindProjectRoot(buffer, linter) abort
let l:buffer_ale_root = getbufvar(a:buffer, 'ale_root', {})

if type(l:buffer_ale_root) is v:t_string
return l:buffer_ale_root
endif

" Try to get a buffer-local setting for the root
if has_key(l:buffer_ale_root, a:linter.name)
let l:Root = l:buffer_ale_root[a:linter.name]

if type(l:Root) is v:t_func
return l:Root(a:buffer)
else
return l:Root
endif
endif

" Try to get a global setting for the root
if has_key(g:ale_root, a:linter.name)
let l:Root = g:ale_root[a:linter.name]

if type(l:Root) is v:t_func
return l:Root(a:buffer)
else
return l:Root
endif
endif

" Fall back to the linter-specific configuration
if has_key(a:linter, 'project_root')
let l:Root = a:linter.project_root

return type(l:Root) is v:t_func ? l:Root(a:buffer) : l:Root
endif

return ale#util#GetFunction(a:linter.project_root_callback)(a:buffer)
endfunction

" This function is accessible so tests can call it.
function! ale#lsp_linter#OnInit(linter, details, Callback) abort
Expand Down Expand Up @@ -504,7 +466,7 @@ endfunction
function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort
let l:command = ''
let l:address = ''
let l:root = ale#lsp_linter#FindProjectRoot(a:buffer, a:linter)
let l:root = ale#linter#GetRoot(a:buffer, a:linter)

if empty(l:root) && a:linter.lsp isnot# 'tsserver'
" If there's no project root, then we can't check files with LSP,
Expand Down
6 changes: 6 additions & 0 deletions autoload/ale/python.vim
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ endfunction
" through paths, including the current directory, until no __init__.py files
" is found.
function! ale#python#FindProjectRoot(buffer) abort
let l:root = ale#linter#GetRoot(a:buffer, {'name': 'python'})

if !empty(l:root)
return l:root
endif

let l:ini_root = ale#python#FindProjectRootIni(a:buffer)

if !empty(l:ini_root)
Expand Down
3 changes: 3 additions & 0 deletions doc/ale-python.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ For some linters, ALE will search for a Python project root by looking at the
files in directories on or above where a file being checked is. ALE applies
the following methods, in order:

If |g:ale_root| or |b:ale_root| provides a value, that value is used as the
project root instead and the searching described below is skipped.

1. Find the first directory containing a common Python configuration file.
2. If no configuration file can be found, use the first directory which does
not contain a readable file named `__init__.py`.
Expand Down
17 changes: 9 additions & 8 deletions doc/ale.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2297,17 +2297,18 @@ g:ale_root
Type: |Dictionary| or |String|
Default: `{}`

This option is used to determine the project root for a linter. If the value
is a |Dictionary|, it maps a linter to either a |String| containing the
project root or a |Funcref| to call to look up the root. The |Funcref| is
provided the buffer number as its argument.
This option is used to determine the project root for a linter. When set to a
|String| it will be used for all linters. When set to a |Dictionary|, the
keys are linter names and the values are either |Strings| containing project
roots or |Funcref|s which are passed the buffer number.

The buffer-specific variable may additionally be a string containing the
The buffer-specific variable may additionally be a |String| containing the
project root itself.

If neither variable yields a result, a linter-specific function is invoked to
detect a project root. If this, too, yields no result, and the linter is an
LSP linter, it will not run.
If a value can be found from either variable, ALE uses it directly and skips
searching for a project root. If no value is found, a linter-specific
function is invoked to detect a project root. If this, too, yields no result
and the linter is an LSP linter, it will not run.

*ale-options.save_hidden*
*g:ale_save_hidden*
Expand Down
4 changes: 2 additions & 2 deletions test/test_linter_defintion_processing.vader
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ Execute(PreProcess should allow the `project_root` to be set as a String):
\ 'project_root': '/foo/bar',
\})

AssertEqual '/foo/bar', ale#lsp_linter#FindProjectRoot(0, g:linter)
AssertEqual '/foo/bar', ale#linter#GetRoot(0, g:linter)

Execute(PreProcess should `project_root` be set as a Function):
let g:linter = ale#linter#PreProcess('testft', {
Expand All @@ -418,7 +418,7 @@ Execute(PreProcess should `project_root` be set as a Function):
\ 'project_root': {-> '/foo/bar'},
\})

AssertEqual '/foo/bar', ale#lsp_linter#FindProjectRoot(0, g:linter)
AssertEqual '/foo/bar', ale#linter#GetRoot(0, g:linter)

Execute(PreProcess should complain when `project_root` is invalid):
AssertThrows call ale#linter#PreProcess('testft', {
Expand Down
27 changes: 27 additions & 0 deletions test/test_python_root_option.vader
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Before:
Save g:ale_root
Save b:ale_root
call ale#test#SetDirectory('/testplugin/test')

After:
Restore
call ale#test#RestoreDirectory()

Execute(The global setting is used as the project root):
let g:ale_root = '/foo/python'
call ale#test#SetFilename('test-files/python/no_virtualenv/subdir/foo/bar.py')
AssertEqual '/foo/python', ale#python#FindProjectRoot(bufnr(''))

Execute(The buffer setting overrides the global setting):
let g:ale_root = '/foo/python'
let b:ale_root = '/bar/python'
call ale#test#SetFilename('test-files/python/no_virtualenv/subdir/foo/bar.py')
AssertEqual '/bar/python', ale#python#FindProjectRoot(bufnr(''))

Execute(Fallback to searching when no setting is used):
unlet! g:ale_root
unlet! b:ale_root
call ale#test#SetFilename('test-files/python/no_virtualenv/subdir/foo/bar.py')
AssertEqual \
\ ale#path#Simplify(g:dir . '/../test-files/python/no_virtualenv/subdir'),
\ ale#python#FindProjectRoot(bufnr(''))
Loading