Skip to content

Commit

Permalink
Implement FileParser for Swift
Browse files Browse the repository at this point in the history
  • Loading branch information
deivid-rodriguez committed Jul 19, 2023
1 parent 071fdc6 commit b2501f0
Show file tree
Hide file tree
Showing 58 changed files with 1,767 additions and 2 deletions.
41 changes: 39 additions & 2 deletions swift/lib/dependabot/swift/file_parser.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
# frozen_string_literal: true

require "dependabot/dependency"
require "dependabot/file_parsers"
require "dependabot/file_parsers/base"
require "dependabot/swift/file_parser/dependency_parser"
require "dependabot/swift/file_parser/manifest_parser"

module Dependabot
module Swift
class FileParser < Dependabot::FileParsers::Base
require "dependabot/file_parsers/base/dependency_set"

def parse
raise NotImplementedError
dependency_set = DependencySet.new

dependency_parser.parse.map do |dep|
if dep.top_level?
source = dep.requirements.first[:source]

requirements = ManifestParser.new(package_manifest_file, source: source).requirements

dependency_set << Dependency.new(
name: dep.name,
version: dep.version,
package_manager: dep.package_manager,
requirements: requirements
)
else
dependency_set << dep
end
end

dependency_set.dependencies
end

private

def dependency_parser
DependencyParser.new(
dependency_files: dependency_files,
repo_contents_path: repo_contents_path,
credentials: credentials
)
end

def check_required_files
raise NotImplementedError
raise "No Package.swift!" unless package_manifest_file
end

def package_manifest_file
# TODO: Select version-specific manifest
@package_manifest_file ||= get_original_file("Package.swift")
end
end
end
Expand Down
72 changes: 72 additions & 0 deletions swift/lib/dependabot/swift/file_parser/dependency_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require "dependabot/file_parsers/base"
require "dependabot/shared_helpers"
require "dependabot/dependency"
require "json"

module Dependabot
module Swift
class FileParser < Dependabot::FileParsers::Base
class DependencyParser
def initialize(dependency_files:, repo_contents_path:, credentials:)
@dependency_files = dependency_files
@repo_contents_path = repo_contents_path
@credentials = credentials
end

def parse
SharedHelpers.in_a_temporary_repo_directory(dependency_files.first.directory, repo_contents_path) do
write_temporary_dependency_files

SharedHelpers.with_git_configured(credentials: credentials) do
subdependencies(formatted_deps)
end
end
end

private

def write_temporary_dependency_files
dependency_files.each do |file|
File.write(file.name, file.content)
end
end

def formatted_deps
deps = SharedHelpers.run_shell_command(
"swift package show-dependencies --format json",
stderr_to_stdout: false
)

JSON.parse(deps)
end

def subdependencies(data, level: 0)
data["dependencies"].flat_map { |root| all_dependencies(root, level: level) }
end

def all_dependencies(data, level: 0)
name = data["identity"]
url = data["url"]
version = data["version"]

source = { type: "git", url: url, ref: version, branch: nil }
args = { name: name, version: version, package_manager: "swift", requirements: [] }

if level.zero?
args[:requirements] << { requirement: nil, groups: ["dependencies"], file: nil, source: source }
else
args[:subdependency_metadata] = [{ source: source }]
end

dep = Dependency.new(**args) if data["version"] != "unspecified"

[dep, *subdependencies(data, level: level + 1)].compact
end

attr_reader :dependency_files, :repo_contents_path, :credentials
end
end
end
end
47 changes: 47 additions & 0 deletions swift/lib/dependabot/swift/file_parser/manifest_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require "dependabot/file_parsers/base"
require "dependabot/swift/native_requirement"

module Dependabot
module Swift
class FileParser < Dependabot::FileParsers::Base
class ManifestParser
DEPENDENCY = /(?<declaration>\.package\(\s*(?:name: "[^"]+",\s*)?url: "(?<url>[^"]+)",\s*(?<requirement>.*)\))/

def initialize(manifest, source:)
@manifest = manifest
@source = source
end

def requirements
found = manifest.content.scan(DEPENDENCY).find do |_declaration, url, requirement|
# TODO: Support pinning to specific revisions
next if requirement.start_with?("branch:", ".branch(", "revision:", ".revision(")

url == source[:url]
end

return [] unless found

declaration = found.first
requirement = NativeRequirement.new(found.last)

[
{
requirement: requirement.to_s,
groups: ["dependencies"],
file: manifest.name,
source: source,
metadata: { declaration_string: declaration, requirement_string: requirement.declaration }
}
]
end

private

attr_reader :manifest, :source
end
end
end
end
199 changes: 199 additions & 0 deletions swift/spec/dependabot/swift/file_parser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# frozen_string_literal: true

require "spec_helper"
require "dependabot/dependency_file"
require "dependabot/source"
require "dependabot/swift/file_parser"
require_common_spec "file_parsers/shared_examples_for_file_parsers"

RSpec.describe Dependabot::Swift::FileParser do
it_behaves_like "a dependency file parser"

let(:parser) do
described_class.new(dependency_files: files, source: source, repo_contents_path: repo_contents_path)
end

let(:source) do
Dependabot::Source.new(
provider: "github",
repo: "mona/Example",
directory: "/"
)
end

let(:files) do
[
package_manifest_file,
package_resolved_file
]
end

let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") }

let(:package_manifest_file) do
Dependabot::DependencyFile.new(
name: "Package.swift",
content: fixture("projects", project_name, "Package.swift")
)
end

let(:package_resolved_file) do
Dependabot::DependencyFile.new(
name: "Package.resolved",
content: fixture("projects", project_name, "Package.resolved")
)
end

let(:dependencies) { parser.parse }

shared_examples_for "parse" do
it "parses dependencies fine" do
expectations.each.with_index do |expected, index|
url = expected[:url]
version = expected[:version]
name = expected[:name]
source = { type: "git", url: url, ref: version, branch: nil }

dependency = dependencies[index]

expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq(name)
expect(dependency.version).to eq(version)

if expected[:requirement]
expect(dependency.requirements).to eq([
{
requirement: expected[:requirement],
groups: ["dependencies"],
file: "Package.swift",
source: source,
metadata: {
declaration_string: expected[:declaration_string],
requirement_string: expected[:requirement_string]
}
}
])
else # subdependency
expect(dependency.subdependency_metadata).to eq([
{
source: source
}
])
end
end
end
end

context "with supported declarations" do
let(:project_name) { "Example" }

let(:expectations) do
[
{
name: "reactiveswift",
url: "https://github.com/ReactiveCocoa/ReactiveSwift.git",
version: "7.1.0",
requirement: "= 7.1.0",
declaration_string:
".package(url: \"https://github.com/ReactiveCocoa/ReactiveSwift.git\",\n exact: \"7.1.0\")",
requirement_string: "exact: \"7.1.0\""
},
{
name: "swift-docc-plugin",
url: "https://github.com/apple/swift-docc-plugin",
version: "1.0.0",
requirement: ">= 1.0.0, < 2.0.0",
declaration_string:
".package(\n url: \"https://github.com/apple/swift-docc-plugin\",\n from: \"1.0.0\")",
requirement_string: "from: \"1.0.0\""
},
{
name: "swift-benchmark",
url: "https://github.com/google/swift-benchmark",
version: "0.1.1",
requirement: ">= 0.1.0, < 0.1.2",
declaration_string: ".package(url: \"https://github.com/google/swift-benchmark\", \"0.1.0\"..<\"0.1.2\")",
requirement_string: "\"0.1.0\"..<\"0.1.2\""
},
{
name: "swift-argument-parser",
url: "https://github.com/apple/swift-argument-parser",
version: "0.5.0"
},
{
name: "combine-schedulers",
url: "https://github.com/pointfreeco/combine-schedulers",
version: "0.10.0",
requirement: ">= 0.9.2, <= 0.10.0",
declaration_string:
".package(url: \"https://github.com/pointfreeco/combine-schedulers\", \"0.9.2\"...\"0.10.0\")",
requirement_string: "\"0.9.2\"...\"0.10.0\""
},
{
name: "xctest-dynamic-overlay",
url: "https://github.com/pointfreeco/xctest-dynamic-overlay",
version: "0.8.5"
}
]
end

it_behaves_like "parse"
end

context "with deprecated declarations" do
let(:project_name) { "Example-Deprecated" }

let(:expectations) do
[
{
name: "quick",
url: "https://github.com/Quick/Quick.git",
version: "7.0.2",
requirement: ">= 7.0.0, < 8.0.0",
declaration_string:
".package(url: \"https://github.com/Quick/Quick.git\",\n .upToNextMajor(from: \"7.0.0\"))",
requirement_string: ".upToNextMajor(from: \"7.0.0\")"
},
{
name: "nimble",
url: "https://github.com/Quick/Nimble.git",
version: "9.0.1",
requirement: ">= 9.0.0, < 9.1.0",
declaration_string:
".package(url: \"https://github.com/Quick/Nimble.git\",\n .upToNextMinor(from: \"9.0.0\"))",
requirement_string: ".upToNextMinor(from: \"9.0.0\")"
},
{
name: "swift-docc-plugin",
url: "https://github.com/apple/swift-docc-plugin",
version: "1.0.0",
requirement: "= 1.0.0",
declaration_string:
".package(\n url: \"https://github.com/apple/swift-docc-plugin\",\n .exact(\"1.0.0\"))",
requirement_string: ".exact(\"1.0.0\")"
},
{
name: "swift-benchmark",
url: "https://github.com/google/swift-benchmark",
version: "0.1.1",
requirement: ">= 0.1.0, < 0.1.2",
declaration_string:
".package(name: \"foo\", url: \"https://github.com/google/swift-benchmark\", \"0.1.0\"..<\"0.1.2\")",
requirement_string: "\"0.1.0\"..<\"0.1.2\""
},
{
name: "swift-argument-parser",
url: "https://github.com/apple/swift-argument-parser",
version: "0.5.0"
},
{
name: "xctest-dynamic-overlay",
url: "https://github.com/pointfreeco/xctest-dynamic-overlay",
version: "0.8.5"
}
]
end

it_behaves_like "parse"
end
end
Loading

0 comments on commit b2501f0

Please sign in to comment.