Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ PATH
specs:
dependabot-swift (0.363.0)
dependabot-common (= 0.363.0)
xcodeproj (~> 1.27)

PATH
remote: terraform
Expand All @@ -225,9 +226,11 @@ PATH
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.8)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1220.0)
aws-sdk-codecommit (1.96.0)
Expand All @@ -250,6 +253,8 @@ GEM
benchmark (0.5.0)
bigdecimal (4.0.1)
citrus (3.0.2)
claide (1.1.0)
colored2 (3.1.2)
commonmarker (2.6.3-arm64-darwin)
commonmarker (2.6.3-x86_64-linux)
crack (1.0.1)
Expand Down Expand Up @@ -315,6 +320,7 @@ GEM
mini_portile2 (2.8.9)
multi_xml (0.8.1)
bigdecimal (>= 3.1, < 5)
nanaimo (0.4.0)
net-http (0.9.1)
uri (>= 0.11.1)
netrc (0.11.0)
Expand Down Expand Up @@ -471,6 +477,13 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
yard (0.9.38)
yard-sorbet (0.9.0)
sorbet-runtime
Expand Down Expand Up @@ -533,8 +546,10 @@ DEPENDENCIES
zeitwerk (~> 2.7)

CHECKSUMS
CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261
addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1220.0) sha256=1567da9ae45cba28e1d31f5e996928b2eb92ad01700000846d6d90043be8670f
aws-sdk-codecommit (1.96.0) sha256=4eb0cbd8a18c65856acd7ccb7fd73d9aab260da7557a2a7142b00e224c81a7d5
Expand All @@ -545,6 +560,8 @@ CHECKSUMS
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
citrus (3.0.2) sha256=4ec2412fc389ad186735f4baee1460f7900a8e130ffe3f216b30d4f9c684f650
claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
commonmarker (2.6.3-arm64-darwin) sha256=d6c1e4955619da3f68fed22de99dec49a24925611770c039bf870823846c8b21
commonmarker (2.6.3-x86_64-linux) sha256=e861ba1812721113725ebd8e46e4fee20dc732842f5555db2cfb8dcd74056583
crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
Expand Down Expand Up @@ -613,6 +630,7 @@ CHECKSUMS
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
multi_xml (0.8.1) sha256=addba0290bac34e9088bfe73dc4878530297a82a7bbd66cb44dcd0a4b86edf5a
nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723
net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
nokogiri (1.19.1-arm64-darwin) sha256=dfe2d337e6700eac47290407c289d56bcf85805d128c1b5a6434ddb79731cb9e
Expand Down Expand Up @@ -678,6 +696,7 @@ CHECKSUMS
vcr (6.4.0) sha256=077ac92cc16efc5904eb90492a18153b5e6ca5398046d8a249a7c96a9ea24ae6
webmock (3.26.1) sha256=4f696fb57c90a827c20aadb2d4f9058bbff10f7f043bd0d4c3f58791143b1cd7
webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
xcodeproj (1.27.0) sha256=8cc7a73b4505c227deab044dce118ede787041c702bc47636856a2e566f854d3
yard (0.9.38) sha256=721fb82afb10532aa49860655f6cc2eaa7130889df291b052e1e6b268283010f
yard-sorbet (0.9.0) sha256=03d1aa461b9e9c82b886919a13aa3e09fcf4d1852239d2967ed97e92723ffe21
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
Expand Down
1 change: 1 addition & 0 deletions swift/dependabot-swift.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
spec.files = Dir["lib/**/*"]

spec.add_dependency "dependabot-common", Dependabot::VERSION
spec.add_dependency "xcodeproj", "~> 1.27"

common_gemspec.development_dependencies.each do |dep|
spec.add_development_dependency dep.name, *dep.requirement.as_list
Expand Down
159 changes: 146 additions & 13 deletions swift/lib/dependabot/swift/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
# frozen_string_literal: true

require "dependabot/dependency"
require "dependabot/experiments"
require "dependabot/file_parsers"
require "dependabot/file_parsers/base"
require "dependabot/swift/file_parser/dependency_parser"
require "dependabot/swift/file_parser/manifest_parser"
require "dependabot/swift/file_parser/package_resolved_parser"
require "dependabot/swift/file_parser/pbxproj_parser"
require "dependabot/swift/package_manager"
require "dependabot/swift/language"

Expand All @@ -18,6 +21,34 @@ class FileParser < Dependabot::FileParsers::Base

sig { override.returns(T::Array[Dependabot::Dependency]) }
def parse
if package_manifest_file
parse_classic_spm
elsif xcode_spm_mode?
parse_xcode_spm
else
raise "No Package.swift!"
end
end

sig { returns(Ecosystem) }
def ecosystem
@ecosystem ||= T.let(
begin
Ecosystem.new(
name: ECOSYSTEM,
language: language,
package_manager: package_manager
)
end,
T.nilable(Dependabot::Ecosystem)
)
end

private

# Classic SPM parsing: uses swift CLI via DependencyParser + ManifestParser
sig { returns(T::Array[Dependabot::Dependency]) }
def parse_classic_spm
dependency_set = DependencySet.new

dependency_parser.parse.map do |dep|
Expand All @@ -41,21 +72,91 @@ def parse
dependency_set.dependencies
end

sig { returns(Ecosystem) }
def ecosystem
@ecosystem ||= T.let(
begin
Ecosystem.new(
name: ECOSYSTEM,
language: language,
package_manager: package_manager
)
end,
T.nilable(Dependabot::Ecosystem)
# Xcode SPM parsing: parses Package.resolved JSON directly, enriches
# with requirement info from project.pbxproj files
sig { returns(T::Array[Dependabot::Dependency]) }
def parse_xcode_spm
dependency_set = DependencySet.new

pbxproj_requirements = aggregate_pbxproj_requirements

xcode_resolved_files.each do |resolved_file|
resolved_deps = PackageResolvedParser.new(resolved_file).parse
xcodeproj_dir = extract_xcodeproj_dir(resolved_file.name)

resolved_deps.each do |dep|
enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements, xcodeproj_dir)
dependency_set << enriched
end
end

dependency_set.dependencies
end

sig { returns(T::Boolean) }
def xcode_spm_mode?
Dependabot::Experiments.enabled?(:enable_swift_xcode_spm) &&
xcode_resolved_files.any?
end

# Collects requirement info from all project.pbxproj support files
sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
def aggregate_pbxproj_requirements
requirements = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]])

pbxproj_files.each do |pbxproj_file|
PbxprojParser.new(pbxproj_file).parse.each do |name, req_info|
requirements[name] = req_info
end
end

requirements
end

# Enriches a dependency parsed from Package.resolved with requirement
# info from the matching project.pbxproj
sig do
params(
dep: Dependabot::Dependency,
pbxproj_requirements: T::Hash[String, T::Hash[Symbol, T.untyped]],
xcodeproj_dir: T.nilable(String)
).returns(Dependabot::Dependency)
end
def enrich_with_pbxproj_requirements(dep, pbxproj_requirements, xcodeproj_dir) # rubocop:disable Lint/UnusedMethodArgument
req_info = pbxproj_requirements[dep.name]
return dep unless req_info

pbxproj_file = req_info[:file]
requirement_str = req_info[:requirement]
requirement_string = req_info[:requirement_string]

new_requirements = dep.requirements.map do |req|
req.merge(
requirement: requirement_str || req[:requirement],
file: pbxproj_file,
metadata: {
declaration_string: nil,
requirement_string: requirement_string
}.compact
)
end

Dependency.new(
name: dep.name,
version: dep.version,
package_manager: dep.package_manager,
requirements: new_requirements,
metadata: dep.metadata
)
end

private
# Extracts the .xcodeproj directory name from a Package.resolved path.
# e.g. "MyApp.xcodeproj/project.xcworkspace/.../Package.resolved" -> "MyApp.xcodeproj"
sig { params(resolved_path: String).returns(T.nilable(String)) }
def extract_xcodeproj_dir(resolved_path)
match = resolved_path.match(%r{^([^/]+\.xcodeproj)/})
match&.captures&.first
end

sig { returns(Dependabot::Swift::FileParser::DependencyParser) }
def dependency_parser
Expand All @@ -68,7 +169,15 @@ def dependency_parser

sig { override.void }
def check_required_files
raise "No Package.swift!" unless package_manifest_file
return if package_manifest_file

if Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
return if dependency_files.any? { |f| f.name.end_with?("Package.resolved") }

raise "No Package.swift or Xcode Package.resolved found!"
end

raise "No Package.swift!"
end

sig { returns(T.nilable(Dependabot::DependencyFile)) }
Expand All @@ -77,6 +186,30 @@ def package_manifest_file
@package_manifest_file ||= T.let(get_original_file("Package.swift"), T.nilable(Dependabot::DependencyFile))
end

# All non-support Package.resolved files from Xcode project directories
sig { returns(T::Array[Dependabot::DependencyFile]) }
def xcode_resolved_files
@xcode_resolved_files ||= T.let(
dependency_files.select do |f|
f.name.end_with?("Package.resolved") &&
f.name.include?(".xcodeproj/") &&
!f.support_file?
end,
T.nilable(T::Array[Dependabot::DependencyFile])
)
end

# All project.pbxproj support files
sig { returns(T::Array[Dependabot::DependencyFile]) }
def pbxproj_files
@pbxproj_files ||= T.let(
dependency_files.select do |f|
f.name.end_with?("project.pbxproj") && f.support_file?
end,
T.nilable(T::Array[Dependabot::DependencyFile])
)
end

sig { returns(Ecosystem::VersionManager) }
def package_manager
@package_manager ||= T.let(
Expand Down
Loading
Loading