From 4bee70b82e67e9e920c2820f3ebd853ddf8e0b9e Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Wed, 4 Mar 2026 15:59:26 +0000 Subject: [PATCH 1/8] Add Xcode SPM FileParser support (Part 2) Extend Swift FileParser to handle Xcode-managed SwiftPM projects that don't have a Package.swift file. This adds: - PackageResolvedParser: Parses Package.resolved v1, v2, and v3 schemas into Dependabot::Dependency objects with proper version/source info - PbxprojParser: Extracts XCRemoteSwiftPackageReference entries from project.pbxproj files to enrich dependencies with requirement types (upToNextMajor, upToNextMinor, exact, range, branch, revision) - FileParser dual-mode: Detects Xcode SPM mode (no Package.swift but Package.resolved present) under enable_swift_xcode_spm experiment flag - Support for multiple .xcodeproj directories with separate resolved files - Comprehensive test fixtures and specs for all parsers and edge cases - xcodeproj gem (~> 1.27) added as a dependency --- Gemfile.lock | 19 + swift/dependabot-swift.gemspec | 1 + swift/lib/dependabot/swift/file_parser.rb | 159 +++++- .../file_parser/package_resolved_parser.rb | 181 +++++++ .../swift/file_parser/pbxproj_parser.rb | 192 +++++++ .../package_resolved_parser_spec.rb | 297 +++++++++++ .../swift/file_parser/pbxproj_parser_spec.rb | 146 ++++++ .../spec/dependabot/swift/file_parser_spec.rb | 479 ++++++++++++++++++ .../MyApp.xcodeproj/project.pbxproj | 10 + .../xcshareddata/swiftpm/Package.resolved | 4 + .../MyApp.xcodeproj/project.pbxproj | 10 + .../xcshareddata/swiftpm/Package.resolved | 1 + .../MyApp.xcodeproj/project.pbxproj | 47 ++ .../xcshareddata/swiftpm/Package.resolved | 41 ++ .../MyApp.xcodeproj/project.pbxproj | 22 + .../xcshareddata/swiftpm/Package.resolved | 13 + .../MyApp.xcodeproj/project.pbxproj | 10 + .../xcshareddata/swiftpm/Package.resolved | 4 + .../MyApp.xcodeproj/project.pbxproj | 22 + .../xcshareddata/swiftpm/Package.resolved | 16 + .../MyApp.xcodeproj/project.pbxproj | 22 + .../xcshareddata/swiftpm/Package.resolved | 15 + updater/Gemfile.lock | 138 ++--- 23 files changed, 1777 insertions(+), 72 deletions(-) create mode 100644 swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb create mode 100644 swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb create mode 100644 swift/spec/dependabot/swift/file_parser/package_resolved_parser_spec.rb create mode 100644 swift/spec/dependabot/swift/file_parser/pbxproj_parser_spec.rb create mode 100644 swift/spec/fixtures/projects/xcode_project_empty_pins/MyApp.xcodeproj/project.pbxproj create mode 100644 swift/spec/fixtures/projects/xcode_project_empty_pins/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 swift/spec/fixtures/projects/xcode_project_invalid_json/MyApp.xcodeproj/project.pbxproj create mode 100644 swift/spec/fixtures/projects/xcode_project_invalid_json/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 swift/spec/fixtures/projects/xcode_project_multi_req/MyApp.xcodeproj/project.pbxproj create mode 100644 swift/spec/fixtures/projects/xcode_project_multi_req/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 swift/spec/fixtures/projects/xcode_project_revision_only/MyApp.xcodeproj/project.pbxproj create mode 100644 swift/spec/fixtures/projects/xcode_project_revision_only/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 swift/spec/fixtures/projects/xcode_project_unknown_schema/MyApp.xcodeproj/project.pbxproj create mode 100644 swift/spec/fixtures/projects/xcode_project_unknown_schema/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 swift/spec/fixtures/projects/xcode_project_v1_resolved/MyApp.xcodeproj/project.pbxproj create mode 100644 swift/spec/fixtures/projects/xcode_project_v1_resolved/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 swift/spec/fixtures/projects/xcode_project_v3_resolved/MyApp.xcodeproj/project.pbxproj create mode 100644 swift/spec/fixtures/projects/xcode_project_v3_resolved/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Gemfile.lock b/Gemfile.lock index 271a6b876d4..09a747f913a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -202,6 +202,7 @@ PATH specs: dependabot-swift (0.364.0) dependabot-common (= 0.364.0) + xcodeproj (~> 1.27) PATH remote: terraform @@ -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) @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/swift/dependabot-swift.gemspec b/swift/dependabot-swift.gemspec index 29f02441cf3..665eabf4ef1 100644 --- a/swift/dependabot-swift.gemspec +++ b/swift/dependabot-swift.gemspec @@ -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 diff --git a/swift/lib/dependabot/swift/file_parser.rb b/swift/lib/dependabot/swift/file_parser.rb index 681f27b8765..40f7f618da1 100644 --- a/swift/lib/dependabot/swift/file_parser.rb +++ b/swift/lib/dependabot/swift/file_parser.rb @@ -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" @@ -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| @@ -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 @@ -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)) } @@ -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( diff --git a/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb b/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb new file mode 100644 index 00000000000..577cf17a343 --- /dev/null +++ b/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb @@ -0,0 +1,181 @@ +# typed: strict +# frozen_string_literal: true + +require "json" +require "uri" +require "sorbet-runtime" +require "dependabot/dependency" +require "dependabot/errors" +require "dependabot/shared_helpers" +require "dependabot/swift/file_parser" + +module Dependabot + module Swift + class FileParser < Dependabot::FileParsers::Base + class PackageResolvedParser + extend T::Sig + + SUPPORTED_VERSIONS = T.let([1, 2, 3].freeze, T::Array[Integer]) + + sig { params(resolved_file: Dependabot::DependencyFile).void } + def initialize(resolved_file) + @resolved_file = resolved_file + end + + sig { returns(T::Array[Dependabot::Dependency]) } + def parse + parsed = parse_json + schema_version = detect_schema_version(parsed) + pins = extract_pins(parsed, schema_version) + + pins.filter_map { |pin| build_dependency(pin, schema_version) } + end + + private + + sig { returns(Dependabot::DependencyFile) } + attr_reader :resolved_file + + sig { returns(T::Hash[String, T.untyped]) } + def parse_json + JSON.parse(T.must(resolved_file.content)) + rescue JSON::ParserError => e + raise Dependabot::DependencyFileNotParseable.new( + resolved_file.name, + "#{resolved_file.name} is not valid JSON: #{e.message}" + ) + end + + sig { params(parsed: T::Hash[String, T.untyped]).returns(Integer) } + def detect_schema_version(parsed) + version = parsed["version"] + + unless version.is_a?(Integer) && SUPPORTED_VERSIONS.include?(version) + raise Dependabot::DependencyFileNotParseable.new( + resolved_file.name, + "#{resolved_file.name} has unsupported schema version: #{version.inspect}. " \ + "Supported versions: #{SUPPORTED_VERSIONS.join(', ')}" + ) + end + + version + end + + sig do + params( + parsed: T::Hash[String, T.untyped], + schema_version: Integer + ).returns(T::Array[T::Hash[String, T.untyped]]) + end + def extract_pins(parsed, schema_version) + pins = if schema_version == 1 + parsed.dig("object", "pins") + else + # v2 and v3 use the same top-level "pins" key + parsed["pins"] + end + + unless pins.is_a?(Array) + raise Dependabot::DependencyFileNotParseable.new( + resolved_file.name, + "#{resolved_file.name} is missing the expected 'pins' array " \ + "(schema version #{schema_version})" + ) + end + + pins + end + + sig do + params( + pin: T::Hash[String, T.untyped], + schema_version: Integer + ).returns(T.nilable(Dependabot::Dependency)) + end + def build_dependency(pin, schema_version) + if schema_version == 1 + build_v1_dependency(pin) + else + build_v2_dependency(pin) + end + end + + sig { params(pin: T::Hash[String, T.untyped]).returns(T.nilable(Dependabot::Dependency)) } + def build_v1_dependency(pin) + url = pin["repositoryURL"] + return nil unless url.is_a?(String) && !url.empty? + + state = pin["state"] || {} + version = state["version"] + revision = state["revision"] + branch = state["branch"] + identity = pin["package"]&.downcase + + build_dependency_object( + identity: identity, + url: url, + version: version, + revision: revision, + branch: branch + ) + end + + sig { params(pin: T::Hash[String, T.untyped]).returns(T.nilable(Dependabot::Dependency)) } + def build_v2_dependency(pin) + url = pin["location"] + return nil unless url.is_a?(String) && !url.empty? + + state = pin["state"] || {} + version = state["version"] + revision = state["revision"] + branch = state["branch"] + identity = pin["identity"] + + build_dependency_object( + identity: identity, + url: url, + version: version, + revision: revision, + branch: branch + ) + end + + sig do + params( + identity: T.nilable(String), + url: String, + version: T.nilable(String), + revision: T.nilable(String), + branch: T.nilable(String) + ).returns(T.nilable(Dependabot::Dependency)) + end + def build_dependency_object(identity:, url:, version:, revision:, branch:) + normalized_url = SharedHelpers.scp_to_standard(url) + name = normalize_name(normalized_url) + ref = version || revision + + source = { type: "git", url: normalized_url, ref: ref, branch: branch } + + Dependency.new( + name: name, + version: version, + package_manager: "swift", + requirements: [{ + requirement: version ? "= #{version}" : nil, + groups: ["dependencies"], + file: resolved_file.name, + source: source + }], + metadata: { identity: identity } + ) + end + + sig { params(source: String).returns(String) } + def normalize_name(source) + uri = URI.parse(source.downcase) + "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git") + end + end + end + end +end diff --git a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb new file mode 100644 index 00000000000..e7cbcd81598 --- /dev/null +++ b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb @@ -0,0 +1,192 @@ +# typed: strict +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/errors" +require "dependabot/shared_helpers" +require "dependabot/swift/file_parser" +require "dependabot/swift/native_requirement" + +module Dependabot + module Swift + class FileParser < Dependabot::FileParsers::Base + # Parses XCRemoteSwiftPackageReference entries from a project.pbxproj file + # to extract dependency requirement constraints declared in Xcode. + # + # Returns a hash keyed by normalized URL mapping to requirement metadata, + # so the main parser can enrich Package.resolved dependencies with + # requirement info from the Xcode project. + class PbxprojParser + extend T::Sig + + # Regex to extract XCRemoteSwiftPackageReference blocks from pbxproj + PACKAGE_REF_BLOCK = T.let( + / + isa\s*=\s*XCRemoteSwiftPackageReference;\s* + repositoryURL\s*=\s*"(?[^"]+)";\s* + requirement\s*=\s*\{(?[^}]*)\}; + /mx, + Regexp + ) + + # Patterns for extracting requirement fields + KIND_PATTERN = T.let(/kind\s*=\s*(\w+);/, Regexp) + MIN_VERSION_PATTERN = T.let(/minimumVersion\s*=\s*([\d.]+);/, Regexp) + MAX_VERSION_PATTERN = T.let(/maximumVersion\s*=\s*([\d.]+);/, Regexp) + VERSION_PATTERN = T.let(/version\s*=\s*([\d.]+);/, Regexp) + BRANCH_PATTERN = T.let(/branch\s*=\s*"?([^";]+)"?;/, Regexp) + REVISION_PATTERN = T.let(/revision\s*=\s*"?([^";]+)"?;/, Regexp) + + sig { params(pbxproj_file: Dependabot::DependencyFile).void } + def initialize(pbxproj_file) + @pbxproj_file = pbxproj_file + end + + # Returns a hash mapping normalized URL to requirement metadata. + # Each entry includes the Dependabot requirement string and the raw + # Xcode requirement kind/version info for use in metadata. + sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) } + def parse + content = pbxproj_file.content + return {} unless content + + requirements = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]]) + + content.scan(PACKAGE_REF_BLOCK).each do |url, requirement_block| + url = T.cast(url, String) + requirement_block = T.cast(requirement_block, String) + normalized_url = SharedHelpers.scp_to_standard(url) + name = normalize_name(normalized_url) + + req_info = parse_requirement_block(requirement_block, url) + next unless req_info + + requirements[name] = req_info.merge( + url: normalized_url, + file: pbxproj_file.name + ) + end + + requirements + end + + private + + sig { returns(Dependabot::DependencyFile) } + attr_reader :pbxproj_file + + sig do + params(block: String, url: String) + .returns(T.nilable(T::Hash[Symbol, T.untyped])) + end + def parse_requirement_block(block, url) + kind = block.match(KIND_PATTERN)&.captures&.first + return nil unless kind + + case kind + when "upToNextMajorVersion" + build_up_to_next_major(block, url) + when "upToNextMinorVersion" + build_up_to_next_minor(block, url) + when "exactVersion" + build_exact(block, url) + when "versionRange" + build_range(block, url) + when "branch" + build_branch(block, url) + when "revision" + build_revision(block, url) + end + end + + sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } + def build_up_to_next_major(block, _url) + min_version = extract_version(block, MIN_VERSION_PATTERN) + requirement_string = "from: \"#{min_version}\"" + native_req = NativeRequirement.new(requirement_string) + + { + requirement: native_req.to_s, + requirement_string: requirement_string, + kind: "upToNextMajorVersion" + } + end + + sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } + def build_up_to_next_minor(block, _url) + min_version = extract_version(block, MIN_VERSION_PATTERN) + requirement_string = ".upToNextMinor(from: \"#{min_version}\")" + native_req = NativeRequirement.new(requirement_string) + + { + requirement: native_req.to_s, + requirement_string: requirement_string, + kind: "upToNextMinorVersion" + } + end + + sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } + def build_exact(block, _url) + version = extract_version(block, MIN_VERSION_PATTERN) || extract_version(block, VERSION_PATTERN) + requirement_string = "exact: \"#{version}\"" + native_req = NativeRequirement.new(requirement_string) + + { + requirement: native_req.to_s, + requirement_string: requirement_string, + kind: "exactVersion" + } + end + + sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } + def build_range(block, _url) + min_version = extract_version(block, MIN_VERSION_PATTERN) + max_version = extract_version(block, MAX_VERSION_PATTERN) + requirement_string = "\"#{min_version}\"..<\"#{max_version}\"" + native_req = NativeRequirement.new(requirement_string) + + { + requirement: native_req.to_s, + requirement_string: requirement_string, + kind: "versionRange" + } + end + + sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } + def build_branch(block, _url) + branch = block.match(BRANCH_PATTERN)&.captures&.first + + { + requirement: nil, + requirement_string: nil, + kind: "branch", + branch: branch + } + end + + sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } + def build_revision(block, _url) + revision = block.match(REVISION_PATTERN)&.captures&.first + + { + requirement: nil, + requirement_string: nil, + kind: "revision", + revision: revision + } + end + + sig { params(block: String, pattern: Regexp).returns(T.nilable(String)) } + def extract_version(block, pattern) + block.match(pattern)&.captures&.first + end + + sig { params(source: String).returns(String) } + def normalize_name(source) + uri = URI.parse(source.downcase) + "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git") + end + end + end + end +end diff --git a/swift/spec/dependabot/swift/file_parser/package_resolved_parser_spec.rb b/swift/spec/dependabot/swift/file_parser/package_resolved_parser_spec.rb new file mode 100644 index 00000000000..dd1aca35ce1 --- /dev/null +++ b/swift/spec/dependabot/swift/file_parser/package_resolved_parser_spec.rb @@ -0,0 +1,297 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency_file" +require "dependabot/swift/file_parser/package_resolved_parser" + +RSpec.describe Dependabot::Swift::FileParser::PackageResolvedParser do + subject(:parser) { described_class.new(resolved_file) } + + let(:resolved_file) do + Dependabot::DependencyFile.new( + name: file_name, + content: file_content + ) + end + + describe "#parse" do + context "with v2 schema" do + let(:file_name) { "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" } + let(:file_content) do + fixture( + "projects", + "xcode_project", + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + end + + it "parses dependencies correctly" do + deps = parser.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + expect(dep.package_manager).to eq("swift") + expect(dep.metadata).to eq({ identity: "swift-nio" }) + end + + it "sets correct requirements" do + dep = parser.parse.first + req = dep.requirements.first + + expect(req[:requirement]).to eq("= 2.54.0") + expect(req[:groups]).to eq(["dependencies"]) + expect(req[:file]).to eq(file_name) + expect(req[:source]).to eq( + { + type: "git", + url: "https://github.com/apple/swift-nio.git", + ref: "2.54.0", + branch: nil + } + ) + end + end + + context "with v1 schema" do + let(:file_name) { "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" } + let(:file_content) do + fixture( + "projects", + "xcode_project_v1_resolved", + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + end + + it "parses dependencies correctly" do + deps = parser.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + expect(dep.package_manager).to eq("swift") + expect(dep.metadata).to eq({ identity: "swift-nio" }) + end + + it "normalizes v1 identity from package name" do + dep = parser.parse.first + # v1 uses "package" field, lowercased for identity + expect(dep.metadata[:identity]).to eq("swift-nio") + end + end + + context "with v3 schema" do + let(:file_name) { "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" } + let(:file_content) do + fixture( + "projects", + "xcode_project_v3_resolved", + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + end + + it "parses dependencies correctly (same as v2 structure)" do + deps = parser.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + end + end + + context "with revision-only pin (no version)" do + let(:file_name) { "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" } + let(:file_content) do + fixture( + "projects", + "xcode_project_revision_only", + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + end + + it "parses with nil version" do + deps = parser.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to be_nil + end + + it "uses revision as ref in source" do + dep = parser.parse.first + source = dep.requirements.first[:source] + + expect(source[:ref]).to eq("6213ba7a06febe8fef60563a4a7d26a4085783cf") + expect(source[:branch]).to be_nil + end + + it "sets requirement to nil for revision-only pins" do + dep = parser.parse.first + expect(dep.requirements.first[:requirement]).to be_nil + end + end + + context "with multiple dependencies" do + let(:file_name) { "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" } + let(:file_content) do + fixture( + "projects", + "xcode_project_multi_req", + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + end + + it "parses all dependencies" do + deps = parser.parse + expect(deps.length).to eq(4) + + names = deps.map(&:name) + expect(names).to contain_exactly( + "github.com/apple/swift-nio", + "github.com/apple/swift-collections", + "github.com/apple/swift-argument-parser", + "github.com/apple/swift-log" + ) + end + end + + context "with empty pins array" do + let(:file_name) { "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" } + let(:file_content) do + fixture( + "projects", + "xcode_project_empty_pins", + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + end + + it "returns an empty array" do + expect(parser.parse).to be_empty + end + end + + context "with invalid JSON" do + let(:file_name) { "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" } + let(:file_content) do + fixture( + "projects", + "xcode_project_invalid_json", + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + end + + it "raises DependencyFileNotParseable with file path" do + expect { parser.parse }.to raise_error( + Dependabot::DependencyFileNotParseable + ) do |error| + expect(error.file_path).to eq(file_name) + expect(error.message).to include("not valid JSON") + end + end + end + + context "with unknown schema version" do + let(:file_name) { "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" } + let(:file_content) do + fixture( + "projects", + "xcode_project_unknown_schema", + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + end + + it "raises DependencyFileNotParseable with schema info" do + expect { parser.parse }.to raise_error( + Dependabot::DependencyFileNotParseable + ) do |error| + expect(error.file_path).to eq(file_name) + expect(error.message).to include("unsupported schema version") + expect(error.message).to include("99") + end + end + end + + context "with existing v1 fixture (ReactiveCocoa)" do + let(:file_name) { "Package.resolved" } + let(:file_content) { fixture("projects", "ReactiveCocoa", "Package.resolved") } + + it "parses all v1 dependencies" do + deps = parser.parse + expect(deps.length).to eq(5) + + names = deps.map(&:name) + expect(names).to include("github.com/quick/quick") + expect(names).to include("github.com/quick/nimble") + expect(names).to include("github.com/reactivecocoa/reactiveswift") + end + + it "normalizes URLs correctly" do + quick_dep = parser.parse.find { |d| d.metadata[:identity] == "quick" } + expect(quick_dep.name).to eq("github.com/quick/quick") + expect(quick_dep.requirements.first[:source][:url]).to eq("https://github.com/Quick/Quick.git") + end + end + + context "with SCP-style URL in pin" do + let(:file_name) { "Package.resolved" } + let(:file_content) do + <<~JSON + { + "pins": [ + { + "identity": "my-package", + "kind": "remoteSourceControl", + "location": "git@github.com:owner/my-package.git", + "state": { "revision": "abc123", "version": "1.0.0" } + } + ], + "version": 2 + } + JSON + end + + it "normalizes SCP URLs to HTTPS" do + dep = parser.parse.first + expect(dep.name).to eq("github.com/owner/my-package") + expect(dep.requirements.first[:source][:url]).to eq("https://github.com/owner/my-package.git") + end + end + end +end diff --git a/swift/spec/dependabot/swift/file_parser/pbxproj_parser_spec.rb b/swift/spec/dependabot/swift/file_parser/pbxproj_parser_spec.rb new file mode 100644 index 00000000000..b8642c7db22 --- /dev/null +++ b/swift/spec/dependabot/swift/file_parser/pbxproj_parser_spec.rb @@ -0,0 +1,146 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency_file" +require "dependabot/swift/file_parser/pbxproj_parser" + +RSpec.describe Dependabot::Swift::FileParser::PbxprojParser do + subject(:parser) { described_class.new(pbxproj_file) } + + let(:pbxproj_file) do + Dependabot::DependencyFile.new( + name: file_name, + content: file_content, + support_file: true + ) + end + + describe "#parse" do + context "with upToNextMajorVersion requirement" do + let(:file_name) { "MyApp.xcodeproj/project.pbxproj" } + let(:file_content) { fixture("projects", "xcode_project", "MyApp.xcodeproj", "project.pbxproj") } + + it "parses the requirement correctly" do + reqs = parser.parse + expect(reqs.length).to eq(1) + + name = reqs.keys.first + expect(name).to eq("github.com/apple/swift-nio") + + req_info = reqs[name] + expect(req_info[:requirement]).to eq(">= 2.54.0, < 3.0.0") + expect(req_info[:requirement_string]).to eq("from: \"2.54.0\"") + expect(req_info[:kind]).to eq("upToNextMajorVersion") + expect(req_info[:file]).to eq(file_name) + expect(req_info[:url]).to eq("https://github.com/apple/swift-nio.git") + end + end + + context "with multiple requirement types" do + let(:file_name) { "MyApp.xcodeproj/project.pbxproj" } + let(:file_content) { fixture("projects", "xcode_project_multi_req", "MyApp.xcodeproj", "project.pbxproj") } + + it "parses all package references" do + reqs = parser.parse + expect(reqs.length).to eq(4) + end + + it "parses upToNextMajorVersion correctly" do + req = parser.parse["github.com/apple/swift-nio"] + expect(req[:requirement]).to eq(">= 2.54.0, < 3.0.0") + expect(req[:requirement_string]).to eq("from: \"2.54.0\"") + expect(req[:kind]).to eq("upToNextMajorVersion") + end + + it "parses upToNextMinorVersion correctly" do + req = parser.parse["github.com/apple/swift-collections"] + expect(req[:requirement]).to eq(">= 1.0.0, < 1.1.0") + expect(req[:requirement_string]).to eq(".upToNextMinor(from: \"1.0.0\")") + expect(req[:kind]).to eq("upToNextMinorVersion") + end + + it "parses exactVersion correctly" do + req = parser.parse["github.com/apple/swift-argument-parser"] + expect(req[:requirement]).to eq("= 1.2.0") + expect(req[:requirement_string]).to eq("exact: \"1.2.0\"") + expect(req[:kind]).to eq("exactVersion") + end + + it "parses versionRange correctly" do + req = parser.parse["github.com/apple/swift-log"] + expect(req[:requirement]).to eq(">= 1.4.0, < 2.0.0") + expect(req[:requirement_string]).to eq("\"1.4.0\"..<\"2.0.0\"") + expect(req[:kind]).to eq("versionRange") + end + end + + context "with revision requirement" do + let(:file_name) { "MyApp.xcodeproj/project.pbxproj" } + let(:file_content) do + fixture("projects", "xcode_project_revision_only", "MyApp.xcodeproj", "project.pbxproj") + end + + it "returns nil requirement for revision-pinned packages" do + reqs = parser.parse + expect(reqs.length).to eq(1) + + req = reqs["github.com/apple/swift-nio"] + expect(req[:requirement]).to be_nil + expect(req[:requirement_string]).to be_nil + expect(req[:kind]).to eq("revision") + expect(req[:revision]).to eq("6213ba7a06febe8fef60563a4a7d26a4085783cf") + end + end + + context "with no XCRemoteSwiftPackageReference entries" do + let(:file_name) { "MyApp.xcodeproj/project.pbxproj" } + let(:file_content) do + fixture("projects", "xcode_project_empty_pins", "MyApp.xcodeproj", "project.pbxproj") + end + + it "returns an empty hash" do + expect(parser.parse).to be_empty + end + end + + context "with nil content" do + let(:file_name) { "MyApp.xcodeproj/project.pbxproj" } + let(:file_content) { nil } + + it "returns an empty hash" do + expect(parser.parse).to be_empty + end + end + + context "with branch requirement" do + let(:file_name) { "MyApp.xcodeproj/project.pbxproj" } + let(:file_content) do + <<~PBXPROJ + // !$*UTF8*$! + { + archiveVersion = 1; + objects = { + A1B2C3D4E5F60001 /* XCRemoteSwiftPackageReference "swift-nio" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio.git"; + requirement = { + kind = branch; + branch = main; + }; + }; + }; + rootObject = A1B2C3D4E5F60000; + } + PBXPROJ + end + + it "returns nil requirement with branch info" do + req = parser.parse["github.com/apple/swift-nio"] + expect(req[:requirement]).to be_nil + expect(req[:kind]).to eq("branch") + expect(req[:branch]).to eq("main") + end + end + end +end diff --git a/swift/spec/dependabot/swift/file_parser_spec.rb b/swift/spec/dependabot/swift/file_parser_spec.rb index 835a74d926a..38bc3949319 100644 --- a/swift/spec/dependabot/swift/file_parser_spec.rb +++ b/swift/spec/dependabot/swift/file_parser_spec.rb @@ -339,4 +339,483 @@ end end end + + context "when enable_swift_xcode_spm experiment is enabled" do + before { Dependabot::Experiments.register(:enable_swift_xcode_spm, true) } + after { Dependabot::Experiments.register(:enable_swift_xcode_spm, false) } + + context "with a single Xcode project (v2 Package.resolved)" do + let(:project_name) { "xcode_project" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "parses Xcode SPM dependencies" do + deps = parser.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + expect(dep.package_manager).to eq("swift") + end + + it "enriches dependencies with pbxproj requirements" do + dep = parser.parse.first + req = dep.requirements.first + + expect(req[:requirement]).to eq(">= 2.54.0, < 3.0.0") + expect(req[:file]).to eq("MyApp.xcodeproj/project.pbxproj") + expect(req[:metadata][:requirement_string]).to eq("from: \"2.54.0\"") + end + + it "sets correct source info" do + dep = parser.parse.first + source = dep.requirements.first[:source] + + expect(source[:type]).to eq("git") + expect(source[:url]).to eq("https://github.com/apple/swift-nio.git") + expect(source[:ref]).to eq("2.54.0") + end + end + + context "with v1 Package.resolved" do + let(:project_name) { "xcode_project_v1_resolved" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "parses v1 format dependencies" do + deps = parser.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + end + end + + context "with v3 Package.resolved" do + let(:project_name) { "xcode_project_v3_resolved" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "parses v3 format dependencies" do + deps = parser.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + end + end + + context "with multiple dependencies and requirement types" do + let(:project_name) { "xcode_project_multi_req" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "parses all dependencies" do + deps = parser.parse + expect(deps.length).to eq(4) + + names = deps.map(&:name) + expect(names).to contain_exactly( + "github.com/apple/swift-nio", + "github.com/apple/swift-collections", + "github.com/apple/swift-argument-parser", + "github.com/apple/swift-log" + ) + end + + it "applies correct requirement types from pbxproj" do + deps = parser.parse + nio = deps.find { |d| d.name == "github.com/apple/swift-nio" } + collections = deps.find { |d| d.name == "github.com/apple/swift-collections" } + parser_dep = deps.find { |d| d.name == "github.com/apple/swift-argument-parser" } + log = deps.find { |d| d.name == "github.com/apple/swift-log" } + + expect(nio.requirements.first[:requirement]).to eq(">= 2.54.0, < 3.0.0") + expect(collections.requirements.first[:requirement]).to eq(">= 1.0.0, < 1.1.0") + expect(parser_dep.requirements.first[:requirement]).to eq("= 1.2.0") + expect(log.requirements.first[:requirement]).to eq(">= 1.4.0, < 2.0.0") + end + end + + context "with multiple .xcodeproj directories" do + let(:project_name) { "xcode_project_multiple" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "AppA.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "AppA.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "AppA.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ), + Dependabot::DependencyFile.new( + name: "AppB.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "AppB.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "AppB.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "AppB.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "parses dependencies from all resolved files" do + deps = parser.parse + names = deps.map(&:name) + + expect(names).to include("github.com/apple/swift-nio") + expect(names).to include("github.com/apple/swift-collections") + end + + it "associates requirements with correct pbxproj files" do + deps = parser.parse + nio = deps.find { |d| d.name == "github.com/apple/swift-nio" } + collections = deps.find { |d| d.name == "github.com/apple/swift-collections" } + + expect(nio.requirements.first[:file]).to include("AppA.xcodeproj/project.pbxproj") + .or include("AppB.xcodeproj/project.pbxproj") + expect(collections.requirements.first[:file]).to include("AppB.xcodeproj/project.pbxproj") + end + end + + context "with revision-only pin (no version)" do + let(:project_name) { "xcode_project_revision_only" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "parses with nil version" do + dep = parser.parse.first + expect(dep.version).to be_nil + end + + it "records revision in source ref" do + dep = parser.parse.first + source = dep.requirements.first[:source] + expect(source[:ref]).to eq("6213ba7a06febe8fef60563a4a7d26a4085783cf") + end + end + + context "with no pbxproj file (only Package.resolved)" do + let(:project_name) { "xcode_project" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "parses dependencies without requirement enrichment" do + deps = parser.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + # Without pbxproj, requirement comes from Package.resolved only + expect(dep.requirements.first[:requirement]).to eq("= 2.54.0") + end + end + + context "with invalid JSON in Package.resolved" do + let(:project_name) { "xcode_project_invalid_json" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "raises DependencyFileNotParseable" do + expect { parser.parse }.to raise_error(Dependabot::DependencyFileNotParseable) + end + end + + context "with unknown schema version" do + let(:project_name) { "xcode_project_unknown_schema" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "raises DependencyFileNotParseable with schema info" do + expect { parser.parse }.to raise_error(Dependabot::DependencyFileNotParseable) do |error| + expect(error.message).to include("unsupported schema version") + end + end + end + + context "with empty pins" do + let(:project_name) { "xcode_project_empty_pins" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "returns an empty dependency list" do + expect(parser.parse).to be_empty + end + end + + context "with both Package.swift and .xcodeproj present" do + let(:project_name) { "xcode_project_with_manifest" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "Package.swift", + content: fixture("projects", project_name, "Package.swift") + ), + Dependabot::DependencyFile.new( + name: "Package.resolved", + content: fixture("projects", project_name, "Package.resolved") + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "uses classic SPM path (Package.swift takes precedence)" do + deps = parser.parse + # Classic SPM parses via swift CLI, so requirements come from Package.swift + dep = deps.find { |d| d.name == "github.com/apple/swift-nio" } + expect(dep).not_to be_nil + expect(dep.requirements.first[:file]).to eq("Package.swift") + end + end + end + + context "when enable_swift_xcode_spm experiment is disabled" do + before { Dependabot::Experiments.register(:enable_swift_xcode_spm, false) } + + context "with only Xcode files (no Package.swift)" do + let(:project_name) { "xcode_project" } + let(:files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "raises an error about missing Package.swift" do + expect { parser }.to raise_error("No Package.swift!") + end + end + end end diff --git a/swift/spec/fixtures/projects/xcode_project_empty_pins/MyApp.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_project_empty_pins/MyApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..f86e0ed8e59 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_empty_pins/MyApp.xcodeproj/project.pbxproj @@ -0,0 +1,10 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + }; + rootObject = A1B2C3D4E5F60000 /* Project object */; +} diff --git a/swift/spec/fixtures/projects/xcode_project_empty_pins/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_project_empty_pins/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..f17c4c2bc29 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_empty_pins/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,4 @@ +{ + "pins" : [], + "version" : 2 +} diff --git a/swift/spec/fixtures/projects/xcode_project_invalid_json/MyApp.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_project_invalid_json/MyApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..f86e0ed8e59 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_invalid_json/MyApp.xcodeproj/project.pbxproj @@ -0,0 +1,10 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + }; + rootObject = A1B2C3D4E5F60000 /* Project object */; +} diff --git a/swift/spec/fixtures/projects/xcode_project_invalid_json/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_project_invalid_json/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..558ad3d3bc8 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_invalid_json/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1 @@ +{ this is not valid json !!! diff --git a/swift/spec/fixtures/projects/xcode_project_multi_req/MyApp.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_project_multi_req/MyApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..61cec5122f6 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_multi_req/MyApp.xcodeproj/project.pbxproj @@ -0,0 +1,47 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin XCRemoteSwiftPackageReference section */ + A1B2C3D4E5F60001 /* XCRemoteSwiftPackageReference "swift-nio" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.54.0; + }; + }; + A1B2C3D4E5F60002 /* XCRemoteSwiftPackageReference "swift-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-collections.git"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.0.0; + }; + }; + A1B2C3D4E5F60003 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-argument-parser"; + requirement = { + kind = exactVersion; + minimumVersion = 1.2.0; + }; + }; + A1B2C3D4E5F60004 /* XCRemoteSwiftPackageReference "swift-log" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-log.git"; + requirement = { + kind = versionRange; + minimumVersion = 1.4.0; + maximumVersion = 2.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + + }; + rootObject = A1B2C3D4E5F60000 /* Project object */; +} diff --git a/swift/spec/fixtures/projects/xcode_project_multi_req/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_project_multi_req/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..06aade84383 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_multi_req/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,41 @@ +{ + "pins" : [ + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "6213ba7a06febe8fef60563a4a7d26a4085783cf", + "version" : "2.54.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "532d8b529501fb73a2166571de84ab462956c153", + "version" : "1.5.4" + } + } + ], + "version" : 2 +} diff --git a/swift/spec/fixtures/projects/xcode_project_revision_only/MyApp.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_project_revision_only/MyApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..a73b5c39f56 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_revision_only/MyApp.xcodeproj/project.pbxproj @@ -0,0 +1,22 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin XCRemoteSwiftPackageReference section */ + A1B2C3D4E5F60001 /* XCRemoteSwiftPackageReference "swift-nio" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio.git"; + requirement = { + kind = revision; + revision = 6213ba7a06febe8fef60563a4a7d26a4085783cf; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + + }; + rootObject = A1B2C3D4E5F60000 /* Project object */; +} diff --git a/swift/spec/fixtures/projects/xcode_project_revision_only/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_project_revision_only/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..5f9a09f6916 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_revision_only/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,13 @@ +{ + "pins" : [ + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "6213ba7a06febe8fef60563a4a7d26a4085783cf" + } + } + ], + "version" : 2 +} diff --git a/swift/spec/fixtures/projects/xcode_project_unknown_schema/MyApp.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_project_unknown_schema/MyApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..f86e0ed8e59 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_unknown_schema/MyApp.xcodeproj/project.pbxproj @@ -0,0 +1,10 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + }; + rootObject = A1B2C3D4E5F60000 /* Project object */; +} diff --git a/swift/spec/fixtures/projects/xcode_project_unknown_schema/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_project_unknown_schema/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..decbb5e1ea6 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_unknown_schema/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,4 @@ +{ + "pins" : [], + "version" : 99 +} diff --git a/swift/spec/fixtures/projects/xcode_project_v1_resolved/MyApp.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_project_v1_resolved/MyApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..3d547e1ee6d --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_v1_resolved/MyApp.xcodeproj/project.pbxproj @@ -0,0 +1,22 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin XCRemoteSwiftPackageReference section */ + A1B2C3D4E5F60001 /* XCRemoteSwiftPackageReference "swift-nio" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.40.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + + }; + rootObject = A1B2C3D4E5F60000 /* Project object */; +} diff --git a/swift/spec/fixtures/projects/xcode_project_v1_resolved/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_project_v1_resolved/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..81794724110 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_v1_resolved/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "6213ba7a06febe8fef60563a4a7d26a4085783cf", + "version": "2.54.0" + } + } + ] + }, + "version": 1 +} diff --git a/swift/spec/fixtures/projects/xcode_project_v3_resolved/MyApp.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_project_v3_resolved/MyApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..4f1a4b117b8 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_v3_resolved/MyApp.xcodeproj/project.pbxproj @@ -0,0 +1,22 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin XCRemoteSwiftPackageReference section */ + A1B2C3D4E5F60001 /* XCRemoteSwiftPackageReference "swift-nio" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.54.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + + }; + rootObject = A1B2C3D4E5F60000 /* Project object */; +} diff --git a/swift/spec/fixtures/projects/xcode_project_v3_resolved/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_project_v3_resolved/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..9e38d7cf143 --- /dev/null +++ b/swift/spec/fixtures/projects/xcode_project_v3_resolved/MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "abc123", + "pins" : [ + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "6213ba7a06febe8fef60563a4a7d26a4085783cf", + "version" : "2.54.0" + } + } + ], + "version" : 3 +} diff --git a/updater/Gemfile.lock b/updater/Gemfile.lock index 78da0bb0155..8d24c746a94 100644 --- a/updater/Gemfile.lock +++ b/updater/Gemfile.lock @@ -202,6 +202,7 @@ PATH specs: dependabot-swift (0.364.0) dependabot-common (= 0.364.0) + xcodeproj (~> 1.27) PATH remote: ../terraform @@ -225,11 +226,13 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.8.8) + CFPropertyList (3.0.8) + addressable (2.8.9) 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-partitions (1.1221.0) aws-sdk-codecommit (1.96.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) @@ -249,6 +252,8 @@ GEM base64 (0.3.0) bigdecimal (4.0.1) citrus (3.0.2) + claide (1.1.0) + colored2 (3.1.2) commonmarker (2.6.3) rb_sys (~> 0.9) commonmarker (2.6.3-aarch64-linux) @@ -267,12 +272,12 @@ GEM irb (~> 1.10) reline (>= 0.3.8) diff-lcs (1.6.2) - docile (1.4.0) + docile (1.4.1) docker_registry2 (1.18.2) rest-client (>= 1.8.0) domain_name (0.6.20240107) - erb (6.0.1) - excon (1.3.2) + erb (6.0.2) + excon (1.4.0) logger faraday (2.14.1) faraday-net_http (>= 2.0, < 3.5) @@ -301,33 +306,33 @@ GEM base64 httparty (~> 0.20) terminal-table (>= 1.5.1) - google-protobuf (4.33.5) + google-protobuf (4.34.0) bigdecimal - rake (>= 13) - google-protobuf (4.33.5-aarch64-linux-gnu) + rake (~> 13.3) + google-protobuf (4.34.0-aarch64-linux-gnu) bigdecimal - rake (>= 13) - google-protobuf (4.33.5-aarch64-linux-musl) + rake (~> 13.3) + google-protobuf (4.34.0-aarch64-linux-musl) bigdecimal - rake (>= 13) - google-protobuf (4.33.5-arm64-darwin) + rake (~> 13.3) + google-protobuf (4.34.0-arm64-darwin) bigdecimal - rake (>= 13) - google-protobuf (4.33.5-x86-linux-gnu) + rake (~> 13.3) + google-protobuf (4.34.0-x86-linux-gnu) bigdecimal - rake (>= 13) - google-protobuf (4.33.5-x86-linux-musl) + rake (~> 13.3) + google-protobuf (4.34.0-x86-linux-musl) bigdecimal - rake (>= 13) - google-protobuf (4.33.5-x86_64-darwin) + rake (~> 13.3) + google-protobuf (4.34.0-x86_64-darwin) bigdecimal - rake (>= 13) - google-protobuf (4.33.5-x86_64-linux-gnu) + rake (~> 13.3) + google-protobuf (4.34.0-x86_64-linux-gnu) bigdecimal - rake (>= 13) - google-protobuf (4.33.5-x86_64-linux-musl) + rake (~> 13.3) + google-protobuf (4.34.0-x86_64-linux-musl) bigdecimal - rake (>= 13) + rake (~> 13.3) googleapis-common-protos-types (1.22.0) google-protobuf (~> 4.26) gpgme (2.0.26) @@ -363,15 +368,17 @@ GEM ffi-compiler (~> 1.0) rake (~> 13.0) logger (1.7.0) - mcp (0.7.1) + mcp (0.8.0) json-schema (>= 4.1) - mime-types (3.4.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types (3.7.0) + logger + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2026.0303) mini_mime (1.1.5) 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) @@ -459,7 +466,7 @@ GEM opentelemetry-semantic_conventions (1.36.0) opentelemetry-api (~> 1.0) parallel (1.27.0) - parallel_tests (4.7.1) + parallel_tests (4.10.1) parallel parseconfig (1.1.2) parser (3.3.10.2) @@ -472,7 +479,7 @@ GEM psych (5.3.1) date stringio - public_suffix (7.0.2) + public_suffix (7.0.5) racc (1.8.1) rainbow (3.1.1) rake (13.3.1) @@ -504,7 +511,7 @@ GEM rspec-its (2.0.0) rspec-core (>= 3.13.0) rspec-expectations (>= 3.13.0) - rspec-mocks (3.13.7) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-sorbet (1.9.2) @@ -539,10 +546,10 @@ GEM sawyer (0.9.3) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - sentry-opentelemetry (6.4.0) + sentry-opentelemetry (6.4.1) opentelemetry-sdk (~> 1.0) - sentry-ruby (~> 6.4.0) - sentry-ruby (6.4.0) + sentry-ruby (~> 6.4.1) + sentry-ruby (6.4.1) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) logger @@ -550,9 +557,9 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - sorbet-runtime (0.6.12977) + sorbet-runtime (0.6.12984) stackprof (0.2.28) stringio (3.2.0) terminal-table (4.0.0) @@ -574,6 +581,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) zeitwerk (2.7.5) PLATFORMS @@ -658,10 +672,12 @@ DEPENDENCIES zeitwerk (~> 2.7) CHECKSUMS - addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057 + CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 + addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485 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-partitions (1.1221.0) sha256=f09304480191f5ff03f8994705067779bc8fe5b4731183ce45f092afb706e8eb aws-sdk-codecommit (1.96.0) sha256=4eb0cbd8a18c65856acd7ccb7fd73d9aab260da7557a2a7142b00e224c81a7d5 aws-sdk-core (3.242.0) sha256=c17b3003acc78d80c1a8437b285a1cfc5e4d7749ce7821cf3071e847535a29a0 aws-sdk-ecr (1.122.0) sha256=dc0fb76e3ddd9475e0d07a8eaa8e65db337f6f86e859d1f73496fb23ce6a0714 @@ -669,6 +685,8 @@ CHECKSUMS base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b 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) sha256=6e303356552de951bf6c2a53e3d10118f888ab3ff4d53a3dee57e12d0a7836f8 commonmarker (2.6.3-aarch64-linux) sha256=73795e80ab5ef1e4b5b83ada6f082bccb0ed7eae0b910232e13af1b2d71b14d6 commonmarker (2.6.3-arm-linux) sha256=62b9f32d7d3f85d47988a4a98a2e66e60ca42b894687047db8332f1e80caff7b @@ -714,11 +732,11 @@ CHECKSUMS dependabot-uv (0.364.0) dependabot-vcpkg (0.364.0) diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 - docile (1.4.0) sha256=5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3 + docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e docker_registry2 (1.18.2) sha256=2ace909110fbca29d69dd1cdec99f555024aa6f6577798638139c8e8e556910f domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 - erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5 - excon (1.3.2) sha256=a089babe98638e58042a7d542b2bbd183304527e33d612b6dde22fa491a544a5 + erb (6.0.2) sha256=9fe6264d44f79422c87490a1558479bd0e7dad4dd0e317656e67ea3077b5242b + excon (1.4.0) sha256=5d2bc9d2c79511a562e7fcac77cc7a40acd9cebcc55b80e537975ad8187f2924 faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe @@ -736,15 +754,15 @@ CHECKSUMS ffi-compiler (1.3.2) sha256=a94f3d81d12caf5c5d4ecf13980a70d0aeaa72268f3b9cc13358bcc6509184a0 flamegraph (0.9.5) sha256=a683020637ffa0e14a72640fa41babf14d926bfeaed87e31907cfd06ab2de8dc gitlab (6.1.0) sha256=4ae1f866a1f5ae07aa1125e86c0a656b2ad0fe1d78d5ad91b8bc3b67d66063a5 - google-protobuf (4.33.5) sha256=1b64fb774c101b23ac3f6923eca24be04fd971635d235c4cd4cfe0d752620da0 - google-protobuf (4.33.5-aarch64-linux-gnu) sha256=f70ca066e37a7ac60b4f34a836bb48ca3fc41a9371310052e484d8c9f925ff39 - google-protobuf (4.33.5-aarch64-linux-musl) sha256=d9ae90025f05db642e5603de5dbb2390cd1215bac7507fa575cc20b0db7e11a1 - google-protobuf (4.33.5-arm64-darwin) sha256=996d4e93c4232cc42f0facd821a92b4f4a926c3c9c1a768e7d768b33d9ef72f9 - google-protobuf (4.33.5-x86-linux-gnu) sha256=8d0d056743449221c723bffcf423ee8028a7028b4fca159db9692ec79fa4d185 - google-protobuf (4.33.5-x86-linux-musl) sha256=08de722ce05e619dcfa75a1a998615694e7a0c4254d4bb1557834d98d5090851 - google-protobuf (4.33.5-x86_64-darwin) sha256=173d1d6c9f0de93fd9ee25fde172d6fb6376099dca8844e19bc5782bbc7b93b0 - google-protobuf (4.33.5-x86_64-linux-gnu) sha256=a782adf86bfba207740b49d7bb9ccdc25c4fb8f800fe222af62bce951149338a - google-protobuf (4.33.5-x86_64-linux-musl) sha256=d14feec9118f44cfdc3ee4a1d1baa4e6dd77fa418967ccf22ecbe76b8c1bacbf + google-protobuf (4.34.0) sha256=bffaea30fbe2807c80667a78953b15645b3bef62b25c10ca187e4418119be531 + google-protobuf (4.34.0-aarch64-linux-gnu) sha256=0ab8a8a97976a2265d647e69b3ff1980c89184abdaf06d36091856c5ab37cc55 + google-protobuf (4.34.0-aarch64-linux-musl) sha256=0632a86df6d320eac3b335bd779499d43ad8ee6d1f8c8494b773ed5d3d5c6ab4 + google-protobuf (4.34.0-arm64-darwin) sha256=f83967a8095a9da676b79ba372c58fef2ca3878428bd40febfce65b3752c90d1 + google-protobuf (4.34.0-x86-linux-gnu) sha256=21108e5cee407cb46c38f96fa93ea1ea8e463a45c8031261df3f9131458db4e3 + google-protobuf (4.34.0-x86-linux-musl) sha256=58cb40ce43e2dc559eef112ff8d522aad489ba90f693eb95fdd946eaa6cc81e9 + google-protobuf (4.34.0-x86_64-darwin) sha256=4a5b67281993345adca54bb32947f25a289597eafaa240e5b714d0a740f99321 + google-protobuf (4.34.0-x86_64-linux-gnu) sha256=bbb333fbe79c16f35a2e2154cf29f3ce26f60390dba286b339861206d5435ef9 + google-protobuf (4.34.0-x86_64-linux-musl) sha256=0b75858a388b17e73aa4176df2e722762dbc92551b7075fdc562d33c1c6de0b0 googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7 gpgme (2.0.26) sha256=1aebfd2eb83b745341e6f416f318597568af5ad4d7d1f55bfab4f1078123abaa hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 @@ -762,12 +780,13 @@ CHECKSUMS lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 llhttp-ffi (0.5.1) sha256=9a25a7fc19311f691a78c9c0ac0fbf4675adbd0cca74310228fdf841018fa7bc logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - mcp (0.7.1) sha256=fa967895d6952bad0d981ea907731d8528d2c246d2079d56a9c8bae83d14f1c7 - mime-types (3.4.1) sha256=6bcf8b0e656b6ae9977bdc1351ef211d0383252d2f759a59ef4bcf254542fc46 - mime-types-data (3.2022.0105) sha256=d8c401ba9ea8b648b7145b90081789ec714e91fd625d82c5040079c5ea696f00 + mcp (0.8.0) sha256=ae8bd146bb8e168852866fd26f805f52744f6326afb3211e073f78a95e0c34fb + mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56 + mime-types-data (3.2026.0303) sha256=164af1de5824c5195d4b503b0a62062383b65c08671c792412450cd22d3bc224 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) sha256=598b327f36df0b172abd57b68b18979a6e14219353bca87180c31a51a00d5ad3 @@ -798,14 +817,14 @@ CHECKSUMS opentelemetry-sdk (1.10.0) sha256=43719949be8df24dcaeb86ebbf75636cda87d51a01af2729499b92a48b80521a opentelemetry-semantic_conventions (1.36.0) sha256=c1b1607dbc7853aac7f9e23f6e8b76969c45b07f2b812a4aa4383c19a3b0f617 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 - parallel_tests (4.7.1) sha256=98ad977f5e5a28df77c0364504bdea21f0a2a9ea86eae238668fdfec341ab860 + parallel_tests (4.10.1) sha256=df05458c691462b210f7a41fc2651d4e4e8a881e8190e6d1e122c92c07735d70 parseconfig (1.1.2) sha256=e52247d15070fb47f9e58f44f7888d1e7f65775274cd60f9ab4b7acd7943b291 parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 - public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c @@ -820,7 +839,7 @@ CHECKSUMS rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 rspec-its (2.0.0) sha256=a88e8bc38149f2835e93533591ec4f5c829aacbfd41269a2e6f9f5b82f5260df - rspec-mocks (3.13.7) sha256=0979034e64b1d7a838aaaddf12bf065ea4dc40ef3d4c39f01f93ae2c66c62b1c + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 rspec-sorbet (1.9.2) sha256=28bf3969fa136ed22edd05b00d23aab52c276f2fcc89fccbb16b8fd0c48931c8 rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c rubocop (1.85.0) sha256=317407feb681a07d54f64d2f9e1d6b6af1ce7678e51cd658e3ad8bd66da48c01 @@ -830,12 +849,12 @@ CHECKSUMS rubocop-sorbet (0.12.0) sha256=195521e132500555819313df5accaaf56bf721a63de3e5b7b1d0b25f696b1f39 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 sawyer (0.9.3) sha256=0d0f19298408047037638639fe62f4794483fb04320269169bd41af2bdcf5e41 - sentry-opentelemetry (6.4.0) sha256=e0f54eda8bf2b8941fcadad23a72ecc706f7350fa1991ba754790bf3738643bf - sentry-ruby (6.4.0) sha256=562bee79aea8f92825ac9df3ea01cc7788f9170d382f8d38947895280dc3be06 + sentry-opentelemetry (6.4.1) sha256=9d5066d59e59d8700ce5bbbcf7488cf324837e6029af5841665b7e7deb957cc1 + sentry-ruby (6.4.1) sha256=dac04976f791ad6ecd4fd30440c29d9b73aee08f790eeca73b439b5d67370f38 simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 - simplecov-html (0.12.3) sha256=4b1aad33259ffba8b29c6876c12db70e5750cb9df829486e4c6e5da4fa0aa07b + simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 - sorbet-runtime (0.6.12977) sha256=67e659fd940cd3ea8022548fb49256e3d87f3353ea8657c5bd26110f54478387 + sorbet-runtime (0.6.12984) sha256=3fff20a5b147a2e191210563d61886ac121fc1cd8b5e0faf6bc18873139e0fe4 stackprof (0.2.28) sha256=4ec2ace02f386012b40ca20ef80c030ad711831f59511da12e83b34efb0f9a04 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 @@ -848,6 +867,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 zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd BUNDLED WITH From 4f37d499e5a88d9bbdeb1dffa6a18564f9f12221 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Fri, 6 Mar 2026 15:21:10 +0000 Subject: [PATCH 2/8] Refactor: address code review feedback for Xcode SPM file parser - Extract shared UrlHelpers.normalize_name module to eliminate duplication across PackageResolvedParser, PbxprojParser, and DependencyParser - Remove xcodeproj gem dependency (stick with regex parsing) - Consolidate build_v1_dependency/build_v2_dependency into single build_dependency method using PIN_KEYS mapping constant - Remove unused _url parameter from PbxprojParser build_* methods - Scope pbxproj requirements by xcodeproj_dir to prevent last-writer-wins on duplicate deps across xcodeprojs - Remove unused xcodeproj_dir parameter from enrich_with_pbxproj_requirements and associated rubocop:disable - Fix extract_xcodeproj_dir regex to handle nested paths - Add extract_xcodeproj_dir unit tests - Add regex assumption comment in PbxprojParser - Update lockfiles to remove xcodeproj transitive dependencies --- Gemfile.lock | 27 -------- swift/dependabot-swift.gemspec | 1 - swift/lib/dependabot/swift/file_parser.rb | 34 ++++++---- .../swift/file_parser/dependency_parser.rb | 11 +-- .../file_parser/package_resolved_parser.rb | 67 ++++++------------- .../swift/file_parser/pbxproj_parser.rb | 55 +++++++-------- swift/lib/dependabot/swift/url_helpers.rb | 22 ++++++ .../spec/dependabot/swift/file_parser_spec.rb | 34 +++++++++- updater/Gemfile.lock | 38 +++-------- 9 files changed, 132 insertions(+), 157 deletions(-) create mode 100644 swift/lib/dependabot/swift/url_helpers.rb diff --git a/Gemfile.lock b/Gemfile.lock index 09a747f913a..51ad370ce4e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -202,7 +202,6 @@ PATH specs: dependabot-swift (0.364.0) dependabot-common (= 0.364.0) - xcodeproj (~> 1.27) PATH remote: terraform @@ -226,11 +225,9 @@ 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) @@ -253,9 +250,6 @@ 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) bigdecimal @@ -320,12 +314,9 @@ 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) - nokogiri (1.19.1-arm64-darwin) - racc (~> 1.4) nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) octokit (10.0.0) @@ -431,7 +422,6 @@ GEM sorbet (0.6.12977) sorbet-static (= 0.6.12977) sorbet-runtime (0.6.12977) - sorbet-static (0.6.12977-universal-darwin) sorbet-static (0.6.12977-x86_64-linux) sorbet-static-and-runtime (0.6.12977) sorbet (= 0.6.12977) @@ -477,13 +467,6 @@ 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 @@ -491,7 +474,6 @@ GEM zeitwerk (2.7.5) PLATFORMS - arm64-darwin-25 x86_64-linux DEPENDENCIES @@ -546,10 +528,8 @@ 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 @@ -560,9 +540,6 @@ 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 csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f @@ -630,10 +607,8 @@ 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 nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a octokit (10.0.0) sha256=82e99a539b7637b7e905e6d277bb0c1a4bed56735935cc33db6da7eae49a24e8 opentelemetry-api (1.7.0) sha256=ccfd264ea6f2db5bf4185e3c07a1297977b44a944e2ce65457c4fe63a697214f @@ -678,7 +653,6 @@ CHECKSUMS simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 sorbet (0.6.12977) sha256=bf1477d3b3f01ca728eba25573aa2d390da9a946a986c39567dbf97778d75a03 sorbet-runtime (0.6.12977) sha256=67e659fd940cd3ea8022548fb49256e3d87f3353ea8657c5bd26110f54478387 - sorbet-static (0.6.12977-universal-darwin) sha256=79e38754fcdfda662bd27d511ecf7b2f1ee256fffe3bd28b4f98b5379627cfbb sorbet-static (0.6.12977-x86_64-linux) sha256=9b2abd604d4cea5d829fb9144a70c3f2fd6be29311fad9e87b44d52b5a43134a sorbet-static-and-runtime (0.6.12977) sha256=98a46d7fe866b07a3e6dae7385550d64f1cc1a5a19f80ca066b3d17ca1c5b302 spoom (1.7.11) sha256=4e27384af6d3fde5aadc0287c51e6f76c0802259cbb3b6a67603bf718352f4cf @@ -696,7 +670,6 @@ 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 diff --git a/swift/dependabot-swift.gemspec b/swift/dependabot-swift.gemspec index 665eabf4ef1..29f02441cf3 100644 --- a/swift/dependabot-swift.gemspec +++ b/swift/dependabot-swift.gemspec @@ -28,7 +28,6 @@ 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 diff --git a/swift/lib/dependabot/swift/file_parser.rb b/swift/lib/dependabot/swift/file_parser.rb index 40f7f618da1..a0297d11a86 100644 --- a/swift/lib/dependabot/swift/file_parser.rb +++ b/swift/lib/dependabot/swift/file_parser.rb @@ -78,14 +78,15 @@ def parse_classic_spm def parse_xcode_spm dependency_set = DependencySet.new - pbxproj_requirements = aggregate_pbxproj_requirements + scoped_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) + pbxproj_requirements = scoped_requirements.fetch(xcodeproj_dir, {}) resolved_deps.each do |dep| - enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements, xcodeproj_dir) + enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements) dependency_set << enriched end end @@ -99,18 +100,23 @@ def xcode_spm_mode? xcode_resolved_files.any? end - # Collects requirement info from all project.pbxproj support files - sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) } + # Collects requirement info from all project.pbxproj support files, + # keyed by xcodeproj directory so each resolved file only sees + # requirements from its own Xcode project. + sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) } def aggregate_pbxproj_requirements - requirements = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]]) + scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) pbxproj_files.each do |pbxproj_file| + xcodeproj_dir = extract_xcodeproj_dir(pbxproj_file.name) + scoped[xcodeproj_dir] ||= {} + PbxprojParser.new(pbxproj_file).parse.each do |name, req_info| - requirements[name] = req_info + T.must(scoped[xcodeproj_dir])[name] = req_info end end - requirements + scoped end # Enriches a dependency parsed from Package.resolved with requirement @@ -118,11 +124,10 @@ def aggregate_pbxproj_requirements sig do params( dep: Dependabot::Dependency, - pbxproj_requirements: T::Hash[String, T::Hash[Symbol, T.untyped]], - xcodeproj_dir: T.nilable(String) + pbxproj_requirements: T::Hash[String, T::Hash[Symbol, T.untyped]] ).returns(Dependabot::Dependency) end - def enrich_with_pbxproj_requirements(dep, pbxproj_requirements, xcodeproj_dir) # rubocop:disable Lint/UnusedMethodArgument + def enrich_with_pbxproj_requirements(dep, pbxproj_requirements) req_info = pbxproj_requirements[dep.name] return dep unless req_info @@ -150,11 +155,12 @@ def enrich_with_pbxproj_requirements(dep, pbxproj_requirements, xcodeproj_dir) # ) end - # Extracts the .xcodeproj directory name from a Package.resolved path. + # Extracts the .xcodeproj directory name from a file 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)/}) + # e.g. "sub/dir/App.xcodeproj/project.pbxproj" -> "sub/dir/App.xcodeproj" + sig { params(path: String).returns(T.nilable(String)) } + def extract_xcodeproj_dir(path) + match = path.match(%r{^(.*?\.xcodeproj)/}) match&.captures&.first end diff --git a/swift/lib/dependabot/swift/file_parser/dependency_parser.rb b/swift/lib/dependabot/swift/file_parser/dependency_parser.rb index 3b754579916..ac446cc7429 100644 --- a/swift/lib/dependabot/swift/file_parser/dependency_parser.rb +++ b/swift/lib/dependabot/swift/file_parser/dependency_parser.rb @@ -5,8 +5,8 @@ require "dependabot/file_parsers/base" require "dependabot/shared_helpers" require "dependabot/dependency" +require "dependabot/swift/url_helpers" require "json" -require "uri" module Dependabot module Swift @@ -79,7 +79,7 @@ def subdependencies(data, level: 0) def all_dependencies(data, level: 0) identity = data["identity"] url = SharedHelpers.scp_to_standard(data["url"]) - name = normalize(url) + name = UrlHelpers.normalize_name(url) version = data["version"] source = { type: "git", url: url, ref: version, branch: nil } @@ -97,13 +97,6 @@ def all_dependencies(data, level: 0) [dep, *subdependencies(data, level: level + 1)].compact end - sig { params(source: String).returns(String) } - def normalize(source) - uri = URI.parse(source.downcase) - - "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git") - end - sig { returns(T::Array[Dependabot::DependencyFile]) } attr_reader :dependency_files diff --git a/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb b/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb index 577cf17a343..0d26ca687b6 100644 --- a/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb +++ b/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb @@ -2,12 +2,12 @@ # frozen_string_literal: true require "json" -require "uri" require "sorbet-runtime" require "dependabot/dependency" require "dependabot/errors" require "dependabot/shared_helpers" require "dependabot/swift/file_parser" +require "dependabot/swift/url_helpers" module Dependabot module Swift @@ -17,6 +17,16 @@ class PackageResolvedParser SUPPORTED_VERSIONS = T.let([1, 2, 3].freeze, T::Array[Integer]) + # Maps schema version to the JSON keys used for each pin field + PIN_KEYS = T.let( + { + 1 => { url: "repositoryURL", identity: "package", state: "state" }, + 2 => { url: "location", identity: "identity", state: "state" }, + 3 => { url: "location", identity: "identity", state: "state" } + }.freeze, + T::Hash[Integer, T::Hash[Symbol, String]] + ) + sig { params(resolved_file: Dependabot::DependencyFile).void } def initialize(resolved_file) @resolved_file = resolved_file @@ -93,50 +103,21 @@ def extract_pins(parsed, schema_version) ).returns(T.nilable(Dependabot::Dependency)) end def build_dependency(pin, schema_version) - if schema_version == 1 - build_v1_dependency(pin) - else - build_v2_dependency(pin) - end - end - - sig { params(pin: T::Hash[String, T.untyped]).returns(T.nilable(Dependabot::Dependency)) } - def build_v1_dependency(pin) - url = pin["repositoryURL"] + keys = T.must(PIN_KEYS[schema_version]) + url = pin[keys[:url]] return nil unless url.is_a?(String) && !url.empty? - state = pin["state"] || {} - version = state["version"] - revision = state["revision"] - branch = state["branch"] - identity = pin["package"]&.downcase + state = pin[keys[:state]] || {} + identity = pin[keys[:identity]] + # v1 uses a display name for "package"; normalize to lowercase like v2/v3 "identity" + identity = identity&.downcase if schema_version == 1 build_dependency_object( identity: identity, url: url, - version: version, - revision: revision, - branch: branch - ) - end - - sig { params(pin: T::Hash[String, T.untyped]).returns(T.nilable(Dependabot::Dependency)) } - def build_v2_dependency(pin) - url = pin["location"] - return nil unless url.is_a?(String) && !url.empty? - - state = pin["state"] || {} - version = state["version"] - revision = state["revision"] - branch = state["branch"] - identity = pin["identity"] - - build_dependency_object( - identity: identity, - url: url, - version: version, - revision: revision, - branch: branch + version: state["version"], + revision: state["revision"], + branch: state["branch"] ) end @@ -151,7 +132,7 @@ def build_v2_dependency(pin) end def build_dependency_object(identity:, url:, version:, revision:, branch:) normalized_url = SharedHelpers.scp_to_standard(url) - name = normalize_name(normalized_url) + name = UrlHelpers.normalize_name(normalized_url) ref = version || revision source = { type: "git", url: normalized_url, ref: ref, branch: branch } @@ -169,12 +150,6 @@ def build_dependency_object(identity:, url:, version:, revision:, branch:) metadata: { identity: identity } ) end - - sig { params(source: String).returns(String) } - def normalize_name(source) - uri = URI.parse(source.downcase) - "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git") - end end end end diff --git a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb index e7cbcd81598..3da6cc6209d 100644 --- a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb +++ b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb @@ -6,6 +6,7 @@ require "dependabot/shared_helpers" require "dependabot/swift/file_parser" require "dependabot/swift/native_requirement" +require "dependabot/swift/url_helpers" module Dependabot module Swift @@ -19,7 +20,9 @@ class FileParser < Dependabot::FileParsers::Base class PbxprojParser extend T::Sig - # Regex to extract XCRemoteSwiftPackageReference blocks from pbxproj + # Regex to extract XCRemoteSwiftPackageReference blocks from pbxproj. + # Uses [^}]* to match the requirement block content — this is safe because + # Xcode requirement blocks are always flat dictionaries with no nested braces. PACKAGE_REF_BLOCK = T.let( / isa\s*=\s*XCRemoteSwiftPackageReference;\s* @@ -56,9 +59,9 @@ def parse url = T.cast(url, String) requirement_block = T.cast(requirement_block, String) normalized_url = SharedHelpers.scp_to_standard(url) - name = normalize_name(normalized_url) + name = UrlHelpers.normalize_name(normalized_url) - req_info = parse_requirement_block(requirement_block, url) + req_info = parse_requirement_block(requirement_block) next unless req_info requirements[name] = req_info.merge( @@ -76,31 +79,31 @@ def parse attr_reader :pbxproj_file sig do - params(block: String, url: String) + params(block: String) .returns(T.nilable(T::Hash[Symbol, T.untyped])) end - def parse_requirement_block(block, url) + def parse_requirement_block(block) kind = block.match(KIND_PATTERN)&.captures&.first return nil unless kind case kind when "upToNextMajorVersion" - build_up_to_next_major(block, url) + build_up_to_next_major(block) when "upToNextMinorVersion" - build_up_to_next_minor(block, url) + build_up_to_next_minor(block) when "exactVersion" - build_exact(block, url) + build_exact(block) when "versionRange" - build_range(block, url) + build_range(block) when "branch" - build_branch(block, url) + build_branch(block) when "revision" - build_revision(block, url) + build_revision(block) end end - sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } - def build_up_to_next_major(block, _url) + sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) } + def build_up_to_next_major(block) min_version = extract_version(block, MIN_VERSION_PATTERN) requirement_string = "from: \"#{min_version}\"" native_req = NativeRequirement.new(requirement_string) @@ -112,8 +115,8 @@ def build_up_to_next_major(block, _url) } end - sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } - def build_up_to_next_minor(block, _url) + sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) } + def build_up_to_next_minor(block) min_version = extract_version(block, MIN_VERSION_PATTERN) requirement_string = ".upToNextMinor(from: \"#{min_version}\")" native_req = NativeRequirement.new(requirement_string) @@ -125,8 +128,8 @@ def build_up_to_next_minor(block, _url) } end - sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } - def build_exact(block, _url) + sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) } + def build_exact(block) version = extract_version(block, MIN_VERSION_PATTERN) || extract_version(block, VERSION_PATTERN) requirement_string = "exact: \"#{version}\"" native_req = NativeRequirement.new(requirement_string) @@ -138,8 +141,8 @@ def build_exact(block, _url) } end - sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } - def build_range(block, _url) + sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) } + def build_range(block) min_version = extract_version(block, MIN_VERSION_PATTERN) max_version = extract_version(block, MAX_VERSION_PATTERN) requirement_string = "\"#{min_version}\"..<\"#{max_version}\"" @@ -152,8 +155,8 @@ def build_range(block, _url) } end - sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } - def build_branch(block, _url) + sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) } + def build_branch(block) branch = block.match(BRANCH_PATTERN)&.captures&.first { @@ -164,8 +167,8 @@ def build_branch(block, _url) } end - sig { params(block: String, _url: String).returns(T::Hash[Symbol, T.untyped]) } - def build_revision(block, _url) + sig { params(block: String).returns(T::Hash[Symbol, T.untyped]) } + def build_revision(block) revision = block.match(REVISION_PATTERN)&.captures&.first { @@ -180,12 +183,6 @@ def build_revision(block, _url) def extract_version(block, pattern) block.match(pattern)&.captures&.first end - - sig { params(source: String).returns(String) } - def normalize_name(source) - uri = URI.parse(source.downcase) - "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git") - end end end end diff --git a/swift/lib/dependabot/swift/url_helpers.rb b/swift/lib/dependabot/swift/url_helpers.rb new file mode 100644 index 00000000000..348e775ab51 --- /dev/null +++ b/swift/lib/dependabot/swift/url_helpers.rb @@ -0,0 +1,22 @@ +# typed: strict +# frozen_string_literal: true + +require "uri" +require "sorbet-runtime" + +module Dependabot + module Swift + # Shared URL normalization utilities used by multiple parsers. + # Produces a canonical dependency name from a git repository URL + # by stripping the scheme, "www." prefix, and ".git" suffix. + module UrlHelpers + extend T::Sig + + sig { params(source: String).returns(String) } + def self.normalize_name(source) + uri = URI.parse(source.downcase) + "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git") + end + end + end +end diff --git a/swift/spec/dependabot/swift/file_parser_spec.rb b/swift/spec/dependabot/swift/file_parser_spec.rb index 38bc3949319..8b3cf153705 100644 --- a/swift/spec/dependabot/swift/file_parser_spec.rb +++ b/swift/spec/dependabot/swift/file_parser_spec.rb @@ -575,9 +575,9 @@ nio = deps.find { |d| d.name == "github.com/apple/swift-nio" } collections = deps.find { |d| d.name == "github.com/apple/swift-collections" } - expect(nio.requirements.first[:file]).to include("AppA.xcodeproj/project.pbxproj") - .or include("AppB.xcodeproj/project.pbxproj") - expect(collections.requirements.first[:file]).to include("AppB.xcodeproj/project.pbxproj") + # With scoped requirements, nio comes from AppA and collections from AppB + expect(nio.requirements.first[:file]).to eq("AppA.xcodeproj/project.pbxproj") + expect(collections.requirements.first[:file]).to eq("AppB.xcodeproj/project.pbxproj") end end @@ -818,4 +818,32 @@ end end end + + describe "#extract_xcodeproj_dir (private)" do + let(:project_name) { "ReactiveCocoa" } + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + it "extracts xcodeproj dir from a resolved file path" do + result = parser.send( + :extract_xcodeproj_dir, + "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" + ) + expect(result).to eq("MyApp.xcodeproj") + end + + it "extracts xcodeproj dir from a pbxproj path" do + result = parser.send(:extract_xcodeproj_dir, "MyApp.xcodeproj/project.pbxproj") + expect(result).to eq("MyApp.xcodeproj") + end + + it "handles nested directory paths" do + result = parser.send(:extract_xcodeproj_dir, "sub/dir/App.xcodeproj/project.pbxproj") + expect(result).to eq("sub/dir/App.xcodeproj") + end + + it "returns nil for paths without xcodeproj" do + result = parser.send(:extract_xcodeproj_dir, "Package.resolved") + expect(result).to be_nil + end + end end diff --git a/updater/Gemfile.lock b/updater/Gemfile.lock index 8d24c746a94..1b57de5fa82 100644 --- a/updater/Gemfile.lock +++ b/updater/Gemfile.lock @@ -226,17 +226,15 @@ PATH GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.8) addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) - atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1221.0) + aws-partitions (1.1222.0) aws-sdk-codecommit (1.96.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-core (3.242.0) + aws-sdk-core (3.243.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -252,8 +250,6 @@ GEM base64 (0.3.0) bigdecimal (4.0.1) citrus (3.0.2) - claide (1.1.0) - colored2 (3.1.2) commonmarker (2.6.3) rb_sys (~> 0.9) commonmarker (2.6.3-aarch64-linux) @@ -359,7 +355,7 @@ GEM reline (>= 0.4.2) jmespath (1.6.2) json (2.18.1) - json-schema (6.1.0) + json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) language_server-protocol (3.17.0.5) @@ -378,7 +374,6 @@ 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) @@ -517,7 +512,7 @@ GEM rspec-sorbet (1.9.2) sorbet-runtime rspec-support (3.13.7) - rubocop (1.85.0) + rubocop (1.85.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -559,7 +554,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - sorbet-runtime (0.6.12984) + sorbet-runtime (0.6.12993) stackprof (0.2.28) stringio (3.2.0) terminal-table (4.0.0) @@ -581,13 +576,6 @@ 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) zeitwerk (2.7.5) PLATFORMS @@ -672,21 +660,17 @@ DEPENDENCIES zeitwerk (~> 2.7) CHECKSUMS - CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485 ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 - atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b - aws-partitions (1.1221.0) sha256=f09304480191f5ff03f8994705067779bc8fe5b4731183ce45f092afb706e8eb + aws-partitions (1.1222.0) sha256=e86b1c65f5cedff52586f9f2b288448d5a069376cbfe1a8abdc29f5e06411bd5 aws-sdk-codecommit (1.96.0) sha256=4eb0cbd8a18c65856acd7ccb7fd73d9aab260da7557a2a7142b00e224c81a7d5 - aws-sdk-core (3.242.0) sha256=c17b3003acc78d80c1a8437b285a1cfc5e4d7749ce7821cf3071e847535a29a0 + aws-sdk-core (3.243.0) sha256=a014eef785124b71d28325783fa422a1512f8421ec9b6e3931c8b0ca3fbb0f1c aws-sdk-ecr (1.122.0) sha256=dc0fb76e3ddd9475e0d07a8eaa8e65db337f6f86e859d1f73496fb23ce6a0714 aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b 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) sha256=6e303356552de951bf6c2a53e3d10118f888ab3ff4d53a3dee57e12d0a7836f8 commonmarker (2.6.3-aarch64-linux) sha256=73795e80ab5ef1e4b5b83ada6f082bccb0ed7eae0b910232e13af1b2d71b14d6 commonmarker (2.6.3-arm-linux) sha256=62b9f32d7d3f85d47988a4a98a2e66e60ca42b894687047db8332f1e80caff7b @@ -775,7 +759,7 @@ CHECKSUMS irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986 - json-schema (6.1.0) sha256=6bf70a2cfb6dfd5a06da28093fa8190f324c88eabd36a7f47097f227321dc702 + json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 llhttp-ffi (0.5.1) sha256=9a25a7fc19311f691a78c9c0ac0fbf4675adbd0cca74310228fdf841018fa7bc @@ -786,7 +770,6 @@ 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) sha256=598b327f36df0b172abd57b68b18979a6e14219353bca87180c31a51a00d5ad3 @@ -842,7 +825,7 @@ CHECKSUMS rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 rspec-sorbet (1.9.2) sha256=28bf3969fa136ed22edd05b00d23aab52c276f2fcc89fccbb16b8fd0c48931c8 rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c - rubocop (1.85.0) sha256=317407feb681a07d54f64d2f9e1d6b6af1ce7678e51cd658e3ad8bd66da48c01 + rubocop (1.85.1) sha256=3dbcf9e961baa4c376eeeb2a03913dca5e3987033b04d38fa538aa1e7406cc77 rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2 @@ -854,7 +837,7 @@ CHECKSUMS simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 - sorbet-runtime (0.6.12984) sha256=3fff20a5b147a2e191210563d61886ac121fc1cd8b5e0faf6bc18873139e0fe4 + sorbet-runtime (0.6.12993) sha256=5720d6e70063ed39528ddb18248c13a8072cf6991cf7d6652dcc0b8e9bc6b4ac stackprof (0.2.28) sha256=4ec2ace02f386012b40ca20ef80c030ad711831f59511da12e83b34efb0f9a04 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 @@ -867,7 +850,6 @@ 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 zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd BUNDLED WITH From 57e8fdbc4433f53b08c4d71f217f234ac911883d Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Fri, 6 Mar 2026 16:11:18 +0000 Subject: [PATCH 3/8] Address remaining review feedback for Xcode SPM file parser - Add nil-content guard in PackageResolvedParser#parse_json with clear DependencyFileNotParseable error instead of Sorbet T.must error - Add error handling for NativeRequirement.new in PbxprojParser via parse_native_requirement helper that rescues malformed version strings - Fix Sorbet type errors: use T.must for PIN_KEYS hash lookups - Fix updater/Gemfile.lock: remove stale xcodeproj dependency reference - Fix RuboCop Style/MultilineIfModifier offense --- .../file_parser/package_resolved_parser.rb | 16 +++++++++--- .../swift/file_parser/pbxproj_parser.rb | 26 +++++++++++++------ updater/Gemfile.lock | 1 - 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb b/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb index 0d26ca687b6..5117891d727 100644 --- a/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb +++ b/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb @@ -48,7 +48,15 @@ def parse sig { returns(T::Hash[String, T.untyped]) } def parse_json - JSON.parse(T.must(resolved_file.content)) + content = resolved_file.content + unless content + raise Dependabot::DependencyFileNotParseable.new( + resolved_file.name, + "#{resolved_file.name} has no content" + ) + end + + JSON.parse(content) rescue JSON::ParserError => e raise Dependabot::DependencyFileNotParseable.new( resolved_file.name, @@ -104,11 +112,11 @@ def extract_pins(parsed, schema_version) end def build_dependency(pin, schema_version) keys = T.must(PIN_KEYS[schema_version]) - url = pin[keys[:url]] + url = pin[T.must(keys[:url])] return nil unless url.is_a?(String) && !url.empty? - state = pin[keys[:state]] || {} - identity = pin[keys[:identity]] + state = pin[T.must(keys[:state])] || {} + identity = pin[T.must(keys[:identity])] # v1 uses a display name for "package"; normalize to lowercase like v2/v3 "identity" identity = identity&.downcase if schema_version == 1 diff --git a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb index 3da6cc6209d..389deed0032 100644 --- a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb +++ b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb @@ -106,10 +106,10 @@ def parse_requirement_block(block) def build_up_to_next_major(block) min_version = extract_version(block, MIN_VERSION_PATTERN) requirement_string = "from: \"#{min_version}\"" - native_req = NativeRequirement.new(requirement_string) + requirement = parse_native_requirement(requirement_string) { - requirement: native_req.to_s, + requirement: requirement, requirement_string: requirement_string, kind: "upToNextMajorVersion" } @@ -119,10 +119,10 @@ def build_up_to_next_major(block) def build_up_to_next_minor(block) min_version = extract_version(block, MIN_VERSION_PATTERN) requirement_string = ".upToNextMinor(from: \"#{min_version}\")" - native_req = NativeRequirement.new(requirement_string) + requirement = parse_native_requirement(requirement_string) { - requirement: native_req.to_s, + requirement: requirement, requirement_string: requirement_string, kind: "upToNextMinorVersion" } @@ -132,10 +132,10 @@ def build_up_to_next_minor(block) def build_exact(block) version = extract_version(block, MIN_VERSION_PATTERN) || extract_version(block, VERSION_PATTERN) requirement_string = "exact: \"#{version}\"" - native_req = NativeRequirement.new(requirement_string) + requirement = parse_native_requirement(requirement_string) { - requirement: native_req.to_s, + requirement: requirement, requirement_string: requirement_string, kind: "exactVersion" } @@ -146,10 +146,10 @@ def build_range(block) min_version = extract_version(block, MIN_VERSION_PATTERN) max_version = extract_version(block, MAX_VERSION_PATTERN) requirement_string = "\"#{min_version}\"..<\"#{max_version}\"" - native_req = NativeRequirement.new(requirement_string) + requirement = parse_native_requirement(requirement_string) { - requirement: native_req.to_s, + requirement: requirement, requirement_string: requirement_string, kind: "versionRange" } @@ -183,6 +183,16 @@ def build_revision(block) def extract_version(block, pattern) block.match(pattern)&.captures&.first end + + # Parses a requirement string into a Dependabot requirement via + # NativeRequirement. Returns nil if the string is malformed rather + # than raising, so a single bad entry doesn't stop parsing. + sig { params(requirement_string: String).returns(T.nilable(String)) } + def parse_native_requirement(requirement_string) + NativeRequirement.new(requirement_string).to_s + rescue StandardError + nil + end end end end diff --git a/updater/Gemfile.lock b/updater/Gemfile.lock index 1b57de5fa82..b92dab11b9c 100644 --- a/updater/Gemfile.lock +++ b/updater/Gemfile.lock @@ -202,7 +202,6 @@ PATH specs: dependabot-swift (0.364.0) dependabot-common (= 0.364.0) - xcodeproj (~> 1.27) PATH remote: ../terraform From 3e40a20818e81796e270939e99adf5373a92cb1d Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Fri, 6 Mar 2026 16:45:47 +0000 Subject: [PATCH 4/8] Revert lockfiles --- Gemfile.lock | 8 +++ updater/Gemfile.lock | 131 +++++++++++++++++++++---------------------- 2 files changed, 73 insertions(+), 66 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 51ad370ce4e..271a6b876d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -250,6 +250,7 @@ GEM benchmark (0.5.0) bigdecimal (4.0.1) citrus (3.0.2) + commonmarker (2.6.3-arm64-darwin) commonmarker (2.6.3-x86_64-linux) crack (1.0.1) bigdecimal @@ -317,6 +318,8 @@ GEM net-http (0.9.1) uri (>= 0.11.1) netrc (0.11.0) + nokogiri (1.19.1-arm64-darwin) + racc (~> 1.4) nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) octokit (10.0.0) @@ -422,6 +425,7 @@ GEM sorbet (0.6.12977) sorbet-static (= 0.6.12977) sorbet-runtime (0.6.12977) + sorbet-static (0.6.12977-universal-darwin) sorbet-static (0.6.12977-x86_64-linux) sorbet-static-and-runtime (0.6.12977) sorbet (= 0.6.12977) @@ -474,6 +478,7 @@ GEM zeitwerk (2.7.5) PLATFORMS + arm64-darwin-25 x86_64-linux DEPENDENCIES @@ -540,6 +545,7 @@ CHECKSUMS benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7 citrus (3.0.2) sha256=4ec2412fc389ad186735f4baee1460f7900a8e130ffe3f216b30d4f9c684f650 + commonmarker (2.6.3-arm64-darwin) sha256=d6c1e4955619da3f68fed22de99dec49a24925611770c039bf870823846c8b21 commonmarker (2.6.3-x86_64-linux) sha256=e861ba1812721113725ebd8e46e4fee20dc732842f5555db2cfb8dcd74056583 crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f @@ -609,6 +615,7 @@ CHECKSUMS multi_xml (0.8.1) sha256=addba0290bac34e9088bfe73dc4878530297a82a7bbd66cb44dcd0a4b86edf5a net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f + nokogiri (1.19.1-arm64-darwin) sha256=dfe2d337e6700eac47290407c289d56bcf85805d128c1b5a6434ddb79731cb9e nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a octokit (10.0.0) sha256=82e99a539b7637b7e905e6d277bb0c1a4bed56735935cc33db6da7eae49a24e8 opentelemetry-api (1.7.0) sha256=ccfd264ea6f2db5bf4185e3c07a1297977b44a944e2ce65457c4fe63a697214f @@ -653,6 +660,7 @@ CHECKSUMS simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 sorbet (0.6.12977) sha256=bf1477d3b3f01ca728eba25573aa2d390da9a946a986c39567dbf97778d75a03 sorbet-runtime (0.6.12977) sha256=67e659fd940cd3ea8022548fb49256e3d87f3353ea8657c5bd26110f54478387 + sorbet-static (0.6.12977-universal-darwin) sha256=79e38754fcdfda662bd27d511ecf7b2f1ee256fffe3bd28b4f98b5379627cfbb sorbet-static (0.6.12977-x86_64-linux) sha256=9b2abd604d4cea5d829fb9144a70c3f2fd6be29311fad9e87b44d52b5a43134a sorbet-static-and-runtime (0.6.12977) sha256=98a46d7fe866b07a3e6dae7385550d64f1cc1a5a19f80ca066b3d17ca1c5b302 spoom (1.7.11) sha256=4e27384af6d3fde5aadc0287c51e6f76c0802259cbb3b6a67603bf718352f4cf diff --git a/updater/Gemfile.lock b/updater/Gemfile.lock index b92dab11b9c..78da0bb0155 100644 --- a/updater/Gemfile.lock +++ b/updater/Gemfile.lock @@ -225,15 +225,15 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.8.9) + addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1222.0) + aws-partitions (1.1220.0) aws-sdk-codecommit (1.96.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-core (3.243.0) + aws-sdk-core (3.242.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -267,12 +267,12 @@ GEM irb (~> 1.10) reline (>= 0.3.8) diff-lcs (1.6.2) - docile (1.4.1) + docile (1.4.0) docker_registry2 (1.18.2) rest-client (>= 1.8.0) domain_name (0.6.20240107) - erb (6.0.2) - excon (1.4.0) + erb (6.0.1) + excon (1.3.2) logger faraday (2.14.1) faraday-net_http (>= 2.0, < 3.5) @@ -301,33 +301,33 @@ GEM base64 httparty (~> 0.20) terminal-table (>= 1.5.1) - google-protobuf (4.34.0) + google-protobuf (4.33.5) bigdecimal - rake (~> 13.3) - google-protobuf (4.34.0-aarch64-linux-gnu) + rake (>= 13) + google-protobuf (4.33.5-aarch64-linux-gnu) bigdecimal - rake (~> 13.3) - google-protobuf (4.34.0-aarch64-linux-musl) + rake (>= 13) + google-protobuf (4.33.5-aarch64-linux-musl) bigdecimal - rake (~> 13.3) - google-protobuf (4.34.0-arm64-darwin) + rake (>= 13) + google-protobuf (4.33.5-arm64-darwin) bigdecimal - rake (~> 13.3) - google-protobuf (4.34.0-x86-linux-gnu) + rake (>= 13) + google-protobuf (4.33.5-x86-linux-gnu) bigdecimal - rake (~> 13.3) - google-protobuf (4.34.0-x86-linux-musl) + rake (>= 13) + google-protobuf (4.33.5-x86-linux-musl) bigdecimal - rake (~> 13.3) - google-protobuf (4.34.0-x86_64-darwin) + rake (>= 13) + google-protobuf (4.33.5-x86_64-darwin) bigdecimal - rake (~> 13.3) - google-protobuf (4.34.0-x86_64-linux-gnu) + rake (>= 13) + google-protobuf (4.33.5-x86_64-linux-gnu) bigdecimal - rake (~> 13.3) - google-protobuf (4.34.0-x86_64-linux-musl) + rake (>= 13) + google-protobuf (4.33.5-x86_64-linux-musl) bigdecimal - rake (~> 13.3) + rake (>= 13) googleapis-common-protos-types (1.22.0) google-protobuf (~> 4.26) gpgme (2.0.26) @@ -354,7 +354,7 @@ GEM reline (>= 0.4.2) jmespath (1.6.2) json (2.18.1) - json-schema (6.2.0) + json-schema (6.1.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) language_server-protocol (3.17.0.5) @@ -363,12 +363,11 @@ GEM ffi-compiler (~> 1.0) rake (~> 13.0) logger (1.7.0) - mcp (0.8.0) + mcp (0.7.1) json-schema (>= 4.1) - mime-types (3.7.0) - logger - mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2026.0303) + mime-types (3.4.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2022.0105) mini_mime (1.1.5) mini_portile2 (2.8.9) multi_xml (0.8.1) @@ -460,7 +459,7 @@ GEM opentelemetry-semantic_conventions (1.36.0) opentelemetry-api (~> 1.0) parallel (1.27.0) - parallel_tests (4.10.1) + parallel_tests (4.7.1) parallel parseconfig (1.1.2) parser (3.3.10.2) @@ -473,7 +472,7 @@ GEM psych (5.3.1) date stringio - public_suffix (7.0.5) + public_suffix (7.0.2) racc (1.8.1) rainbow (3.1.1) rake (13.3.1) @@ -505,13 +504,13 @@ GEM rspec-its (2.0.0) rspec-core (>= 3.13.0) rspec-expectations (>= 3.13.0) - rspec-mocks (3.13.8) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-sorbet (1.9.2) sorbet-runtime rspec-support (3.13.7) - rubocop (1.85.1) + rubocop (1.85.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -540,10 +539,10 @@ GEM sawyer (0.9.3) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - sentry-opentelemetry (6.4.1) + sentry-opentelemetry (6.4.0) opentelemetry-sdk (~> 1.0) - sentry-ruby (~> 6.4.1) - sentry-ruby (6.4.1) + sentry-ruby (~> 6.4.0) + sentry-ruby (6.4.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) logger @@ -551,9 +550,9 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.2) + simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sorbet-runtime (0.6.12993) + sorbet-runtime (0.6.12977) stackprof (0.2.28) stringio (3.2.0) terminal-table (4.0.0) @@ -659,12 +658,12 @@ DEPENDENCIES zeitwerk (~> 2.7) CHECKSUMS - addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485 + addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057 ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b - aws-partitions (1.1222.0) sha256=e86b1c65f5cedff52586f9f2b288448d5a069376cbfe1a8abdc29f5e06411bd5 + aws-partitions (1.1220.0) sha256=1567da9ae45cba28e1d31f5e996928b2eb92ad01700000846d6d90043be8670f aws-sdk-codecommit (1.96.0) sha256=4eb0cbd8a18c65856acd7ccb7fd73d9aab260da7557a2a7142b00e224c81a7d5 - aws-sdk-core (3.243.0) sha256=a014eef785124b71d28325783fa422a1512f8421ec9b6e3931c8b0ca3fbb0f1c + aws-sdk-core (3.242.0) sha256=c17b3003acc78d80c1a8437b285a1cfc5e4d7749ce7821cf3071e847535a29a0 aws-sdk-ecr (1.122.0) sha256=dc0fb76e3ddd9475e0d07a8eaa8e65db337f6f86e859d1f73496fb23ce6a0714 aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b @@ -715,11 +714,11 @@ CHECKSUMS dependabot-uv (0.364.0) dependabot-vcpkg (0.364.0) diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 - docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e + docile (1.4.0) sha256=5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3 docker_registry2 (1.18.2) sha256=2ace909110fbca29d69dd1cdec99f555024aa6f6577798638139c8e8e556910f domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 - erb (6.0.2) sha256=9fe6264d44f79422c87490a1558479bd0e7dad4dd0e317656e67ea3077b5242b - excon (1.4.0) sha256=5d2bc9d2c79511a562e7fcac77cc7a40acd9cebcc55b80e537975ad8187f2924 + erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5 + excon (1.3.2) sha256=a089babe98638e58042a7d542b2bbd183304527e33d612b6dde22fa491a544a5 faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe @@ -737,15 +736,15 @@ CHECKSUMS ffi-compiler (1.3.2) sha256=a94f3d81d12caf5c5d4ecf13980a70d0aeaa72268f3b9cc13358bcc6509184a0 flamegraph (0.9.5) sha256=a683020637ffa0e14a72640fa41babf14d926bfeaed87e31907cfd06ab2de8dc gitlab (6.1.0) sha256=4ae1f866a1f5ae07aa1125e86c0a656b2ad0fe1d78d5ad91b8bc3b67d66063a5 - google-protobuf (4.34.0) sha256=bffaea30fbe2807c80667a78953b15645b3bef62b25c10ca187e4418119be531 - google-protobuf (4.34.0-aarch64-linux-gnu) sha256=0ab8a8a97976a2265d647e69b3ff1980c89184abdaf06d36091856c5ab37cc55 - google-protobuf (4.34.0-aarch64-linux-musl) sha256=0632a86df6d320eac3b335bd779499d43ad8ee6d1f8c8494b773ed5d3d5c6ab4 - google-protobuf (4.34.0-arm64-darwin) sha256=f83967a8095a9da676b79ba372c58fef2ca3878428bd40febfce65b3752c90d1 - google-protobuf (4.34.0-x86-linux-gnu) sha256=21108e5cee407cb46c38f96fa93ea1ea8e463a45c8031261df3f9131458db4e3 - google-protobuf (4.34.0-x86-linux-musl) sha256=58cb40ce43e2dc559eef112ff8d522aad489ba90f693eb95fdd946eaa6cc81e9 - google-protobuf (4.34.0-x86_64-darwin) sha256=4a5b67281993345adca54bb32947f25a289597eafaa240e5b714d0a740f99321 - google-protobuf (4.34.0-x86_64-linux-gnu) sha256=bbb333fbe79c16f35a2e2154cf29f3ce26f60390dba286b339861206d5435ef9 - google-protobuf (4.34.0-x86_64-linux-musl) sha256=0b75858a388b17e73aa4176df2e722762dbc92551b7075fdc562d33c1c6de0b0 + google-protobuf (4.33.5) sha256=1b64fb774c101b23ac3f6923eca24be04fd971635d235c4cd4cfe0d752620da0 + google-protobuf (4.33.5-aarch64-linux-gnu) sha256=f70ca066e37a7ac60b4f34a836bb48ca3fc41a9371310052e484d8c9f925ff39 + google-protobuf (4.33.5-aarch64-linux-musl) sha256=d9ae90025f05db642e5603de5dbb2390cd1215bac7507fa575cc20b0db7e11a1 + google-protobuf (4.33.5-arm64-darwin) sha256=996d4e93c4232cc42f0facd821a92b4f4a926c3c9c1a768e7d768b33d9ef72f9 + google-protobuf (4.33.5-x86-linux-gnu) sha256=8d0d056743449221c723bffcf423ee8028a7028b4fca159db9692ec79fa4d185 + google-protobuf (4.33.5-x86-linux-musl) sha256=08de722ce05e619dcfa75a1a998615694e7a0c4254d4bb1557834d98d5090851 + google-protobuf (4.33.5-x86_64-darwin) sha256=173d1d6c9f0de93fd9ee25fde172d6fb6376099dca8844e19bc5782bbc7b93b0 + google-protobuf (4.33.5-x86_64-linux-gnu) sha256=a782adf86bfba207740b49d7bb9ccdc25c4fb8f800fe222af62bce951149338a + google-protobuf (4.33.5-x86_64-linux-musl) sha256=d14feec9118f44cfdc3ee4a1d1baa4e6dd77fa418967ccf22ecbe76b8c1bacbf googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7 gpgme (2.0.26) sha256=1aebfd2eb83b745341e6f416f318597568af5ad4d7d1f55bfab4f1078123abaa hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 @@ -758,14 +757,14 @@ CHECKSUMS irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986 - json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666 + json-schema (6.1.0) sha256=6bf70a2cfb6dfd5a06da28093fa8190f324c88eabd36a7f47097f227321dc702 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 llhttp-ffi (0.5.1) sha256=9a25a7fc19311f691a78c9c0ac0fbf4675adbd0cca74310228fdf841018fa7bc logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - mcp (0.8.0) sha256=ae8bd146bb8e168852866fd26f805f52744f6326afb3211e073f78a95e0c34fb - mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56 - mime-types-data (3.2026.0303) sha256=164af1de5824c5195d4b503b0a62062383b65c08671c792412450cd22d3bc224 + mcp (0.7.1) sha256=fa967895d6952bad0d981ea907731d8528d2c246d2079d56a9c8bae83d14f1c7 + mime-types (3.4.1) sha256=6bcf8b0e656b6ae9977bdc1351ef211d0383252d2f759a59ef4bcf254542fc46 + mime-types-data (3.2022.0105) sha256=d8c401ba9ea8b648b7145b90081789ec714e91fd625d82c5040079c5ea696f00 mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289 multi_xml (0.8.1) sha256=addba0290bac34e9088bfe73dc4878530297a82a7bbd66cb44dcd0a4b86edf5a @@ -799,14 +798,14 @@ CHECKSUMS opentelemetry-sdk (1.10.0) sha256=43719949be8df24dcaeb86ebbf75636cda87d51a01af2729499b92a48b80521a opentelemetry-semantic_conventions (1.36.0) sha256=c1b1607dbc7853aac7f9e23f6e8b76969c45b07f2b812a4aa4383c19a3b0f617 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 - parallel_tests (4.10.1) sha256=df05458c691462b210f7a41fc2651d4e4e8a881e8190e6d1e122c92c07735d70 + parallel_tests (4.7.1) sha256=98ad977f5e5a28df77c0364504bdea21f0a2a9ea86eae238668fdfec341ab860 parseconfig (1.1.2) sha256=e52247d15070fb47f9e58f44f7888d1e7f65775274cd60f9ab4b7acd7943b291 parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 - public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c @@ -821,22 +820,22 @@ CHECKSUMS rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 rspec-its (2.0.0) sha256=a88e8bc38149f2835e93533591ec4f5c829aacbfd41269a2e6f9f5b82f5260df - rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-mocks (3.13.7) sha256=0979034e64b1d7a838aaaddf12bf065ea4dc40ef3d4c39f01f93ae2c66c62b1c rspec-sorbet (1.9.2) sha256=28bf3969fa136ed22edd05b00d23aab52c276f2fcc89fccbb16b8fd0c48931c8 rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c - rubocop (1.85.1) sha256=3dbcf9e961baa4c376eeeb2a03913dca5e3987033b04d38fa538aa1e7406cc77 + rubocop (1.85.0) sha256=317407feb681a07d54f64d2f9e1d6b6af1ce7678e51cd658e3ad8bd66da48c01 rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2 rubocop-sorbet (0.12.0) sha256=195521e132500555819313df5accaaf56bf721a63de3e5b7b1d0b25f696b1f39 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 sawyer (0.9.3) sha256=0d0f19298408047037638639fe62f4794483fb04320269169bd41af2bdcf5e41 - sentry-opentelemetry (6.4.1) sha256=9d5066d59e59d8700ce5bbbcf7488cf324837e6029af5841665b7e7deb957cc1 - sentry-ruby (6.4.1) sha256=dac04976f791ad6ecd4fd30440c29d9b73aee08f790eeca73b439b5d67370f38 + sentry-opentelemetry (6.4.0) sha256=e0f54eda8bf2b8941fcadad23a72ecc706f7350fa1991ba754790bf3738643bf + sentry-ruby (6.4.0) sha256=562bee79aea8f92825ac9df3ea01cc7788f9170d382f8d38947895280dc3be06 simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 - simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 + simplecov-html (0.12.3) sha256=4b1aad33259ffba8b29c6876c12db70e5750cb9df829486e4c6e5da4fa0aa07b simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 - sorbet-runtime (0.6.12993) sha256=5720d6e70063ed39528ddb18248c13a8072cf6991cf7d6652dcc0b8e9bc6b4ac + sorbet-runtime (0.6.12977) sha256=67e659fd940cd3ea8022548fb49256e3d87f3353ea8657c5bd26110f54478387 stackprof (0.2.28) sha256=4ec2ace02f386012b40ca20ef80c030ad711831f59511da12e83b34efb0f9a04 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 From 1475db94fcff8318dd5804efed3b64d3fe69dd6a Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Fri, 6 Mar 2026 17:28:17 +0000 Subject: [PATCH 5/8] Address code review feedback for Xcode SPM file parser\n\n- Remove direct testing of private #extract_xcodeproj_dir method\n- Align check_required_files guard to match xcode_resolved_files filtering\n- Harmonize error messages between check_required_files and parse\n- Add comment explaining declaration_string: nil in Xcode SPM metadata\n- Handle URI::InvalidURIError in UrlHelpers.normalize_name\n- Expand comment on v1-only identity downcasing in PackageResolvedParser" --- swift/lib/dependabot/swift/file_parser.rb | 6 +++-- .../file_parser/package_resolved_parser.rb | 3 ++- swift/lib/dependabot/swift/url_helpers.rb | 2 ++ .../spec/dependabot/swift/file_parser_spec.rb | 27 ------------------- 4 files changed, 8 insertions(+), 30 deletions(-) diff --git a/swift/lib/dependabot/swift/file_parser.rb b/swift/lib/dependabot/swift/file_parser.rb index a0297d11a86..b5de7c35356 100644 --- a/swift/lib/dependabot/swift/file_parser.rb +++ b/swift/lib/dependabot/swift/file_parser.rb @@ -26,7 +26,7 @@ def parse elsif xcode_spm_mode? parse_xcode_spm else - raise "No Package.swift!" + raise "No Package.swift or Xcode Package.resolved found!" end end @@ -140,6 +140,8 @@ def enrich_with_pbxproj_requirements(dep, pbxproj_requirements) requirement: requirement_str || req[:requirement], file: pbxproj_file, metadata: { + # declaration_string is not applicable for Xcode-managed SPM + # (no Package.swift manifest to extract it from) declaration_string: nil, requirement_string: requirement_string }.compact @@ -178,7 +180,7 @@ def check_required_files 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") } + return if dependency_files.any? { |f| f.name.end_with?("Package.resolved") && f.name.include?(".xcodeproj/") } raise "No Package.swift or Xcode Package.resolved found!" end diff --git a/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb b/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb index 5117891d727..a1928f93b90 100644 --- a/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb +++ b/swift/lib/dependabot/swift/file_parser/package_resolved_parser.rb @@ -117,7 +117,8 @@ def build_dependency(pin, schema_version) state = pin[T.must(keys[:state])] || {} identity = pin[T.must(keys[:identity])] - # v1 uses a display name for "package"; normalize to lowercase like v2/v3 "identity" + # v1 uses a display name for "package"; normalize to lowercase like v2/v3 "identity". + # v2/v3 identity is always lowercase per spec, so only v1 needs downcasing. identity = identity&.downcase if schema_version == 1 build_dependency_object( diff --git a/swift/lib/dependabot/swift/url_helpers.rb b/swift/lib/dependabot/swift/url_helpers.rb index 348e775ab51..c4326905b64 100644 --- a/swift/lib/dependabot/swift/url_helpers.rb +++ b/swift/lib/dependabot/swift/url_helpers.rb @@ -16,6 +16,8 @@ module UrlHelpers def self.normalize_name(source) uri = URI.parse(source.downcase) "#{uri.host}#{uri.path}".delete_prefix("www.").delete_suffix(".git") + rescue URI::InvalidURIError + source.downcase.delete_suffix(".git") end end end diff --git a/swift/spec/dependabot/swift/file_parser_spec.rb b/swift/spec/dependabot/swift/file_parser_spec.rb index 8b3cf153705..6ecf29c69e0 100644 --- a/swift/spec/dependabot/swift/file_parser_spec.rb +++ b/swift/spec/dependabot/swift/file_parser_spec.rb @@ -819,31 +819,4 @@ end end - describe "#extract_xcodeproj_dir (private)" do - let(:project_name) { "ReactiveCocoa" } - let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } - - it "extracts xcodeproj dir from a resolved file path" do - result = parser.send( - :extract_xcodeproj_dir, - "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" - ) - expect(result).to eq("MyApp.xcodeproj") - end - - it "extracts xcodeproj dir from a pbxproj path" do - result = parser.send(:extract_xcodeproj_dir, "MyApp.xcodeproj/project.pbxproj") - expect(result).to eq("MyApp.xcodeproj") - end - - it "handles nested directory paths" do - result = parser.send(:extract_xcodeproj_dir, "sub/dir/App.xcodeproj/project.pbxproj") - expect(result).to eq("sub/dir/App.xcodeproj") - end - - it "returns nil for paths without xcodeproj" do - result = parser.send(:extract_xcodeproj_dir, "Package.resolved") - expect(result).to be_nil - end - end end From 909e7b0d9391729ffc81a035be73ba6f394cf659 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Fri, 6 Mar 2026 19:11:09 +0000 Subject: [PATCH 6/8] Change Sorbet type from strict to strong in pbxproj_parser and url_helpers; remove unnecessary blank line in file_parser_spec --- swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb | 2 +- swift/lib/dependabot/swift/url_helpers.rb | 2 +- swift/spec/dependabot/swift/file_parser_spec.rb | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb index 389deed0032..d603d8047e2 100644 --- a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb +++ b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb @@ -1,4 +1,4 @@ -# typed: strict +# typed: strong # frozen_string_literal: true require "sorbet-runtime" diff --git a/swift/lib/dependabot/swift/url_helpers.rb b/swift/lib/dependabot/swift/url_helpers.rb index c4326905b64..438ced41050 100644 --- a/swift/lib/dependabot/swift/url_helpers.rb +++ b/swift/lib/dependabot/swift/url_helpers.rb @@ -1,4 +1,4 @@ -# typed: strict +# typed: strong # frozen_string_literal: true require "uri" diff --git a/swift/spec/dependabot/swift/file_parser_spec.rb b/swift/spec/dependabot/swift/file_parser_spec.rb index 6ecf29c69e0..c8332a54126 100644 --- a/swift/spec/dependabot/swift/file_parser_spec.rb +++ b/swift/spec/dependabot/swift/file_parser_spec.rb @@ -818,5 +818,4 @@ end end end - end From b3ac91ee175ace38af0a265d8c1a62ca13ffbdd2 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Mon, 9 Mar 2026 15:14:50 +0000 Subject: [PATCH 7/8] Address review feedback: consistent check_required_files, fix comment, narrow rescue - Use xcode_resolved_files.any? in check_required_files to match the succeeding when only support Package.resolved files are present - Fix PbxprojParser class comment to say 'keyed by normalized dependency name' instead of 'keyed by normalized URL' - Narrow rescue in parse_native_requirement from StandardError to RuntimeError and Gem::Requirement::BadRequirementError to avoid masking unexpected bugs --- swift/lib/dependabot/swift/file_parser.rb | 2 +- swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/swift/lib/dependabot/swift/file_parser.rb b/swift/lib/dependabot/swift/file_parser.rb index b5de7c35356..68102a577f3 100644 --- a/swift/lib/dependabot/swift/file_parser.rb +++ b/swift/lib/dependabot/swift/file_parser.rb @@ -180,7 +180,7 @@ def check_required_files 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") && f.name.include?(".xcodeproj/") } + return if xcode_resolved_files.any? raise "No Package.swift or Xcode Package.resolved found!" end diff --git a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb index d603d8047e2..1168256ce3e 100644 --- a/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb +++ b/swift/lib/dependabot/swift/file_parser/pbxproj_parser.rb @@ -14,9 +14,9 @@ class FileParser < Dependabot::FileParsers::Base # Parses XCRemoteSwiftPackageReference entries from a project.pbxproj file # to extract dependency requirement constraints declared in Xcode. # - # Returns a hash keyed by normalized URL mapping to requirement metadata, - # so the main parser can enrich Package.resolved dependencies with - # requirement info from the Xcode project. + # Returns a hash keyed by normalized dependency name (e.g. "github.com/owner/repo") + # mapping to requirement metadata, so the main parser can enrich + # Package.resolved dependencies with requirement info from the Xcode project. class PbxprojParser extend T::Sig @@ -190,7 +190,7 @@ def extract_version(block, pattern) sig { params(requirement_string: String).returns(T.nilable(String)) } def parse_native_requirement(requirement_string) NativeRequirement.new(requirement_string).to_s - rescue StandardError + rescue RuntimeError, Gem::Requirement::BadRequirementError nil end end From 7ba615679a17dcbc746de1b2458e2390b8988d8b Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Mon, 9 Mar 2026 16:41:14 +0000 Subject: [PATCH 8/8] Extract Xcode SPM orchestration into FileParser::XcodeSpmResolver\n\nMoves the Xcode-managed SwiftPM parsing logic out of FileParser into a\ndedicated helper class under file_parser/, following the same pattern\nused by other ecosystems (e.g. bundler's GemfileDeclarationFinder).\n\nThe new XcodeSpmResolver handles:\n- Aggregating pbxproj requirements scoped by xcodeproj directory\n- Enriching Package.resolved dependencies with pbxproj metadata\n- Extracting xcodeproj directory paths from file names\n\nFileParser#parse_xcode_spm now delegates to the resolver, keeping the\nmain class focused on routing between classic SPM and Xcode SPM paths." --- swift/lib/dependabot/swift/file_parser.rb | 93 +---- .../swift/file_parser/xcode_spm_resolver.rb | 129 ++++++ .../file_parser/xcode_spm_resolver_spec.rb | 384 ++++++++++++++++++ 3 files changed, 520 insertions(+), 86 deletions(-) create mode 100644 swift/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb create mode 100644 swift/spec/dependabot/swift/file_parser/xcode_spm_resolver_spec.rb diff --git a/swift/lib/dependabot/swift/file_parser.rb b/swift/lib/dependabot/swift/file_parser.rb index 68102a577f3..cca6c20a34f 100644 --- a/swift/lib/dependabot/swift/file_parser.rb +++ b/swift/lib/dependabot/swift/file_parser.rb @@ -7,8 +7,7 @@ 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/file_parser/xcode_spm_resolver" require "dependabot/swift/package_manager" require "dependabot/swift/language" @@ -72,26 +71,14 @@ def parse_classic_spm dependency_set.dependencies end - # Xcode SPM parsing: parses Package.resolved JSON directly, enriches - # with requirement info from project.pbxproj files + # Xcode SPM parsing: delegates to XcodeSpmResolver which parses + # Package.resolved JSON and enriches with project.pbxproj requirements sig { returns(T::Array[Dependabot::Dependency]) } def parse_xcode_spm - dependency_set = DependencySet.new - - scoped_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) - pbxproj_requirements = scoped_requirements.fetch(xcodeproj_dir, {}) - - resolved_deps.each do |dep| - enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements) - dependency_set << enriched - end - end - - dependency_set.dependencies + XcodeSpmResolver.new( + xcode_resolved_files: xcode_resolved_files, + pbxproj_files: pbxproj_files + ).parse end sig { returns(T::Boolean) } @@ -100,72 +87,6 @@ def xcode_spm_mode? xcode_resolved_files.any? end - # Collects requirement info from all project.pbxproj support files, - # keyed by xcodeproj directory so each resolved file only sees - # requirements from its own Xcode project. - sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) } - def aggregate_pbxproj_requirements - scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) - - pbxproj_files.each do |pbxproj_file| - xcodeproj_dir = extract_xcodeproj_dir(pbxproj_file.name) - scoped[xcodeproj_dir] ||= {} - - PbxprojParser.new(pbxproj_file).parse.each do |name, req_info| - T.must(scoped[xcodeproj_dir])[name] = req_info - end - end - - scoped - 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]] - ).returns(Dependabot::Dependency) - end - def enrich_with_pbxproj_requirements(dep, pbxproj_requirements) - 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 is not applicable for Xcode-managed SPM - # (no Package.swift manifest to extract it from) - 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 - - # Extracts the .xcodeproj directory name from a file path. - # e.g. "MyApp.xcodeproj/project.xcworkspace/.../Package.resolved" -> "MyApp.xcodeproj" - # e.g. "sub/dir/App.xcodeproj/project.pbxproj" -> "sub/dir/App.xcodeproj" - sig { params(path: String).returns(T.nilable(String)) } - def extract_xcodeproj_dir(path) - match = path.match(%r{^(.*?\.xcodeproj)/}) - match&.captures&.first - end - sig { returns(Dependabot::Swift::FileParser::DependencyParser) } def dependency_parser DependencyParser.new( diff --git a/swift/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb b/swift/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb new file mode 100644 index 00000000000..61c84574158 --- /dev/null +++ b/swift/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb @@ -0,0 +1,129 @@ +# typed: strict +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/dependency" +require "dependabot/file_parsers/base/dependency_set" +require "dependabot/swift/file_parser" +require "dependabot/swift/file_parser/package_resolved_parser" +require "dependabot/swift/file_parser/pbxproj_parser" + +module Dependabot + module Swift + class FileParser < Dependabot::FileParsers::Base + # Orchestrates Xcode-managed SwiftPM dependency parsing. + # + # Parses Package.resolved JSON files found inside .xcodeproj directories, + # then enriches each dependency with requirement info extracted from the + # corresponding project.pbxproj files. + class XcodeSpmResolver + extend T::Sig + + sig do + params( + xcode_resolved_files: T::Array[Dependabot::DependencyFile], + pbxproj_files: T::Array[Dependabot::DependencyFile] + ).void + end + def initialize(xcode_resolved_files:, pbxproj_files:) + @xcode_resolved_files = xcode_resolved_files + @pbxproj_files = pbxproj_files + end + + sig { returns(T::Array[Dependabot::Dependency]) } + def parse + dependency_set = Dependabot::FileParsers::Base::DependencySet.new + + scoped_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) + pbxproj_requirements = scoped_requirements.fetch(xcodeproj_dir, {}) + + resolved_deps.each do |dep| + enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements) + dependency_set << enriched + end + end + + dependency_set.dependencies + end + + private + + sig { returns(T::Array[Dependabot::DependencyFile]) } + attr_reader :xcode_resolved_files + + sig { returns(T::Array[Dependabot::DependencyFile]) } + attr_reader :pbxproj_files + + # Collects requirement info from all project.pbxproj support files, + # keyed by xcodeproj directory so each resolved file only sees + # requirements from its own Xcode project. + sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) } + def aggregate_pbxproj_requirements + scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) + + pbxproj_files.each do |pbxproj_file| + xcodeproj_dir = extract_xcodeproj_dir(pbxproj_file.name) + scoped[xcodeproj_dir] ||= {} + + PbxprojParser.new(pbxproj_file).parse.each do |name, req_info| + T.must(scoped[xcodeproj_dir])[name] = req_info + end + end + + scoped + 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]] + ).returns(Dependabot::Dependency) + end + def enrich_with_pbxproj_requirements(dep, pbxproj_requirements) + 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 is not applicable for Xcode-managed SPM + # (no Package.swift manifest to extract it from) + 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 + + # Extracts the .xcodeproj directory name from a file path. + # e.g. "MyApp.xcodeproj/project.xcworkspace/.../Package.resolved" -> "MyApp.xcodeproj" + # e.g. "sub/dir/App.xcodeproj/project.pbxproj" -> "sub/dir/App.xcodeproj" + sig { params(path: String).returns(T.nilable(String)) } + def extract_xcodeproj_dir(path) + match = path.match(%r{^(.*?\.xcodeproj)/}) + match&.captures&.first + end + end + end + end +end diff --git a/swift/spec/dependabot/swift/file_parser/xcode_spm_resolver_spec.rb b/swift/spec/dependabot/swift/file_parser/xcode_spm_resolver_spec.rb new file mode 100644 index 00000000000..262e83744f6 --- /dev/null +++ b/swift/spec/dependabot/swift/file_parser/xcode_spm_resolver_spec.rb @@ -0,0 +1,384 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency_file" +require "dependabot/swift/file_parser/xcode_spm_resolver" + +RSpec.describe Dependabot::Swift::FileParser::XcodeSpmResolver do + subject(:resolver) do + described_class.new( + xcode_resolved_files: xcode_resolved_files, + pbxproj_files: pbxproj_files + ) + end + + let(:xcode_resolved_files) { [] } + let(:pbxproj_files) { [] } + + describe "#parse" do + context "with a single Xcode project (v2 Package.resolved)" do + let(:project_name) { "xcode_project" } + let(:xcode_resolved_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:pbxproj_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ) + ] + end + + it "parses dependencies" do + deps = resolver.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + expect(dep.package_manager).to eq("swift") + end + + it "enriches dependencies with pbxproj requirements" do + dep = resolver.parse.first + req = dep.requirements.first + + expect(req[:requirement]).to eq(">= 2.54.0, < 3.0.0") + expect(req[:file]).to eq("MyApp.xcodeproj/project.pbxproj") + expect(req[:metadata][:requirement_string]).to eq("from: \"2.54.0\"") + end + + it "sets correct source info" do + dep = resolver.parse.first + source = dep.requirements.first[:source] + + expect(source[:type]).to eq("git") + expect(source[:url]).to eq("https://github.com/apple/swift-nio.git") + expect(source[:ref]).to eq("2.54.0") + end + end + + context "with multiple .xcodeproj directories" do + let(:project_name) { "xcode_project_multiple" } + let(:xcode_resolved_files) do + [ + Dependabot::DependencyFile.new( + name: "AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "AppA.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ), + Dependabot::DependencyFile.new( + name: "AppB.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "AppB.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:pbxproj_files) do + [ + Dependabot::DependencyFile.new( + name: "AppA.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "AppA.xcodeproj", "project.pbxproj"), + support_file: true + ), + Dependabot::DependencyFile.new( + name: "AppB.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "AppB.xcodeproj", "project.pbxproj"), + support_file: true + ) + ] + end + + it "parses dependencies from all resolved files" do + deps = resolver.parse + names = deps.map(&:name) + + expect(names).to include("github.com/apple/swift-nio") + expect(names).to include("github.com/apple/swift-collections") + end + + it "associates requirements with correct pbxproj files" do + deps = resolver.parse + nio = deps.find { |d| d.name == "github.com/apple/swift-nio" } + collections = deps.find { |d| d.name == "github.com/apple/swift-collections" } + + expect(nio.requirements.first[:file]).to eq("AppA.xcodeproj/project.pbxproj") + expect(collections.requirements.first[:file]).to eq("AppB.xcodeproj/project.pbxproj") + end + end + + context "with multiple dependencies and requirement types" do + let(:project_name) { "xcode_project_multi_req" } + let(:xcode_resolved_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:pbxproj_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ) + ] + end + + it "parses all dependencies" do + deps = resolver.parse + expect(deps.length).to eq(4) + + names = deps.map(&:name) + expect(names).to contain_exactly( + "github.com/apple/swift-nio", + "github.com/apple/swift-collections", + "github.com/apple/swift-argument-parser", + "github.com/apple/swift-log" + ) + end + + it "applies correct requirement types from pbxproj" do + deps = resolver.parse + nio = deps.find { |d| d.name == "github.com/apple/swift-nio" } + collections = deps.find { |d| d.name == "github.com/apple/swift-collections" } + parser_dep = deps.find { |d| d.name == "github.com/apple/swift-argument-parser" } + log = deps.find { |d| d.name == "github.com/apple/swift-log" } + + expect(nio.requirements.first[:requirement]).to eq(">= 2.54.0, < 3.0.0") + expect(collections.requirements.first[:requirement]).to eq(">= 1.0.0, < 1.1.0") + expect(parser_dep.requirements.first[:requirement]).to eq("= 1.2.0") + expect(log.requirements.first[:requirement]).to eq(">= 1.4.0, < 2.0.0") + end + end + + context "with no pbxproj file (only Package.resolved)" do + let(:project_name) { "xcode_project" } + let(:xcode_resolved_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:pbxproj_files) { [] } + + it "parses dependencies without requirement enrichment" do + deps = resolver.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + expect(dep.requirements.first[:requirement]).to eq("= 2.54.0") + end + end + + context "with empty pins" do + let(:project_name) { "xcode_project_empty_pins" } + let(:xcode_resolved_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:pbxproj_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ) + ] + end + + it "returns an empty dependency list" do + expect(resolver.parse).to be_empty + end + end + + context "with revision-only pin (no version)" do + let(:project_name) { "xcode_project_revision_only" } + let(:xcode_resolved_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:pbxproj_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ) + ] + end + + it "parses with nil version" do + dep = resolver.parse.first + expect(dep.version).to be_nil + end + + it "records revision in source ref" do + dep = resolver.parse.first + source = dep.requirements.first[:source] + expect(source[:ref]).to eq("6213ba7a06febe8fef60563a4a7d26a4085783cf") + end + end + + context "with v1 Package.resolved" do + let(:project_name) { "xcode_project_v1_resolved" } + let(:xcode_resolved_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:pbxproj_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ) + ] + end + + it "parses v1 format dependencies" do + deps = resolver.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + end + end + + context "with v3 Package.resolved" do + let(:project_name) { "xcode_project_v3_resolved" } + let(:xcode_resolved_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", + content: fixture( + "projects", + project_name, + "MyApp.xcodeproj", + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved" + ) + ) + ] + end + let(:pbxproj_files) do + [ + Dependabot::DependencyFile.new( + name: "MyApp.xcodeproj/project.pbxproj", + content: fixture("projects", project_name, "MyApp.xcodeproj", "project.pbxproj"), + support_file: true + ) + ] + end + + it "parses v3 format dependencies" do + deps = resolver.parse + expect(deps.length).to eq(1) + + dep = deps.first + expect(dep.name).to eq("github.com/apple/swift-nio") + expect(dep.version).to eq("2.54.0") + end + end + + context "with no resolved files" do + let(:xcode_resolved_files) { [] } + let(:pbxproj_files) { [] } + + it "returns an empty dependency list" do + expect(resolver.parse).to be_empty + end + end + end +end