From fd110a5b30166a2cabbbdb07c3fd65cbffb4c114 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Fri, 28 Aug 2026 16:53:07 +0100 Subject: [PATCH] Preserve Swift source coordinates --- internal/swift/swift.go | 55 +++++++++++++++++++++++-------- internal/swift/swift_test.go | 37 ++++++++++++++------- manifests_test.go | 29 ++++++++++++++++ testdata/swift/Package.resolved.2 | 10 +++++- 4 files changed, 104 insertions(+), 27 deletions(-) diff --git a/internal/swift/swift.go b/internal/swift/swift.go index bb4f20b..1025e22 100644 --- a/internal/swift/swift.go +++ b/internal/swift/swift.go @@ -2,9 +2,11 @@ package swift import ( "encoding/json" - "github.com/git-pkgs/manifests/internal/core" + "net/url" "regexp" "strings" + + "github.com/git-pkgs/manifests/internal/core" ) func init() { @@ -56,7 +58,7 @@ func (p *packageSwiftParser) Parse(filename string, content []byte) (*core.Resul version = match[versionGroup] } - name := extractSwiftPackageName(url) + name := swiftSourceCoordinate(url) if name == "" || seen[name] { continue } @@ -74,16 +76,38 @@ func (p *packageSwiftParser) Parse(filename string, content []byte) (*core.Resul return &core.Result{Name: selfName, Dependencies: deps}, nil } -// extractSwiftPackageName extracts the package name from a git URL. -func extractSwiftPackageName(url string) string { - // Remove .git suffix - url = strings.TrimSuffix(url, ".git") +func swiftSourceCoordinate(rawURL string) string { + if strings.Contains(rawURL, "://") { + parsed, err := url.Parse(rawURL) + if err != nil || parsed.Hostname() == "" { + return "" + } + + host := strings.ToLower(parsed.Hostname()) + if parsed.Port() != "" { + host += ":" + parsed.Port() + } + return joinSwiftSourceCoordinate(host, parsed.Path) + } - // Get last path component - if idx := strings.LastIndex(url, "/"); idx >= 0 { - return url[idx+1:] + separator := strings.IndexByte(rawURL, ':') + if separator <= 0 { + return "" } - return url + host := rawURL[:separator] + if at := strings.LastIndexByte(host, '@'); at >= 0 { + host = host[at+1:] + } + return joinSwiftSourceCoordinate(strings.ToLower(host), rawURL[separator+1:]) +} + +func joinSwiftSourceCoordinate(host, path string) string { + path = strings.Trim(strings.TrimSpace(path), "/") + path = strings.TrimSuffix(path, ".git") + if host == "" || path == "" { + return "" + } + return host + "/" + path } // packageResolvedParser parses Package.resolved files. @@ -112,6 +136,7 @@ type packageResolvedV2 struct { type packageResolvedPinV2 struct { Identity string `json:"identity"` + Kind string `json:"kind"` Location string `json:"location"` State struct { Version string `json:"version"` @@ -147,9 +172,9 @@ func parsePackageResolvedV1(filename string, content []byte) ([]core.Dependency, var deps []core.Dependency for _, pin := range resolved.Object.Pins { - name := pin.Package + name := swiftSourceCoordinate(pin.RepositoryURL) if name == "" { - name = extractSwiftPackageName(pin.RepositoryURL) + name = pin.Package } deps = append(deps, core.Dependency{ @@ -172,8 +197,10 @@ func parsePackageResolvedV2(filename string, content []byte) ([]core.Dependency, var deps []core.Dependency for _, pin := range resolved.Pins { name := pin.Identity - if name == "" { - name = extractSwiftPackageName(pin.Location) + if pin.Kind == "remoteSourceControl" { + if coordinate := swiftSourceCoordinate(pin.Location); coordinate != "" { + name = coordinate + } } deps = append(deps, core.Dependency{ diff --git a/internal/swift/swift_test.go b/internal/swift/swift_test.go index 18c5e56..829f9c4 100644 --- a/internal/swift/swift_test.go +++ b/internal/swift/swift_test.go @@ -28,11 +28,10 @@ func TestPackageSwift(t *testing.T) { depMap[d.Name] = d } - // All 3 packages (extracted from git URLs) expected := []string{ - "vapor", - "Tasks", - "Environment", + "github.com/qutheory/vapor", + "github.com/czechboy0/Tasks", + "github.com/czechboy0/Environment", } for _, name := range expected { @@ -63,9 +62,8 @@ func TestPackageResolved(t *testing.T) { depMap[d.Name] = d } - // Check Yams - if dep, ok := depMap["Yams"]; !ok { - t.Error("expected Yams dependency") + if dep, ok := depMap["github.com/jpsim/Yams"]; !ok { + t.Error("expected github.com/jpsim/Yams dependency") } else if dep.Version != "5.0.1" { t.Errorf("Yams version = %q, want %q", dep.Version, "5.0.1") } @@ -83,8 +81,8 @@ func TestPackageResolvedV2(t *testing.T) { t.Fatalf("Parse failed: %v", err) } - if len(res.Dependencies) != 2 { - t.Fatalf("expected 2 dependencies, got %d", len(res.Dependencies)) + if len(res.Dependencies) != 3 { + t.Fatalf("expected 3 dependencies, got %d", len(res.Dependencies)) } depMap := make(map[string]core.Dependency) @@ -92,10 +90,10 @@ func TestPackageResolvedV2(t *testing.T) { depMap[d.Name] = d } - // All 2 packages with versions expected := map[string]string{ - "cryptoswift": "1.6.0", - "swift-docc-plugin": "1.0.0", + "github.com/krzyzanowskim/CryptoSwift": "1.6.0", + "github.com/apple/swift-docc-plugin": "1.0.0", + "apple.swift-argument-parser": "1.2.3", } for name, wantVer := range expected { @@ -109,3 +107,18 @@ func TestPackageResolvedV2(t *testing.T) { } } } + +func TestSwiftSourceCoordinate(t *testing.T) { + tests := map[string]string{ + "https://github.com/apple/swift-argument-parser.git": "github.com/apple/swift-argument-parser", + "ssh://git@github.com/apple/swift-nio.git": "github.com/apple/swift-nio", + "git@github.com:apple/swift-log.git": "github.com/apple/swift-log", + "../local-package": "", + } + + for rawURL, want := range tests { + if got := swiftSourceCoordinate(rawURL); got != want { + t.Errorf("swiftSourceCoordinate(%q) = %q, want %q", rawURL, got, want) + } + } +} diff --git a/manifests_test.go b/manifests_test.go index a5bfdf9..bb5dee3 100644 --- a/manifests_test.go +++ b/manifests_test.go @@ -605,6 +605,35 @@ func TestPURL(t *testing.T) { t.Error("express dependency not found") } +func TestSwiftSourcePURLs(t *testing.T) { + content, err := os.ReadFile("testdata/swift/Package.resolved.2") + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + result, err := Parse("Package.resolved", content) + if err != nil { + t.Fatalf("Parse: %v", err) + } + want := map[string]string{ + "github.com/krzyzanowskim/CryptoSwift": "pkg:swift/github.com/krzyzanowskim/CryptoSwift@1.6.0", + "github.com/apple/swift-docc-plugin": "pkg:swift/github.com/apple/swift-docc-plugin@1.0.0", + "apple.swift-argument-parser": "", + } + if len(result.Dependencies) != len(want) { + t.Fatalf("Dependencies has %d entries, want %d: %+v", len(result.Dependencies), len(want), result.Dependencies) + } + for _, dependency := range result.Dependencies { + wantPURL, ok := want[dependency.Name] + if !ok { + t.Errorf("unexpected dependency: %+v", dependency) + continue + } + if dependency.PURL != wantPURL { + t.Errorf("%s PURL = %q, want %q", dependency.Name, dependency.PURL, wantPURL) + } + } +} + func TestParsePEP508ParenthesizedRequirements(t *testing.T) { content, err := os.ReadFile("testdata/pypi/pep508-parenthesized/pyproject.toml") if err != nil { diff --git a/testdata/swift/Package.resolved.2 b/testdata/swift/Package.resolved.2 index 234fc69..8ff645a 100644 --- a/testdata/swift/Package.resolved.2 +++ b/testdata/swift/Package.resolved.2 @@ -17,7 +17,15 @@ "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6", "version" : "1.0.0" } + }, + { + "identity" : "apple.swift-argument-parser", + "kind" : "registry", + "location" : "apple.swift-argument-parser", + "state" : { + "version" : "1.2.3" + } } ], "version" : 2 -} \ No newline at end of file +}