dependabot-swift 0.386.0 → 0.387.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b519a5e6fa6c528dbf17979ac4f6098c72fd5d2dd8556227d54cbfb037c1f798
4
- data.tar.gz: aa846626e5bd32753a4693312002ae13e64940d1274693c15d8fc5fd042c9db1
3
+ metadata.gz: b354cacba27d8bed15800f9406537dfab055c1ddd4f71805b578e8d7a443e1a7
4
+ data.tar.gz: '00208626e381f350b287ca78ac59a30066243f925503454145b3c7cca8c143f1'
5
5
  SHA512:
6
- metadata.gz: cefbb851396594af4d6d7085975b6b60a6eebacdb9ee6c77c4341d4e48ac1368b3eb127283a33d0eb0f104e3a06327fed46da5c73d44459ff657bbe33a54dbe9
7
- data.tar.gz: 12fdf67a310d1433d310527eb29c8bbf2b36fa4beff1fa2f2a4767f6bd61120e12e9cc5fabb1136d66070a1c7152c5480ccea74f9057eeb468a5184e7494204b
6
+ metadata.gz: 9043fdeb9fffbb3fb4473c687a5e7cbb75c3405c767739be645efc8b57cba715e139394f41d1729652c021f14347154f62b288b37bf7feb6e76832269e624682
7
+ data.tar.gz: a7568ae5132652a801a449504187e19e6ce10ad59f8290ed696bf556f6612f52edd1b9435137cf14586fd8984191987266e20e66a89d0059efe949bd4be0c561
@@ -48,7 +48,7 @@ module Dependabot
48
48
  end
49
49
  end
50
50
 
51
- sig { returns(T::Hash[String, T.untyped]) }
51
+ sig { returns(T::Hash[String, Object]) }
52
52
  def formatted_deps
53
53
  deps = SharedHelpers.run_shell_command(
54
54
  "swift package show-dependencies --format json",
@@ -60,30 +60,41 @@ module Dependabot
60
60
 
61
61
  sig do
62
62
  params(
63
- data: T::Hash[String, T.untyped],
63
+ data: T::Hash[String, Object],
64
64
  level: Integer
65
65
  )
66
66
  .returns(T::Array[Dependabot::Dependency])
67
67
  end
68
68
  def subdependencies(data, level: 0)
69
- data["dependencies"].flat_map { |root| all_dependencies(root, level: level) }
69
+ dependencies = data["dependencies"]
70
+ return [] unless dependencies.is_a?(Array)
71
+
72
+ dependencies.flat_map do |root|
73
+ next [] unless root.is_a?(Hash)
74
+
75
+ all_dependencies(root, level: level)
76
+ end
70
77
  end
71
78
 
72
79
  sig do
73
80
  params(
74
- data: T::Hash[String, T.untyped],
81
+ data: T::Hash[String, Object],
75
82
  level: Integer
76
83
  )
77
84
  .returns(T::Array[Dependabot::Dependency])
78
85
  end
79
86
  def all_dependencies(data, level: 0)
80
87
  identity = data["identity"]
81
- url = SharedHelpers.scp_to_standard(data["url"])
82
- name = UrlHelpers.normalize_name(url)
88
+ url_value = data["url"]
83
89
  version = data["version"]
90
+ return subdependencies(data, level: level + 1) unless url_value.is_a?(String)
91
+
92
+ url = SharedHelpers.scp_to_standard(url_value)
93
+ name = UrlHelpers.normalize_name(url)
94
+ version = nil unless version.is_a?(String)
84
95
 
85
96
  source = { type: "git", url: url, ref: version, branch: nil }
86
- metadata = { identity: identity }
97
+ metadata = { identity: identity.is_a?(String) ? identity : nil }
87
98
  args = { name: name, version: version, package_manager: "swift", requirements: [], metadata: metadata }
88
99
 
89
100
  if level.zero?
@@ -92,7 +103,7 @@ module Dependabot
92
103
  args[:subdependency_metadata] = [{ source: source }]
93
104
  end
94
105
 
95
- dep = Dependency.new(**args) if data["version"] != "unspecified"
106
+ dep = Dependency.new(**args) if version != "unspecified"
96
107
 
97
108
  [dep, *subdependencies(data, level: level + 1)].compact
98
109
  end
@@ -27,7 +27,7 @@ module Dependabot
27
27
  @source = source
28
28
  end
29
29
 
30
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
30
+ sig { returns(T::Array[T::Hash[Symbol, Object]]) }
31
31
  def requirements
32
32
  found = manifest.content&.scan(DEPENDENCY)&.find do |_declaration, url, _requirement|
33
33
  SharedHelpers.scp_to_standard(url.to_s) == source[:url]
@@ -46,7 +46,7 @@ module Dependabot
46
46
  sig { returns(Dependabot::DependencyFile) }
47
47
  attr_reader :resolved_file
48
48
 
49
- sig { returns(T::Hash[String, T.untyped]) }
49
+ sig { returns(T::Hash[String, Object]) }
50
50
  def parse_json
51
51
  content = resolved_file.content
52
52
  unless content
@@ -64,7 +64,7 @@ module Dependabot
64
64
  )
65
65
  end
66
66
 
67
- sig { params(parsed: T::Hash[String, T.untyped]).returns(Integer) }
67
+ sig { params(parsed: T::Hash[String, Object]).returns(Integer) }
68
68
  def detect_schema_version(parsed)
69
69
  version = parsed["version"]
70
70
 
@@ -81,13 +81,14 @@ module Dependabot
81
81
 
82
82
  sig do
83
83
  params(
84
- parsed: T::Hash[String, T.untyped],
84
+ parsed: T::Hash[String, Object],
85
85
  schema_version: Integer
86
- ).returns(T::Array[T::Hash[String, T.untyped]])
86
+ ).returns(T::Array[T::Hash[String, Object]])
87
87
  end
88
88
  def extract_pins(parsed, schema_version)
89
89
  pins = if schema_version == 1
90
- parsed.dig("object", "pins")
90
+ object = parsed["object"]
91
+ object["pins"] if object.is_a?(Hash)
91
92
  else
92
93
  # v2 and v3 use the same top-level "pins" key
93
94
  parsed["pins"]
@@ -106,7 +107,7 @@ module Dependabot
106
107
 
107
108
  sig do
108
109
  params(
109
- pin: T::Hash[String, T.untyped],
110
+ pin: T::Hash[String, Object],
110
111
  schema_version: Integer
111
112
  ).returns(T.nilable(Dependabot::Dependency))
112
113
  end
@@ -115,21 +116,34 @@ module Dependabot
115
116
  url = pin[T.must(keys[:url])]
116
117
  return nil unless url.is_a?(String) && !url.empty?
117
118
 
118
- state = pin[T.must(keys[:state])] || {}
119
- identity = pin[T.must(keys[:identity])]
120
- # v1 uses a display name for "package"; normalize to lowercase like v2/v3 "identity".
121
- # v2/v3 identity is always lowercase per spec, so only v1 needs downcasing.
122
- identity = identity&.downcase if schema_version == 1
119
+ state = pin_state(pin, T.must(keys[:state]))
123
120
 
124
121
  build_dependency_object(
125
- identity: identity,
122
+ identity: pin_identity(pin, T.must(keys[:identity]), schema_version),
126
123
  url: url,
127
- version: state["version"],
128
- revision: state["revision"],
129
- branch: state["branch"]
124
+ version: string_value(state["version"]),
125
+ revision: string_value(state["revision"]),
126
+ branch: string_value(state["branch"])
130
127
  )
131
128
  end
132
129
 
130
+ sig { params(pin: T::Hash[String, Object], state_key: String).returns(T::Hash[String, Object]) }
131
+ def pin_state(pin, state_key)
132
+ state = pin[state_key]
133
+ state.is_a?(Hash) ? state : {}
134
+ end
135
+
136
+ sig { params(pin: T::Hash[String, Object], identity_key: String, schema_version: Integer).returns(T.nilable(String)) }
137
+ def pin_identity(pin, identity_key, schema_version)
138
+ identity = string_value(pin[identity_key])
139
+ schema_version == 1 ? identity&.downcase : identity
140
+ end
141
+
142
+ sig { params(value: T.nilable(Object)).returns(T.nilable(String)) }
143
+ def string_value(value)
144
+ value if value.is_a?(String)
145
+ end
146
+
133
147
  sig do
134
148
  params(
135
149
  identity: T.nilable(String),
@@ -49,12 +49,12 @@ module Dependabot
49
49
  # Returns a hash mapping normalized URL to requirement metadata.
50
50
  # Each entry includes the Dependabot requirement string and the raw
51
51
  # Xcode requirement kind/version info for use in metadata.
52
- sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
52
+ sig { returns(T::Hash[String, T::Hash[Symbol, Object]]) }
53
53
  def parse
54
54
  content = pbxproj_file.content
55
55
  return {} unless content
56
56
 
57
- requirements = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]])
57
+ requirements = T.let({}, T::Hash[String, T::Hash[Symbol, Object]])
58
58
 
59
59
  content.scan(PACKAGE_REF_BLOCK).each do |url, requirement_block|
60
60
  url = T.cast(url, String)
@@ -81,7 +81,7 @@ module Dependabot
81
81
 
82
82
  sig do
83
83
  params(block: String)
84
- .returns(T.nilable(T::Hash[Symbol, T.untyped]))
84
+ .returns(T.nilable(T::Hash[Symbol, Object]))
85
85
  end
86
86
  def parse_requirement_block(block)
87
87
  kind = block.match(KIND_PATTERN)&.captures&.first
@@ -103,7 +103,7 @@ module Dependabot
103
103
  end
104
104
  end
105
105
 
106
- sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
106
+ sig { params(block: String).returns(T::Hash[Symbol, Object]) }
107
107
  def build_up_to_next_major(block)
108
108
  min_version = extract_version(block, MIN_VERSION_PATTERN)
109
109
  requirement_string = "from: \"#{min_version}\""
@@ -116,7 +116,7 @@ module Dependabot
116
116
  }
117
117
  end
118
118
 
119
- sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
119
+ sig { params(block: String).returns(T::Hash[Symbol, Object]) }
120
120
  def build_up_to_next_minor(block)
121
121
  min_version = extract_version(block, MIN_VERSION_PATTERN)
122
122
  requirement_string = ".upToNextMinor(from: \"#{min_version}\")"
@@ -129,7 +129,7 @@ module Dependabot
129
129
  }
130
130
  end
131
131
 
132
- sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
132
+ sig { params(block: String).returns(T::Hash[Symbol, Object]) }
133
133
  def build_exact(block)
134
134
  version = extract_version(block, MIN_VERSION_PATTERN) || extract_version(block, VERSION_PATTERN)
135
135
  requirement_string = "exact: \"#{version}\""
@@ -142,7 +142,7 @@ module Dependabot
142
142
  }
143
143
  end
144
144
 
145
- sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
145
+ sig { params(block: String).returns(T::Hash[Symbol, Object]) }
146
146
  def build_range(block)
147
147
  min_version = extract_version(block, MIN_VERSION_PATTERN)
148
148
  max_version = extract_version(block, MAX_VERSION_PATTERN)
@@ -156,7 +156,7 @@ module Dependabot
156
156
  }
157
157
  end
158
158
 
159
- sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
159
+ sig { params(block: String).returns(T::Hash[Symbol, Object]) }
160
160
  def build_branch(block)
161
161
  branch = block.match(BRANCH_PATTERN)&.captures&.first
162
162
 
@@ -168,7 +168,7 @@ module Dependabot
168
168
  }
169
169
  end
170
170
 
171
- sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
171
+ sig { params(block: String).returns(T::Hash[Symbol, Object]) }
172
172
  def build_revision(block)
173
173
  revision = block.match(REVISION_PATTERN)&.captures&.first
174
174
 
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -65,9 +65,9 @@ module Dependabot
65
65
  # Collects requirement info from all project.pbxproj support files,
66
66
  # keyed by Xcode scope directory so each resolved file can be enriched
67
67
  # by requirements from its closest matching Xcode scope.
68
- sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) }
68
+ sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, Object]]]) }
69
69
  def aggregate_pbxproj_requirements
70
- scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]])
70
+ scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, Object]]])
71
71
 
72
72
  pbxproj_files.each do |pbxproj_file|
73
73
  xcode_scope_dir = extract_xcode_scope_dir(pbxproj_file.name)
@@ -83,8 +83,8 @@ module Dependabot
83
83
 
84
84
  sig do
85
85
  params(
86
- scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]
87
- ).returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
86
+ scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, Object]]]
87
+ ).returns(T::Hash[String, T::Hash[Symbol, Object]])
88
88
  end
89
89
  def merge_scopes(scoped_requirements)
90
90
  scoped_requirements.values.each_with_object({}) do |requirements, merged|
@@ -94,9 +94,9 @@ module Dependabot
94
94
 
95
95
  sig do
96
96
  params(
97
- scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]],
97
+ scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, Object]]],
98
98
  resolved_file_name: String
99
- ).returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
99
+ ).returns(T::Hash[String, T::Hash[Symbol, Object]])
100
100
  end
101
101
  def requirements_for_resolved_file(scoped_requirements:, resolved_file_name:)
102
102
  scope_dir = extract_xcode_scope_dir(resolved_file_name)
@@ -129,7 +129,7 @@ module Dependabot
129
129
  sig do
130
130
  params(
131
131
  dep: Dependabot::Dependency,
132
- pbxproj_requirements: T::Hash[String, T::Hash[Symbol, T.untyped]]
132
+ pbxproj_requirements: T::Hash[String, T::Hash[Symbol, Object]]
133
133
  ).returns(Dependabot::Dependency)
134
134
  end
135
135
  def enrich_with_pbxproj_requirements(dep, pbxproj_requirements)
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/file_updaters/base"
@@ -14,8 +14,8 @@ module Dependabot
14
14
  sig do
15
15
  params(
16
16
  content: String,
17
- old_requirements: T::Array[T::Hash[Symbol, T.untyped]],
18
- new_requirements: T::Array[T::Hash[Symbol, T.untyped]]
17
+ old_requirements: T::Array[T::Hash[Symbol, Object]],
18
+ new_requirements: T::Array[T::Hash[Symbol, Object]]
19
19
  )
20
20
  .void
21
21
  end
@@ -30,11 +30,13 @@ module Dependabot
30
30
  updated_content = content
31
31
 
32
32
  old_requirements.zip(new_requirements).each do |old, new|
33
+ old_metadata = metadata(old)
34
+ new_metadata = metadata(T.must(new))
33
35
  updated_content = RequirementReplacer.new(
34
36
  content: updated_content,
35
- declaration: old[:metadata][:declaration_string],
36
- old_requirement: old[:metadata][:requirement_string],
37
- new_requirement: T.must(new)[:metadata][:requirement_string]
37
+ declaration: string_value(old_metadata, :declaration_string),
38
+ old_requirement: string_value(old_metadata, :requirement_string),
39
+ new_requirement: string_value(new_metadata, :requirement_string)
38
40
  ).updated_content
39
41
  end
40
42
 
@@ -46,11 +48,27 @@ module Dependabot
46
48
  sig { returns(String) }
47
49
  attr_reader :content
48
50
 
49
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
51
+ sig { returns(T::Array[T::Hash[Symbol, Object]]) }
50
52
  attr_reader :old_requirements
51
53
 
52
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
54
+ sig { returns(T::Array[T::Hash[Symbol, Object]]) }
53
55
  attr_reader :new_requirements
56
+
57
+ sig { params(requirement: T::Hash[Symbol, Object]).returns(T::Hash[Symbol, Object]) }
58
+ def metadata(requirement)
59
+ value = requirement[:metadata]
60
+ raise TypeError, "Expected metadata to be a Hash" unless value.is_a?(Hash)
61
+
62
+ value
63
+ end
64
+
65
+ sig { params(hash: T::Hash[Symbol, Object], key: Symbol).returns(String) }
66
+ def string_value(hash, key)
67
+ value = hash.fetch(key)
68
+ raise TypeError, "Expected #{key} to be a String" unless value.is_a?(String)
69
+
70
+ value
71
+ end
54
72
  end
55
73
  end
56
74
  end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/file_updaters/base"
@@ -26,7 +26,7 @@ module Dependabot
26
26
  2 => { url: "location", identity: "identity", pins_path: ["pins"] },
27
27
  3 => { url: "location", identity: "identity", pins_path: ["pins"] }
28
28
  }.freeze,
29
- T::Hash[Integer, T::Hash[Symbol, T.untyped]]
29
+ T::Hash[Integer, T::Hash[Symbol, Object]]
30
30
  )
31
31
 
32
32
  sig do
@@ -87,7 +87,7 @@ module Dependabot
87
87
  sig { returns(T::Array[Dependabot::DependencyFile]) }
88
88
  attr_reader :workspace_files
89
89
 
90
- sig { params(content: String).returns(T::Hash[String, T.untyped]) }
90
+ sig { params(content: String).returns(T::Hash[String, Object]) }
91
91
  def parse_json(content)
92
92
  JSON.parse(content)
93
93
  rescue JSON::ParserError => e
@@ -97,7 +97,7 @@ module Dependabot
97
97
  )
98
98
  end
99
99
 
100
- sig { params(parsed: T::Hash[String, T.untyped]).returns(Integer) }
100
+ sig { params(parsed: T::Hash[String, Object]).returns(Integer) }
101
101
  def detect_schema_version(parsed)
102
102
  version = parsed["version"]
103
103
 
@@ -114,9 +114,9 @@ module Dependabot
114
114
 
115
115
  sig do
116
116
  params(
117
- parsed: T::Hash[String, T.untyped],
117
+ parsed: T::Hash[String, Object],
118
118
  schema_version: Integer,
119
- keys: T::Hash[Symbol, T.untyped]
119
+ keys: T::Hash[Symbol, Object]
120
120
  ).void
121
121
  end
122
122
  def update_pins(parsed, schema_version, keys)
@@ -138,14 +138,14 @@ module Dependabot
138
138
 
139
139
  sig do
140
140
  params(
141
- parsed: T::Hash[String, T.untyped],
141
+ parsed: T::Hash[String, Object],
142
142
  path: T::Array[String]
143
- ).returns(T.untyped)
143
+ ).returns(Object)
144
144
  end
145
145
  def dig_pins(parsed, path)
146
146
  # Navigate nested hash using path keys
147
147
  # Path is either ["object", "pins"] for v1 or ["pins"] for v2/v3
148
- current = T.let(parsed, T.untyped)
148
+ current = T.let(parsed, Object)
149
149
  path.each do |key|
150
150
  break unless current.is_a?(Hash)
151
151
 
@@ -156,9 +156,9 @@ module Dependabot
156
156
 
157
157
  sig do
158
158
  params(
159
- pins: T::Array[T::Hash[String, T.untyped]],
159
+ pins: T::Array[T::Hash[String, Object]],
160
160
  dependency: Dependabot::Dependency,
161
- keys: T::Hash[Symbol, T.untyped],
161
+ keys: T::Hash[Symbol, Object],
162
162
  schema_version: Integer
163
163
  ).void
164
164
  end
@@ -192,11 +192,11 @@ module Dependabot
192
192
 
193
193
  sig do
194
194
  params(
195
- pins: T::Array[T::Hash[String, T.untyped]],
195
+ pins: T::Array[T::Hash[String, Object]],
196
196
  dependency: Dependabot::Dependency,
197
- keys: T::Hash[Symbol, T.untyped],
197
+ keys: T::Hash[Symbol, Object],
198
198
  schema_version: Integer
199
- ).returns(T.nilable(T::Hash[String, T.untyped]))
199
+ ).returns(T.nilable(T::Hash[String, Object]))
200
200
  end
201
201
  def find_pin_for_dependency(pins, dependency, keys, schema_version)
202
202
  identity_key = T.cast(keys[:identity], String)
@@ -205,6 +205,7 @@ module Dependabot
205
205
 
206
206
  pins.find do |pin|
207
207
  pin_identity = pin[identity_key]
208
+ pin_identity = nil unless pin_identity.is_a?(String)
208
209
  pin_identity = pin_identity&.downcase if schema_version == 1
209
210
 
210
211
  if identity && pin_identity == identity
@@ -22,14 +22,20 @@ module Dependabot
22
22
  sig do
23
23
  type_parameters(:T)
24
24
  .params(
25
- requirements: T::Array[T::Hash[Symbol, T.untyped]],
25
+ requirements: T::Array[T::Hash[Symbol, Object]],
26
26
  _blk: T.proc.params(declaration: NativeRequirement).returns(String)
27
27
  )
28
- .returns(T::Array[T::Hash[Symbol, T.untyped]])
28
+ .returns(T::Array[T::Hash[Symbol, Object]])
29
29
  end
30
30
  def self.map_requirements(requirements, &_blk)
31
31
  requirements.map do |requirement|
32
- declaration = new(requirement[:metadata][:requirement_string])
32
+ metadata = requirement[:metadata]
33
+ next requirement unless metadata.is_a?(Hash)
34
+
35
+ requirement_string = metadata[:requirement_string]
36
+ next requirement unless requirement_string.is_a?(String)
37
+
38
+ declaration = new(requirement_string)
33
39
 
34
40
  new_declaration = yield(declaration)
35
41
  new_requirement = new(new_declaration)
@@ -5,12 +5,47 @@ require "sorbet-runtime"
5
5
 
6
6
  require "dependabot/requirement"
7
7
  require "dependabot/utils"
8
+ require "dependabot/swift/version"
8
9
 
9
10
  module Dependabot
10
11
  module Swift
11
12
  class Requirement < Dependabot::Requirement
12
13
  extend T::Sig
13
14
 
15
+ quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|")
16
+ # Swift requirement grammar (intentionally not strict SemVer): allows multi-segment numeric
17
+ # cores like "4.2.5.1" for compatibility bounds, but still rejects leading zeros and malformed
18
+ # prerelease/build tails via SemVer-style dot-separated identifiers.
19
+ num = "(?:0|[1-9][0-9]*)"
20
+ pre_id = "(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
21
+ version_pattern = "#{num}(?:\\.#{num})*" \
22
+ "(?:-#{pre_id}(?:\\.#{pre_id})*)?" \
23
+ '(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?'
24
+
25
+ PATTERN = T.let(/\A\s*(#{quoted})?\s*(#{version_pattern})\s*\z/, Regexp)
26
+
27
+ # Parse each boundary into a Swift::Version so prerelease boundaries use SemVer section 11 ordering.
28
+ sig { override.params(obj: T.any(Gem::Version, String)).returns([String, Gem::Version]) }
29
+ def self.parse(obj)
30
+ # Preserve a Swift::Version; convert other stable Gem::Versions so the boundary stays orderable.
31
+ return ["=", obj] if obj.is_a?(Swift::Version)
32
+
33
+ if obj.is_a?(Gem::Version)
34
+ # Reject prerelease Gem::Versions: #to_s is lossily canonicalized (e.g. "1.0.0-alpha" -> "1.0.0.pre.alpha").
35
+ raise BadRequirementError, "Illformed requirement [#{obj.inspect}]" if obj.prerelease?
36
+
37
+ return ["=", Swift::Version.new(obj.to_s)]
38
+ end
39
+
40
+ unless (matches = PATTERN.match(obj.to_s))
41
+ raise BadRequirementError, "Illformed requirement [#{obj.inspect}]"
42
+ end
43
+
44
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
45
+
46
+ [matches[1] || "=", Swift::Version.new(T.must(matches[2]))]
47
+ end
48
+
14
49
  # For consistency with other languages, we define a requirements array.
15
50
  # Swift doesn't have an `OR` separator for requirements, so it
16
51
  # always contains a single element.
@@ -93,8 +93,8 @@ module Dependabot
93
93
 
94
94
  sig do
95
95
  params(
96
- source: T.nilable(T::Hash[T.any(String, Symbol), T.untyped])
97
- ).returns(T.nilable(T::Hash[T.any(String, Symbol), T.untyped]))
96
+ source: T.nilable(T::Hash[T.any(String, Symbol), Object])
97
+ ).returns(T.nilable(T::Hash[T.any(String, Symbol), Object]))
98
98
  end
99
99
  def update_source_ref(source)
100
100
  return source unless source && target_version
@@ -38,16 +38,19 @@ module Dependabot
38
38
  tag = latest_resolvable_version_tag
39
39
  return nil unless tag
40
40
 
41
- Version.new(tag.fetch(:version))
41
+ version = tag_version(tag)
42
+ return nil unless version
43
+
44
+ Version.new(version)
42
45
  end
43
46
 
44
47
  # Returns the full tag info including commit_sha for the latest resolvable version
45
48
  # Memoized to avoid redundant computation when called from UpdateChecker
46
- sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
49
+ sig { returns(T.nilable(T::Hash[Symbol, Object])) }
47
50
  def latest_resolvable_version_tag
48
51
  @latest_resolvable_version_tag ||= T.let(
49
52
  compute_latest_resolvable_version_tag,
50
- T.nilable(T::Hash[Symbol, T.untyped])
53
+ T.nilable(T::Hash[Symbol, Object])
51
54
  )
52
55
  end
53
56
 
@@ -56,16 +59,17 @@ module Dependabot
56
59
  return nil unless version_pinned?
57
60
 
58
61
  tags = git_commit_checker.local_tags_for_allowed_versions
59
- relevant_tags = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(
60
- tags,
62
+ relevant_versions = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(
63
+ tags.filter_map { |tag| tag_version(tag) },
61
64
  security_advisories
62
65
  )
66
+ relevant_tags = tags.select { |tag| (version = tag_version(tag)) && relevant_versions.include?(version) }
63
67
  relevant_tags = filter_lower_tags(relevant_tags)
64
68
 
65
- lowest_tag = relevant_tags.min_by { |tag| tag.fetch(:version) }
69
+ lowest_tag = relevant_tags.min_by { |tag| T.must(tag_version(tag)) }
66
70
  return nil unless lowest_tag
67
71
 
68
- Version.new(lowest_tag.fetch(:version))
72
+ Version.new(T.must(tag_version(lowest_tag)))
69
73
  end
70
74
 
71
75
  sig { returns(T::Boolean) }
@@ -86,7 +90,7 @@ module Dependabot
86
90
  sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
87
91
  attr_reader :security_advisories
88
92
 
89
- sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
93
+ sig { returns(T.nilable(T::Hash[Symbol, Object])) }
90
94
  def compute_latest_resolvable_version_tag
91
95
  return nil unless version_pinned?
92
96
 
@@ -106,7 +110,7 @@ module Dependabot
106
110
  # For versionRange requirements, find the highest version that satisfies
107
111
  # the explicit upper bound constraint. We don't filter out lower versions here
108
112
  # because `can_update?` will decide whether an update is actually needed.
109
- sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
113
+ sig { returns(T.nilable(T::Hash[Symbol, Object])) }
110
114
  def compute_latest_version_in_range
111
115
  requirement = dependency_requirement
112
116
  return nil unless requirement
@@ -132,7 +136,7 @@ module Dependabot
132
136
  dependency.requirements.first&.dig(:metadata, :kind)
133
137
  end
134
138
 
135
- sig { params(version: T.untyped).returns(T::Boolean) }
139
+ sig { params(version: Object).returns(T::Boolean) }
136
140
  def version_meets_requirements?(version)
137
141
  kind = requirement_kind
138
142
 
@@ -174,14 +178,23 @@ module Dependabot
174
178
 
175
179
  sig do
176
180
  params(
177
- tags: T::Array[T::Hash[Symbol, T.untyped]]
178
- ).returns(T::Array[T::Hash[Symbol, T.untyped]])
181
+ tags: T::Array[T::Hash[Symbol, Object]]
182
+ ).returns(T::Array[T::Hash[Symbol, Object]])
179
183
  end
180
184
  def filter_lower_tags(tags)
181
185
  current = current_version
182
186
  return tags unless current
183
187
 
184
- tags.select { |tag| tag.fetch(:version) > current }
188
+ tags.select do |tag|
189
+ version = tag_version(tag)
190
+ version && version > current
191
+ end
192
+ end
193
+
194
+ sig { params(tag: T::Hash[Symbol, Object]).returns(T.nilable(Gem::Version)) }
195
+ def tag_version(tag)
196
+ version = tag[:version]
197
+ version if version.is_a?(Gem::Version)
185
198
  end
186
199
 
187
200
  sig { returns(T.nilable(Dependabot::Version)) }
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -81,7 +81,8 @@ module Dependabot
81
81
  latest = latest_resolvable_version
82
82
  if latest && target == latest
83
83
  tag = xcode_version_resolver.latest_resolvable_version_tag
84
- commit_sha = tag&.fetch(:commit_sha, nil)
84
+ tag_commit_sha = tag&.fetch(:commit_sha, nil)
85
+ commit_sha = tag_commit_sha if tag_commit_sha.is_a?(String)
85
86
  end
86
87
 
87
88
  RequirementsUpdater.new(
@@ -111,7 +112,7 @@ module Dependabot
111
112
  tag = latest_version_tag
112
113
  return unless tag
113
114
 
114
- tag.fetch(:version)
115
+ tag_version(tag)
115
116
  end
116
117
 
117
118
  sig { returns(T.nilable(Dependabot::Version)) }
@@ -123,7 +124,7 @@ module Dependabot
123
124
  tag = latest_version_tag
124
125
  return unless tag
125
126
 
126
- tag.fetch(:version)
127
+ tag_version(tag)
127
128
  end
128
129
 
129
130
  sig { returns(T.nilable(Dependabot::Version)) }
@@ -135,7 +136,7 @@ module Dependabot
135
136
  tag = lowest_security_fix_version_tag
136
137
  return unless tag
137
138
 
138
- tag.fetch(:version)
139
+ tag_version(tag)
139
140
  end
140
141
 
141
142
  sig { returns(T.nilable(Dependabot::Version)) }
@@ -170,7 +171,7 @@ module Dependabot
170
171
  Version.new(lowest_resolvable_security_fix_version)
171
172
  end
172
173
 
173
- sig { params(requirements: T::Array[T::Hash[Symbol, T.untyped]]).returns(VersionResolver) }
174
+ sig { params(requirements: T::Array[T::Hash[Symbol, Object]]).returns(VersionResolver) }
174
175
  def version_resolver_for(requirements)
175
176
  VersionResolver.new(
176
177
  dependency: dependency,
@@ -181,26 +182,26 @@ module Dependabot
181
182
  )
182
183
  end
183
184
 
184
- sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
185
+ sig { returns(T.nilable(T::Hash[Symbol, Object])) }
185
186
  def latest_version_tag
186
187
  git_commit_checker.local_tag_for_latest_version(update_cooldown)
187
188
  end
188
189
 
189
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
190
+ sig { returns(T::Array[T::Hash[Symbol, Object]]) }
190
191
  def unlocked_requirements
191
192
  NativeRequirement.map_requirements(old_requirements) do |_old_requirement|
192
193
  "\"#{dependency.version}\"...\"#{latest_version}\""
193
194
  end
194
195
  end
195
196
 
196
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
197
+ sig { returns(T::Array[T::Hash[Symbol, Object]]) }
197
198
  def force_lowest_security_fix_requirements
198
199
  NativeRequirement.map_requirements(old_requirements) do |_old_requirement|
199
200
  "\"#{lowest_security_fix_version}\"...\"#{lowest_security_fix_version}\""
200
201
  end
201
202
  end
202
203
 
203
- sig { params(new_requirements: T::Array[T::Hash[Symbol, T.untyped]]).returns(Dependabot::DependencyFile) }
204
+ sig { params(new_requirements: T::Array[T::Hash[Symbol, Object]]).returns(Dependabot::DependencyFile) }
204
205
  def prepare_manifest_for(new_requirements)
205
206
  manifest_file = T.must(manifest)
206
207
 
@@ -256,26 +257,39 @@ module Dependabot
256
257
  )
257
258
  end
258
259
 
259
- sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
260
+ sig { returns(T.nilable(T::Hash[Symbol, Object])) }
260
261
  def lowest_security_fix_version_tag
261
262
  tags = git_commit_checker.local_tags_for_allowed_versions
262
263
  find_lowest_secure_version(tags)
263
264
  end
264
265
 
265
- sig { params(tags: T::Array[T::Hash[Symbol, T.untyped]]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
266
+ sig { params(tags: T::Array[T::Hash[Symbol, Object]]).returns(T.nilable(T::Hash[Symbol, Object])) }
266
267
  def find_lowest_secure_version(tags)
267
- relevant_tags = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(tags, security_advisories)
268
+ relevant_versions = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(
269
+ tags.filter_map { |tag| tag_version(tag) },
270
+ security_advisories
271
+ )
272
+ relevant_tags = tags.select { |tag| (version = tag_version(tag)) && relevant_versions.include?(version) }
268
273
  relevant_tags = filter_lower_tags(relevant_tags)
269
274
 
270
- relevant_tags.min_by { |tag| tag.fetch(:version) }
275
+ relevant_tags.min_by { |tag| T.must(tag_version(tag)) }
271
276
  end
272
277
 
273
- sig { params(tags_array: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
278
+ sig { params(tags_array: T::Array[T::Hash[Symbol, Object]]).returns(T::Array[T::Hash[Symbol, Object]]) }
274
279
  def filter_lower_tags(tags_array)
275
280
  return tags_array unless current_version
276
281
 
277
282
  tags_array
278
- .select { |tag| tag.fetch(:version) > current_version }
283
+ .select do |tag|
284
+ version = tag_version(tag)
285
+ version && version > current_version
286
+ end
287
+ end
288
+
289
+ sig { params(tag: T::Hash[Symbol, Object]).returns(T.nilable(Dependabot::Version)) }
290
+ def tag_version(tag)
291
+ version = tag[:version]
292
+ Dependabot::Swift::Version.new(version.to_s) if version.is_a?(Gem::Version)
279
293
  end
280
294
 
281
295
  sig { returns(XcodeVersionResolver) }
@@ -1,12 +1,226 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
4
6
  require "dependabot/utils"
5
7
  require "dependabot/version"
6
8
 
7
9
  module Dependabot
8
10
  module Swift
11
+ # Swift uses SemVer: strict validation in .correct?, section 11 pre-release precedence in #<=>.
12
+ # #initialize stays tolerant because GitCommitChecker builds unchecked versions (e.g. "0.0.0.0").
9
13
  class Version < Dependabot::Version
14
+ extend T::Sig
15
+
16
+ # SEMVER_REGEX uses ^/$ line anchors; anchor to the whole string to reject multiline.
17
+ SEMVER_ANCHORED = T.let(/\A#{SEMVER_REGEX.source}\z/x, Regexp)
18
+
19
+ sig { override.params(version: VersionParameter).returns(T::Boolean) }
20
+ def self.correct?(version)
21
+ return false if version.nil?
22
+
23
+ # Strict SemVer only: rejects "1.0.0.alpha", "0.0.0.0", "01.2.3" and multiline input.
24
+ version.to_s.delete_prefix("v").match?(SEMVER_ANCHORED)
25
+ end
26
+
27
+ sig { override.params(version: VersionParameter).void }
28
+ def initialize(version)
29
+ @version_string = T.let(version.to_s.dup.freeze, String)
30
+
31
+ stripped = version.to_s.delete_prefix("v")
32
+ pre_part, plus, build = stripped.partition("+")
33
+ # Strip only well-formed build metadata (SemVer rule 10 ignores it); a malformed "+…" tail is
34
+ # kept so it stays distinct and sorts below the release instead of canonicalizing to it.
35
+ build_ok = plus.empty? || build.match?(/\A[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*\z/)
36
+ @semver = T.let((build_ok ? pre_part : stripped).freeze, String)
37
+
38
+ base, suffix = pre_part.split("-", 2)
39
+ # Split on "-" only when the core before it is numeric (else the hyphen is part of the tail).
40
+ if suffix && !base.to_s.match?(/\A[0-9]+(?:\.[0-9]+)*\z/)
41
+ base = pre_part
42
+ suffix = nil
43
+ end
44
+ release, inferred = normalize_release(T.must(base))
45
+ suffix ||= inferred
46
+ # Fold a malformed build tail into the pre-release suffix so it stays distinct and orderable.
47
+ suffix = [suffix, "+#{build}"].compact.join(".") unless build_ok
48
+ @prerelease_suffix = T.let(suffix, T.nilable(String))
49
+ @release_core = T.let(Gem::Version.new(release), Gem::Version)
50
+ # Seed the superclass with a strict-SemVer form so tolerant tags never raise, while mixed
51
+ # comparisons still see the pre-release state.
52
+ super(semver_seed(release, @prerelease_suffix))
53
+ end
54
+
55
+ sig { override.returns(String) }
56
+ def to_s
57
+ @version_string
58
+ end
59
+
60
+ sig { override.returns(String) }
61
+ def to_semver
62
+ @semver
63
+ end
64
+
65
+ sig { returns(Gem::Version) }
66
+ def bump
67
+ # Bump the full-precision release core so "~>" bounds ignore the padded 3-segment seed.
68
+ @release_core.bump
69
+ end
70
+
71
+ sig { override.returns(T::Boolean) }
72
+ def prerelease?
73
+ return true unless @prerelease_suffix.nil?
74
+
75
+ super
76
+ end
77
+
78
+ # Strict-SemVer range with "-0" bounds (earliest prerelease) so the grammar accepts it.
79
+ sig { override.returns(T::Array[String]) }
80
+ def ignored_minor_versions
81
+ parts = to_semver.split(".")
82
+ major = parts[0].to_i
83
+ minor = parts.fetch(1, 0).to_i
84
+ [">= #{major}.#{minor + 1}.0-0, < #{major + 1}.0.0-0"]
85
+ end
86
+
87
+ # Strict-SemVer floor at "-0" (earliest prerelease) so a semver-major ignore covers numeric prereleases too.
88
+ sig { override.returns(T::Array[String]) }
89
+ def ignored_major_versions
90
+ major = T.must(to_semver.split(".").first).to_i
91
+ [">= #{major + 1}.0.0-0"]
92
+ end
93
+
94
+ sig { params(other: Object).returns(T::Boolean) }
95
+ def eql?(other)
96
+ return false unless other.is_a?(Version)
97
+
98
+ to_semver == other.to_semver
99
+ end
100
+
101
+ sig { override.returns(Integer) }
102
+ def hash
103
+ to_semver.hash
104
+ end
105
+
106
+ sig { params(other: Object).returns(T::Boolean) }
107
+ def ==(other)
108
+ cmp = self <=> other
109
+ !cmp.nil? && cmp.zero?
110
+ end
111
+
112
+ sig { params(other: Object).returns(T.nilable(Integer)) }
113
+ def <=>(other)
114
+ return nil if other.nil?
115
+
116
+ resolved = coerce_operand(other)
117
+ return nil if resolved.nil?
118
+
119
+ # Compare release cores directly (not via super, whose seeded state uses RubyGems
120
+ # pre-release ordering) so section 11 governs the pre-release tiebreak.
121
+ release_cmp = release_core <=> resolved.release_core
122
+ return release_cmp unless release_cmp&.zero?
123
+
124
+ compare_semver_prerelease(@prerelease_suffix, resolved.prerelease_suffix)
125
+ rescue ArgumentError
126
+ nil
127
+ end
128
+
129
+ protected
130
+
131
+ sig { returns(T.nilable(String)) }
132
+ attr_reader :prerelease_suffix
133
+
134
+ sig { returns(Gem::Version) }
135
+ attr_reader :release_core
136
+
137
+ private
138
+
139
+ # Coerce an operand to a Swift::Version for section 11 ordering, or nil if not orderable.
140
+ # A raw Gem::Version pre-release is declined (lossy #to_s, divergent RubyGems ordering); any
141
+ # #to_s failure also yields nil to keep comparison safe (never raises).
142
+ sig { params(other: Object).returns(T.nilable(Version)) }
143
+ def coerce_operand(other)
144
+ return other if other.is_a?(Version)
145
+ return nil if other.is_a?(Gem::Version) && other.prerelease?
146
+
147
+ normalized = other.to_s.delete_prefix("v")
148
+ if normalized.include?("+")
149
+ return nil unless Version.correct?(normalized)
150
+
151
+ normalized = T.must(normalized.split("+").first)
152
+ end
153
+ return nil unless Gem::Version.correct?(normalized)
154
+
155
+ T.cast(Version.new(normalized), Version)
156
+ rescue StandardError
157
+ nil
158
+ end
159
+
160
+ # Returns [release core, inferred pre-release or nil]: a non-SemVer tag is reduced to its
161
+ # numeric core with the non-numeric tail as pre-release ("1.0.0.alpha" -> "alpha").
162
+ sig { params(base: String).returns([String, T.nilable(String)]) }
163
+ def normalize_release(base)
164
+ return [base, nil] if self.class.correct?(base)
165
+
166
+ parts = base.split(".")
167
+ numeric = parts.take_while { |s| s.match?(/\A\d+\z/) }
168
+ raise ArgumentError, "Malformed version number string #{@version_string}" if numeric.empty?
169
+
170
+ tail = parts.drop(numeric.length)
171
+ # Preserve every numeric segment so multi-segment requirement bounds keep full precision.
172
+ release = numeric.map { |s| s.to_i.to_s }.join(".")
173
+ [release, tail.empty? ? nil : tail.join(".")]
174
+ end
175
+
176
+ # Strict-SemVer seed for the Gem superclass: Gem::Version#initialize validates via
177
+ # self.class.correct? (strict 3-part SemVer here), so the seed is a 3-segment core plus the
178
+ # suffix only when it is valid SemVer, letting tolerant tags (e.g. "0.0.0.0") never raise.
179
+ sig { params(release: String, suffix: T.nilable(String)).returns(String) }
180
+ def semver_seed(release, suffix)
181
+ core = (release.split(".").first(3) + %w(0 0 0)).first(3).join(".")
182
+ return core unless suffix && self.class.correct?("0.0.0-#{suffix}")
183
+
184
+ "#{core}-#{suffix}"
185
+ end
186
+
187
+ # SemVer section 11 pre-release precedence: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0.
188
+ sig { params(left: T.nilable(String), right: T.nilable(String)).returns(Integer) }
189
+ def compare_semver_prerelease(left, right)
190
+ return 0 if left.nil? && right.nil?
191
+ return 1 if left.nil?
192
+ return -1 if right.nil?
193
+
194
+ left_ids = left.split(".")
195
+ right_ids = right.split(".")
196
+
197
+ # Compare identifiers pairwise over the common prefix.
198
+ [left_ids.length, right_ids.length].min.times do |i|
199
+ cmp = compare_semver_identifier(T.must(left_ids[i]), T.must(right_ids[i]))
200
+ return cmp unless cmp.zero?
201
+ end
202
+
203
+ # All shared identifiers equal: more pre-release fields ranks higher.
204
+ left_ids.length <=> right_ids.length
205
+ end
206
+
207
+ sig { params(left: String, right: String).returns(Integer) }
208
+ def compare_semver_identifier(left, right)
209
+ left_numeric = left.match?(/\A\d+\z/)
210
+ right_numeric = right.match?(/\A\d+\z/)
211
+
212
+ if left_numeric && right_numeric
213
+ cmp = left.to_i <=> right.to_i
214
+ # Tiebreak textually so SemVer-invalid leading-zero ids ("01" vs "1") stay distinct.
215
+ cmp.zero? ? T.must(left <=> right) : cmp
216
+ elsif left_numeric
217
+ -1
218
+ elsif right_numeric
219
+ 1
220
+ else
221
+ T.must(left <=> right)
222
+ end
223
+ end
10
224
  end
11
225
  end
12
226
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-swift
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.386.0
4
+ version: 0.387.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.386.0
18
+ version: 0.387.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.386.0
25
+ version: 0.387.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -272,7 +272,7 @@ licenses:
272
272
  - MIT
273
273
  metadata:
274
274
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
275
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.386.0
275
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.387.0
276
276
  rdoc_options: []
277
277
  require_paths:
278
278
  - lib