fix(lua): revert vim.tbl_extend behavior change and document it

Problem: vim.tbl_deep_extend had an undocumented feature where arrays
(integer-indexed tables) were not merged but compared literally (used
for merging default and user config, where one list should overwrite the
other completely). Turns out this behavior was relied on in quite a
number of plugins (even though it wasn't a robust solution even for that
use case, since lists of tables (e.g., plugin specs) can be array-like
as well).

Solution: Revert the removal of this special feature. Check for
list-like (contiguous integer indices) instead, as this is closer to the
intent. Document this behavior.
This commit is contained in:
Christian Clason 2024-09-08 18:23:46 +02:00
parent 08153ddd1c
commit 3a88113246
4 changed files with 21 additions and 8 deletions

View File

@ -2230,6 +2230,12 @@ vim.tbl_count({t}) *vim.tbl_count()*
vim.tbl_deep_extend({behavior}, {...}) *vim.tbl_deep_extend()*
Merges recursively two or more tables.
Only values that are empty tables or tables that are not |lua-list|s
(indexed by consecutive integers starting from 1) are merged recursively.
This is useful for merging nested tables like default and user
configurations where lists should be treated as literals (i.e., are
overwritten instead of merged).
Parameters: ~
• {behavior} (`'error'|'keep'|'force'`) Decides what to do if a key is
found in more than one map:

View File

@ -214,9 +214,6 @@ These existing features changed their behavior.
more emoji characters than before, including those encoded with multiple
emoji codepoints combined with ZWJ (zero width joiner) codepoints.
• |vim.tbl_deep_extend()| no longer ignores any values for which |vim.isarray()|
returns `true`.
==============================================================================
REMOVED FEATURES *news-removed*

View File

@ -354,6 +354,12 @@ function vim.tbl_isempty(t)
return next(t) == nil
end
--- We only merge empty tables or tables that are not list-like (indexed by consecutive integers
--- starting from 1)
local function can_merge(v)
return type(v) == 'table' and (vim.tbl_isempty(v) or not vim.islist(v))
end
--- Recursive worker for tbl_extend
--- @param behavior 'error'|'keep'|'force'
--- @param deep_extend boolean
@ -368,7 +374,7 @@ local function tbl_extend_rec(behavior, deep_extend, ...)
local tbl = select(i, ...) --[[@as table<any,any>]]
if tbl then
for k, v in pairs(tbl) do
if deep_extend and type(v) == 'table' and type(ret[k]) == 'table' then
if deep_extend and can_merge(v) and can_merge(ret[k]) then
ret[k] = tbl_extend_rec(behavior, true, ret[k], v)
elseif behavior ~= 'force' and ret[k] ~= nil then
if behavior == 'error' then
@ -421,6 +427,11 @@ end
--- Merges recursively two or more tables.
---
--- Only values that are empty tables or tables that are not |lua-list|s (indexed by consecutive
--- integers starting from 1) are merged recursively. This is useful for merging nested tables
--- like default and user configurations where lists should be treated as literals (i.e., are
--- overwritten instead of merged).
---
---@see |vim.tbl_extend()|
---
---@generic T1: table

View File

@ -1071,12 +1071,11 @@ describe('lua stdlib', function()
]])
)
-- Fix github issue #23654
ok(exec_lua([[
local a = { sub = { [1] = 'a' } }
local b = { sub = { b = 'a' } }
local a = { sub = { 'a', 'b' } }
local b = { sub = { 'b', 'c' } }
local c = vim.tbl_deep_extend('force', a, b)
return vim.deep_equal(c, { sub = { [1] = 'a', b = 'a' } })
return vim.deep_equal(c, { sub = { 'b', 'c' } })
]]))
matches('invalid "behavior": nil', pcall_err(exec_lua, [[return vim.tbl_deep_extend()]]))