semver_dialects 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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