1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
|
local M = {}
---@private
--- Compares the prerelease component of the two versions.
---@param v1_parsed table Parsed version.
---@param v2_parsed table Parsed version.
---@return integer `-1` if `v1_parsed < v2_parsed`, `0` if `v1_parsed == v2_parsed`, `1` if `v1_parsed > v2_parsed`.
local function cmp_prerelease(v1_parsed, v2_parsed)
if v1_parsed.prerelease and not v2_parsed.prerelease then
return -1
end
if not v1_parsed.prerelease and v2_parsed.prerelease then
return 1
end
if not v1_parsed.prerelease and not v2_parsed.prerelease then
return 0
end
local v1_identifiers = vim.split(v1_parsed.prerelease, '.', { plain = true })
local v2_identifiers = vim.split(v2_parsed.prerelease, '.', { plain = true })
local i = 1
local max = math.max(vim.tbl_count(v1_identifiers), vim.tbl_count(v2_identifiers))
while i <= max do
local v1_identifier = v1_identifiers[i]
local v2_identifier = v2_identifiers[i]
if v1_identifier ~= v2_identifier then
local v1_num = tonumber(v1_identifier)
local v2_num = tonumber(v2_identifier)
local is_number = v1_num and v2_num
if is_number then
-- Number comparisons
if not v1_num and v2_num then
return -1
end
if v1_num and not v2_num then
return 1
end
if v1_num == v2_num then
return 0
end
if v1_num > v2_num then
return 1
end
if v1_num < v2_num then
return -1
end
else
-- String comparisons
if v1_identifier and not v2_identifier then
return 1
end
if not v1_identifier and v2_identifier then
return -1
end
if v1_identifier < v2_identifier then
return -1
end
if v1_identifier > v2_identifier then
return 1
end
if v1_identifier == v2_identifier then
return 0
end
end
end
i = i + 1
end
return 0
end
---@private
--- Compares the version core component of the two versions.
---@param v1_parsed table Parsed version.
---@param v2_parsed table Parsed version.
---@return integer `-1` if `v1_parsed < v2_parsed`, `0` if `v1_parsed == v2_parsed`, `1` if `v1_parsed > v2_parsed`.
local function cmp_version_core(v1_parsed, v2_parsed)
if
v1_parsed.major == v2_parsed.major
and v1_parsed.minor == v2_parsed.minor
and v1_parsed.patch == v2_parsed.patch
then
return 0
end
if
v1_parsed.major > v2_parsed.major
or v1_parsed.minor > v2_parsed.minor
or v1_parsed.patch > v2_parsed.patch
then
return 1
end
return -1
end
--- Compares two strings (`v1` and `v2`) in semver format.
---@param v1 string Version.
---@param v2 string Version to be compared with v1.
---@param opts table|nil Optional keyword arguments:
--- - strict (boolean): see `semver.parse` for details. Defaults to false.
---@return integer `-1` if `v1 < v2`, `0` if `v1 == v2`, `1` if `v1 > v2`.
function M.cmp(v1, v2, opts)
opts = opts or { strict = false }
local v1_parsed = M.parse(v1, opts)
local v2_parsed = M.parse(v2, opts)
local result = cmp_version_core(v1_parsed, v2_parsed)
if result == 0 then
result = cmp_prerelease(v1_parsed, v2_parsed)
end
return result
end
---@private
---@param labels string Prerelease and build component of semantic version string e.g. "-rc1+build.0".
---@return string|nil
local function parse_prerelease(labels)
-- This pattern matches "-(alpha)+build.15".
-- '^%-[%w%.]+$'
local result = labels:match('^%-([%w%.]+)+.+$')
if result then
return result
end
-- This pattern matches "-(alpha)".
result = labels:match('^%-([%w%.]+)')
if result then
return result
end
return nil
end
---@private
---@param labels string Prerelease and build component of semantic version string e.g. "-rc1+build.0".
---@return string|nil
local function parse_build(labels)
-- Pattern matches "-alpha+(build.15)".
local result = labels:match('^%-[%w%.]+%+([%w%.]+)$')
if result then
return result
end
-- Pattern matches "+(build.15)".
result = labels:match('^%+([%w%.]+)$')
if result then
return result
end
return nil
end
---@private
--- Extracts the major, minor, patch and preprelease and build components from
--- `version`.
---@param version string Version string
local function extract_components_strict(version)
local major, minor, patch, prerelease_and_build = version:match('^v?(%d+)%.(%d+)%.(%d+)(.*)$')
return tonumber(major), tonumber(minor), tonumber(patch), prerelease_and_build
end
---@private
--- Extracts the major, minor, patch and preprelease and build components from
--- `version`. When `minor` and `patch` components are not found (nil), coerce
--- them to 0.
---@param version string Version string
local function extract_components_loose(version)
local major, minor, patch, prerelease_and_build = version:match('^v?(%d+)%.?(%d*)%.?(%d*)(.*)$')
major = tonumber(major)
minor = tonumber(minor) or 0
patch = tonumber(patch) or 0
return major, minor, patch, prerelease_and_build
end
---@private
--- Validates the prerelease and build string e.g. "-rc1+build.0". If the
--- prerelease, build or both are valid forms then it will return true, if it
--- is not of any valid form, it will return false.
---@param prerelease_and_build string
---@return boolean
local function is_prerelease_and_build_valid(prerelease_and_build)
if prerelease_and_build == '' then
return true
end
local has_build = parse_build(prerelease_and_build) ~= nil
local has_prerelease = parse_prerelease(prerelease_and_build) ~= nil
local has_prerelease_and_build = has_prerelease and has_build
return has_build or has_prerelease or has_prerelease_and_build
end
---@private
---@param version string
---@return string
local function create_err_msg(version)
return string.format('invalid version: "%s"', version)
end
--- Parses a semantically formatted version string into a table.
---
--- Supports leading "v" and leading and trailing whitespace in the version
--- string. e.g. `" v1.0.1-rc1+build.2"` , `"1.0.1-rc1+build.2"`, `"v1.0.1-rc1+build.2"`
--- and `"v1.0.1-rc1+build.2 "` will be parsed as:
---
--- `{ major = 1, minor = 0, patch = 1, prerelease = 'rc1', build = 'build.2' }`
---
---@param version string Version string to be parsed.
---@param opts table|nil Optional keyword arguments:
--- - strict (boolean): when set to `true` an error will be thrown for version
--- strings that do not conform to the semver specification (v2.0.0) (see
--- semver.org/spec/v2.0.0.html for details). This means that
--- `semver.parse('v1.2)` will throw an error. When set to `false`,
--- `semver.parse('v1.2)` will coerce 'v1.2' to 'v1.2.0' and return the table:
--- `{ major = 1, minor = 2, patch = 0 }`. Defaults to false.
---@return table|nil parsed_version Parsed version table or `nil` if `version` is not valid.
function M.parse(version, opts)
if type(version) ~= 'string' then
error(create_err_msg(version))
end
opts = opts or { strict = false }
version = vim.trim(version)
local extract_components = opts.strict and extract_components_strict or extract_components_loose
local major, minor, patch, prerelease_and_build = extract_components(version)
-- If major is nil then that means that the version does not begin with a
-- digit with or without a "v" prefix.
if major == nil or not is_prerelease_and_build_valid(prerelease_and_build) then
return nil
end
local prerelease = nil
local build = nil
if prerelease_and_build ~= nil then
prerelease = parse_prerelease(prerelease_and_build)
build = parse_build(prerelease_and_build)
end
return {
major = major,
minor = minor,
patch = patch,
prerelease = prerelease,
build = build,
}
end
---@private
--- Throws an error if `version` cannot be parsed.
---@param version string
local function assert_version(version)
if M.parse(version) == nil then
error(create_err_msg(version))
end
end
---Returns `true` if `v1` are `v2` are equal versions.
---@param version_1 string
---@param version_2 string
---@return boolean
function M.eq(version_1, version_2)
assert_version(version_1)
assert_version(version_2)
return M.cmp(version_1, version_2) == 0
end
---Returns `true` if `v1` is less than `v2`.
---@param version_1 string
---@param version_2 string
---@return boolean
function M.lt(version_1, version_2)
assert_version(version_1)
assert_version(version_2)
return M.cmp(version_1, version_2) == -1
end
---Returns `true` if `v1` is greater than `v2`.
---@param version_1 string
---@param version_2 string
---@return boolean
function M.gt(version_1, version_2)
assert_version(version_1)
assert_version(version_2)
return M.cmp(version_1, version_2) == 1
end
setmetatable(M, {
__call = function()
return vim.fn.api_info().version
end,
})
return M
|