From c0cec7b5943f48a96090d61199d6b73001493429 Mon Sep 17 00:00:00 2001 From: "Ryan Brandenburg (He/Him)" Date: Wed, 30 Sep 2026 16:01:26 -0700 Subject: [PATCH 1/2] Swift relies on Version, not hash --- .../TypedComponent/SwiftComponent.cs | 25 +-- .../swiftpm/SwiftResolvedComponentDetector.cs | 22 ++- .../TypedComponentSerializationTests.cs | 14 +- .../SwiftComponentTests.cs | 173 ++++++------------ .../SwiftResolvedDetectorTests.cs | 74 ++++---- 5 files changed, 116 insertions(+), 192 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs index ca516d022..de64905e1 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs @@ -11,29 +11,6 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent; /// public class SwiftComponent : TypedComponent { - /// - /// Initializes a new instance of the class. - /// - /// The name of the component. - /// The version of the component. - /// The source repository URL of the component. - /// The Git commit hash of the component. - /// The Swift package kind. - public SwiftComponent(string name, string version, string repositoryUrl, string hash, string kind) - { - this.Name = this.ValidateRequiredInput(name, nameof(name), nameof(ComponentType.Swift)); - this.Version = this.ValidateRequiredInput(version, nameof(version), nameof(ComponentType.Swift)); - this.ValidateRequiredInput(repositoryUrl, nameof(repositoryUrl), nameof(ComponentType.Swift)); - this.RepositoryUrl = new Uri(repositoryUrl); - this.Kind = this.ValidateRequiredInput(kind, nameof(kind), nameof(ComponentType.Swift)); - this.CommitHash = this.ValidateRequiredInput(hash, nameof(hash), nameof(ComponentType.Swift)); - } - - public SwiftComponent() - { - /* Reserved for deserialization */ - } - [JsonPropertyName("name")] public string Name { get; set; } @@ -68,7 +45,7 @@ public SwiftComponent() }, subpath: null); - protected override string ComputeBaseId() => $"{this.RepositoryUrl.AbsoluteUri} {this.CommitHash} - {this.Type}"; + protected override string ComputeBaseId() => $"{this.RepositoryUrl.AbsoluteUri} {this.Version} - {this.Type}"; private string GetNamespaceFromPackageUrl() { diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs index 241b7cbb3..3640a4065 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs @@ -77,15 +77,19 @@ private void ProcessPackageResolvedFile(ISingleFileComponentRecorder singleFileC ArgumentException.ThrowIfNullOrWhiteSpace(package.Identity); // The version of the package is not always available. - var version = package.State.Version ?? package.State.Branch ?? package.State.Revision; - - var detectedComponent = new SwiftComponent( - name: package.Identity, - version: version, - repositoryUrl: package.Location, - hash: package.State.Revision, - kind: package.Kind); - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(component: detectedComponent)); + // For now when there's no version we don't report. + if (!string.IsNullOrWhiteSpace(package.State.Version)) + { + var detectedComponent = new SwiftComponent + { + Name = package.Identity, + Version = package.State.Version, + RepositoryUrl = new Uri(package.Location), + CommitHash = package.State.Revision, + Kind = package.Kind, + }; + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(component: detectedComponent)); + } } else { diff --git a/test/Microsoft.ComponentDetection.Contracts.Tests/TypedComponentSerializationTests.cs b/test/Microsoft.ComponentDetection.Contracts.Tests/TypedComponentSerializationTests.cs index a351bd674..600fa1c8e 100644 --- a/test/Microsoft.ComponentDetection.Contracts.Tests/TypedComponentSerializationTests.cs +++ b/test/Microsoft.ComponentDetection.Contracts.Tests/TypedComponentSerializationTests.cs @@ -175,12 +175,14 @@ public void TypedComponent_Serialization_Git() [TestMethod] public void TypedComponent_Serialization_Swift() { - TypedComponent component = new SwiftComponent( - "alamofire", - "5.9.1", - "https://github.com/Alamofire/Alamofire", - "f455c2975872ccd2d9c81594c658af65716e9b9a", - "remoteSourceControl"); + TypedComponent component = new SwiftComponent + { + Name = "alamofire", + Version = "5.9.1", + RepositoryUrl = new Uri("https://github.com/Alamofire/Alamofire"), + CommitHash = "f455c2975872ccd2d9c81594c658af65716e9b9a", + Kind = "remoteSourceControl", + }; var json = JsonSerializer.Serialize(component); var deserializedComponent = JsonSerializer.Deserialize(json); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs index d0d75f0bd..085501ed1 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs @@ -21,7 +21,14 @@ public void Constructor_ShouldInitializeProperties() var kind = "remoteSourceControl"; var commitHash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; - var component = new SwiftComponent(name, version, repositoryUrl, commitHash, kind); + var component = new SwiftComponent + { + Name = name, + Version = version, + RepositoryUrl = new Uri(repositoryUrl), + CommitHash = commitHash, + Kind = kind, + }; component.Name.Should().Be(name); component.Version.Should().Be(version); @@ -30,45 +37,44 @@ public void Constructor_ShouldInitializeProperties() component.RepositoryUrl.Should().Be(new Uri(repositoryUrl)); component.Type.Should().Be(ComponentType.Swift); component.Id.Should().Be( - $"{repositoryUrl} {commitHash} - {component.Type}"); + $"{repositoryUrl} {version} - {component.Type}"); } [TestMethod] - public void Id_ShouldDistinguishRepositoriesAndCommitHashes() + public void Id_ShouldDistinguishRepositories() { - var component = new SwiftComponent( - "alamofire", - "5.9.1", - "https://github.com/Alamofire/Alamofire", - "f455c2975872ccd2d9c81594c658af65716e9b9a", - "remoteSourceControl"); - var differentRepository = new SwiftComponent( - "alamofire", - "5.9.1", - "https://github.com/example/Alamofire", - "f455c2975872ccd2d9c81594c658af65716e9b9a", - "remoteSourceControl"); - var differentCommit = new SwiftComponent( - "alamofire", - "5.9.1", - "https://github.com/Alamofire/Alamofire", - "63dfa86548c4e5d5c6fd6ed42f638e388cbce529", - "remoteSourceControl"); + var component = new SwiftComponent + { + Name = "alamofire", + Version = "5.9.1", + RepositoryUrl = new Uri("https://github.com/Alamofire/Alamofire"), + CommitHash = "f455c2975872ccd2d9c81594c658af65716e9b9a", + Kind = "remoteSourceControl", + }; + var differentRepository = new SwiftComponent + { + Name = "alamofire", + Version = "5.9.1", + RepositoryUrl = new Uri("https://github.com/example/Alamofire"), + CommitHash = "f455c2975872ccd2d9c81594c658af65716e9b9a", + Kind = "remoteSourceControl", + }; component.Id.Should().NotBe(differentRepository.Id); - component.Id.Should().NotBe(differentCommit.Id); } [TestMethod] public void Serialization_ShouldIncludeCommitHash() { var commitHash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; - TypedComponent component = new SwiftComponent( - name: "alamofire", - version: "5.9.1", - repositoryUrl: "https://github.com/Alamofire/Alamofire", - hash: commitHash, - kind: "remoteSourceControl"); + TypedComponent component = new SwiftComponent + { + Name = "alamofire", + Version = "5.9.1", + RepositoryUrl = new Uri("https://github.com/Alamofire/Alamofire"), + CommitHash = commitHash, + Kind = "remoteSourceControl", + }; using var json = JsonDocument.Parse(JsonSerializer.Serialize(component)); @@ -76,90 +82,6 @@ public void Serialization_ShouldIncludeCommitHash() json.RootElement.GetProperty("commitHash").GetString().Should().Be(commitHash); } - [TestMethod] - public void Constructor_ShouldThrowException_WhenKindIsNull() - { - Action action = () => - new SwiftComponent( - "alamofire", - "5.9.1", - "https://github.com/Alamofire/Alamofire", - "f455c2975872ccd2d9c81594c658af65716e9b9a", - null - ); - action.Should().Throw().WithMessage("*kind*"); - } - - [TestMethod] - public void Constructor_ShouldThrowException_WhenNameIsNull() - { - Action action = () => - new SwiftComponent( - null, - "5.9.1", - "https://github.com/Alamofire/Alamofire", - "f455c2975872ccd2d9c81594c658af65716e9b9a", - "remoteSourceControl" - ); - action.Should().Throw().WithMessage("*name*"); - } - - [TestMethod] - public void Constructor_ShouldThrowException_WhenVersionIsNull() - { - Action action = () => - new SwiftComponent( - "alamofire", - null, - "https://github.com/Alamofire/Alamofire", - "f455c2975872ccd2d9c81594c658af65716e9b9a", - "remoteSourceControl" - ); - action.Should().Throw().WithMessage("*version*"); - } - - [TestMethod] - public void Constructor_ShouldThrowException_WhenRepositoryUrlIsNull() - { - Action action = () => - new SwiftComponent( - "alamofire", - "5.9.1", - null, - "f455c2975872ccd2d9c81594c658af65716e9b9a", - "remoteSourceControl" - ); - action.Should().Throw().WithMessage("*repositoryUrl*"); - } - - [TestMethod] - public void Constructor_ShouldThrowException_WhenHashIsNull() - { - Action action = () => - new SwiftComponent( - "alamofire", - "5.9.1", - "https://github.com/Alamofire/Alamofire", - null, - "remoteSourceControl" - ); - action.Should().Throw().WithMessage("*hash*"); - } - - [TestMethod] - public void Constructor_ShouldThrowException_WhenRepositoryUrlIsInvalid() - { - Action action = () => - new SwiftComponent( - "alamofire", - "5.9.1", - "invalid-url", - "f455c2975872ccd2d9c81594c658af65716e9b9a", - "remoteSourceControl" - ); - action.Should().Throw(); - } - [TestMethod] public void PackageURL_ShouldReturnCorrectPackageURL_GithubHostname() { @@ -168,7 +90,14 @@ public void PackageURL_ShouldReturnCorrectPackageURL_GithubHostname() var repositoryUrl = "https://github.com/Alamofire/Alamofire"; var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; - var component = new SwiftComponent(name, version, repositoryUrl, hash, "remoteSourceControl"); + var component = new SwiftComponent + { + Name = name, + Version = version, + RepositoryUrl = new Uri(repositoryUrl), + CommitHash = hash, + Kind = "remoteSourceControl", + }; var expectedPackageURL = new PackageURL( type: "swift", @@ -190,7 +119,14 @@ public void PackageURL_ShouldReturnCorrectPackageURL_GithubHostname_Alternate() var repositoryUrl = "https://giTHub.com/Alamofire/Alamofire"; var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; - var component = new SwiftComponent(name, version, repositoryUrl, hash, "remoteSourceControl"); + var component = new SwiftComponent + { + Name = name, + Version = version, + RepositoryUrl = new Uri(repositoryUrl), + CommitHash = hash, + Kind = "remoteSourceControl", + }; var expectedPackageURL = new PackageURL( type: "swift", @@ -215,7 +151,14 @@ public void PackageURL_ShouldReturnCorrectPackageURL_OtherHostname() var repositoryUrl = "https://otherhostname.com/Alamofire/Alamofire"; var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; - var component = new SwiftComponent(name, version, repositoryUrl, hash, "remoteSourceControl"); + var component = new SwiftComponent + { + Name = name, + Version = version, + RepositoryUrl = new Uri(repositoryUrl), + CommitHash = hash, + Kind = "remoteSourceControl", + }; var expectedPackageURL = new PackageURL( type: "swift", diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs index e8f13b0eb..ffc58333f 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs @@ -1,6 +1,7 @@ #nullable disable namespace Microsoft.ComponentDetection.Detectors.Tests.Swift; +using System; using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; @@ -48,12 +49,14 @@ public async Task Test_GivenDetectorWithValidFile_WhenScan_ThenScanIsSuccessfulA var typedComponents = detectedComponents.Select(c => c.Component).ToList(); typedComponents.Should().ContainEquivalentOf( - new SwiftComponent( - name: "alamofire", - version: "5.9.1", - repositoryUrl: "https://github.com/Alamofire/Alamofire", - hash: "f455c2975872ccd2d9c81594c658af65716e9b9a", - kind: "remoteSourceControl")); + new SwiftComponent + { + Name = "alamofire", + Version = "5.9.1", + RepositoryUrl = new Uri("https://github.com/Alamofire/Alamofire"), + CommitHash = "f455c2975872ccd2d9c81594c658af65716e9b9a", + Kind = "remoteSourceControl", + }); } // Test for several packages @@ -76,20 +79,24 @@ public async Task Test_GivenDetectorWithValidFileWithMultiplePackages_WhenScan_T var typedComponents = detectedComponents.Select(c => c.Component).ToList(); typedComponents.Should().ContainEquivalentOf( - new SwiftComponent( - name: "alamofire", - version: "5.6.0", - repositoryUrl: "https://github.com/Alamofire/Alamofire", - hash: "63dfa86548c4e5d5c6fd6ed42f638e388cbce529", - kind: "remoteSourceControl")); + new SwiftComponent + { + Name = "alamofire", + Version = "5.6.0", + RepositoryUrl = new Uri("https://github.com/Alamofire/Alamofire"), + CommitHash = "63dfa86548c4e5d5c6fd6ed42f638e388cbce529", + Kind = "remoteSourceControl", + }); typedComponents.Should().ContainEquivalentOf( - new SwiftComponent( - name: "asyncextensions", - version: "0.5.3", - repositoryUrl: "https://github.com/sideeffect-io/AsyncExtensions", - hash: "3442d3d046800f1974bda096faaf0ac510b21154", - kind: "remoteSourceControl")); + new SwiftComponent + { + Name = "asyncextensions", + Version = "0.5.3", + RepositoryUrl = new Uri("https://github.com/sideeffect-io/AsyncExtensions"), + CommitHash = "3442d3d046800f1974bda096faaf0ac510b21154", + Kind = "remoteSourceControl", + }); } // Duplicate packages @@ -137,12 +144,14 @@ public async Task Test_GivenDetectorWithValidFileWithDuplicatePackages_WhenScan_ var typedComponents = detectedComponents.Select(c => c.Component).ToList(); typedComponents.Should().ContainEquivalentOf( - new SwiftComponent( - name: "alamofire", - version: "5.9.1", - repositoryUrl: "https://github.com/Alamofire/Alamofire", - hash: "f455c2975872ccd2d9c81594c658af65716e9b9a", - kind: "remoteSourceControl")); + new SwiftComponent + { + Name = "alamofire", + Version = "5.9.1", + RepositoryUrl = new Uri("https://github.com/Alamofire/Alamofire"), + CommitHash = "f455c2975872ccd2d9c81594c658af65716e9b9a", + Kind = "remoteSourceControl", + }); } [TestMethod] @@ -340,7 +349,7 @@ public async Task Test_GivenResolvedPackageWithEmptyState_WhenScan_ThenScanIsSuc } [TestMethod] - public async Task Test_GivenResolvedPackageWithoutRevision_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + public async Task Test_GivenResolvedPackageWithoutRevision_WhenScan_ThenScanIsSuccessfulAndComponentsRegistered() { var resolvedPackageWithoutRevision = """ { @@ -364,7 +373,7 @@ public async Task Test_GivenResolvedPackageWithoutRevision_WhenScan_ThenScanIsSu .ExecuteDetectorAsync(); scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().BeEmpty(); + componentRecorder.GetDetectedComponents().Should().HaveCount(1); } [TestMethod] @@ -392,18 +401,7 @@ public async Task Test_GivenResolvedPackageWithoutVersion_WhenScan_ThenScanIsSuc .ExecuteDetectorAsync(); scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().ContainSingle(); - - var detectedComponents = componentRecorder.GetDetectedComponents(); - var typedComponents = detectedComponents.Select(c => c.Component).ToList(); - - typedComponents.Should().ContainEquivalentOf( - new SwiftComponent( - name: "alamofire", - version: "f455c2975872ccd2d9c81594c658af65716e9b9a", - repositoryUrl: "https://github.com/Alamofire/Alamofire", - hash: "f455c2975872ccd2d9c81594c658af65716e9b9a", - kind: "remoteSourceControl")); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); } [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Test data that is better placed at the end of the file.")] From 553e928d2d2b9c0bf8e3e60fa1a65cc1bc8c0a4b Mon Sep 17 00:00:00 2001 From: "Ryan Brandenburg (He/Him)" Date: Wed, 30 Sep 2026 16:17:14 -0700 Subject: [PATCH 2/2] PR feedback --- .../swiftpm/SwiftResolvedComponentDetector.cs | 2 +- .../SwiftResolvedDetectorTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs index 3640a4065..d6cff8635 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs @@ -39,7 +39,7 @@ public SwiftResolvedComponentDetector( public override IEnumerable SupportedComponentTypes => [ComponentType.Swift]; - public override int Version => 2; + public override int Version => 3; protected override Task OnFileFoundAsync( ProcessRequest processRequest, diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs index ffc58333f..25322b00a 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs @@ -377,7 +377,7 @@ public async Task Test_GivenResolvedPackageWithoutRevision_WhenScan_ThenScanIsSu } [TestMethod] - public async Task Test_GivenResolvedPackageWithoutVersion_WhenScan_ThenScanIsSuccessfulAndComponentRegisteredWithRevisionHashAsVersion() + public async Task Test_GivenResolvedPackageWithoutVersion_WhenScan_ThenScanIsSuccessfulAndNoComponentRegistered() { var resolvedPackageWithoutVersion = """ {