Skip to content

Commit d1f0184

Browse files
committed
feat: warn about semver-like version on vim.pack.add error
When vim.pack.add fails, detect if user has semver-like version strings (e.g., "1.*", "^1.0.0") and suggest using sem_version instead. This helps users who may have confused the version field (expects branch/tag/commit) with sem_version (semver ranges). Also: - Add utils.notify with consistent [zpack.nvim] prefix - Remove redundant [zpack] prefix from deprecation messages - Fix test framework to cleanup on failure (prevent cascading)
1 parent c301f59 commit d1f0184

File tree

8 files changed

+231
-7
lines changed

8 files changed

+231
-7
lines changed

lua/zpack/deprecation.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ M.notify_removed = function(key)
3838
local entry = M.removed[key]
3939
if not entry then return end
4040
utils.schedule_notify(
41-
("[zpack] REMOVED: %s\n\n%s"):format(entry.message, entry.replacement),
41+
("REMOVED: %s\n\n%s"):format(entry.message, entry.replacement),
4242
vim.log.levels.WARN
4343
)
4444
end
@@ -47,7 +47,7 @@ M.notify_deprecated = function(key)
4747
local entry = M.deprecated[key]
4848
if not entry then return end
4949
utils.schedule_notify(
50-
("[zpack] DEPRECATED: %s\n\n%s"):format(entry.message, entry.replacement),
50+
("DEPRECATED: %s\n\n%s"):format(entry.message, entry.replacement),
5151
vim.log.levels.WARN
5252
)
5353
end

lua/zpack/init.lua

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ end
2727

2828
local function check_version()
2929
if vim.fn.has('nvim-0.12') ~= 1 then
30-
vim.schedule(function()
31-
vim.notify('zpack.nvim requires Neovim 0.12+', vim.log.levels.ERROR)
32-
end)
30+
require('zpack.utils').schedule_notify('requires Neovim 0.12+', vim.log.levels.ERROR)
3331
return false
3432
end
3533
return true

lua/zpack/registration.lua

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ local M = {}
66

77
---@param ctx zpack.ProcessContext
88
M.register_all = function(ctx)
9-
vim.pack.add(ctx.vim_packs, {
9+
local ok, err = pcall(vim.pack.add, ctx.vim_packs, {
1010
confirm = ctx.confirm,
1111
load = function(plugin)
1212
local pack_spec = plugin.spec
@@ -43,6 +43,24 @@ M.register_all = function(ctx)
4343
end
4444
})
4545

46+
if not ok then
47+
local semver_like_specs = {}
48+
for _, pack_spec in ipairs(ctx.vim_packs) do
49+
if pack_spec.version and utils.is_semver_like(pack_spec.version) then
50+
table.insert(semver_like_specs, pack_spec)
51+
end
52+
end
53+
for _, pack_spec in ipairs(semver_like_specs) do
54+
utils.notify(
55+
('`vim.pack.add` failed.\nIs `version = "%s"` for %s meant to be a semver range?\n'
56+
.. 'Consider using `sem_version = "%s"` or `version = vim.version.range("%s")` instead.')
57+
:format(pack_spec.version, pack_spec.src, pack_spec.version, pack_spec.version),
58+
vim.log.levels.WARN
59+
)
60+
end
61+
error(err)
62+
end
63+
4664
table.sort(ctx.registered_startup_packs, utils.compare_priority)
4765
table.sort(ctx.registered_lazy_packs, utils.compare_priority)
4866
table.sort(state.registered_plugin_names, function(a, b) return a:lower() < b:lower() end)

lua/zpack/utils.lua

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,13 @@ M.reset_lsdir_cache = function()
4242
lsdir_cache = {}
4343
end
4444

45+
M.notify = function(msg, level)
46+
vim.notify('[zpack.nvim] ' .. msg, level)
47+
end
48+
4549
M.schedule_notify = function(msg, level)
4650
vim.schedule(function()
47-
vim.notify(msg, level)
51+
M.notify(msg, level)
4852
end)
4953
end
5054

@@ -168,6 +172,21 @@ M.normalize_name = function(name)
168172
return norm
169173
end
170174

175+
---Check if a string looks like a semver range (not a branch/tag/commit)
176+
---@param str any
177+
---@return boolean
178+
M.is_semver_like = function(str)
179+
if type(str) ~= 'string' then
180+
return false
181+
end
182+
return str:match('%*') ~= nil
183+
or str:match('^%d[%d%.]*%.[xX]$') ~= nil
184+
or str:match('^%d[%d%.]*%.%a') ~= nil
185+
or str:match('^[>=<^~]') ~= nil
186+
or str:match('[>=<]') ~= nil
187+
or str:match('^%d+[%d%.]*$') ~= nil
188+
end
189+
171190
---Normalize plugin version using priority: version > sem_version > branch > tag > commit
172191
---@param spec zpack.Spec
173192
---@return string|vim.VersionRange|nil version

tests/helpers.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---@diagnostic disable: duplicate-set-field
12
local M = {}
23

34
M.test_results = {}
@@ -74,6 +75,7 @@ function M.test(name, fn)
7475
table.insert(M.test_results, { name = name, passed = false, error = err })
7576
print(string.format("✗ %s", name))
7677
print(string.format(" Error: %s", err))
78+
M.cleanup_test_env()
7779
end
7880
end
7981

tests/module_loader_test.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---@diagnostic disable: duplicate-set-field
12
local helpers = require('helpers')
23

34
return function()

tests/utils_test.lua

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,74 @@ return function()
118118
helpers.cleanup_test_env()
119119
end)
120120
end)
121+
122+
helpers.describe("is_semver_like utility", function()
123+
helpers.test("detects wildcard patterns", function()
124+
helpers.setup_test_env()
125+
local utils = require('zpack.utils')
126+
127+
helpers.assert_equal(utils.is_semver_like('1.*'), true)
128+
helpers.assert_equal(utils.is_semver_like('*'), true)
129+
helpers.assert_equal(utils.is_semver_like('1.2.*'), true)
130+
helpers.assert_equal(utils.is_semver_like('1.2.x'), true)
131+
helpers.assert_equal(utils.is_semver_like('1.x'), true)
132+
helpers.assert_equal(utils.is_semver_like('1.X'), true)
133+
helpers.assert_equal(utils.is_semver_like('1.2.X'), true)
134+
helpers.assert_equal(utils.is_semver_like('1.2.y'), true)
135+
helpers.assert_equal(utils.is_semver_like('1.a.b'), true)
136+
137+
helpers.cleanup_test_env()
138+
end)
139+
140+
helpers.test("detects range operators", function()
141+
helpers.setup_test_env()
142+
local utils = require('zpack.utils')
143+
144+
helpers.assert_equal(utils.is_semver_like('>=1.0.0'), true)
145+
helpers.assert_equal(utils.is_semver_like('<=2.0.0'), true)
146+
helpers.assert_equal(utils.is_semver_like('>1.0'), true)
147+
helpers.assert_equal(utils.is_semver_like('<2.0'), true)
148+
helpers.assert_equal(utils.is_semver_like('^1.0.0'), true)
149+
helpers.assert_equal(utils.is_semver_like('~1.0.0'), true)
150+
helpers.assert_equal(utils.is_semver_like('foo=bar'), true)
151+
152+
helpers.cleanup_test_env()
153+
end)
154+
155+
helpers.test("detects bare semver patterns", function()
156+
helpers.setup_test_env()
157+
local utils = require('zpack.utils')
158+
159+
helpers.assert_equal(utils.is_semver_like('1.0.0'), true)
160+
helpers.assert_equal(utils.is_semver_like('1.2'), true)
161+
helpers.assert_equal(utils.is_semver_like('1.2.3.4'), true)
162+
163+
helpers.cleanup_test_env()
164+
end)
165+
166+
helpers.test("returns false for branch/tag names", function()
167+
helpers.setup_test_env()
168+
local utils = require('zpack.utils')
169+
170+
helpers.assert_equal(utils.is_semver_like('main'), false)
171+
helpers.assert_equal(utils.is_semver_like('master'), false)
172+
helpers.assert_equal(utils.is_semver_like('v1.0.0'), false)
173+
helpers.assert_equal(utils.is_semver_like('v1.x'), false)
174+
helpers.assert_equal(utils.is_semver_like('release-1.0'), false)
175+
helpers.assert_equal(utils.is_semver_like('feature/foo'), false)
176+
177+
helpers.cleanup_test_env()
178+
end)
179+
180+
helpers.test("returns false for non-strings", function()
181+
helpers.setup_test_env()
182+
local utils = require('zpack.utils')
183+
184+
helpers.assert_equal(utils.is_semver_like(nil), false)
185+
helpers.assert_equal(utils.is_semver_like(123), false)
186+
helpers.assert_equal(utils.is_semver_like({}), false)
187+
188+
helpers.cleanup_test_env()
189+
end)
190+
end)
121191
end

tests/version_test.lua

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---@diagnostic disable: duplicate-set-field
12
local helpers = require('helpers')
23

34
return function()
@@ -213,5 +214,120 @@ return function()
213214

214215
helpers.cleanup_test_env()
215216
end)
217+
218+
helpers.test("semver-like version emits warning when vim.pack.add fails", function()
219+
helpers.setup_test_env()
220+
221+
local mock_vim_pack_add = vim.pack.add
222+
vim.pack.add = function()
223+
error("some error from vim.pack.add")
224+
end
225+
226+
local ok = pcall(function()
227+
require('zpack').setup({
228+
spec = {
229+
{
230+
'test/plugin',
231+
version = '1.*',
232+
},
233+
},
234+
defaults = { confirm = false },
235+
})
236+
end)
237+
238+
helpers.assert_equal(ok, false, "setup should fail when vim.pack.add throws")
239+
helpers.assert_equal(#_G.test_state.notifications >= 1, true, "warning should be emitted")
240+
241+
local found_warning = false
242+
for _, notif in ipairs(_G.test_state.notifications) do
243+
if notif.msg:match('`vim%.pack%.add` failed') and notif.msg:match('sem_version') and notif.msg:match('1%.%*') then
244+
found_warning = true
245+
break
246+
end
247+
end
248+
helpers.assert_equal(found_warning, true, "warning should mention vim.pack.add failed and sem_version")
249+
250+
vim.pack.add = mock_vim_pack_add
251+
helpers.cleanup_test_env()
252+
end)
253+
254+
helpers.test("multiple semver-like versions emit multiple warnings", function()
255+
helpers.setup_test_env()
256+
257+
local mock_vim_pack_add = vim.pack.add
258+
vim.pack.add = function()
259+
error("some error from vim.pack.add")
260+
end
261+
262+
local ok = pcall(function()
263+
require('zpack').setup({
264+
spec = {
265+
{ 'test/plugin-a', version = '1.*' },
266+
{ 'test/plugin-b', version = '^2.0.0' },
267+
{ 'test/plugin-c', version = 'main' },
268+
},
269+
defaults = { confirm = false },
270+
})
271+
end)
272+
273+
helpers.assert_equal(ok, false, "setup should fail when vim.pack.add throws")
274+
275+
local warning_count = 0
276+
local found_plugin_a = false
277+
local found_plugin_b = false
278+
for _, notif in ipairs(_G.test_state.notifications) do
279+
if notif.msg:match('sem_version') then
280+
warning_count = warning_count + 1
281+
if notif.msg:match('1%.%*') and notif.msg:match('plugin%-a') then
282+
found_plugin_a = true
283+
end
284+
if notif.msg:match('%^2%.0%.0') and notif.msg:match('plugin%-b') then
285+
found_plugin_b = true
286+
end
287+
end
288+
end
289+
290+
helpers.assert_equal(warning_count, 2, "should emit exactly 2 warnings for semver-like versions")
291+
helpers.assert_equal(found_plugin_a, true, "should warn about plugin-a with 1.*")
292+
helpers.assert_equal(found_plugin_b, true, "should warn about plugin-b with ^2.0.0")
293+
294+
vim.pack.add = mock_vim_pack_add
295+
helpers.cleanup_test_env()
296+
end)
297+
298+
helpers.test("no warning when vim.pack.add fails but no semver-like versions", function()
299+
helpers.setup_test_env()
300+
301+
local mock_vim_pack_add = vim.pack.add
302+
vim.pack.add = function()
303+
error("some error from vim.pack.add")
304+
end
305+
306+
local ok = pcall(function()
307+
require('zpack').setup({
308+
spec = {
309+
{
310+
'test/plugin',
311+
version = 'nonexistent-branch',
312+
},
313+
},
314+
defaults = { confirm = false },
315+
})
316+
end)
317+
318+
helpers.assert_equal(ok, false, "setup should fail when vim.pack.add throws")
319+
320+
local found_warning = false
321+
for _, notif in ipairs(_G.test_state.notifications) do
322+
if notif.msg:match('sem_version') then
323+
found_warning = true
324+
break
325+
end
326+
end
327+
helpers.assert_equal(found_warning, false, "no warning should be emitted for non-semver-like versions")
328+
329+
vim.pack.add = mock_vim_pack_add
330+
helpers.cleanup_test_env()
331+
end)
216332
end)
217333
end

0 commit comments

Comments
 (0)