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 +4 -4
- data/lib/dependabot/swift/file_parser/dependency_parser.rb +2 -9
- data/lib/dependabot/swift/file_parser/package_resolved_parser.rb +165 -0
- data/lib/dependabot/swift/file_parser/pbxproj_parser.rb +199 -0
- data/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb +129 -0
- data/lib/dependabot/swift/file_parser.rb +76 -14
- data/lib/dependabot/swift/url_helpers.rb +24 -0
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 35c7a26b4868b5df0a08396136836264f47a3e1d97219ceb705698bb1eae21f3
|
|
4
|
+
data.tar.gz: 3c22d53cfb502e71c17ba6edc2d5d0d6b6dbbfc92fdbfb9fc897579d6c15269f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 =
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|