dependabot-swift 0.365.0 → 0.366.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: 35c7a26b4868b5df0a08396136836264f47a3e1d97219ceb705698bb1eae21f3
4
- data.tar.gz: 3c22d53cfb502e71c17ba6edc2d5d0d6b6dbbfc92fdbfb9fc897579d6c15269f
3
+ metadata.gz: bfecded3f835cd8011a78e5cd10c42eaf1f4a518594428a6467c64300fbe1e2a
4
+ data.tar.gz: a742aa0fab89643edc392546758b904c0e6b8b7a750d7886c470caed4dd46f2f
5
5
  SHA512:
6
- metadata.gz: 919317db274d1166b9ad1fb3118e24ed162051839faf392963510986637805dcbd7f5a991f3e7256ee4ad09b626da1aafe133939b91fe35edf00edb7b15600b6
7
- data.tar.gz: 98b040a728cfa847e5f8ba4474bbf64ef335e47141f94caad1c0a8e9f693a2bc396c2664143ca8f822f599c16ecab8fff8f7a45f66ae29af971df2d3847fdd32
6
+ metadata.gz: 5ad87b7b2c883cb85b11d4c4f21d1ee99730b3ceff9b2d38ca6dc62e2640e854ab5036d38545a239bf387df39d9d6f81f4a77756195556bd4f0c74050bd50736
7
+ data.tar.gz: 9bcebc8617174bbeb25bbb92c9ab6710fcada5a749e118898937d535543d6b65dea35f210fe646682dd29bc343377a3040ad9420e2636d98fb481aea807967e3
@@ -5,20 +5,24 @@ require "sorbet-runtime"
5
5
  require "dependabot/experiments"
6
6
  require "dependabot/file_fetchers"
7
7
  require "dependabot/file_fetchers/base"
8
+ require "dependabot/swift/xcode_file_helpers"
8
9
 
9
10
  module Dependabot
10
11
  module Swift
11
12
  class FileFetcher < Dependabot::FileFetchers::Base
12
13
  extend T::Sig
13
14
 
15
+ XCODEPROJ_SUFFIX = ".xcodeproj"
16
+ XCWORKSPACE_SUFFIX = ".xcworkspace"
14
17
  XCODE_SPM_PACKAGE_RESOLVED_PATH = "project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
18
+ XCWORKSPACE_PACKAGE_RESOLVED_PATH = "xcshareddata/swiftpm/Package.resolved"
15
19
 
16
20
  sig { override.params(filenames: T::Array[String]).returns(T::Boolean) }
17
21
  def self.required_files_in?(filenames)
18
22
  return true if filenames.include?("Package.swift")
19
23
 
20
24
  if Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
21
- return filenames.any? { |f| f.end_with?("Package.resolved") }
25
+ return filenames.any? { |f| XcodeFileHelpers.xcode_resolved_path?(f) }
22
26
  end
23
27
 
24
28
  false
@@ -28,7 +32,7 @@ module Dependabot
28
32
  def self.required_files_message
29
33
  if Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
30
34
  "Repo must contain a Package.swift configuration file or " \
31
- "an .xcodeproj directory with a Package.resolved file."
35
+ "an .xcodeproj/.xcworkspace directory with a Package.resolved file."
32
36
  else
33
37
  "Repo must contain a Package.swift configuration file."
34
38
  end
@@ -74,17 +78,62 @@ module Dependabot
74
78
  resolved = fetch_file_if_present(File.join(xcodeproj_path, XCODE_SPM_PACKAGE_RESOLVED_PATH))
75
79
  fetched_files << resolved if resolved
76
80
  end
81
+
82
+ xcworkspace_dirs.each do |workspace_path|
83
+ workspace_data = fetch_support_file(File.join(workspace_path, "contents.xcworkspacedata"))
84
+ fetched_files << workspace_data if workspace_data
85
+
86
+ resolved = fetch_file_if_present(File.join(workspace_path, XCWORKSPACE_PACKAGE_RESOLVED_PATH))
87
+ fetched_files << resolved if resolved
88
+ end
77
89
  end
78
90
 
79
91
  sig { returns(T::Array[String]) }
80
92
  def xcodeproj_dirs
81
93
  @xcodeproj_dirs ||= T.let(
82
- repo_contents(dir: ".", raise_errors: false)
83
- .select { |entry| entry.type == "dir" && entry.name.end_with?(".xcodeproj") }
84
- .map(&:name),
94
+ discover_dirs_with_suffix(XCODEPROJ_SUFFIX),
85
95
  T.nilable(T::Array[String])
86
96
  )
87
97
  end
98
+
99
+ sig { returns(T::Array[String]) }
100
+ def xcworkspace_dirs
101
+ @xcworkspace_dirs ||= T.let(
102
+ discover_dirs_with_suffix(XCWORKSPACE_SUFFIX)
103
+ .reject { |path| path.include?("#{XCODEPROJ_SUFFIX}/") },
104
+ T.nilable(T::Array[String])
105
+ )
106
+ end
107
+
108
+ sig { params(suffix: String).returns(T::Array[String]) }
109
+ def discover_dirs_with_suffix(suffix)
110
+ discovered = T.let([], T::Array[String])
111
+ queue = T.let(["."], T::Array[String])
112
+ visited = T.let({}, T::Hash[String, T::Boolean])
113
+ index = T.let(0, Integer)
114
+
115
+ while index < queue.length
116
+ dir = T.must(queue[index])
117
+ index += 1
118
+ next if visited[dir]
119
+
120
+ visited[dir] = true
121
+
122
+ entries = repo_contents(dir: dir, raise_errors: false)
123
+ entries.each do |entry|
124
+ next unless entry.type == "dir"
125
+
126
+ next_dir = dir == "." ? entry.name : File.join(dir, entry.name)
127
+ if entry.name.end_with?(suffix)
128
+ discovered << next_dir
129
+ elsif !entry.name.start_with?(".")
130
+ queue << next_dir
131
+ end
132
+ end
133
+ end
134
+
135
+ discovered.sort
136
+ end
88
137
  end
89
138
  end
90
139
  end
@@ -7,14 +7,16 @@ require "dependabot/file_parsers/base/dependency_set"
7
7
  require "dependabot/swift/file_parser"
8
8
  require "dependabot/swift/file_parser/package_resolved_parser"
9
9
  require "dependabot/swift/file_parser/pbxproj_parser"
10
+ require "dependabot/swift/xcode_file_helpers"
10
11
 
11
12
  module Dependabot
12
13
  module Swift
13
14
  class FileParser < Dependabot::FileParsers::Base
14
15
  # Orchestrates Xcode-managed SwiftPM dependency parsing.
15
16
  #
16
- # Parses Package.resolved JSON files found inside .xcodeproj directories,
17
- # then enriches each dependency with requirement info extracted from the
17
+ # Parses Package.resolved JSON files found inside Xcode project and
18
+ # workspace directories (e.g., .xcodeproj and .xcworkspace), then
19
+ # enriches each dependency with requirement info extracted from the
18
20
  # corresponding project.pbxproj files.
19
21
  class XcodeSpmResolver
20
22
  extend T::Sig
@@ -38,8 +40,10 @@ module Dependabot
38
40
 
39
41
  xcode_resolved_files.each do |resolved_file|
40
42
  resolved_deps = PackageResolvedParser.new(resolved_file).parse
41
- xcodeproj_dir = extract_xcodeproj_dir(resolved_file.name)
42
- pbxproj_requirements = scoped_requirements.fetch(xcodeproj_dir, {})
43
+ pbxproj_requirements = requirements_for_resolved_file(
44
+ scoped_requirements: scoped_requirements,
45
+ resolved_file_name: resolved_file.name
46
+ )
43
47
 
44
48
  resolved_deps.each do |dep|
45
49
  enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements)
@@ -59,24 +63,67 @@ module Dependabot
59
63
  attr_reader :pbxproj_files
60
64
 
61
65
  # Collects requirement info from all project.pbxproj support files,
62
- # keyed by xcodeproj directory so each resolved file only sees
63
- # requirements from its own Xcode project.
66
+ # keyed by Xcode scope directory so each resolved file can be enriched
67
+ # by requirements from its closest matching Xcode scope.
64
68
  sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) }
65
69
  def aggregate_pbxproj_requirements
66
70
  scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]])
67
71
 
68
72
  pbxproj_files.each do |pbxproj_file|
69
- xcodeproj_dir = extract_xcodeproj_dir(pbxproj_file.name)
70
- scoped[xcodeproj_dir] ||= {}
73
+ xcode_scope_dir = extract_xcode_scope_dir(pbxproj_file.name)
74
+ scoped[xcode_scope_dir] ||= {}
71
75
 
72
76
  PbxprojParser.new(pbxproj_file).parse.each do |name, req_info|
73
- T.must(scoped[xcodeproj_dir])[name] = req_info
77
+ T.must(scoped[xcode_scope_dir])[name] = req_info
74
78
  end
75
79
  end
76
80
 
77
81
  scoped
78
82
  end
79
83
 
84
+ sig do
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]])
88
+ end
89
+ def merge_scopes(scoped_requirements)
90
+ scoped_requirements.values.each_with_object({}) do |requirements, merged|
91
+ requirements.each { |name, req_info| merged[name] = req_info }
92
+ end
93
+ end
94
+
95
+ sig do
96
+ params(
97
+ scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]],
98
+ resolved_file_name: String
99
+ ).returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
100
+ end
101
+ def requirements_for_resolved_file(scoped_requirements:, resolved_file_name:)
102
+ scope_dir = extract_xcode_scope_dir(resolved_file_name)
103
+ return T.must(scoped_requirements[scope_dir]) if scoped_requirements.key?(scope_dir)
104
+
105
+ workspace_root = workspace_root_for_scope(scope_dir)
106
+ return {} unless workspace_root
107
+
108
+ local_scopes = scoped_requirements.select do |candidate_scope, _|
109
+ scope_in_workspace_root?(candidate_scope, workspace_root)
110
+ end
111
+ return {} if local_scopes.empty?
112
+
113
+ merge_scopes(local_scopes)
114
+ end
115
+
116
+ sig { params(candidate_scope: T.nilable(String), workspace_root: String).returns(T::Boolean) }
117
+ def scope_in_workspace_root?(candidate_scope, workspace_root)
118
+ return false unless candidate_scope
119
+
120
+ if workspace_root == "."
121
+ !candidate_scope.include?("/")
122
+ else
123
+ candidate_scope.start_with?("#{workspace_root}/")
124
+ end
125
+ end
126
+
80
127
  # Enriches a dependency parsed from Package.resolved with requirement
81
128
  # info from the matching project.pbxproj
82
129
  sig do
@@ -92,6 +139,7 @@ module Dependabot
92
139
  pbxproj_file = req_info[:file]
93
140
  requirement_str = req_info[:requirement]
94
141
  requirement_string = req_info[:requirement_string]
142
+ kind = req_info[:kind]
95
143
 
96
144
  new_requirements = dep.requirements.map do |req|
97
145
  req.merge(
@@ -101,7 +149,8 @@ module Dependabot
101
149
  # declaration_string is not applicable for Xcode-managed SPM
102
150
  # (no Package.swift manifest to extract it from)
103
151
  declaration_string: nil,
104
- requirement_string: requirement_string
152
+ requirement_string: requirement_string,
153
+ kind: kind
105
154
  }.compact
106
155
  )
107
156
  end
@@ -115,13 +164,18 @@ module Dependabot
115
164
  )
116
165
  end
117
166
 
118
- # Extracts the .xcodeproj directory name from a file path.
119
- # e.g. "MyApp.xcodeproj/project.xcworkspace/.../Package.resolved" -> "MyApp.xcodeproj"
120
- # e.g. "sub/dir/App.xcodeproj/project.pbxproj" -> "sub/dir/App.xcodeproj"
167
+ # Extracts the Xcode scope directory (.xcodeproj or .xcworkspace)
168
+ # from a file path.
121
169
  sig { params(path: String).returns(T.nilable(String)) }
122
- def extract_xcodeproj_dir(path)
123
- match = path.match(%r{^(.*?\.xcodeproj)/})
124
- match&.captures&.first
170
+ def extract_xcode_scope_dir(path)
171
+ XcodeFileHelpers.extract_xcode_scope_dir(path)
172
+ end
173
+
174
+ sig { params(scope_dir: T.nilable(String)).returns(T.nilable(String)) }
175
+ def workspace_root_for_scope(scope_dir)
176
+ return nil unless scope_dir&.end_with?(".xcworkspace")
177
+
178
+ File.dirname(scope_dir)
125
179
  end
126
180
  end
127
181
  end
@@ -10,6 +10,7 @@ require "dependabot/swift/file_parser/manifest_parser"
10
10
  require "dependabot/swift/file_parser/xcode_spm_resolver"
11
11
  require "dependabot/swift/package_manager"
12
12
  require "dependabot/swift/language"
13
+ require "dependabot/swift/xcode_file_helpers"
13
14
 
14
15
  module Dependabot
15
16
  module Swift
@@ -115,13 +116,12 @@ module Dependabot
115
116
  @package_manifest_file ||= T.let(get_original_file("Package.swift"), T.nilable(Dependabot::DependencyFile))
116
117
  end
117
118
 
118
- # All non-support Package.resolved files from Xcode project directories
119
+ # All non-support Package.resolved files from Xcode project and workspace directories (.xcodeproj, .xcworkspace)
119
120
  sig { returns(T::Array[Dependabot::DependencyFile]) }
120
121
  def xcode_resolved_files
121
122
  @xcode_resolved_files ||= T.let(
122
123
  dependency_files.select do |f|
123
- f.name.end_with?("Package.resolved") &&
124
- f.name.include?(".xcodeproj/") &&
124
+ XcodeFileHelpers.xcode_resolved_path?(f.name) &&
125
125
  !f.support_file?
126
126
  end,
127
127
  T.nilable(T::Array[Dependabot::DependencyFile])
@@ -0,0 +1,303 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "sorbet-runtime"
6
+ require "dependabot/dependency"
7
+ require "dependabot/dependency_file"
8
+ require "dependabot/errors"
9
+ require "dependabot/shared_helpers"
10
+ require "dependabot/swift/file_updater"
11
+ require "dependabot/swift/url_helpers"
12
+ require "dependabot/swift/xcode_file_helpers"
13
+
14
+ module Dependabot
15
+ module Swift
16
+ class FileUpdater < Dependabot::FileUpdaters::Base
17
+ class XcodeLockfileUpdater
18
+ extend T::Sig
19
+
20
+ SUPPORTED_VERSIONS = T.let([1, 2, 3].freeze, T::Array[Integer])
21
+
22
+ # Maps schema version to the JSON keys used for each pin field
23
+ PIN_KEYS = T.let(
24
+ {
25
+ 1 => { url: "repositoryURL", identity: "package", pins_path: %w(object pins) },
26
+ 2 => { url: "location", identity: "identity", pins_path: ["pins"] },
27
+ 3 => { url: "location", identity: "identity", pins_path: ["pins"] }
28
+ }.freeze,
29
+ T::Hash[Integer, T::Hash[Symbol, T.untyped]]
30
+ )
31
+
32
+ sig do
33
+ params(
34
+ resolved_file: Dependabot::DependencyFile,
35
+ dependencies: T::Array[Dependabot::Dependency],
36
+ workspace_files: T::Array[Dependabot::DependencyFile]
37
+ ).void
38
+ end
39
+ def initialize(resolved_file:, dependencies:, workspace_files: [])
40
+ @resolved_file = resolved_file
41
+ @dependencies = dependencies
42
+ @workspace_files = workspace_files
43
+ end
44
+
45
+ sig { returns(String) }
46
+ def updated_lockfile_content
47
+ content = resolved_file.content
48
+ unless content
49
+ raise Dependabot::DependencyFileNotParseable.new(
50
+ resolved_file.name,
51
+ "#{resolved_file.name} has no content"
52
+ )
53
+ end
54
+
55
+ parsed = parse_json(content)
56
+ schema_version = detect_schema_version(parsed)
57
+ keys = T.must(PIN_KEYS[schema_version])
58
+
59
+ update_pins(parsed, schema_version, keys)
60
+
61
+ # Use JSON.pretty_generate to match Xcode's output format:
62
+ # - 2-space indentation
63
+ # - space before colon (e.g., "key" : "value")
64
+ JSON.pretty_generate(
65
+ parsed,
66
+ indent: " ",
67
+ space: " ",
68
+ space_before: " ",
69
+ object_nl: "\n",
70
+ array_nl: "\n"
71
+ ) + "\n"
72
+ end
73
+
74
+ sig { returns(T::Boolean) }
75
+ def lockfile_changed?
76
+ dependencies_for_file.any?
77
+ end
78
+
79
+ private
80
+
81
+ sig { returns(Dependabot::DependencyFile) }
82
+ attr_reader :resolved_file
83
+
84
+ sig { returns(T::Array[Dependabot::Dependency]) }
85
+ attr_reader :dependencies
86
+
87
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
88
+ attr_reader :workspace_files
89
+
90
+ sig { params(content: String).returns(T::Hash[String, T.untyped]) }
91
+ def parse_json(content)
92
+ JSON.parse(content)
93
+ rescue JSON::ParserError => e
94
+ raise Dependabot::DependencyFileNotParseable.new(
95
+ resolved_file.name,
96
+ "#{resolved_file.name} is not valid JSON: #{e.message}"
97
+ )
98
+ end
99
+
100
+ sig { params(parsed: T::Hash[String, T.untyped]).returns(Integer) }
101
+ def detect_schema_version(parsed)
102
+ version = parsed["version"]
103
+
104
+ unless version.is_a?(Integer) && SUPPORTED_VERSIONS.include?(version)
105
+ raise Dependabot::DependencyFileNotParseable.new(
106
+ resolved_file.name,
107
+ "#{resolved_file.name} has unsupported schema version: #{version.inspect}. " \
108
+ "Supported versions: #{SUPPORTED_VERSIONS.join(', ')}"
109
+ )
110
+ end
111
+
112
+ version
113
+ end
114
+
115
+ sig do
116
+ params(
117
+ parsed: T::Hash[String, T.untyped],
118
+ schema_version: Integer,
119
+ keys: T::Hash[Symbol, T.untyped]
120
+ ).void
121
+ end
122
+ def update_pins(parsed, schema_version, keys)
123
+ pins_path = T.cast(keys[:pins_path], T::Array[String])
124
+ pins = dig_pins(parsed, pins_path)
125
+
126
+ unless pins.is_a?(Array)
127
+ raise Dependabot::DependencyFileNotParseable.new(
128
+ resolved_file.name,
129
+ "#{resolved_file.name} is missing the expected 'pins' array " \
130
+ "(schema version #{schema_version})"
131
+ )
132
+ end
133
+
134
+ dependencies_for_file.each do |dep|
135
+ update_pin_for_dependency(pins, dep, keys, schema_version)
136
+ end
137
+ end
138
+
139
+ sig do
140
+ params(
141
+ parsed: T::Hash[String, T.untyped],
142
+ path: T::Array[String]
143
+ ).returns(T.untyped)
144
+ end
145
+ def dig_pins(parsed, path)
146
+ # Navigate nested hash using path keys
147
+ # Path is either ["object", "pins"] for v1 or ["pins"] for v2/v3
148
+ current = T.let(parsed, T.untyped)
149
+ path.each do |key|
150
+ break unless current.is_a?(Hash)
151
+
152
+ current = current[key]
153
+ end
154
+ current
155
+ end
156
+
157
+ sig do
158
+ params(
159
+ pins: T::Array[T::Hash[String, T.untyped]],
160
+ dependency: Dependabot::Dependency,
161
+ keys: T::Hash[Symbol, T.untyped],
162
+ schema_version: Integer
163
+ ).void
164
+ end
165
+ def update_pin_for_dependency(pins, dependency, keys, schema_version)
166
+ pin = find_pin_for_dependency(pins, dependency, keys, schema_version)
167
+ return unless pin
168
+
169
+ state = pin["state"]
170
+ return unless state.is_a?(Hash)
171
+
172
+ source = dependency.requirements.first&.dig(:source)
173
+ new_version = dependency.version
174
+ new_ref = source&.dig(:ref)
175
+
176
+ if new_version
177
+ state["version"] = new_version
178
+ # When updating to a new version, update revision if provided in source
179
+ # The ref from source is typically the git SHA corresponding to the version tag
180
+ state["revision"] = new_ref if new_ref && looks_like_sha?(new_ref)
181
+ elsif new_ref
182
+ # Revision-only update (no version, just SHA)
183
+ state["revision"] = new_ref
184
+ state.delete("version")
185
+ end
186
+ end
187
+
188
+ sig { params(str: String).returns(T::Boolean) }
189
+ def looks_like_sha?(str)
190
+ str.match?(/\A[0-9a-f]{40}\z/i)
191
+ end
192
+
193
+ sig do
194
+ params(
195
+ pins: T::Array[T::Hash[String, T.untyped]],
196
+ dependency: Dependabot::Dependency,
197
+ keys: T::Hash[Symbol, T.untyped],
198
+ schema_version: Integer
199
+ ).returns(T.nilable(T::Hash[String, T.untyped]))
200
+ end
201
+ def find_pin_for_dependency(pins, dependency, keys, schema_version)
202
+ identity_key = T.cast(keys[:identity], String)
203
+ url_key = T.cast(keys[:url], String)
204
+ identity = dependency.metadata[:identity]
205
+
206
+ pins.find do |pin|
207
+ pin_identity = pin[identity_key]
208
+ pin_identity = pin_identity&.downcase if schema_version == 1
209
+
210
+ if identity && pin_identity == identity
211
+ true
212
+ else
213
+ # Fall back to URL matching
214
+ pin_url = pin[url_key]
215
+ next false unless pin_url.is_a?(String)
216
+
217
+ normalized_pin_url = SharedHelpers.scp_to_standard(pin_url)
218
+ pin_name = UrlHelpers.normalize_name(normalized_pin_url)
219
+
220
+ pin_name == dependency.name
221
+ end
222
+ end
223
+ end
224
+
225
+ sig { returns(T::Array[Dependabot::Dependency]) }
226
+ def dependencies_for_file
227
+ @dependencies_for_file ||= T.let(
228
+ dependencies.select do |dep|
229
+ dep.requirements.any? do |req|
230
+ req_file_matches_resolved_scope?(req[:file])
231
+ end
232
+ end,
233
+ T.nilable(T::Array[Dependabot::Dependency])
234
+ )
235
+ end
236
+
237
+ sig { params(req_file: T.nilable(String)).returns(T::Boolean) }
238
+ def req_file_matches_resolved_scope?(req_file)
239
+ return false unless req_file
240
+ return true if req_file == resolved_file.name
241
+ return false unless req_file.include?(".xcodeproj/") || req_file.include?(".xcworkspace/")
242
+
243
+ req_scope = extract_xcode_scope_dir(req_file)
244
+ resolved_scope = extract_xcode_scope_dir(resolved_file.name)
245
+
246
+ return true if req_scope && resolved_scope && req_scope == resolved_scope
247
+
248
+ workspace_related_dependency?(req_file)
249
+ end
250
+
251
+ # Extracts the Xcode scope directory (.xcodeproj or .xcworkspace)
252
+ # from a file path.
253
+ sig { params(path: String).returns(T.nilable(String)) }
254
+ def extract_xcode_scope_dir(path)
255
+ XcodeFileHelpers.extract_xcode_scope_dir(path)
256
+ end
257
+
258
+ sig { params(req_file: T.nilable(String)).returns(T::Boolean) }
259
+ def workspace_related_dependency?(req_file)
260
+ return false unless req_file
261
+
262
+ workspace_scope = extract_xcode_scope_dir(resolved_file.name)
263
+ return false unless workspace_scope&.end_with?(".xcworkspace")
264
+ return false unless req_file.include?(".xcodeproj/")
265
+
266
+ req_scope = extract_xcode_scope_dir(req_file)
267
+ return false unless req_scope
268
+
269
+ referenced = referenced_project_scopes_for_workspace(workspace_scope)
270
+ return referenced.include?(req_scope) if referenced.any?
271
+
272
+ workspace_root = File.dirname(workspace_scope)
273
+
274
+ if workspace_root == "."
275
+ !req_scope.include?("/")
276
+ else
277
+ req_scope.start_with?("#{workspace_root}/")
278
+ end
279
+ end
280
+
281
+ sig { params(workspace_scope: String).returns(T::Set[String]) }
282
+ def referenced_project_scopes_for_workspace(workspace_scope)
283
+ workspace_data_path = "#{workspace_scope}/contents.xcworkspacedata"
284
+ file = workspace_files.find { |workspace_file| workspace_file.name == workspace_data_path }
285
+ return Set.new unless file&.content
286
+
287
+ project_refs = T.must(file.content).scan(/location\s*=\s*"(?:group:)?([^"\n]+\.xcodeproj)"/).flatten
288
+ workspace_root = File.dirname(workspace_scope)
289
+
290
+ Set.new(
291
+ project_refs.map do |project_ref|
292
+ if workspace_root == "."
293
+ project_ref
294
+ else
295
+ File.join(workspace_root, project_ref)
296
+ end
297
+ end
298
+ )
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end
@@ -1,10 +1,13 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "dependabot/experiments"
4
5
  require "dependabot/file_updaters"
5
6
  require "dependabot/file_updaters/base"
6
7
  require "dependabot/swift/file_updater/lockfile_updater"
7
8
  require "dependabot/swift/file_updater/manifest_updater"
9
+ require "dependabot/swift/file_updater/xcode_lockfile_updater"
10
+ require "dependabot/swift/xcode_file_helpers"
8
11
 
9
12
  module Dependabot
10
13
  module Swift
@@ -13,6 +16,18 @@ module Dependabot
13
16
 
14
17
  sig { override.returns(T::Array[Dependabot::DependencyFile]) }
15
18
  def updated_dependency_files
19
+ if xcode_spm_mode?
20
+ updated_xcode_spm_files
21
+ else
22
+ updated_classic_spm_files
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # Classic SPM update: uses swift CLI to resolve and update
29
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
30
+ def updated_classic_spm_files
16
31
  updated_files = T.let([], T::Array[Dependabot::DependencyFile])
17
32
 
18
33
  SharedHelpers.in_a_temporary_repo_directory(T.must(manifest).directory, repo_contents_path) do
@@ -31,7 +46,35 @@ module Dependabot
31
46
  updated_files
32
47
  end
33
48
 
34
- private
49
+ # Xcode SPM update: updates Package.resolved files in-place without CLI
50
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
51
+ def updated_xcode_spm_files
52
+ updated_files = T.let([], T::Array[Dependabot::DependencyFile])
53
+
54
+ xcode_resolved_files.each do |resolved_file|
55
+ updater = XcodeLockfileUpdater.new(
56
+ resolved_file: resolved_file,
57
+ dependencies: dependencies,
58
+ workspace_files: xcode_workspace_files
59
+ )
60
+
61
+ next unless updater.lockfile_changed?
62
+
63
+ updated_content = updater.updated_lockfile_content
64
+ next if updated_content == resolved_file.content
65
+
66
+ updated_files << updated_file(file: resolved_file, content: updated_content)
67
+ end
68
+
69
+ if updated_files.empty?
70
+ raise Dependabot::DependencyFileNotFound.new(
71
+ nil,
72
+ "No Package.resolved files needed updating for the specified dependencies"
73
+ )
74
+ end
75
+
76
+ updated_files
77
+ end
35
78
 
36
79
  sig { returns(Dependabot::Dependency) }
37
80
  def dependency
@@ -42,7 +85,38 @@ module Dependabot
42
85
 
43
86
  sig { override.void }
44
87
  def check_required_files
45
- raise "A Package.swift file must be provided!" unless manifest
88
+ return if manifest
89
+ return if xcode_spm_mode? && xcode_resolved_files.any?
90
+
91
+ raise "A Package.swift file or Xcode Package.resolved must be provided!"
92
+ end
93
+
94
+ sig { returns(T::Boolean) }
95
+ def xcode_spm_mode?
96
+ return false unless Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
97
+
98
+ manifest.nil? && xcode_resolved_files.any?
99
+ end
100
+
101
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
102
+ def xcode_resolved_files
103
+ @xcode_resolved_files ||= T.let(
104
+ dependency_files.select do |f|
105
+ XcodeFileHelpers.xcode_resolved_path?(f.name) &&
106
+ !f.support_file?
107
+ end,
108
+ T.nilable(T::Array[Dependabot::DependencyFile])
109
+ )
110
+ end
111
+
112
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
113
+ def xcode_workspace_files
114
+ @xcode_workspace_files ||= T.let(
115
+ dependency_files.select do |f|
116
+ f.name.end_with?("contents.xcworkspacedata") && f.support_file?
117
+ end,
118
+ T.nilable(T::Array[Dependabot::DependencyFile])
119
+ )
46
120
  end
47
121
 
48
122
  sig { returns(String) }
@@ -17,6 +17,11 @@ module Dependabot
17
17
  case new_source_type
18
18
  when "git" then find_source_from_git_url
19
19
  when "registry" then find_source_from_registry
20
+ when "default", nil
21
+ # For dependencies without explicit source info (e.g., Xcode-managed
22
+ # SPM dependencies parsed from Package.resolved), attempt to infer
23
+ # source from the dependency name which is typically a normalized URL
24
+ find_source_from_dependency_name
20
25
  else raise "Unexpected source type: #{new_source_type}"
21
26
  end
22
27
  end
@@ -34,6 +39,15 @@ module Dependabot
34
39
  Source.from_url(url)
35
40
  end
36
41
 
42
+ sig { returns(T.nilable(Dependabot::Source)) }
43
+ def find_source_from_dependency_name
44
+ name = dependency.name
45
+ return nil unless name.include?("/")
46
+
47
+ url = "https://#{name}"
48
+ Source.from_url(url)
49
+ end
50
+
37
51
  sig { returns(T.noreturn) }
38
52
  def find_source_from_registry
39
53
  raise NotImplementedError
@@ -1,4 +1,4 @@
1
- # typed: strong
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/update_checkers/base"
@@ -14,11 +14,13 @@ module Dependabot
14
14
  sig do
15
15
  params(
16
16
  requirements: T::Array[T::Hash[Symbol, T.untyped]],
17
- target_version: T.nilable(T.any(String, Gem::Version))
17
+ target_version: T.nilable(T.any(String, Gem::Version)),
18
+ xcode_mode: T::Boolean
18
19
  ).void
19
20
  end
20
- def initialize(requirements:, target_version:)
21
+ def initialize(requirements:, target_version:, xcode_mode: false)
21
22
  @requirements = requirements
23
+ @xcode_mode = xcode_mode
22
24
 
23
25
  return unless target_version && Version.correct?(target_version)
24
26
 
@@ -27,6 +29,8 @@ module Dependabot
27
29
 
28
30
  sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
29
31
  def updated_requirements
32
+ return updated_xcode_requirements if xcode_mode
33
+
30
34
  NativeRequirement.map_requirements(requirements) do |requirement|
31
35
  T.must(requirement.update_if_needed(T.must(target_version)))
32
36
  end
@@ -39,6 +43,116 @@ module Dependabot
39
43
 
40
44
  sig { returns(T.nilable(Gem::Version)) }
41
45
  attr_reader :target_version
46
+
47
+ sig { returns(T::Boolean) }
48
+ attr_reader :xcode_mode
49
+
50
+ # For Xcode projects, we update the version in the requirement while preserving the kind.
51
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
52
+ def updated_xcode_requirements
53
+ requirements.map do |req|
54
+ next req unless target_version
55
+
56
+ updated_req = update_xcode_requirement(req)
57
+ updated_req
58
+ end
59
+ end
60
+
61
+ sig { params(requirement: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
62
+ def update_xcode_requirement(requirement)
63
+ metadata = requirement[:metadata] || {}
64
+ requirement_string = metadata[:requirement_string]
65
+ kind = metadata[:kind]
66
+
67
+ new_requirement_string = build_xcode_requirement_string(requirement_string, kind)
68
+ new_requirement = build_xcode_requirement(requirement_string, kind)
69
+
70
+ requirement.merge(
71
+ requirement: new_requirement,
72
+ metadata: metadata.merge(
73
+ requirement_string: new_requirement_string
74
+ ).compact
75
+ )
76
+ end
77
+
78
+ sig do
79
+ params(
80
+ requirement_string: T.nilable(String),
81
+ kind: T.nilable(String)
82
+ ).returns(T.nilable(String))
83
+ end
84
+ def build_xcode_requirement_string(requirement_string, kind)
85
+ return requirement_string unless target_version
86
+
87
+ case kind
88
+ when "upToNextMajorVersion"
89
+ "from: \"#{target_version}\""
90
+ when "upToNextMinorVersion"
91
+ ".upToNextMinor(from: \"#{target_version}\")"
92
+ when "exactVersion"
93
+ "exact: \"#{target_version}\""
94
+ when "versionRange"
95
+ max = extract_version_range_max(requirement_string)
96
+ "\"#{target_version}\"..<\"#{max}\""
97
+ else
98
+ # Default: update to exact version for unknown kinds
99
+ "exact: \"#{target_version}\""
100
+ end
101
+ end
102
+
103
+ sig do
104
+ params(
105
+ requirement_string: T.nilable(String),
106
+ kind: T.nilable(String)
107
+ ).returns(T.nilable(String))
108
+ end
109
+ def build_xcode_requirement(requirement_string, kind)
110
+ return nil unless target_version
111
+
112
+ case kind
113
+ when "upToNextMajorVersion"
114
+ max = bump_version(target_version.to_s, :major)
115
+ ">= #{target_version}, < #{max}"
116
+ when "upToNextMinorVersion"
117
+ max = bump_version(target_version.to_s, :minor)
118
+ ">= #{target_version}, < #{max}"
119
+ when "exactVersion"
120
+ "= #{target_version}"
121
+ when "versionRange"
122
+ max = extract_version_range_max(requirement_string)
123
+ ">= #{target_version}, < #{max}"
124
+ else
125
+ # Default: exact version
126
+ "= #{target_version}"
127
+ end
128
+ end
129
+
130
+ # Extracts the upper bound from a versionRange requirement string.
131
+ # Format: "min"..<"max" or "min"..."max"
132
+ sig { params(requirement_string: T.nilable(String)).returns(String) }
133
+ def extract_version_range_max(requirement_string)
134
+ return bump_version(target_version.to_s, :major) unless requirement_string
135
+
136
+ # Match patterns like "1.0.0"..<"2.0.0" or "1.0.0"..."2.0.0"
137
+ match = requirement_string.match(/\.{2,3}<?"(\d+\.\d+\.\d+)"/)
138
+ return bump_version(target_version.to_s, :major) unless match
139
+
140
+ match[1].to_s
141
+ end
142
+
143
+ sig { params(version_str: String, bump_type: Symbol).returns(String) }
144
+ def bump_version(version_str, bump_type)
145
+ parts = version_str.split(".").map(&:to_i)
146
+
147
+ case bump_type
148
+ when :major
149
+ [(parts[0] || 0) + 1, 0, 0]
150
+ when :minor
151
+ [parts[0] || 0, (parts[1] || 0) + 1, 0]
152
+ else
153
+ parts
154
+ end.join(".")
155
+ end
42
156
  end
43
157
  end
44
158
  end
@@ -0,0 +1,124 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/git_commit_checker"
6
+ require "dependabot/swift/update_checker"
7
+ require "dependabot/swift/requirement"
8
+ require "dependabot/swift/version"
9
+ require "dependabot/update_checkers/version_filters"
10
+
11
+ module Dependabot
12
+ module Swift
13
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
14
+ # Resolves versions for Xcode-only SwiftPM projects (no Package.swift).
15
+ #
16
+ # Unlike the classic VersionResolver which relies on `swift package update`,
17
+ # this resolver uses GitCommitChecker to find the latest available version
18
+ # from git tags, since we cannot run the Swift CLI without a manifest.
19
+ class XcodeVersionResolver
20
+ extend T::Sig
21
+
22
+ sig do
23
+ params(
24
+ dependency: Dependabot::Dependency,
25
+ git_commit_checker: Dependabot::GitCommitChecker,
26
+ security_advisories: T::Array[Dependabot::SecurityAdvisory]
27
+ ).void
28
+ end
29
+ def initialize(dependency:, git_commit_checker:, security_advisories:)
30
+ @dependency = dependency
31
+ @git_commit_checker = git_commit_checker
32
+ @security_advisories = security_advisories
33
+ end
34
+
35
+ sig { returns(T.nilable(Dependabot::Version)) }
36
+ def latest_resolvable_version
37
+ return nil unless version_pinned?
38
+
39
+ tag = git_commit_checker.local_tag_for_latest_version
40
+ return nil unless tag
41
+
42
+ version = tag.fetch(:version)
43
+ return nil unless version_meets_requirements?(version)
44
+
45
+ Version.new(version)
46
+ end
47
+
48
+ sig { returns(T.nilable(Dependabot::Version)) }
49
+ def lowest_security_fix_version
50
+ return nil unless version_pinned?
51
+
52
+ tags = git_commit_checker.local_tags_for_allowed_versions
53
+ relevant_tags = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(
54
+ tags,
55
+ security_advisories
56
+ )
57
+ relevant_tags = filter_lower_tags(relevant_tags)
58
+
59
+ lowest_tag = relevant_tags.min_by { |tag| tag.fetch(:version) }
60
+ return nil unless lowest_tag
61
+
62
+ Version.new(lowest_tag.fetch(:version))
63
+ end
64
+
65
+ sig { returns(T::Boolean) }
66
+ def version_pinned?
67
+ return false unless dependency.version
68
+
69
+ Version.correct?(dependency.version)
70
+ end
71
+
72
+ private
73
+
74
+ sig { returns(Dependabot::Dependency) }
75
+ attr_reader :dependency
76
+
77
+ sig { returns(Dependabot::GitCommitChecker) }
78
+ attr_reader :git_commit_checker
79
+
80
+ sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
81
+ attr_reader :security_advisories
82
+
83
+ sig { returns(T.nilable(Dependabot::Swift::Requirement)) }
84
+ def dependency_requirement
85
+ req_string = dependency.requirements.first&.dig(:requirement)
86
+ return nil unless req_string
87
+
88
+ Dependabot::Swift::Requirement.new(req_string)
89
+ rescue Gem::Requirement::BadRequirementError
90
+ nil
91
+ end
92
+
93
+ sig { params(version: T.untyped).returns(T::Boolean) }
94
+ def version_meets_requirements?(version)
95
+ requirement = dependency_requirement
96
+ return true unless requirement
97
+
98
+ requirement.satisfied_by?(version)
99
+ end
100
+
101
+ sig do
102
+ params(
103
+ tags: T::Array[T::Hash[Symbol, T.untyped]]
104
+ ).returns(T::Array[T::Hash[Symbol, T.untyped]])
105
+ end
106
+ def filter_lower_tags(tags)
107
+ current = current_version
108
+ return tags unless current
109
+
110
+ tags.select { |tag| tag.fetch(:version) > current }
111
+ end
112
+
113
+ sig { returns(T.nilable(Dependabot::Version)) }
114
+ def current_version
115
+ return nil unless dependency.version
116
+
117
+ Version.new(dependency.version)
118
+ rescue ArgumentError
119
+ nil
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -2,12 +2,14 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
+ require "dependabot/experiments"
5
6
  require "dependabot/update_checkers"
6
7
  require "dependabot/update_checkers/base"
7
8
  require "dependabot/update_checkers/version_filters"
8
9
  require "dependabot/git_commit_checker"
9
10
  require "dependabot/swift/native_requirement"
10
11
  require "dependabot/swift/file_updater/manifest_updater"
12
+ require "dependabot/swift/xcode_file_helpers"
11
13
 
12
14
  module Dependabot
13
15
  module Swift
@@ -17,6 +19,7 @@ module Dependabot
17
19
  require_relative "update_checker/requirements_updater"
18
20
  require_relative "update_checker/version_resolver"
19
21
  require_relative "update_checker/latest_version_resolver"
22
+ require_relative "update_checker/xcode_version_resolver"
20
23
 
21
24
  sig { override.returns(T.nilable(Dependabot::Version)) }
22
25
  def latest_version
@@ -50,21 +53,50 @@ module Dependabot
50
53
 
51
54
  sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
52
55
  def updated_requirements
56
+ return updated_xcode_requirements if xcode_spm_mode?
57
+
58
+ # If no target version is available, return old requirements unchanged
59
+ target = preferred_resolvable_version
60
+ return old_requirements unless target
61
+
53
62
  RequirementsUpdater.new(
54
63
  requirements: old_requirements,
55
- target_version: T.must(preferred_resolvable_version)
64
+ target_version: target
56
65
  ).updated_requirements
57
66
  end
58
67
 
59
68
  private
60
69
 
70
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
71
+ def updated_xcode_requirements
72
+ # If no target version is available (e.g., revision-only or branch-pinned
73
+ # dependency), return old requirements unchanged
74
+ target = preferred_resolvable_version
75
+ return old_requirements unless target
76
+
77
+ RequirementsUpdater.new(
78
+ requirements: old_requirements,
79
+ target_version: target,
80
+ xcode_mode: true
81
+ ).updated_requirements
82
+ end
83
+
61
84
  sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
62
85
  def old_requirements
63
86
  dependency.requirements
64
87
  end
65
88
 
89
+ sig { returns(T::Boolean) }
90
+ def xcode_spm_mode?
91
+ return false unless Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
92
+
93
+ manifest.nil? && xcode_resolved_files.any?
94
+ end
95
+
66
96
  sig { returns(T.nilable(Dependabot::Version)) }
67
97
  def fetch_latest_version
98
+ return fetch_xcode_latest_version if xcode_spm_mode?
99
+
68
100
  return unless git_commit_checker.pinned_ref_looks_like_version? && latest_version_tag
69
101
 
70
102
  tag = latest_version_tag
@@ -73,8 +105,22 @@ module Dependabot
73
105
  tag.fetch(:version)
74
106
  end
75
107
 
108
+ sig { returns(T.nilable(Dependabot::Version)) }
109
+ def fetch_xcode_latest_version
110
+ # For branch-pinned or revision-only dependencies, don't report a latest version
111
+ # since they can't be meaningfully updated to version-based pins
112
+ return nil unless xcode_version_resolver.version_pinned?
113
+
114
+ tag = latest_version_tag
115
+ return unless tag
116
+
117
+ tag.fetch(:version)
118
+ end
119
+
76
120
  sig { returns(T.nilable(Dependabot::Version)) }
77
121
  def fetch_lowest_security_fix_version
122
+ return fetch_xcode_lowest_security_fix_version if xcode_spm_mode?
123
+
78
124
  return unless git_commit_checker.pinned_ref_looks_like_version? && latest_version_tag
79
125
 
80
126
  tag = lowest_security_fix_version_tag
@@ -83,16 +129,30 @@ module Dependabot
83
129
  tag.fetch(:version)
84
130
  end
85
131
 
132
+ sig { returns(T.nilable(Dependabot::Version)) }
133
+ def fetch_xcode_lowest_security_fix_version
134
+ xcode_version_resolver.lowest_security_fix_version
135
+ end
136
+
86
137
  sig { returns(T.nilable(Dependabot::Version)) }
87
138
  def fetch_latest_resolvable_version
139
+ return fetch_xcode_latest_resolvable_version if xcode_spm_mode?
140
+
88
141
  latest_resolvable_version = version_resolver_for(unlocked_requirements).latest_resolvable_version
89
142
  return current_version unless latest_resolvable_version
90
143
 
91
144
  Version.new(latest_resolvable_version)
92
145
  end
93
146
 
147
+ sig { returns(T.nilable(Dependabot::Version)) }
148
+ def fetch_xcode_latest_resolvable_version
149
+ xcode_version_resolver.latest_resolvable_version
150
+ end
151
+
94
152
  sig { returns(T.nilable(Dependabot::Version)) }
95
153
  def fetch_lowest_resolvable_security_fix_version
154
+ return fetch_xcode_lowest_security_fix_version if xcode_spm_mode?
155
+
96
156
  lowest_resolvable_security_fix_version = version_resolver_for(
97
157
  force_lowest_security_fix_requirements
98
158
  ).latest_resolvable_version
@@ -218,6 +278,29 @@ module Dependabot
218
278
  tags_array
219
279
  .select { |tag| tag.fetch(:version) > current_version }
220
280
  end
281
+
282
+ sig { returns(XcodeVersionResolver) }
283
+ def xcode_version_resolver
284
+ @xcode_version_resolver ||= T.let(
285
+ XcodeVersionResolver.new(
286
+ dependency: dependency,
287
+ git_commit_checker: git_commit_checker,
288
+ security_advisories: security_advisories
289
+ ),
290
+ T.nilable(XcodeVersionResolver)
291
+ )
292
+ end
293
+
294
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
295
+ def xcode_resolved_files
296
+ @xcode_resolved_files ||= T.let(
297
+ dependency_files.select do |f|
298
+ XcodeFileHelpers.xcode_resolved_path?(f.name) &&
299
+ !f.support_file?
300
+ end,
301
+ T.nilable(T::Array[Dependabot::DependencyFile])
302
+ )
303
+ end
221
304
  end
222
305
  end
223
306
  end
@@ -0,0 +1,47 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ module Swift
8
+ module XcodeFileHelpers
9
+ extend T::Sig
10
+
11
+ XCODEPROJ_SUFFIX = ".xcodeproj/"
12
+ XCWORKSPACE_SUFFIX = ".xcworkspace/"
13
+ PACKAGE_RESOLVED = "Package.resolved"
14
+
15
+ sig { params(path: String).returns(T::Boolean) }
16
+ def self.xcode_resolved_path?(path)
17
+ return false unless path.end_with?(PACKAGE_RESOLVED)
18
+
19
+ path.include?(XCODEPROJ_SUFFIX) || path.include?(XCWORKSPACE_SUFFIX)
20
+ end
21
+
22
+ sig { params(path: String).returns(T.nilable(String)) }
23
+ def self.extract_xcode_scope_dir(path)
24
+ # Find the first occurrence of .xcodeproj/ or .xcworkspace/
25
+ xcodeproj_idx = path.index(XCODEPROJ_SUFFIX)
26
+ xcworkspace_idx = path.index(XCWORKSPACE_SUFFIX)
27
+
28
+ # Determine which match to use (earliest occurrence)
29
+ match_idx = T.let(nil, T.nilable(Integer))
30
+ suffix_len = T.let(0, Integer)
31
+
32
+ if xcodeproj_idx && (xcworkspace_idx.nil? || xcodeproj_idx < xcworkspace_idx)
33
+ match_idx = xcodeproj_idx
34
+ suffix_len = XCODEPROJ_SUFFIX.length
35
+ elsif xcworkspace_idx
36
+ match_idx = xcworkspace_idx
37
+ suffix_len = XCWORKSPACE_SUFFIX.length
38
+ end
39
+
40
+ return nil if match_idx.nil?
41
+
42
+ # Return path up to and including the suffix (minus trailing /)
43
+ path[0, match_idx + suffix_len - 1]
44
+ end
45
+ end
46
+ end
47
+ 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.365.0
4
+ version: 0.366.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.365.0
18
+ version: 0.366.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.365.0
25
+ version: 0.366.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -253,6 +253,7 @@ files:
253
253
  - lib/dependabot/swift/file_updater/lockfile_updater.rb
254
254
  - lib/dependabot/swift/file_updater/manifest_updater.rb
255
255
  - lib/dependabot/swift/file_updater/requirement_replacer.rb
256
+ - lib/dependabot/swift/file_updater/xcode_lockfile_updater.rb
256
257
  - lib/dependabot/swift/language.rb
257
258
  - lib/dependabot/swift/metadata_finder.rb
258
259
  - lib/dependabot/swift/native_requirement.rb
@@ -263,14 +264,16 @@ files:
263
264
  - lib/dependabot/swift/update_checker/latest_version_resolver.rb
264
265
  - lib/dependabot/swift/update_checker/requirements_updater.rb
265
266
  - lib/dependabot/swift/update_checker/version_resolver.rb
267
+ - lib/dependabot/swift/update_checker/xcode_version_resolver.rb
266
268
  - lib/dependabot/swift/url_helpers.rb
267
269
  - lib/dependabot/swift/version.rb
270
+ - lib/dependabot/swift/xcode_file_helpers.rb
268
271
  homepage: https://github.com/dependabot/dependabot-core
269
272
  licenses:
270
273
  - MIT
271
274
  metadata:
272
275
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
273
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.365.0
276
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.366.0
274
277
  rdoc_options: []
275
278
  require_paths:
276
279
  - lib