mirror of
https://github.com/neovim/neovim.git
synced 2026-07-09 10:59:38 +00:00
fix(vim.fs): dir(), find() error-reporting
This commit is contained in:
@@ -2727,10 +2727,8 @@ vim.fs.find({names}, {opts}) *vim.fs.find()*
|
||||
|
||||
Return (multiple): ~
|
||||
(`string[]`) matches Normalized paths |vim.fs.normalize()| of all
|
||||
matching items
|
||||
(`table[]`) errors list of errors encountered during the search
|
||||
• {err} (`string`) Error message
|
||||
• {path} (`string`) Path at which error was encountered
|
||||
matching items.
|
||||
(`string[]`) errors Errors for directories that could not be searched.
|
||||
|
||||
vim.fs.joinpath({...}) *vim.fs.joinpath()*
|
||||
Concatenates partial paths (one absolute or relative path followed by zero
|
||||
|
||||
@@ -187,7 +187,6 @@ end
|
||||
--- (default: `false`)
|
||||
--- @field follow? boolean
|
||||
|
||||
---@alias Iterator fun(): string?, string?, string?
|
||||
|
||||
--- Gets an iterator over items found in `path` (normalized via |vim.fs.normalize()|).
|
||||
---
|
||||
@@ -210,12 +209,10 @@ end
|
||||
---@since 10
|
||||
---@param path (string) Directory to iterate over, normalized via |vim.fs.normalize()|.
|
||||
---@param opts? vim.fs.dir.Opts Optional keyword arguments:
|
||||
---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type", along
|
||||
--- with an optional value "err"
|
||||
--- "name" is the basename of the item relative to {path}.
|
||||
--- "type" is one of the following:
|
||||
--- "file", "directory", "link", "fifo", "socket", "char", "block", "unknown".
|
||||
--- "err" is a string with the format {errname}: {message}
|
||||
---@return fun(): string?, string?, string? # Iterator over items in {path}. Each iteration yields three values: name, type, err.
|
||||
--- - name: Basename of the item relative to {path}.
|
||||
--- - type: One of: "file", "directory", "link", "fifo", "socket", "char", "block", "unknown".
|
||||
--- - err: Error string, or nil.
|
||||
---@return string? err Error encountered while scanning the base directory
|
||||
function M.dir(path, opts)
|
||||
opts = opts or {}
|
||||
@@ -304,15 +301,6 @@ end
|
||||
--- (default: `false`)
|
||||
--- @field follow? boolean
|
||||
|
||||
--- @class vim.fs.find.Error
|
||||
--- @inlinedoc
|
||||
---
|
||||
--- Path at which error was encountered
|
||||
--- @field path string
|
||||
---
|
||||
--- Error message
|
||||
--- @field err string
|
||||
|
||||
--- Find files or directories (or other items as specified by `opts.type`) in the given path.
|
||||
---
|
||||
--- Finds items given in {names} starting from {path}. If {upward} is "true"
|
||||
@@ -350,8 +338,8 @@ end
|
||||
--- The function should return `true` if the given item is considered a match.
|
||||
---
|
||||
---@param opts? vim.fs.find.Opts Optional keyword arguments:
|
||||
---@return string[] matches # Normalized paths |vim.fs.normalize()| of all matching items
|
||||
---@return vim.fs.find.Error[] errors # list of errors encountered during the search
|
||||
---@return string[] matches # Normalized paths |vim.fs.normalize()| of all matching items.
|
||||
---@return string[] errors # Errors collected while searching.
|
||||
function M.find(names, opts)
|
||||
opts = opts or {}
|
||||
vim.validate('names', names, { 'string', 'table', 'function' })
|
||||
@@ -371,7 +359,7 @@ function M.find(names, opts)
|
||||
local limit = opts.limit or 1
|
||||
|
||||
local matches = {} --- @type string[]
|
||||
local errors = {} --- @type vim.fs.find.Error[]
|
||||
local errors = {} --- @type string[]
|
||||
|
||||
local function add(match)
|
||||
matches[#matches + 1] = M.normalize(match)
|
||||
@@ -388,12 +376,12 @@ function M.find(names, opts)
|
||||
local t = {}
|
||||
local it, base_err = M.dir(p)
|
||||
if base_err ~= nil then
|
||||
table.insert(errors, { path = p, err = base_err })
|
||||
table.insert(errors, base_err)
|
||||
end
|
||||
for name, type, err in it do
|
||||
local f = M.joinpath(p, name)
|
||||
if err ~= nil then
|
||||
table.insert(errors, { path = f, err = err })
|
||||
table.insert(errors, err)
|
||||
end
|
||||
if (not opts.type or opts.type == type) and names(name, p) then
|
||||
table.insert(t, f)
|
||||
@@ -404,9 +392,9 @@ function M.find(names, opts)
|
||||
else
|
||||
test = function(p)
|
||||
local t = {} --- @type string[]
|
||||
local _, base_err = M.dir(p)
|
||||
if base_err ~= nil then -- Check if the directory itself is readable
|
||||
table.insert(errors, { path = p, err = base_err })
|
||||
local ok, aerr = uv.fs_access(p, 'R') -- Check if the directory itself is readable
|
||||
if not ok then
|
||||
table.insert(errors, aerr)
|
||||
return t
|
||||
end
|
||||
for _, name in ipairs(names) do
|
||||
@@ -448,12 +436,12 @@ function M.find(names, opts)
|
||||
|
||||
local it, base_err = M.dir(dir)
|
||||
if base_err ~= nil then
|
||||
table.insert(errors, { path = dir, err = base_err })
|
||||
table.insert(errors, base_err)
|
||||
end
|
||||
for other, type_, err in it do
|
||||
local f = M.joinpath(dir, other)
|
||||
if err ~= nil then
|
||||
table.insert(errors, { path = f, err = err })
|
||||
table.insert(errors, err)
|
||||
end
|
||||
if type(names) == 'function' then
|
||||
if (not opts.type or opts.type == type_) and names(other, dir) then
|
||||
|
||||
@@ -288,56 +288,53 @@ describe('vim.fs', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('error handling', function()
|
||||
before_each(function()
|
||||
mkdir('testdir')
|
||||
mkdir('testdir/noaccess')
|
||||
mkdir('testdir/a')
|
||||
mkdir('testdir/a/noaccess')
|
||||
end)
|
||||
after_each(function()
|
||||
it('reports errors', function()
|
||||
mkdir('testdir')
|
||||
mkdir('testdir/noaccess')
|
||||
mkdir('testdir/a')
|
||||
mkdir('testdir/a/noaccess')
|
||||
finally(function()
|
||||
rmdir('testdir')
|
||||
end)
|
||||
|
||||
it('returns error for unreadable root directory', function()
|
||||
local result = exec_lua(function()
|
||||
-- Unreadable root directory: error is the second return value.
|
||||
eq(
|
||||
'ENOENT: no such file or directory: does-not-exist',
|
||||
exec_lua(function()
|
||||
local _, err = vim.fs.dir('does-not-exist')
|
||||
return err
|
||||
end)
|
||||
eq('ENOENT: no such file or directory: does-not-exist', result)
|
||||
end)
|
||||
)
|
||||
|
||||
it('returns iterator errors for unreadable child directories', function()
|
||||
local result = exec_lua(function()
|
||||
-- Stub fs_scandir since chmod doesn't work reliably on Windows.
|
||||
local orig_scandir = vim.uv.fs_scandir
|
||||
vim.uv.fs_scandir = function(path, ...)
|
||||
if path == 'testdir/noaccess' then
|
||||
return nil, 'EACCES: permission denied: testdir/noaccess'
|
||||
end
|
||||
return orig_scandir(path, ...)
|
||||
-- Unreadable child directory: error is the iterator's third value.
|
||||
local result = exec_lua(function()
|
||||
-- Stub fs_scandir since chmod doesn't work reliably on Windows.
|
||||
local orig_scandir = vim.uv.fs_scandir
|
||||
vim.uv.fs_scandir = function(path, ...)
|
||||
if path == 'testdir/noaccess' then
|
||||
return nil, 'EACCES: permission denied: testdir/noaccess'
|
||||
end
|
||||
return orig_scandir(path, ...)
|
||||
end
|
||||
|
||||
local ok, errors_or_err = pcall(function()
|
||||
local errors = {} ---@type table<string, string>
|
||||
local it = vim.fs.dir('testdir', { depth = 2 })
|
||||
for f, _, err in it do
|
||||
errors[f] = err
|
||||
end
|
||||
return errors
|
||||
end)
|
||||
|
||||
vim.uv.fs_scandir = orig_scandir
|
||||
|
||||
if not ok then
|
||||
error(errors_or_err)
|
||||
local ok, errors_or_err = pcall(function()
|
||||
local errors = {} ---@type table<string, string>
|
||||
for f, _, err in vim.fs.dir('testdir', { depth = 2 }) do
|
||||
errors[f] = err
|
||||
end
|
||||
return errors_or_err
|
||||
return errors
|
||||
end)
|
||||
eq('EACCES: permission denied: testdir/noaccess', result['noaccess'])
|
||||
-- This should be nil because with a depth of 2 we shouldn't be scanning it
|
||||
eq(nil, result['a/noaccess'])
|
||||
|
||||
vim.uv.fs_scandir = orig_scandir
|
||||
|
||||
if not ok then
|
||||
error(errors_or_err)
|
||||
end
|
||||
return errors_or_err
|
||||
end)
|
||||
eq('EACCES: permission denied: testdir/noaccess', result['noaccess'])
|
||||
-- nil: with depth=2 we don't scan testdir/a/noaccess.
|
||||
eq(nil, result['a/noaccess'])
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -447,111 +444,83 @@ describe('vim.fs', function()
|
||||
)
|
||||
end)
|
||||
|
||||
describe('error handling', function()
|
||||
before_each(function()
|
||||
mkdir('testdir')
|
||||
mkdir('testdir/noaccess')
|
||||
mkdir('testdir/a')
|
||||
mkdir('testdir/a/noaccess')
|
||||
t.write_file('testdir/a/match.lua', '')
|
||||
|
||||
exec_lua(function()
|
||||
-- Stub fs_scandir since chmod doesn't work reliably on Windows.
|
||||
local blocked_prefixes = { 'testdir/noaccess', 'testdir/a/noaccess' }
|
||||
local function is_blocked(path)
|
||||
for _, prefix in ipairs(blocked_prefixes) do
|
||||
if path == prefix or vim.startswith(path, prefix .. '/') then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
_G.__orig_fs_scandir = vim.uv.fs_scandir
|
||||
vim.uv.fs_scandir = function(path, ...)
|
||||
if is_blocked(path) then
|
||||
return nil, 'EACCES: permission denied: ' .. path
|
||||
end
|
||||
return _G.__orig_fs_scandir(path, ...)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
exec_lua(function()
|
||||
vim.uv.fs_scandir = _G.__orig_fs_scandir
|
||||
_G.__orig_fs_scandir = nil
|
||||
end)
|
||||
it('reports errors', function()
|
||||
mkdir('testdir')
|
||||
mkdir('testdir/noaccess')
|
||||
mkdir('testdir/a')
|
||||
mkdir('testdir/a/noaccess')
|
||||
t.write_file('testdir/a/match.lua', '')
|
||||
finally(function()
|
||||
rmdir('testdir')
|
||||
end)
|
||||
|
||||
it('returns an error for a nonexistent root path', function()
|
||||
local result = exec_lua(function()
|
||||
local matches, errors = vim.fs.find('foo', { path = 'does-not-exist' })
|
||||
return { matches = matches, errors = errors }
|
||||
end)
|
||||
eq({}, result.matches)
|
||||
eq(1, #result.errors)
|
||||
eq('does-not-exist', result.errors[1].path)
|
||||
eq('ENOENT: no such file or directory: does-not-exist', result.errors[1].err)
|
||||
end)
|
||||
|
||||
it('returns an error for an unreadable root path', function()
|
||||
local result = exec_lua(function()
|
||||
local matches, errors = vim.fs.find('foo', { path = 'testdir/noaccess' })
|
||||
return { matches = matches, errors = errors }
|
||||
end)
|
||||
eq({}, result.matches)
|
||||
eq(1, #result.errors)
|
||||
eq('testdir/noaccess', result.errors[1].path)
|
||||
eq('EACCES: permission denied: testdir/noaccess', result.errors[1].err)
|
||||
end)
|
||||
|
||||
it('collects errors for unreadable child directories during downward search', function()
|
||||
local result = exec_lua(function()
|
||||
local matches, errors = vim.fs.find('match.lua', {
|
||||
path = 'testdir',
|
||||
limit = math.huge,
|
||||
type = 'file',
|
||||
})
|
||||
--- @type table<string, string>
|
||||
local err_by_path = {}
|
||||
for _, e in ipairs(errors) do
|
||||
err_by_path[e.path] = e.err
|
||||
-- Scenarios run in a shared setup(clear) so the fs_scandir/fs_access stubs (chmod is unreliable on
|
||||
-- Windows) are installed and restored.
|
||||
local res = exec_lua(function()
|
||||
local orig_scandir = vim.uv.fs_scandir
|
||||
local orig_access = vim.uv.fs_access
|
||||
local function is_blocked(path)
|
||||
for _, prefix in ipairs({ 'testdir/noaccess', 'testdir/a/noaccess' }) do
|
||||
if path == prefix or vim.startswith(path, prefix .. '/') then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return { matches = matches, errors = err_by_path }
|
||||
return false
|
||||
end
|
||||
vim.uv.fs_scandir = function(path, ...)
|
||||
if is_blocked(path) then
|
||||
return nil, 'EACCES: permission denied: ' .. path
|
||||
end
|
||||
return orig_scandir(path, ...)
|
||||
end
|
||||
-- find()'s upward search probes readability via fs_access (not fs_scandir), so stub it too.
|
||||
vim.uv.fs_access = function(path, ...)
|
||||
if is_blocked(path) then
|
||||
return nil, 'EACCES: permission denied: ' .. path
|
||||
end
|
||||
return orig_access(path, ...)
|
||||
end
|
||||
|
||||
local ok, out = pcall(function()
|
||||
local r = {}
|
||||
r.nonexistent = { vim.fs.find('foo', { path = 'does-not-exist' }) }
|
||||
r.unreadable_root = { vim.fs.find('foo', { path = 'testdir/noaccess' }) }
|
||||
r.downward =
|
||||
{ vim.fs.find('match.lua', { path = 'testdir', limit = math.huge, type = 'file' }) }
|
||||
r.upward = select(
|
||||
2,
|
||||
vim.fs.find('match.lua', {
|
||||
path = 'testdir/noaccess/x',
|
||||
upward = true,
|
||||
stop = 'testdir',
|
||||
})
|
||||
)
|
||||
return r
|
||||
end)
|
||||
eq(1, #result.matches)
|
||||
eq('EACCES: permission denied: testdir/noaccess', result.errors['testdir/noaccess'])
|
||||
eq('EACCES: permission denied: testdir/a/noaccess', result.errors['testdir/a/noaccess'])
|
||||
|
||||
vim.uv.fs_scandir = orig_scandir
|
||||
vim.uv.fs_access = orig_access
|
||||
if not ok then
|
||||
error(out)
|
||||
end
|
||||
return out
|
||||
end)
|
||||
|
||||
it('still returns matches found despite errors elsewhere in the tree', function()
|
||||
local result = exec_lua(function()
|
||||
local matches, errors = vim.fs.find('match.lua', {
|
||||
path = 'testdir',
|
||||
limit = math.huge,
|
||||
type = 'file',
|
||||
})
|
||||
return { matches = matches, has_errors = #errors > 0 }
|
||||
end)
|
||||
eq(1, #result.matches)
|
||||
eq(true, result.has_errors)
|
||||
end)
|
||||
-- Nonexistent / unreadable root path: no matches, one error.
|
||||
eq({ {}, { 'ENOENT: no such file or directory: does-not-exist' } }, res.nonexistent)
|
||||
eq({ {}, { 'EACCES: permission denied: testdir/noaccess' } }, res.unreadable_root)
|
||||
|
||||
it('reports errors for unreadable directories during upward search', function()
|
||||
local result = exec_lua(function()
|
||||
local matches, errors = vim.fs.find('match.lua', {
|
||||
path = 'testdir/noaccess/x',
|
||||
upward = true,
|
||||
stop = 'testdir',
|
||||
})
|
||||
return { matches = matches, errors = errors }
|
||||
end)
|
||||
eq('EACCES: permission denied: testdir/noaccess/x', result.errors[1].err)
|
||||
eq('EACCES: permission denied: testdir/noaccess', result.errors[2].err)
|
||||
eq('testdir/noaccess/x', result.errors[1].path)
|
||||
eq('testdir/noaccess', result.errors[2].path)
|
||||
end)
|
||||
-- Downward search collects child errors, yet still returns the match found elsewhere.
|
||||
eq(1, #res.downward[1])
|
||||
eq(2, #res.downward[2])
|
||||
eq(true, vim.tbl_contains(res.downward[2], 'EACCES: permission denied: testdir/noaccess'))
|
||||
eq(true, vim.tbl_contains(res.downward[2], 'EACCES: permission denied: testdir/a/noaccess'))
|
||||
|
||||
-- Upward search reports an error for each unreadable ancestor, in traversal order.
|
||||
eq({
|
||||
'EACCES: permission denied: testdir/noaccess/x',
|
||||
'EACCES: permission denied: testdir/noaccess',
|
||||
}, res.upward)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user