dependabot-swift 0.364.0 → 0.365.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: 363036340485e20146d3722b2d6e5636fa4c0ff007002301ebc72fdda60a9014
4
- data.tar.gz: 5d938a9377d8900d3d422c76e66d6955787f40d6d39ec7f7c1fd6bc8123d374c
3
+ metadata.gz: 35c7a26b4868b5df0a08396136836264f47a3e1d97219ceb705698bb1eae21f3
4
+ data.tar.gz: 3c22d53cfb502e71c17ba6edc2d5d0d6b6dbbfc92fdbfb9fc897579d6c15269f
5
5
  SHA512:
6
- metadata.gz: 9bc68ec95a147b232bf55779657fd7ce94e3d957a8f483ba5596bcb1cfd5c950daf1f25718aa1bee501c9cfeff07fb1434ac766b4702d38f2930012ce1d18438
7
- data.tar.gz: c44d03021e516a1b6e854cfece42f3de2ad37543f4634774447db6943e9ae217e2118212baad943fd18ed475c20d0e9142aca4b81629f17908298ca104260b7e
6
+ metadata.gz: 919317db274d1166b9ad1fb3118e24ed162051839faf392963510986637805dcbd7f5a991f3e7256ee4ad09b626da1aafe133939b91fe35edf00edb7b15600b6
7
+ data.tar.gz: 98b040a728cfa847e5f8ba4474bbf64ef335e47141f94caad1c0a8e9f693a2bc396c2664143ca8f822f599c16ecab8fff8f7a45f66ae29af971df2d3847fdd32
@@ -5,8 +5,8 @@ require "sorbet-runtime"
5
5
  require "dependabot/file_parsers/base"
6
6
  require "dependabot/shared_helpers"
7
7
  require "dependabot/dependency"
8
+ require "dependabot/swift/url_helpers"
8
9
  require "json"
9
- require "uri"
10
10
 
11
11
  module Dependabot
12
12
  module Swift
@@ -79,7 +79,7 @@ module Dependabot
79
79
  def all_dependencies(data, level: 0)
80
80
  identity = data["identity"]
81
81
  url = SharedHelpers.scp_to_standard(data["url"])
82
- name = normalize(url)
82
+ name = UrlHelpers.normalize_name(url)
83
83
  version = data["version"]
84
84
 
85
85
  source = { type: "git", url: url, ref: version, branch: nil }
@@ -97,13 +97,6 @@ module Dependabot
97
97
  [dep, *subdependencies(data, level: level + 1)].compact
98
98
  end
99
99
 
100
- sig { params(source: String).returns(String) }
101
- def normalize(source)
102
- uri = URI.parse(source.downcase)
103
-
104
- "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git")
105
- end
106
-
107
100
  sig { returns(T::Array[Dependabot::DependencyFile]) }
108
101
  attr_reader :dependency_files
109
102
 
@@ -0,0 +1,165 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "sorbet-runtime"
6
+ require "dependabot/dependency"
7
+ require "dependabot/errors"
8
+ require "dependabot/shared_helpers"
9
+ require "dependabot/swift/file_parser"
10
+ require "dependabot/swift/url_helpers"
11
+
12
+ module Dependabot
13
+ module Swift
14
+ class FileParser < Dependabot::FileParsers::Base
15
+ class PackageResolvedParser
16
+ extend T::Sig
17
+
18
+ SUPPORTED_VERSIONS = T.let([1, 2, 3].freeze, T::Array[Integer])
19
+
20
+ # Maps schema version to the JSON keys used for each pin field
21
+ PIN_KEYS = T.let(
22
+ {
23
+ 1 => { url: "repositoryURL", identity: "package", state: "state" },
24
+ 2 => { url: "location", identity: "identity", state: "state" },
25
+ 3 => { url: "location", identity: "identity", state: "state" }
26
+ }.freeze,
27
+ T::Hash[Integer, T::Hash[Symbol, String]]
28
+ )
29
+
30
+ sig { params(resolved_file: Dependabot::DependencyFile).void }
31
+ def initialize(resolved_file)
32
+ @resolved_file = resolved_file
33
+ end
34
+
35
+ sig { returns(T::Array[Dependabot::Dependency]) }
36
+ def parse
37
+ parsed = parse_json
38
+ schema_version = detect_schema_version(parsed)
39
+ pins = extract_pins(parsed, schema_version)
40
+
41
+ pins.filter_map { |pin| build_dependency(pin, schema_version) }
42
+ end
43
+
44
+ private
45
+
46
+ sig { returns(Dependabot::DependencyFile) }
47
+ attr_reader :resolved_file
48
+
49
+ sig { returns(T::Hash[String, T.untyped]) }
50
+ def parse_json
51
+ content = resolved_file.content
52
+ unless content
53
+ raise Dependabot::DependencyFileNotParseable.new(
54
+ resolved_file.name,
55
+ "#{resolved_file.name} has no content"
56
+ )
57
+ end
58
+
59
+ JSON.parse(content)
60
+ rescue JSON::ParserError => e
61
+ raise Dependabot::DependencyFileNotParseable.new(
62
+ resolved_file.name,
63
+ "#{resolved_file.name} is not valid JSON: #{e.message}"
64
+ )
65
+ end
66
+
67
+ sig { params(parsed: T::Hash[String, T.untyped]).returns(Integer) }
68
+ def detect_schema_version(parsed)
69
+ version = parsed["version"]
70
+
71
+ unless version.is_a?(Integer) && SUPPORTED_VERSIONS.include?(version)
72
+ raise Dependabot::DependencyFileNotParseable.new(
73
+ resolved_file.name,
74
+ "#{resolved_file.name} has unsupported schema version: #{version.inspect}. " \
75
+ "Supported versions: #{SUPPORTED_VERSIONS.join(', ')}"
76
+ )
77
+ end
78
+
79
+ version
80
+ end
81
+
82
+ sig do
83
+ params(
84
+ parsed: T::Hash[String, T.untyped],
85
+ schema_version: Integer
86
+ ).returns(T::Array[T::Hash[String, T.untyped]])
87
+ end
88
+ def extract_pins(parsed, schema_version)
89
+ pins = if schema_version == 1
90
+ parsed.dig("object", "pins")
91
+ else
92
+ # v2 and v3 use the same top-level "pins" key
93
+ parsed["pins"]
94
+ end
95
+
96
+ unless pins.is_a?(Array)
97
+ raise Dependabot::DependencyFileNotParseable.new(
98
+ resolved_file.name,
99
+ "#{resolved_file.name} is missing the expected 'pins' array " \
100
+ "(schema version #{schema_version})"
101
+ )
102
+ end
103
+
104
+ pins
105
+ end
106
+
107
+ sig do
108
+ params(
109
+ pin: T::Hash[String, T.untyped],
110
+ schema_version: Integer
111
+ ).returns(T.nilable(Dependabot::Dependency))
112
+ end
113
+ def build_dependency(pin, schema_version)
114
+ keys = T.must(PIN_KEYS[schema_version])
115
+ url = pin[T.must(keys[:url])]
116
+ return nil unless url.is_a?(String) && !url.empty?
117
+
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
123
+
124
+ build_dependency_object(
125
+ identity: identity,
126
+ url: url,
127
+ version: state["version"],
128
+ revision: state["revision"],
129
+ branch: state["branch"]
130
+ )
131
+ end
132
+
133
+ sig do
134
+ params(
135
+ identity: T.nilable(String),
136
+ url: String,
137
+ version: T.nilable(String),
138
+ revision: T.nilable(String),
139
+ branch: T.nilable(String)
140
+ ).returns(T.nilable(Dependabot::Dependency))
141
+ end
142
+ def build_dependency_object(identity:, url:, version:, revision:, branch:)
143
+ normalized_url = SharedHelpers.scp_to_standard(url)
144
+ name = UrlHelpers.normalize_name(normalized_url)
145
+ ref = version || revision
146
+
147
+ source = { type: "git", url: normalized_url, ref: ref, branch: branch }
148
+
149
+ Dependency.new(
150
+ name: name,
151
+ version: version,
152
+ package_manager: "swift",
153
+ requirements: [{
154
+ requirement: version ? "= #{version}" : nil,
155
+ groups: ["dependencies"],
156
+ file: resolved_file.name,
157
+ source: source
158
+ }],
159
+ metadata: { identity: identity }
160
+ )
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,199 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/errors"
6
+ require "dependabot/shared_helpers"
7
+ require "dependabot/swift/file_parser"
8
+ require "dependabot/swift/native_requirement"
9
+ require "dependabot/swift/url_helpers"
10
+
11
+ module Dependabot
12
+ module Swift
13
+ class FileParser < Dependabot::FileParsers::Base
14
+ # Parses XCRemoteSwiftPackageReference entries from a project.pbxproj file
15
+ # to extract dependency requirement constraints declared in Xcode.
16
+ #
17
+ # Returns a hash keyed by normalized dependency name (e.g. "github.com/owner/repo")
18
+ # mapping to requirement metadata, so the main parser can enrich
19
+ # Package.resolved dependencies with requirement info from the Xcode project.
20
+ class PbxprojParser
21
+ extend T::Sig
22
+
23
+ # Regex to extract XCRemoteSwiftPackageReference blocks from pbxproj.
24
+ # Uses [^}]* to match the requirement block content — this is safe because
25
+ # Xcode requirement blocks are always flat dictionaries with no nested braces.
26
+ PACKAGE_REF_BLOCK = T.let(
27
+ /
28
+ isa\s*=\s*XCRemoteSwiftPackageReference;\s*
29
+ repositoryURL\s*=\s*"(?<url>[^"]+)";\s*
30
+ requirement\s*=\s*\{(?<requirement>[^}]*)\};
31
+ /mx,
32
+ Regexp
33
+ )
34
+
35
+ # Patterns for extracting requirement fields
36
+ KIND_PATTERN = T.let(/kind\s*=\s*(\w+);/, Regexp)
37
+ MIN_VERSION_PATTERN = T.let(/minimumVersion\s*=\s*([\d.]+);/, Regexp)
38
+ MAX_VERSION_PATTERN = T.let(/maximumVersion\s*=\s*([\d.]+);/, Regexp)
39
+ VERSION_PATTERN = T.let(/version\s*=\s*([\d.]+);/, Regexp)
40
+ BRANCH_PATTERN = T.let(/branch\s*=\s*"?([^";]+)"?;/, Regexp)
41
+ REVISION_PATTERN = T.let(/revision\s*=\s*"?([^";]+)"?;/, Regexp)
42
+
43
+ sig { params(pbxproj_file: Dependabot::DependencyFile).void }
44
+ def initialize(pbxproj_file)
45
+ @pbxproj_file = pbxproj_file
46
+ end
47
+
48
+ # Returns a hash mapping normalized URL to requirement metadata.
49
+ # Each entry includes the Dependabot requirement string and the raw
50
+ # Xcode requirement kind/version info for use in metadata.
51
+ sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
52
+ def parse
53
+ content = pbxproj_file.content
54
+ return {} unless content
55
+
56
+ requirements = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]])
57
+
58
+ content.scan(PACKAGE_REF_BLOCK).each do |url, requirement_block|
59
+ url = T.cast(url, String)
60
+ requirement_block = T.cast(requirement_block, String)
61
+ normalized_url = SharedHelpers.scp_to_standard(url)
62
+ name = UrlHelpers.normalize_name(normalized_url)
63
+
64
+ req_info = parse_requirement_block(requirement_block)
65
+ next unless req_info
66
+
67
+ requirements[name] = req_info.merge(
68
+ url: normalized_url,
69
+ file: pbxproj_file.name
70
+ )
71
+ end
72
+
73
+ requirements
74
+ end
75
+
76
+ private
77
+
78
+ sig { returns(Dependabot::DependencyFile) }
79
+ attr_reader :pbxproj_file
80
+
81
+ sig do
82
+ params(block: String)
83
+ .returns(T.nilable(T::Hash[Symbol, T.untyped]))
84
+ end
85
+ def parse_requirement_block(block)
86
+ kind = block.match(KIND_PATTERN)&.captures&.first
87
+ return nil unless kind
88
+
89
+ case kind
90
+ when "upToNextMajorVersion"
91
+ build_up_to_next_major(block)
92
+ when "upToNextMinorVersion"
93
+ build_up_to_next_minor(block)
94
+ when "exactVersion"
95
+ build_exact(block)
96
+ when "versionRange"
97
+ build_range(block)
98
+ when "branch"
99
+ build_branch(block)
100
+ when "revision"
101
+ build_revision(block)
102
+ end
103
+ end
104
+
105
+ sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
106
+ def build_up_to_next_major(block)
107
+ min_version = extract_version(block, MIN_VERSION_PATTERN)
108
+ requirement_string = "from: \"#{min_version}\""
109
+ requirement = parse_native_requirement(requirement_string)
110
+
111
+ {
112
+ requirement: requirement,
113
+ requirement_string: requirement_string,
114
+ kind: "upToNextMajorVersion"
115
+ }
116
+ end
117
+
118
+ sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
119
+ def build_up_to_next_minor(block)
120
+ min_version = extract_version(block, MIN_VERSION_PATTERN)
121
+ requirement_string = ".upToNextMinor(from: \"#{min_version}\")"
122
+ requirement = parse_native_requirement(requirement_string)
123
+
124
+ {
125
+ requirement: requirement,
126
+ requirement_string: requirement_string,
127
+ kind: "upToNextMinorVersion"
128
+ }
129
+ end
130
+
131
+ sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
132
+ def build_exact(block)
133
+ version = extract_version(block, MIN_VERSION_PATTERN) || extract_version(block, VERSION_PATTERN)
134
+ requirement_string = "exact: \"#{version}\""
135
+ requirement = parse_native_requirement(requirement_string)
136
+
137
+ {
138
+ requirement: requirement,
139
+ requirement_string: requirement_string,
140
+ kind: "exactVersion"
141
+ }
142
+ end
143
+
144
+ sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
145
+ def build_range(block)
146
+ min_version = extract_version(block, MIN_VERSION_PATTERN)
147
+ max_version = extract_version(block, MAX_VERSION_PATTERN)
148
+ requirement_string = "\"#{min_version}\"..<\"#{max_version}\""
149
+ requirement = parse_native_requirement(requirement_string)
150
+
151
+ {
152
+ requirement: requirement,
153
+ requirement_string: requirement_string,
154
+ kind: "versionRange"
155
+ }
156
+ end
157
+
158
+ sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
159
+ def build_branch(block)
160
+ branch = block.match(BRANCH_PATTERN)&.captures&.first
161
+
162
+ {
163
+ requirement: nil,
164
+ requirement_string: nil,
165
+ kind: "branch",
166
+ branch: branch
167
+ }
168
+ end
169
+
170
+ sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) }
171
+ def build_revision(block)
172
+ revision = block.match(REVISION_PATTERN)&.captures&.first
173
+
174
+ {
175
+ requirement: nil,
176
+ requirement_string: nil,
177
+ kind: "revision",
178
+ revision: revision
179
+ }
180
+ end
181
+
182
+ sig { params(block: String, pattern: Regexp).returns(T.nilable(String)) }
183
+ def extract_version(block, pattern)
184
+ block.match(pattern)&.captures&.first
185
+ end
186
+
187
+ # Parses a requirement string into a Dependabot requirement via
188
+ # NativeRequirement. Returns nil if the string is malformed rather
189
+ # than raising, so a single bad entry doesn't stop parsing.
190
+ sig { params(requirement_string: String).returns(T.nilable(String)) }
191
+ def parse_native_requirement(requirement_string)
192
+ NativeRequirement.new(requirement_string).to_s
193
+ rescue RuntimeError, Gem::Requirement::BadRequirementError
194
+ nil
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,129 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/dependency"
6
+ require "dependabot/file_parsers/base/dependency_set"
7
+ require "dependabot/swift/file_parser"
8
+ require "dependabot/swift/file_parser/package_resolved_parser"
9
+ require "dependabot/swift/file_parser/pbxproj_parser"
10
+
11
+ module Dependabot
12
+ module Swift
13
+ class FileParser < Dependabot::FileParsers::Base
14
+ # Orchestrates Xcode-managed SwiftPM dependency parsing.
15
+ #
16
+ # Parses Package.resolved JSON files found inside .xcodeproj directories,
17
+ # then enriches each dependency with requirement info extracted from the
18
+ # corresponding project.pbxproj files.
19
+ class XcodeSpmResolver
20
+ extend T::Sig
21
+
22
+ sig do
23
+ params(
24
+ xcode_resolved_files: T::Array[Dependabot::DependencyFile],
25
+ pbxproj_files: T::Array[Dependabot::DependencyFile]
26
+ ).void
27
+ end
28
+ def initialize(xcode_resolved_files:, pbxproj_files:)
29
+ @xcode_resolved_files = xcode_resolved_files
30
+ @pbxproj_files = pbxproj_files
31
+ end
32
+
33
+ sig { returns(T::Array[Dependabot::Dependency]) }
34
+ def parse
35
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
36
+
37
+ scoped_requirements = aggregate_pbxproj_requirements
38
+
39
+ xcode_resolved_files.each do |resolved_file|
40
+ 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
+
44
+ resolved_deps.each do |dep|
45
+ enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements)
46
+ dependency_set << enriched
47
+ end
48
+ end
49
+
50
+ dependency_set.dependencies
51
+ end
52
+
53
+ private
54
+
55
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
56
+ attr_reader :xcode_resolved_files
57
+
58
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
59
+ attr_reader :pbxproj_files
60
+
61
+ # 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.
64
+ sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) }
65
+ def aggregate_pbxproj_requirements
66
+ scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]])
67
+
68
+ pbxproj_files.each do |pbxproj_file|
69
+ xcodeproj_dir = extract_xcodeproj_dir(pbxproj_file.name)
70
+ scoped[xcodeproj_dir] ||= {}
71
+
72
+ PbxprojParser.new(pbxproj_file).parse.each do |name, req_info|
73
+ T.must(scoped[xcodeproj_dir])[name] = req_info
74
+ end
75
+ end
76
+
77
+ scoped
78
+ end
79
+
80
+ # Enriches a dependency parsed from Package.resolved with requirement
81
+ # info from the matching project.pbxproj
82
+ sig do
83
+ params(
84
+ dep: Dependabot::Dependency,
85
+ pbxproj_requirements: T::Hash[String, T::Hash[Symbol, T.untyped]]
86
+ ).returns(Dependabot::Dependency)
87
+ end
88
+ def enrich_with_pbxproj_requirements(dep, pbxproj_requirements)
89
+ req_info = pbxproj_requirements[dep.name]
90
+ return dep unless req_info
91
+
92
+ pbxproj_file = req_info[:file]
93
+ requirement_str = req_info[:requirement]
94
+ requirement_string = req_info[:requirement_string]
95
+
96
+ new_requirements = dep.requirements.map do |req|
97
+ req.merge(
98
+ requirement: requirement_str || req[:requirement],
99
+ file: pbxproj_file,
100
+ metadata: {
101
+ # declaration_string is not applicable for Xcode-managed SPM
102
+ # (no Package.swift manifest to extract it from)
103
+ declaration_string: nil,
104
+ requirement_string: requirement_string
105
+ }.compact
106
+ )
107
+ end
108
+
109
+ Dependency.new(
110
+ name: dep.name,
111
+ version: dep.version,
112
+ package_manager: dep.package_manager,
113
+ requirements: new_requirements,
114
+ metadata: dep.metadata
115
+ )
116
+ end
117
+
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"
121
+ sig { params(path: String).returns(T.nilable(String)) }
122
+ def extract_xcodeproj_dir(path)
123
+ match = path.match(%r{^(.*?\.xcodeproj)/})
124
+ match&.captures&.first
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -2,10 +2,12 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/dependency"
5
+ require "dependabot/experiments"
5
6
  require "dependabot/file_parsers"
6
7
  require "dependabot/file_parsers/base"
7
8
  require "dependabot/swift/file_parser/dependency_parser"
8
9
  require "dependabot/swift/file_parser/manifest_parser"
10
+ require "dependabot/swift/file_parser/xcode_spm_resolver"
9
11
  require "dependabot/swift/package_manager"
10
12
  require "dependabot/swift/language"
11
13
 
@@ -18,6 +20,34 @@ module Dependabot
18
20
 
19
21
  sig { override.returns(T::Array[Dependabot::Dependency]) }
20
22
  def parse
23
+ if package_manifest_file
24
+ parse_classic_spm
25
+ elsif xcode_spm_mode?
26
+ parse_xcode_spm
27
+ else
28
+ raise "No Package.swift or Xcode Package.resolved found!"
29
+ end
30
+ end
31
+
32
+ sig { returns(Ecosystem) }
33
+ def ecosystem
34
+ @ecosystem ||= T.let(
35
+ begin
36
+ Ecosystem.new(
37
+ name: ECOSYSTEM,
38
+ language: language,
39
+ package_manager: package_manager
40
+ )
41
+ end,
42
+ T.nilable(Dependabot::Ecosystem)
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ # Classic SPM parsing: uses swift CLI via DependencyParser + ManifestParser
49
+ sig { returns(T::Array[Dependabot::Dependency]) }
50
+ def parse_classic_spm
21
51
  dependency_set = DependencySet.new
22
52
 
23
53
  dependency_parser.parse.map do |dep|
@@ -41,21 +71,21 @@ module Dependabot
41
71
  dependency_set.dependencies
42
72
  end
43
73
 
44
- sig { returns(Ecosystem) }
45
- def ecosystem
46
- @ecosystem ||= T.let(
47
- begin
48
- Ecosystem.new(
49
- name: ECOSYSTEM,
50
- language: language,
51
- package_manager: package_manager
52
- )
53
- end,
54
- T.nilable(Dependabot::Ecosystem)
55
- )
74
+ # Xcode SPM parsing: delegates to XcodeSpmResolver which parses
75
+ # Package.resolved JSON and enriches with project.pbxproj requirements
76
+ sig { returns(T::Array[Dependabot::Dependency]) }
77
+ def parse_xcode_spm
78
+ XcodeSpmResolver.new(
79
+ xcode_resolved_files: xcode_resolved_files,
80
+ pbxproj_files: pbxproj_files
81
+ ).parse
56
82
  end
57
83
 
58
- private
84
+ sig { returns(T::Boolean) }
85
+ def xcode_spm_mode?
86
+ Dependabot::Experiments.enabled?(:enable_swift_xcode_spm) &&
87
+ xcode_resolved_files.any?
88
+ end
59
89
 
60
90
  sig { returns(Dependabot::Swift::FileParser::DependencyParser) }
61
91
  def dependency_parser
@@ -68,7 +98,15 @@ module Dependabot
68
98
 
69
99
  sig { override.void }
70
100
  def check_required_files
71
- raise "No Package.swift!" unless package_manifest_file
101
+ return if package_manifest_file
102
+
103
+ if Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
104
+ return if xcode_resolved_files.any?
105
+
106
+ raise "No Package.swift or Xcode Package.resolved found!"
107
+ end
108
+
109
+ raise "No Package.swift!"
72
110
  end
73
111
 
74
112
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
@@ -77,6 +115,30 @@ module Dependabot
77
115
  @package_manifest_file ||= T.let(get_original_file("Package.swift"), T.nilable(Dependabot::DependencyFile))
78
116
  end
79
117
 
118
+ # All non-support Package.resolved files from Xcode project directories
119
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
120
+ def xcode_resolved_files
121
+ @xcode_resolved_files ||= T.let(
122
+ dependency_files.select do |f|
123
+ f.name.end_with?("Package.resolved") &&
124
+ f.name.include?(".xcodeproj/") &&
125
+ !f.support_file?
126
+ end,
127
+ T.nilable(T::Array[Dependabot::DependencyFile])
128
+ )
129
+ end
130
+
131
+ # All project.pbxproj support files
132
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
133
+ def pbxproj_files
134
+ @pbxproj_files ||= T.let(
135
+ dependency_files.select do |f|
136
+ f.name.end_with?("project.pbxproj") && f.support_file?
137
+ end,
138
+ T.nilable(T::Array[Dependabot::DependencyFile])
139
+ )
140
+ end
141
+
80
142
  sig { returns(Ecosystem::VersionManager) }
81
143
  def package_manager
82
144
  @package_manager ||= T.let(
@@ -0,0 +1,24 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "uri"
5
+ require "sorbet-runtime"
6
+
7
+ module Dependabot
8
+ module Swift
9
+ # Shared URL normalization utilities used by multiple parsers.
10
+ # Produces a canonical dependency name from a git repository URL
11
+ # by stripping the scheme, "www." prefix, and ".git" suffix.
12
+ module UrlHelpers
13
+ extend T::Sig
14
+
15
+ sig { params(source: String).returns(String) }
16
+ def self.normalize_name(source)
17
+ uri = URI.parse(source.downcase)
18
+ "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git")
19
+ rescue URI::InvalidURIError
20
+ source.downcase.delete_suffix(".git")
21
+ end
22
+ end
23
+ end
24
+ 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.364.0
4
+ version: 0.365.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.364.0
18
+ version: 0.365.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.364.0
25
+ version: 0.365.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -246,6 +246,9 @@ files:
246
246
  - lib/dependabot/swift/file_parser.rb
247
247
  - lib/dependabot/swift/file_parser/dependency_parser.rb
248
248
  - lib/dependabot/swift/file_parser/manifest_parser.rb
249
+ - lib/dependabot/swift/file_parser/package_resolved_parser.rb
250
+ - lib/dependabot/swift/file_parser/pbxproj_parser.rb
251
+ - lib/dependabot/swift/file_parser/xcode_spm_resolver.rb
249
252
  - lib/dependabot/swift/file_updater.rb
250
253
  - lib/dependabot/swift/file_updater/lockfile_updater.rb
251
254
  - lib/dependabot/swift/file_updater/manifest_updater.rb
@@ -260,13 +263,14 @@ files:
260
263
  - lib/dependabot/swift/update_checker/latest_version_resolver.rb
261
264
  - lib/dependabot/swift/update_checker/requirements_updater.rb
262
265
  - lib/dependabot/swift/update_checker/version_resolver.rb
266
+ - lib/dependabot/swift/url_helpers.rb
263
267
  - lib/dependabot/swift/version.rb
264
268
  homepage: https://github.com/dependabot/dependabot-core
265
269
  licenses:
266
270
  - MIT
267
271
  metadata:
268
272
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
269
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.364.0
273
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.365.0
270
274
  rdoc_options: []
271
275
  require_paths:
272
276
  - lib