-
Notifications
You must be signed in to change notification settings - Fork 328
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added full semver and range parsing
- Loading branch information
Showing
3 changed files
with
303 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"neodev": { | ||
"library": { | ||
"plugins": [ | ||
"plenary.nvim" | ||
] | ||
} | ||
}, | ||
"lspconfig": { | ||
"sumneko_lua": { | ||
"Lua.runtime.version": "LuaJIT", | ||
"Lua.workspace.checkThirdParty": false | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
local M = {} | ||
|
||
---@class Semver | ||
---@field [1] number | ||
---@field [2] number | ||
---@field [3] number | ||
---@field major number | ||
---@field minor number | ||
---@field patch number | ||
---@field prerelease? string | ||
---@field build? string | ||
local Semver = {} | ||
Semver.__index = Semver | ||
|
||
function Semver:__index(key) | ||
return type(key) == "number" and ({ self.major, self.minor, self.patch })[key] or Semver[key] | ||
end | ||
|
||
function Semver:__newindex(key, value) | ||
if key == 1 then | ||
self.major = value | ||
elseif key == 2 then | ||
self.minor = value | ||
elseif key == 3 then | ||
self.patch = value | ||
else | ||
rawset(self, key, value) | ||
end | ||
end | ||
|
||
---@param other Semver | ||
function Semver:__eq(other) | ||
for i = 1, 3 do | ||
if self[i] ~= other[i] then | ||
return false | ||
end | ||
end | ||
return self.prerelease == other.prerelease | ||
end | ||
|
||
function Semver:__tostring() | ||
local ret = table.concat({ self.major, self.minor, self.patch }, ".") | ||
if self.prerelease then | ||
ret = ret .. "-" .. self.prerelease | ||
end | ||
if self.build then | ||
ret = ret .. "+" .. self.build | ||
end | ||
return ret | ||
end | ||
|
||
---@param other Semver | ||
function Semver:__lt(other) | ||
for i = 1, 3 do | ||
if self[i] > other[i] then | ||
return false | ||
elseif self[i] < other[i] then | ||
return true | ||
end | ||
end | ||
if self.prerelease and not other.prerelease then | ||
return true | ||
end | ||
if other.prerelease and not self.prerelease then | ||
return false | ||
end | ||
return (self.prerelease or "") < (other.prerelease or "") | ||
end | ||
|
||
---@param other Semver | ||
function Semver:__le(other) | ||
return self < other or self == other | ||
end | ||
|
||
---@param version string|number[] | ||
---@return Semver? | ||
function M.version(version) | ||
if type(version) == "table" then | ||
return setmetatable({ | ||
major = version[1] or 0, | ||
minor = version[2] or 0, | ||
patch = version[3] or 0, | ||
}, Semver) | ||
end | ||
local major, minor, patch, prerelease, build = version:match("^v?(%d+)%.?(%d*)%.?(%d*)%-?([^+]*)+?(.*)$") | ||
if major then | ||
return setmetatable({ | ||
major = tonumber(major), | ||
minor = minor == "" and 0 or tonumber(minor), | ||
patch = patch == "" and 0 or tonumber(patch), | ||
prerelease = prerelease ~= "" and prerelease or nil, | ||
build = build ~= "" and build or nil, | ||
}, Semver) | ||
end | ||
end | ||
|
||
---@generic T: Semver | ||
---@param versions T[] | ||
---@return T? | ||
function M.last(versions) | ||
local last = versions[1] | ||
for i = 2, #versions do | ||
if versions[i] > last then | ||
last = versions[i] | ||
end | ||
end | ||
return last | ||
end | ||
|
||
---@class SemverRange | ||
---@field from Semver | ||
---@field to? Semver | ||
local Range = {} | ||
|
||
---@param version string|Semver | ||
function Range:matches(version) | ||
if type(version) == "string" then | ||
---@diagnostic disable-next-line: cast-local-type | ||
version = M.version(version) | ||
end | ||
if version then | ||
if version.prerelease ~= self.from.prerelease then | ||
return false | ||
end | ||
return version >= self.from and (self.to == nil or version < self.to) | ||
end | ||
end | ||
|
||
---@param spec string | ||
function M.range(spec) | ||
if spec == "*" or spec == "" then | ||
return setmetatable({ from = M.version("0.0.0") }, { __index = Range }) | ||
end | ||
|
||
---@type number? | ||
local hyphen = spec:find(" - ", 1, true) | ||
if hyphen then | ||
local a = spec:sub(1, hyphen - 1) | ||
local b = spec:sub(hyphen + 3) | ||
local parts = vim.split(b, ".", { plain = true }) | ||
local ra = M.range(a) | ||
local rb = M.range(b) | ||
return setmetatable({ | ||
from = ra and ra.from, | ||
to = rb and (#parts == 3 and rb.from or rb.to), | ||
}, { __index = Range }) | ||
end | ||
---@type string, string | ||
local mods, version = spec:lower():match("^([%^=>~]*)(.*)$") | ||
version = version:gsub("%.[%*x]", "") | ||
local parts = vim.split(version:gsub("%-.*", ""), ".", { plain = true }) | ||
if #parts < 3 and mods == "" then | ||
mods = "~" | ||
end | ||
|
||
local semver = M.version(version) | ||
if semver then | ||
local from = semver | ||
local to = vim.deepcopy(semver) | ||
if mods == "" or mods == "=" then | ||
to.patch = to.patch + 1 | ||
elseif mods == ">" then | ||
from.patch = from.patch + 1 | ||
to = nil | ||
elseif mods == ">=" then | ||
to = nil | ||
elseif mods == "~" then | ||
if #parts >= 2 then | ||
to[2] = to[2] + 1 | ||
to[3] = 0 | ||
else | ||
to[1] = to[1] + 1 | ||
to[2] = 0 | ||
to[3] = 0 | ||
end | ||
elseif mods == "^" then | ||
for i = 1, 3 do | ||
if to[i] ~= 0 then | ||
to[i] = to[i] + 1 | ||
for j = i + 1, 3 do | ||
to[j] = 0 | ||
end | ||
break | ||
end | ||
end | ||
end | ||
return setmetatable({ from = from, to = to }, { __index = Range }) | ||
end | ||
end | ||
|
||
return M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
local Semver = require("lazy.manage.semver") | ||
|
||
local function v(version) | ||
return Semver.version(version) | ||
end | ||
|
||
describe("semver version", function() | ||
local tests = { | ||
["v1.2.3"] = { major = 1, minor = 2, patch = 3 }, | ||
["v1.2"] = { major = 1, minor = 2, patch = 0 }, | ||
["v1.2.3-prerelease"] = { major = 1, minor = 2, patch = 3, prerelease = "prerelease" }, | ||
["v1.2-prerelease"] = { major = 1, minor = 2, patch = 0, prerelease = "prerelease" }, | ||
["v1.2.3-prerelease+build"] = { major = 1, minor = 2, patch = 3, prerelease = "prerelease", build = "build" }, | ||
["1.2.3+build"] = { major = 1, minor = 2, patch = 3, build = "build" }, | ||
} | ||
for input, output in pairs(tests) do | ||
it("correctly parses " .. input, function() | ||
assert.same(output, v(input)) | ||
end) | ||
end | ||
end) | ||
|
||
describe("semver range", function() | ||
local tests = { | ||
["1.2.3"] = { from = { 1, 2, 3 }, to = { 1, 2, 4 } }, | ||
["1.2"] = { from = { 1, 2, 0 }, to = { 1, 3, 0 } }, | ||
["=1.2.3"] = { from = { 1, 2, 3 }, to = { 1, 2, 4 } }, | ||
[">1.2.3"] = { from = { 1, 2, 4 } }, | ||
[">=1.2.3"] = { from = { 1, 2, 3 } }, | ||
["~1.2.3"] = { from = { 1, 2, 3 }, to = { 1, 3, 0 } }, | ||
["^1.2.3"] = { from = { 1, 2, 3 }, to = { 2, 0, 0 } }, | ||
["^0.2.3"] = { from = { 0, 2, 3 }, to = { 0, 3, 0 } }, | ||
["^0.0.1"] = { from = { 0, 0, 1 }, to = { 0, 0, 2 } }, | ||
["^1.2"] = { from = { 1, 2, 0 }, to = { 2, 0, 0 } }, | ||
["~1.2"] = { from = { 1, 2, 0 }, to = { 1, 3, 0 } }, | ||
["~1"] = { from = { 1, 0, 0 }, to = { 2, 0, 0 } }, | ||
["^1"] = { from = { 1, 0, 0 }, to = { 2, 0, 0 } }, | ||
["1.*"] = { from = { 1, 0, 0 }, to = { 2, 0, 0 } }, | ||
["1"] = { from = { 1, 0, 0 }, to = { 2, 0, 0 } }, | ||
["1.x"] = { from = { 1, 0, 0 }, to = { 2, 0, 0 } }, | ||
["1.2.x"] = { from = { 1, 2, 0 }, to = { 1, 3, 0 } }, | ||
["1.2.*"] = { from = { 1, 2, 0 }, to = { 1, 3, 0 } }, | ||
["*"] = { from = { 0, 0, 0 } }, | ||
["1.2 - 2.3.0"] = { from = { 1, 2, 0 }, to = { 2, 3, 0 } }, | ||
["1.2.3 - 2.3.4"] = { from = { 1, 2, 3 }, to = { 2, 3, 4 } }, | ||
["1.2.3 - 2"] = { from = { 1, 2, 3 }, to = { 3, 0, 0 } }, | ||
} | ||
for input, output in pairs(tests) do | ||
output.from = v(output.from) | ||
output.to = output.to and v(output.to) | ||
|
||
local range = Semver.range(input) | ||
it("correctly parses " .. input, function() | ||
assert.same(output, range) | ||
end) | ||
|
||
it("from in range " .. input, function() | ||
assert(range:matches(output.from)) | ||
end) | ||
|
||
it("from - 1 not in range " .. input, function() | ||
local lower = vim.deepcopy(range.from) | ||
lower.major = lower.major - 1 | ||
assert(not range:matches(lower)) | ||
end) | ||
|
||
it("to not in range " .. input .. " to:" .. tostring(range.to), function() | ||
if range.to then | ||
assert(not (range.to < range.to)) | ||
assert(not range:matches(range.to)) | ||
end | ||
end) | ||
end | ||
|
||
it("handles prereleass", function() | ||
assert(not Semver.range("1.2.3"):matches("1.2.3-alpha")) | ||
assert(Semver.range("1.2.3-alpha"):matches("1.2.3-alpha")) | ||
assert(not Semver.range("1.2.3-alpha"):matches("1.2.3-beta")) | ||
end) | ||
end) | ||
|
||
describe("semver order", function() | ||
it("is correct", function() | ||
assert(v("v1.2.3") == v("1.2.3")) | ||
assert(not (v("v1.2.3") < v("1.2.3"))) | ||
assert(v("v1.2.3") > v("1.2.3-prerelease")) | ||
assert(v("v1.2.3-alpha") < v("1.2.3-beta")) | ||
assert(v("v1.2.3-prerelease") < v("1.2.3")) | ||
assert(v("v1.2.3") >= v("1.2.3")) | ||
assert(v("v1.2.3") >= v("1.0.3")) | ||
assert(v("v1.2.3") >= v("1.2.2")) | ||
assert(v("v1.2.3") > v("1.2.2")) | ||
assert(v("v1.2.3") > v("1.0.3")) | ||
assert.same(Semver.last({ v("1.2.3"), v("2.0.0") }), v("2.0.0")) | ||
assert.same(Semver.last({ v("2.0.0"), v("1.2.3") }), v("2.0.0")) | ||
end) | ||
end) |