semver_dialects 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ # IntervalSetParser parses a string that represents an interval set
4
+ # in a syntax that's specific to a package type.
5
+ module SemverDialects
6
+ module IntervalSetParser
7
+ # parse parses a string and returns an IntervalSet.
8
+ # The string is expected to be in a syntax that's specific the given package type.
9
+ def self.parse(typ, interval_set_string)
10
+ IntervalSet.new.tap do |set|
11
+ translate(typ, interval_set_string).each do |interval_str|
12
+ set << IntervalParser.parse(typ, interval_str)
13
+ end
14
+ end
15
+ end
16
+
17
+ def self.translate(typ, interval_set_string)
18
+ case typ
19
+ when 'maven'
20
+ translate_maven(interval_set_string)
21
+ when 'npm'
22
+ translate_npm(interval_set_string)
23
+ when 'conan'
24
+ translate_conan(interval_set_string)
25
+ when 'nuget'
26
+ translate_nuget(interval_set_string)
27
+ when 'go'
28
+ translate_go(interval_set_string)
29
+ when 'gem'
30
+ translate_gem(interval_set_string)
31
+ when 'pypi'
32
+ translate_pypi(interval_set_string)
33
+ when 'packagist'
34
+ translate_packagist(interval_set_string)
35
+ else
36
+ raise UnsupportedPackageTypeError, typ
37
+ end
38
+ end
39
+
40
+ def self.translate_npm(interval_set_string)
41
+ interval_set_string.split('||').map do |item|
42
+ add_missing_operator(single_space_after_operator(item.strip.gsub(/&&/, ' ')))
43
+ end
44
+ end
45
+
46
+ def self.translate_conan(interval_set_string)
47
+ translate_npm(interval_set_string)
48
+ end
49
+
50
+ def self.translate_go(interval_set_string)
51
+ translate_gem(interval_set_string)
52
+ end
53
+
54
+ def self.translate_gem(interval_set_string)
55
+ interval_set_string.split('||').map do |item|
56
+ add_missing_operator(single_space_after_operator(item.strip.gsub(/\s+/, ' ')))
57
+ end
58
+ end
59
+
60
+ def self.translate_packagist(interval_set_string)
61
+ translate_pypi(interval_set_string)
62
+ end
63
+
64
+ def self.translate_pypi(interval_set_string)
65
+ interval_set_string.split('||').map do |item|
66
+ add_missing_operator(single_space_after_operator(comma_to_space(item)))
67
+ end
68
+ end
69
+
70
+ def self.translate_nuget(interval_set_string)
71
+ translate_maven(interval_set_string)
72
+ end
73
+
74
+ def self.translate_maven(interval_set_string)
75
+ lexing_maven_interval_set_string(interval_set_string).map { |item| translate_mvn_version_item(item) }
76
+ end
77
+
78
+ def self.add_missing_operator(interval_set_string)
79
+ starts_with_operator?(interval_set_string) ? interval_set_string : "=#{interval_set_string}"
80
+ end
81
+
82
+ def self.single_space_after_operator(interval_set_string)
83
+ interval_set_string.gsub(/([>=<]+) +/, '\1').gsub(/\s+/, ' ')
84
+ end
85
+
86
+ def self.starts_with_operator?(version_item)
87
+ version_item.match(/^[=><]/) ? true : false
88
+ end
89
+
90
+ def self.comma_to_space(interval_set_string)
91
+ interval_set_string.strip.gsub(/,/, ' ')
92
+ end
93
+
94
+ def self.lexing_maven_interval_set_string(interval_set_string)
95
+ open = false
96
+ substring = ''
97
+ ret = []
98
+ interval_set_string.each_char do |c|
99
+ case c
100
+ when '(', '['
101
+ if open
102
+ puts "malformed maven version string #{interval_set_string}"
103
+ exit(-1)
104
+ else
105
+ unless substring.empty?
106
+ ret << substring
107
+ substring = ''
108
+ end
109
+ open = true
110
+ substring += c
111
+ end
112
+ when ')', ']'
113
+ if !open
114
+ puts "malformed maven version string #{interval_set_string}"
115
+ exit(-1)
116
+ else
117
+ open = false
118
+ substring += c
119
+ ret << substring
120
+ substring = ''
121
+ end
122
+ when ','
123
+ substring += c if open
124
+ when ' '
125
+ # nothing to do
126
+ substring += ''
127
+ else
128
+ substring += c
129
+ end
130
+ end
131
+ if open
132
+ puts "malformed maven version string #{interval_set_string}"
133
+ exit(-1)
134
+ end
135
+ ret << substring unless substring.empty?
136
+ ret
137
+ end
138
+
139
+ def self.parenthesized?(version_item)
140
+ version_item.match(/^[(\[]/) && version_item.match(/[\])]$/)
141
+ end
142
+
143
+ def self.translate_mvn_version_item(version_item)
144
+ content = ''
145
+ parens_pattern = ''
146
+ if parenthesized?(version_item)
147
+ content = version_item[1, version_item.size - 2]
148
+ parens_pattern = version_item[0] + version_item[version_item.size - 1]
149
+ # special case -- unversal version range
150
+ return '=*' if content.strip == ','
151
+ else
152
+ # according to the doc, if there is a plain version string in maven, it means 'starting from version x'
153
+ # https://docs.oracle.com/middleware/1212/core/MAVEN/maven_version.htm#MAVEN8903
154
+ content = "#{version_item},"
155
+ parens_pattern = '[)'
156
+ end
157
+
158
+ args = content.split(',')
159
+ first_non_empty_arg = args.find(&:range_present?)
160
+
161
+ if content.start_with?(',')
162
+ # {,y}
163
+ case parens_pattern
164
+ when '[]'
165
+ "<=#{first_non_empty_arg}"
166
+ when '()'
167
+ "<#{first_non_empty_arg}"
168
+ when '[)'
169
+ "<#{first_non_empty_arg}"
170
+ else
171
+ # par_pattern == "(]"
172
+ "<=#{first_non_empty_arg}"
173
+ end
174
+ elsif content.end_with?(',')
175
+ # {x,}
176
+ case parens_pattern
177
+ when '[]'
178
+ ">=#{first_non_empty_arg}"
179
+ when '()'
180
+ ">#{first_non_empty_arg}"
181
+ when '[)'
182
+ ">=#{first_non_empty_arg}"
183
+ else
184
+ # par_pattern == "(]"
185
+ ">#{first_non_empty_arg}"
186
+ end
187
+ elsif content[','].nil?
188
+ # [x,x]
189
+ "=#{content}"
190
+ else
191
+ case parens_pattern
192
+ when '[]'
193
+ ">=#{args[0]} <=#{args[1]}"
194
+ when '()'
195
+ ">#{args[0]} <#{args[1]}"
196
+ when '[)'
197
+ ">=#{args[0]} <#{args[1]}"
198
+ else
199
+ # par_pattern == "(]"
200
+ ">#{args[0]} <=#{args[1]}"
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ module SemverDialects
6
+ module Maven
7
+ ALPHA = -5
8
+ BETA = -4
9
+ MILESTONE = -3
10
+ RC = -2
11
+ SNAPSHOT = -1
12
+ SP = 'sp'
13
+
14
+ class Version < BaseVersion
15
+ attr_accessor :addition
16
+
17
+ # Return an array similar to the one Maven generates when parsing versions.
18
+ #
19
+ # $ java -jar ${MAVEN_HOME}/lib/maven-artifact-3.9.6.jar 1a1
20
+ # Display parameters as parsed by Maven (in canonical form and as a list of tokens) and comparison result:
21
+ # 1. 1a1 -> 1-alpha-1; tokens: [1, [alpha, [1]]]
22
+ #
23
+ def to_a
24
+ return tokens if addition.nil?
25
+
26
+ tokens.clone.append(addition.to_a)
27
+ end
28
+
29
+ def to_s(as_addition = false)
30
+ s = ''
31
+ if tokens.any?
32
+ s += '-' if as_addition
33
+ s += tokens.map do |token|
34
+ case token
35
+ when String
36
+ token
37
+ when Integer
38
+ case token
39
+ when ALPHA
40
+ 'alpha'
41
+ when BETA
42
+ 'beta'
43
+ when MILESTONE
44
+ 'milestone'
45
+ when RC
46
+ 'rc'
47
+ when SNAPSHOT
48
+ 'snapshot'
49
+ else
50
+ token.to_s
51
+ end
52
+ end
53
+ end.join('.')
54
+ end
55
+ s += addition.to_s(true) if addition
56
+ s
57
+ end
58
+
59
+ private
60
+
61
+ # Compare tokens as specified in https://maven.apache.org/pom.html#version-order-specification.
62
+ # Negative integers are alpha, beta, milestone, rc, and snapshot qualifiers.
63
+ # Special qualifier "sp" is right after GA and before any lexical or numeric token.
64
+ # Strings should be converted to lower case before being compared by this method.
65
+ # 1-a0 == 1-alpha < 1-0 == 1 == 1final == 1 ga < 1sp < 1-a < 1-1
66
+ def compare_token_pair(a = 0, b = 0)
67
+ a ||= 0
68
+ b ||= 0
69
+
70
+ if a.is_a?(Integer) && b.is_a?(String)
71
+ return a <= 0 ? -1 : 1
72
+ end
73
+
74
+ if a.is_a?(String) && b.is_a?(Integer)
75
+ return b <= 0 ? 1 : -1
76
+ end
77
+
78
+ return -1 if a == SP && b.is_a?(String) && b != SP
79
+
80
+ return 1 if b == SP && a.is_a?(String) && a != SP
81
+
82
+ # Identifiers have both the same type.
83
+ # This returns nil if the identifiers can't be compared.
84
+ a <=> b
85
+ end
86
+
87
+ def empty_addition
88
+ Version.new([])
89
+ end
90
+ end
91
+
92
+ class VersionParser
93
+ def self.parse(input)
94
+ new(input).parse
95
+ end
96
+
97
+ attr_reader :input
98
+
99
+ def initialize(input)
100
+ @input = input
101
+ end
102
+
103
+ def parse
104
+ @scanner = StringScanner.new(input.downcase)
105
+ @version = Version.new([])
106
+ @result = @version
107
+ parse_version(false)
108
+ result
109
+ end
110
+
111
+ private
112
+
113
+ attr_reader :scanner, :version, :result
114
+
115
+ # Parse a version and all its additions recursively.
116
+ # It automatically creates a new partition for numbers
117
+ # if number_begins_partition is true.
118
+ def parse_version(number_begins_partition)
119
+ # skip leading v if any
120
+ scanner.skip(/v/)
121
+
122
+ until scanner.eos?
123
+ if (s = scanner.scan(/\d+/))
124
+ if number_begins_partition
125
+ parse_addition(s.to_i)
126
+ else
127
+ version.tokens << s.to_i
128
+ end
129
+
130
+ elsif (s = scanner.match?(/a\d+/))
131
+ # aN is equivalent to alpha-N
132
+ scanner.skip('a')
133
+ parse_addition(ALPHA)
134
+
135
+ elsif (s = scanner.match?(/b\d+/))
136
+ # bN is equivalent to beta-N
137
+ scanner.skip('b')
138
+ parse_addition(BETA)
139
+
140
+ elsif (s = scanner.match?(/m\d+/))
141
+ # mN is equivalent to milestone-N
142
+ scanner.skip('m')
143
+ parse_addition(MILESTONE)
144
+
145
+ elsif (s = scanner.scan(/(alpha|beta|milestone|rc|cr|sp|ga|final|release|snapshot)[a-z]+/))
146
+ # process "alpha" and others as normal lexical tokens if they're followed by a letter
147
+ parse_addition(s)
148
+
149
+ elsif (s = scanner.scan('alpha'))
150
+ # handle alphaN, alpha-X, alpha.X, or ending alpha
151
+ parse_addition(ALPHA)
152
+
153
+ elsif (s = scanner.scan('beta'))
154
+ parse_addition(BETA)
155
+
156
+ elsif (s = scanner.scan('milestone'))
157
+ parse_addition(MILESTONE)
158
+
159
+ elsif (s = scanner.scan(/(rc|cr)/))
160
+ parse_addition(RC)
161
+
162
+ elsif (s = scanner.scan('snapshot'))
163
+ parse_addition(SNAPSHOT)
164
+
165
+ elsif (s = scanner.scan(/ga|final|release/))
166
+ parse_addition
167
+
168
+ elsif (s = scanner.scan('sp'))
169
+ parse_addition(SP)
170
+
171
+ elsif (s = scanner.scan(/[a-z]+/))
172
+ parse_addition(s)
173
+
174
+ elsif (s = scanner.scan('.'))
175
+ number_begins_partition = false
176
+
177
+ elsif (s = scanner.scan('-'))
178
+ number_begins_partition = true
179
+
180
+ else
181
+ raise IncompleteScanError, scanner.rest
182
+ end
183
+ end
184
+ end
185
+
186
+ # Create an addition for the current version, make it the current version, and parse it.
187
+ # Numbers start a new partition.
188
+ def parse_addition(token = nil)
189
+ version.addition = Version.new([token].compact)
190
+ @version = version.addition
191
+
192
+ scanner.skip(/-+/)
193
+ parse_version(true)
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../utils'
4
+
5
+ # SemanticVersion represents a Semver 2 version.
6
+ # Comparison rules are the ones of Semver 2.
7
+ module SemverDialects
8
+ class SemanticVersion
9
+ attr_reader :version_string, :prefix_segments, :suffix_segments, :segments
10
+
11
+ # String to build a regexp that matches a version.
12
+ #
13
+ # A version might start with a leading "v", then it must have a digit,
14
+ # then it might have any sequence made of alphanumerical characters,
15
+ # underscores, dots, dashes, and wildcards.
16
+ VERSION_PATTERN = 'v?[0-9][a-zA-Z0-9_.*+-]*'
17
+
18
+ # Regexp for a string that only contains a single version string.
19
+ VERSION_ONLY_REGEXP = Regexp.new("\\A#{VERSION_PATTERN}\\z").freeze
20
+
21
+ def initialize(version_string)
22
+ raise InvalidVersionError, version_string unless VERSION_ONLY_REGEXP.match version_string
23
+
24
+ @version_string = version_string
25
+ @prefix_segments = []
26
+ @suffix_segments = []
27
+ version, = version_string.delete_prefix('v').split('+')
28
+ @segments = split_version_string!(version)
29
+ end
30
+
31
+ def split_version_string!(version_string)
32
+ delim_pattern = /[.-]/
33
+ split_array = version_string.split(delim_pattern).map do |grp|
34
+ grp.split(/(\d+)/).reject { |cell| cell.nil? || cell.empty? }
35
+ end.flatten
36
+
37
+ # go as far to the right as possible considering numbers and placeholders
38
+ prefix_delimiter = 0
39
+ (0..split_array.size - 1).each do |i|
40
+ break unless split_array[i].number? || split_array[i] == 'X' || split_array[i] == 'x'
41
+
42
+ prefix_delimiter = i
43
+ end
44
+
45
+ # remove redundant trailing zeros
46
+ prefix_delimiter.downto(0).each do |i|
47
+ break unless split_array[i] == '0'
48
+
49
+ split_array.delete_at(i)
50
+ prefix_delimiter -= 1
51
+ end
52
+
53
+ unless prefix_delimiter < 0
54
+ @prefix_segments = split_array[0..prefix_delimiter].map do |group_string|
55
+ SemanticVersionSegment.new(group_string)
56
+ end
57
+ end
58
+ if split_array.size - 1 >= prefix_delimiter + 1
59
+ @suffix_segments = split_array[prefix_delimiter + 1, split_array.size].map do |group_string|
60
+ SemanticVersionSegment.new(group_string)
61
+ end
62
+ end
63
+
64
+ @prefix_segments.clone.concat(@suffix_segments)
65
+ end
66
+
67
+ def _get_equalized_arrays_for(array_a, array_b)
68
+ first_array = array_a.clone
69
+ second_array = array_b.clone
70
+ if first_array.size < second_array.size
71
+ (second_array.size - first_array.size).times do
72
+ first_array << SemanticVersionSegment.new('0')
73
+ end
74
+ elsif first_array.size > second_array.size
75
+ (first_array.size - second_array.size).times do
76
+ second_array << SemanticVersionSegment.new('0')
77
+ end
78
+ end
79
+ [first_array, second_array]
80
+ end
81
+
82
+ def get_equalized_arrays_for(semver_a, semver_b)
83
+ first_array_prefix = semver_a.prefix_segments.clone
84
+ second_array_prefix = semver_b.prefix_segments.clone
85
+ first_array_suffix = semver_a.suffix_segments.clone
86
+ second_array_suffix = semver_b.suffix_segments.clone
87
+ first_array_prefix, second_array_prefix = _get_equalized_arrays_for(first_array_prefix, second_array_prefix)
88
+ first_array_suffix, second_array_suffix = _get_equalized_arrays_for(first_array_suffix, second_array_suffix)
89
+ [first_array_prefix.concat(first_array_suffix), second_array_prefix.concat(second_array_suffix)]
90
+ end
91
+
92
+ def get_equalized_prefix_arrays_for(semver_a, semver_b)
93
+ first_array_prefix = semver_a.prefix_segments.clone
94
+ second_array_prefix = semver_b.prefix_segments.clone
95
+ first_array_prefix, second_array_prefix = _get_equalized_arrays_for(first_array_prefix, second_array_prefix)
96
+ [first_array_prefix, second_array_prefix]
97
+ end
98
+
99
+ def <(other)
100
+ self_array, other_array = get_equalized_arrays_for(self, other)
101
+ (0..self_array.size - 1).each do |i|
102
+ if self_array[i] < other_array[i]
103
+ return true
104
+ elsif self_array[i] > other_array[i]
105
+ return false
106
+ end
107
+ end
108
+ false
109
+ end
110
+
111
+ def is_zero?
112
+ @prefix_segments.empty? || @prefix_segments.all?(&:is_zero?)
113
+ end
114
+
115
+ def pre_release?
116
+ @suffix_segments.any?(&:is_pre_release)
117
+ end
118
+
119
+ def post_release?
120
+ @suffix_segments.any?(&:is_post_release)
121
+ end
122
+
123
+ def >(other)
124
+ self_array, other_array = get_equalized_arrays_for(self, other)
125
+ (0..self_array.size - 1).each do |i|
126
+ if self_array[i] > other_array[i]
127
+ return true
128
+ elsif self_array[i] < other_array[i]
129
+ return false
130
+ end
131
+ end
132
+ false
133
+ end
134
+
135
+ def >=(other)
136
+ self == other || self > other || self == other
137
+ end
138
+
139
+ def <=(other)
140
+ self == other || self < other
141
+ end
142
+
143
+ def ==(other)
144
+ segments_a = []
145
+ segments_b = []
146
+
147
+ segments_a += other.segments
148
+ segments_b += @segments
149
+
150
+ if other.segments.size < @segments.size
151
+ (@segments.size - other.segments.size).times { |_| segments_a << SemanticVersionSegment.new('0') }
152
+ elsif other.segments.size > @segments.size
153
+ (other.segments.size - @segments.size).times { |_| segments_b << SemanticVersionSegment.new('0') }
154
+ end
155
+
156
+ (0..segments_a.size - 1).each do |i|
157
+ return false if segments_a[i] != segments_b[i]
158
+ end
159
+ true
160
+ end
161
+
162
+ def !=(other)
163
+ !(self == other)
164
+ end
165
+
166
+ def to_normalized_s
167
+ @segments.map(&:to_normalized_s).join(':')
168
+ end
169
+
170
+ def to_s
171
+ @version_string
172
+ end
173
+
174
+ def minor
175
+ @prefix_segments.size >= 1 ? @prefix_segments[1].to_s : '0'
176
+ end
177
+
178
+ def major
179
+ @prefix_segments.size >= 2 ? @prefix_segments[0].to_s : '0'
180
+ end
181
+
182
+ def patch
183
+ @prefix_segments.size >= 3 ? @prefix_segments[2].to_s : '0'
184
+ end
185
+ end
186
+
187
+ class SemanticVersionSegment
188
+ attr_accessor :normalized_group_string, :original_group_string, :is_post_release, :is_pre_release
189
+
190
+ @@group_suffixes = {
191
+ # pre-releases
192
+ 'PRE' => -16,
193
+ 'PREVIEW' => -16,
194
+ 'DEV' => -15,
195
+ 'A' => -14,
196
+ 'ALPHA' => -13,
197
+ 'B' => -12,
198
+ 'BETA' => -12,
199
+ 'RC' => -11,
200
+ 'M' => -10,
201
+
202
+ 'RELEASE' => 0,
203
+ 'FINAL' => 0,
204
+ # PHP specific
205
+ 'STABLE' => 0,
206
+
207
+ # post-releases
208
+ 'SP' => 1
209
+ }
210
+
211
+ def initialize(group_string)
212
+ @is_post_release = false
213
+ @is_pre_release = false
214
+
215
+ @version_string = group_string
216
+ @original_group_string = group_string
217
+ # use x as unique placeholder
218
+ group_string_ucase = group_string.to_s.gsub(/\*/, 'x').upcase
219
+
220
+ if @@group_suffixes.key?(group_string_ucase)
221
+ value = @@group_suffixes[group_string_ucase]
222
+ @is_post_release = value > 0
223
+ @is_pre_release = value < 0
224
+ @normalized_group_string = @@group_suffixes[group_string_ucase].to_s
225
+ else
226
+ @normalized_group_string = group_string_ucase
227
+ end
228
+ end
229
+
230
+ def compare(semver_a, semver_b, ret_anr_bnonr, ret_anonr_bnr, comparator)
231
+ if semver_a.number? && semver_b.number?
232
+ semver_a.to_i.send(comparator, semver_b.to_i)
233
+ elsif semver_a.number? && !semver_b.number?
234
+ if semver_b == 'X'
235
+ true
236
+ else
237
+ ret_anr_bnonr
238
+ end
239
+ elsif !semver_a.number? && semver_b.number?
240
+ if semver_a == 'X'
241
+ true
242
+ else
243
+ ret_anonr_bnr
244
+ end
245
+ elsif semver_a == 'X' || semver_b == 'X' # !semantic_version_b.group_string.is_number? && !semantic_version_agrous_string.is_number?
246
+ true
247
+ else
248
+ semver_a.send(comparator, semver_b)
249
+ end
250
+ end
251
+
252
+ def <(other)
253
+ compare(normalized_group_string, other.normalized_group_string, true, false, :<)
254
+ end
255
+
256
+ def >(other)
257
+ compare(normalized_group_string, other.normalized_group_string, false, true, :>)
258
+ end
259
+
260
+ def >=(other)
261
+ self == other || compare(normalized_group_string, other.normalized_group_string, false, true,
262
+ :>)
263
+ end
264
+
265
+ def <=(other)
266
+ self == other || compare(normalized_group_string, other.normalized_group_string, true, false,
267
+ :<)
268
+ end
269
+
270
+ def ==(other)
271
+ normalized_group_string == other.normalized_group_string
272
+ end
273
+
274
+ def !=(other)
275
+ !(self == other)
276
+ end
277
+
278
+ def to_normalized_s
279
+ @normalized_group_string
280
+ end
281
+
282
+ def to_s
283
+ @version_string
284
+ end
285
+
286
+ def is_number?
287
+ normalized_group_string.number?
288
+ end
289
+
290
+ def is_zero?
291
+ is_number? ? normalized_group_string.to_i == 0 : false
292
+ end
293
+ end
294
+ end