From f4729719ac3ff807b11faba610923c205041ce13 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Wed, 19 Nov 2025 15:28:53 -0800 Subject: [PATCH] Migrate to file-scoped namespaces --- docs/creating-a-new-service.md | 34 +- .../uv/UvDependency.cs | 11 +- .../uv/UvLock.cs | 203 +++--- .../uv/UvLockComponentDetector.cs | 153 +++-- .../uv/UvPackage.cs | 29 +- .../uv/UvSource.cs | 11 +- .../UvLockTests.cs | 621 +++++++++--------- 7 files changed, 527 insertions(+), 535 deletions(-) diff --git a/docs/creating-a-new-service.md b/docs/creating-a-new-service.md index a16a889b6..a8bd1592e 100644 --- a/docs/creating-a-new-service.md +++ b/docs/creating-a-new-service.md @@ -7,13 +7,12 @@ Component Detection uses standard .NET dependency injection for service registra 1. **Create your service interface** in `src/Microsoft.ComponentDetection.Contracts/IMyNewService.cs` ```c# -namespace Microsoft.ComponentDetection.Contracts +namespace Microsoft.ComponentDetection.Contracts; + +public interface IMyNewService { - public interface IMyNewService - { - // Define your service methods - string DoSomething(); - } + // Define your service methods + string DoSomething(); } ``` @@ -22,20 +21,19 @@ namespace Microsoft.ComponentDetection.Contracts ```c# using Microsoft.ComponentDetection.Contracts; -namespace Microsoft.ComponentDetection.Common +namespace Microsoft.ComponentDetection.Common; + +public class MyNewService : IMyNewService { - public class MyNewService : IMyNewService + // Inject any dependencies your service needs + public MyNewService(ILogger logger) + { + // Constructor injection + } + + public string DoSomething() { - // Inject any dependencies your service needs - public MyNewService(ILogger logger) - { - // Constructor injection - } - - public string DoSomething() - { - // Implementation - } + // Implementation } } ``` diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs index 6e35866c2..442df6584 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs @@ -1,9 +1,8 @@ -namespace Microsoft.ComponentDetection.Detectors.Uv +namespace Microsoft.ComponentDetection.Detectors.Uv; + +public class UvDependency { - public class UvDependency - { - public required string Name { get; init; } + public required string Name { get; init; } - public string? Specifier { get; set; } - } + public string? Specifier { get; set; } } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs index 5000e1654..0f61f51d7 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs @@ -1,142 +1,141 @@ -namespace Microsoft.ComponentDetection.Detectors.Uv +namespace Microsoft.ComponentDetection.Detectors.Uv; + +using System; +using System.Collections.Generic; +using System.IO; +using Tomlyn; +using Tomlyn.Model; + +public class UvLock { - using System; - using System.Collections.Generic; - using System.IO; - using Tomlyn; - using Tomlyn.Model; + // a list of packages with their dependencies + public List Packages { get; set; } = []; - public class UvLock + // static method to parse the TOML stream into a UvLock model + public static UvLock Parse(Stream tomlStream) { - // a list of packages with their dependencies - public List Packages { get; set; } = []; + using var reader = new StreamReader(tomlStream); + var tomlContent = reader.ReadToEnd(); + var model = Toml.ToModel(tomlContent); + return new UvLock + { + Packages = ParsePackagesFromModel(model), + }; + } - // static method to parse the TOML stream into a UvLock model - public static UvLock Parse(Stream tomlStream) + internal static List ParsePackagesFromModel(object? model) + { + if (model is not TomlTable table) { - using var reader = new StreamReader(tomlStream); - var tomlContent = reader.ReadToEnd(); - var model = Toml.ToModel(tomlContent); - return new UvLock - { - Packages = ParsePackagesFromModel(model), - }; + throw new InvalidOperationException("TOML root is not a table"); } - internal static List ParsePackagesFromModel(object? model) + if (!table.TryGetValue("package", out var packagesObj) || packagesObj is not TomlTableArray packages) { - if (model is not TomlTable table) - { - throw new InvalidOperationException("TOML root is not a table"); - } + return []; + } - if (!table.TryGetValue("package", out var packagesObj) || packagesObj is not TomlTableArray packages) + var result = new List(); + foreach (var pkg in packages) + { + var parsed = ParsePackage(pkg); + if (parsed is not null) { - return []; + result.Add(parsed); } + } - var result = new List(); - foreach (var pkg in packages) - { - var parsed = ParsePackage(pkg); - if (parsed is not null) - { - result.Add(parsed); - } - } + return result; + } - return result; + internal static UvPackage? ParsePackage(object? pkg) + { + if (pkg is not TomlTable pkgTable) + { + return null; } - internal static UvPackage? ParsePackage(object? pkg) + if (pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name && + pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version) { - if (pkg is not TomlTable pkgTable) + var uvPackage = new UvPackage { - return null; - } + Name = name, + Version = version, + Dependencies = [], + MetadataRequiresDist = [], + MetadataRequiresDev = [], + }; - if (pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name && - pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version) + if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray) { - var uvPackage = new UvPackage - { - Name = name, - Version = version, - Dependencies = [], - MetadataRequiresDist = [], - MetadataRequiresDev = [], - }; - - if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray) - { - uvPackage.Dependencies = ParseDependenciesArray(depsArray); - } - - if (pkgTable.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) - { - ParseMetadata(metadataTable, uvPackage); - } - - // Parse source - if (pkgTable.TryGetValue("source", out var sourceObj) && sourceObj is TomlTable sourceTable) - { - var source = new UvSource - { - Registry = sourceTable.TryGetValue("registry", out var regObj) && regObj is string reg ? reg : null, - Virtual = sourceTable.TryGetValue("virtual", out var virtObj) && virtObj is string virt ? virt : null, - }; - uvPackage.Source = source; - } - - return uvPackage; + uvPackage.Dependencies = ParseDependenciesArray(depsArray); } - return null; - } - - internal static List ParseDependenciesArray(TomlArray? depsArray) - { - var deps = new List(); - if (depsArray is null) + if (pkgTable.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable) { - return deps; + ParseMetadata(metadataTable, uvPackage); } - foreach (var dep in depsArray) + // Parse source + if (pkgTable.TryGetValue("source", out var sourceObj) && sourceObj is TomlTable sourceTable) { - if (dep is TomlTable depTable && - depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) + var source = new UvSource { - var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; - deps.Add(new UvDependency - { - Name = depName, - Specifier = depSpec, - }); - } + Registry = sourceTable.TryGetValue("registry", out var regObj) && regObj is string reg ? reg : null, + Virtual = sourceTable.TryGetValue("virtual", out var virtObj) && virtObj is string virt ? virt : null, + }; + uvPackage.Source = source; } + return uvPackage; + } + + return null; + } + + internal static List ParseDependenciesArray(TomlArray? depsArray) + { + var deps = new List(); + if (depsArray is null) + { return deps; } - internal static void ParseMetadata(TomlTable? metadataTable, UvPackage uvPackage) + foreach (var dep in depsArray) { - if (metadataTable is null) + if (dep is TomlTable depTable && + depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName) { - return; + var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null; + deps.Add(new UvDependency + { + Name = depName, + Specifier = depSpec, + }); } + } - if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlArray requiresDistArr) - { - uvPackage.MetadataRequiresDist = ParseDependenciesArray(requiresDistArr); - } + return deps; + } - if (metadataTable.TryGetValue("requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable) + internal static void ParseMetadata(TomlTable? metadataTable, UvPackage uvPackage) + { + if (metadataTable is null) + { + return; + } + + if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlArray requiresDistArr) + { + uvPackage.MetadataRequiresDist = ParseDependenciesArray(requiresDistArr); + } + + if (metadataTable.TryGetValue("requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable) + { + if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlArray devArr) { - if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlArray devArr) - { - uvPackage.MetadataRequiresDev = ParseDependenciesArray(devArr); - } + uvPackage.MetadataRequiresDev = ParseDependenciesArray(devArr); } } } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs index 04acd9740..4b1ec4256 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvLockComponentDetector.cs @@ -1,104 +1,103 @@ -namespace Microsoft.ComponentDetection.Detectors.Uv +namespace Microsoft.ComponentDetection.Detectors.Uv; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.Extensions.Logging; + +public class UvLockComponentDetector : FileComponentDetector, IExperimentalDetector { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.ComponentDetection.Contracts; - using Microsoft.ComponentDetection.Contracts.Internal; - using Microsoft.ComponentDetection.Contracts.TypedComponent; - using Microsoft.Extensions.Logging; - - public class UvLockComponentDetector : FileComponentDetector, IExperimentalDetector + public UvLockComponentDetector( + IComponentStreamEnumerableFactory componentStreamEnumerableFactory, + IObservableDirectoryWalkerFactory walkerFactory, + ILogger logger) { - public UvLockComponentDetector( - IComponentStreamEnumerableFactory componentStreamEnumerableFactory, - IObservableDirectoryWalkerFactory walkerFactory, - ILogger logger) - { - this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; - this.Scanner = walkerFactory; - this.Logger = logger; - } + this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; + this.Scanner = walkerFactory; + this.Logger = logger; + } - public override string Id => "UvLock"; + public override string Id => "UvLock"; - public override IList SearchPatterns { get; } = ["uv.lock"]; + public override IList SearchPatterns { get; } = ["uv.lock"]; - public override IEnumerable SupportedComponentTypes => [ComponentType.Pip]; + public override IEnumerable SupportedComponentTypes => [ComponentType.Pip]; - public override int Version => 1; + public override int Version => 1; - public override IEnumerable Categories => ["Python"]; + public override IEnumerable Categories => ["Python"]; - internal static bool IsRootPackage(UvPackage pck) - { - return pck.Source?.Virtual != null; - } + internal static bool IsRootPackage(UvPackage pck) + { + return pck.Source?.Virtual != null; + } - protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) + protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + try { - var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; - var file = processRequest.ComponentStream; + // Parse the file stream into a UvLock model + file.Stream.Position = 0; // Ensure stream is at the beginning + var uvLock = UvLock.Parse(file.Stream); - try - { - // Parse the file stream into a UvLock model - file.Stream.Position = 0; // Ensure stream is at the beginning - var uvLock = UvLock.Parse(file.Stream); + var rootPackage = uvLock.Packages.FirstOrDefault(IsRootPackage); + var explicitPackages = new HashSet(); + var devPackages = new HashSet(); - var rootPackage = uvLock.Packages.FirstOrDefault(IsRootPackage); - var explicitPackages = new HashSet(); - var devPackages = new HashSet(); + if (rootPackage != null) + { + foreach (var dep in rootPackage.MetadataRequiresDist) + { + explicitPackages.Add(dep.Name); + } - if (rootPackage != null) + foreach (var devDep in rootPackage.MetadataRequiresDev) { - foreach (var dep in rootPackage.MetadataRequiresDist) - { - explicitPackages.Add(dep.Name); - } + devPackages.Add(devDep.Name); + } + } - foreach (var devDep in rootPackage.MetadataRequiresDev) - { - devPackages.Add(devDep.Name); - } + foreach (var pkg in uvLock.Packages) + { + if (IsRootPackage(pkg)) + { + continue; } - foreach (var pkg in uvLock.Packages) + var pipComponent = new PipComponent(pkg.Name, pkg.Version); + var isExplicit = explicitPackages.Contains(pkg.Name); + var isDev = devPackages.Contains(pkg.Name); + var detectedComponent = new DetectedComponent(pipComponent); + singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDev, isExplicitReferencedDependency: isExplicit); + + foreach (var dep in pkg.Dependencies) { - if (IsRootPackage(pkg)) + var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); + if (depPkg != null) { - continue; + var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id); } - - var pipComponent = new PipComponent(pkg.Name, pkg.Version); - var isExplicit = explicitPackages.Contains(pkg.Name); - var isDev = devPackages.Contains(pkg.Name); - var detectedComponent = new DetectedComponent(pipComponent); - singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDev, isExplicitReferencedDependency: isExplicit); - - foreach (var dep in pkg.Dependencies) + else { - var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase)); - if (depPkg != null) - { - var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version); - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id); - } - else - { - this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name); - } + this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name); } } } - catch (Exception ex) - { - this.Logger.LogError(ex, "Failed to parse uv.lock file {File}", file.Location); - } - - return Task.CompletedTask; } + catch (Exception ex) + { + this.Logger.LogError(ex, "Failed to parse uv.lock file {File}", file.Location); + } + + return Task.CompletedTask; } } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs index eae14bbde..9a1c9acd2 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs @@ -1,22 +1,21 @@ -namespace Microsoft.ComponentDetection.Detectors.Uv -{ - using System.Collections.Generic; +namespace Microsoft.ComponentDetection.Detectors.Uv; + +using System.Collections.Generic; - public class UvPackage - { - public required string Name { get; init; } +public class UvPackage +{ + public required string Name { get; init; } - public required string Version { get; init; } + public required string Version { get; init; } - public List Dependencies { get; set; } = []; + public List Dependencies { get; set; } = []; - // Metadata dependencies (requires-dist) - public List MetadataRequiresDist { get; set; } = []; + // Metadata dependencies (requires-dist) + public List MetadataRequiresDist { get; set; } = []; - // Metadata dev dependencies (requires-dev) - public List MetadataRequiresDev { get; set; } = []; + // Metadata dev dependencies (requires-dev) + public List MetadataRequiresDev { get; set; } = []; - // Source property for uv.lock - public UvSource? Source { get; set; } - } + // Source property for uv.lock + public UvSource? Source { get; set; } } diff --git a/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs b/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs index 53b975825..80ea4ba15 100644 --- a/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs +++ b/src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs @@ -1,9 +1,8 @@ -namespace Microsoft.ComponentDetection.Detectors.Uv +namespace Microsoft.ComponentDetection.Detectors.Uv; + +public class UvSource { - public class UvSource - { - public string? Registry { get; set; } + public string? Registry { get; set; } - public string? Virtual { get; set; } - } + public string? Virtual { get; set; } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs index 9dc1dc824..589fa8234 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/UvLockTests.cs @@ -1,22 +1,22 @@ #nullable disable -namespace Microsoft.ComponentDetection.Detectors.Tests +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System; +using System.IO; +using System.Linq; +using System.Text; +using AwesomeAssertions; +using Microsoft.ComponentDetection.Detectors.Uv; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Tomlyn.Model; + +[TestClass] +public class UvLockTests { - using System; - using System.IO; - using System.Linq; - using System.Text; - using AwesomeAssertions; - using Microsoft.ComponentDetection.Detectors.Uv; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Tomlyn.Model; - - [TestClass] - public class UvLockTests + [TestMethod] + public void Parse_ParsesMetadataRequiresDistAndDev() { - [TestMethod] - public void Parse_ParsesMetadataRequiresDistAndDev() - { - var toml = """ + var toml = """ [[package]] name = "component-detection" version = "0.0.0" @@ -35,35 +35,35 @@ public void Parse_ParsesMetadataRequiresDistAndDev() { name = "pytest-env", specifier = ">=1.1.5" }, ] """; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - - var package = uvLock.Packages.First(); - package.Name.Should().Be("component-detection"); - package.Version.Should().Be("0.0.0"); - - package.MetadataRequiresDist.Should().BeEquivalentTo( - [ - new UvDependency { Name = "azure-identity", Specifier = "==1.17.1" }, - new UvDependency { Name = "flask", Specifier = ">2,<3" }, - new UvDependency { Name = "requests", Specifier = ">=2.32.0" }, - ], - options => options.ComparingByMembers()); - - package.MetadataRequiresDev.Should().BeEquivalentTo( - [ - new UvDependency { Name = "pytest", Specifier = ">=8.3.4" }, - new UvDependency { Name = "pytest-cov", Specifier = ">=6.0.0" }, - new UvDependency { Name = "pytest-env", Specifier = ">=1.1.5" }, - ], - options => options.ComparingByMembers()); - } - - [TestMethod] - public void Parse_ParsesPackagesAndDependencies() - { - var toml = @" + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + + var package = uvLock.Packages.First(); + package.Name.Should().Be("component-detection"); + package.Version.Should().Be("0.0.0"); + + package.MetadataRequiresDist.Should().BeEquivalentTo( + [ + new UvDependency { Name = "azure-identity", Specifier = "==1.17.1" }, + new UvDependency { Name = "flask", Specifier = ">2,<3" }, + new UvDependency { Name = "requests", Specifier = ">=2.32.0" }, + ], + options => options.ComparingByMembers()); + + package.MetadataRequiresDev.Should().BeEquivalentTo( + [ + new UvDependency { Name = "pytest", Specifier = ">=8.3.4" }, + new UvDependency { Name = "pytest-cov", Specifier = ">=6.0.0" }, + new UvDependency { Name = "pytest-env", Specifier = ">=1.1.5" }, + ], + options => options.ComparingByMembers()); + } + + [TestMethod] + public void Parse_ParsesPackagesAndDependencies() + { + var toml = @" [[package]] name = 'foo' version = '1.2.3' @@ -74,82 +74,82 @@ public void Parse_ParsesPackagesAndDependencies() name = 'bar' version = '2.0.0' "; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().HaveCount(2); - uvLock.Packages.First().Name.Should().Be("foo"); - uvLock.Packages.First().Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); - } - - [TestMethod] - public void Parse_EmptyStream_ReturnsNoPackages() - { - using var ms = new MemoryStream([]); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().BeEmpty(); - } + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().HaveCount(2); + uvLock.Packages.First().Name.Should().Be("foo"); + uvLock.Packages.First().Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + } - [TestMethod] - public void Parse_TomlNotATable_ThrowsException() - { - var toml = "42"; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - FluentActions.Invoking(() => UvLock.Parse(ms)) - .Should().Throw(); - } - - [TestMethod] - public void Parse_NoPackageKey_ReturnsNoPackages() - { - var toml = "[metadata]\nversion = '1'"; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().BeEmpty(); - } - - [TestMethod] - public void Parse_PackageKeyNotArray_ReturnsNoPackages() - { - var toml = "package = 42"; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().BeEmpty(); - } - - [TestMethod] - public void Parse_PackageMissingNameOrVersion_IgnoresPackage() - { - var toml = @" + [TestMethod] + public void Parse_EmptyStream_ReturnsNoPackages() + { + using var ms = new MemoryStream([]); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_TomlNotATable_ThrowsException() + { + var toml = "42"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + FluentActions.Invoking(() => UvLock.Parse(ms)) + .Should().Throw(); + } + + [TestMethod] + public void Parse_NoPackageKey_ReturnsNoPackages() + { + var toml = "[metadata]\nversion = '1'"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_PackageKeyNotArray_ReturnsNoPackages() + { + var toml = "package = 42"; + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } + + [TestMethod] + public void Parse_PackageMissingNameOrVersion_IgnoresPackage() + { + var toml = @" [[package]] version = '1.2.3' [[package]] name = 'foo' "; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().BeEmpty(); - } + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().BeEmpty(); + } - [TestMethod] - public void Parse_PackageWithMalformedDependencies_IgnoresMalformed() - { - var toml = @" + [TestMethod] + public void Parse_PackageWithMalformedDependencies_IgnoresMalformed() + { + var toml = @" [[package]] name = 'foo' version = '1.2.3' dependencies = [42, { name = 'bar' }] "; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - var pkg = uvLock.Packages.First(); - pkg.Dependencies.Should().ContainSingle(d => d.Name == "bar"); - } - - [TestMethod] - public void Parse_PackageWithMalformedMetadata_IgnoresMalformed() - { - var toml = @" + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Dependencies.Should().ContainSingle(d => d.Name == "bar"); + } + + [TestMethod] + public void Parse_PackageWithMalformedMetadata_IgnoresMalformed() + { + var toml = @" [[package]] name = 'foo' version = '1.2.3' @@ -158,239 +158,238 @@ public void Parse_PackageWithMalformedMetadata_IgnoresMalformed() [package.metadata.requires-dev] dev = [42, { name = 'baz' }] "; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - var pkg = uvLock.Packages.First(); - pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); - pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); - } - - [TestMethod] - public void ParsePackagesFromModel_InvalidRoot_Throws() - { - Action act = () => UvLock.ParsePackagesFromModel(42); - act.Should().Throw(); - } + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); + } - [TestMethod] - public void ParsePackagesFromModel_NoPackages_ReturnsEmpty() - { - var table = new TomlTable(); - var result = UvLock.ParsePackagesFromModel(table); - result.Should().BeEmpty(); - } + [TestMethod] + public void ParsePackagesFromModel_InvalidRoot_Throws() + { + Action act = () => UvLock.ParsePackagesFromModel(42); + act.Should().Throw(); + } - [TestMethod] - public void ParsePackage_ValidPackage_ParsesCorrectly() - { - var pkg = new TomlTable - { - ["name"] = "foo", - ["version"] = "1.0.0", - ["dependencies"] = new TomlArray { new TomlTable { ["name"] = "bar", ["specifier"] = ">=2.0.0" } }, - }; - var result = UvLock.ParsePackage(pkg); - result.Should().NotBeNull(); - result.Name.Should().Be("foo"); - result.Version.Should().Be("1.0.0"); - result.Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); - } - - [TestMethod] - public void ParsePackage_MissingNameOrVersion_ReturnsNull() - { - var pkg1 = new TomlTable { ["version"] = "1.0.0" }; - var pkg2 = new TomlTable { ["name"] = "foo" }; - UvLock.ParsePackage(pkg1).Should().BeNull(); - UvLock.ParsePackage(pkg2).Should().BeNull(); - } - - [TestMethod] - public void ParsePackage_NullOrNonTable_ReturnsNull() - { - UvLock.ParsePackage(null).Should().BeNull(); - UvLock.ParsePackage(42).Should().BeNull(); - } + [TestMethod] + public void ParsePackagesFromModel_NoPackages_ReturnsEmpty() + { + var table = new TomlTable(); + var result = UvLock.ParsePackagesFromModel(table); + result.Should().BeEmpty(); + } - [TestMethod] - public void ParsePackage_BranchCoverage_AllPaths() + [TestMethod] + public void ParsePackage_ValidPackage_ParsesCorrectly() + { + var pkg = new TomlTable { - // Path: pkg is TomlTable, but missing name - var pkgMissingName = new TomlTable { ["version"] = "1.0.0" }; - UvLock.ParsePackage(pkgMissingName).Should().BeNull(); + ["name"] = "foo", + ["version"] = "1.0.0", + ["dependencies"] = new TomlArray { new TomlTable { ["name"] = "bar", ["specifier"] = ">=2.0.0" } }, + }; + var result = UvLock.ParsePackage(pkg); + result.Should().NotBeNull(); + result.Name.Should().Be("foo"); + result.Version.Should().Be("1.0.0"); + result.Dependencies.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + } - // Path: pkg is TomlTable, but missing version - var pkgMissingVersion = new TomlTable { ["name"] = "foo" }; - UvLock.ParsePackage(pkgMissingVersion).Should().BeNull(); - } + [TestMethod] + public void ParsePackage_MissingNameOrVersion_ReturnsNull() + { + var pkg1 = new TomlTable { ["version"] = "1.0.0" }; + var pkg2 = new TomlTable { ["name"] = "foo" }; + UvLock.ParsePackage(pkg1).Should().BeNull(); + UvLock.ParsePackage(pkg2).Should().BeNull(); + } - [TestMethod] - public void ParseDependenciesArray_ParsesValidDepsAndSkipsMalformed() - { - var arr = new TomlArray { 42, new TomlTable { ["name"] = "bar", ["specifier"] = "==1.2.3" }, new TomlTable { ["name"] = "baz" } }; - var result = UvLock.ParseDependenciesArray(arr); - result.Should().Contain(d => d.Name == "bar" && d.Specifier == "==1.2.3"); - result.Should().Contain(d => d.Name == "baz" && d.Specifier == null); - result.Should().HaveCount(2); - } - - [TestMethod] - public void ParseDependenciesArray_NullOrNoValidDeps_ReturnsEmpty() - { - UvLock.ParseDependenciesArray(null).Should().BeEmpty(); - var arr = new TomlArray { 42, "foo", 3.14 }; - UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); - } + [TestMethod] + public void ParsePackage_NullOrNonTable_ReturnsNull() + { + UvLock.ParsePackage(null).Should().BeNull(); + UvLock.ParsePackage(42).Should().BeNull(); + } - [TestMethod] - public void ParseDependenciesArray_BranchCoverage_AllPaths() - { - // Path: dep is TomlTable but missing name - var arr = new TomlArray { new TomlTable { ["specifier"] = "==1.2.3" } }; - UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); - } + [TestMethod] + public void ParsePackage_BranchCoverage_AllPaths() + { + // Path: pkg is TomlTable, but missing name + var pkgMissingName = new TomlTable { ["version"] = "1.0.0" }; + UvLock.ParsePackage(pkgMissingName).Should().BeNull(); - [TestMethod] - public void ParseMetadata_ParsesRequiresDistAndDev() - { - var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; - var metadata = new TomlTable - { - ["requires-dist"] = new TomlArray { new TomlTable { ["name"] = "bar", ["specifier"] = ">=2.0.0" } }, - ["requires-dev"] = new TomlTable { ["dev"] = new TomlArray { new TomlTable { ["name"] = "baz" } } }, - }; - UvLock.ParseMetadata(metadata, pkg); - pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); - pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz" && d.Specifier == null); - } - - [TestMethod] - public void ParseMetadata_NullOrNoRelevantKeys_DoesNothing() + // Path: pkg is TomlTable, but missing version + var pkgMissingVersion = new TomlTable { ["name"] = "foo" }; + UvLock.ParsePackage(pkgMissingVersion).Should().BeNull(); + } + + [TestMethod] + public void ParseDependenciesArray_ParsesValidDepsAndSkipsMalformed() + { + var arr = new TomlArray { 42, new TomlTable { ["name"] = "bar", ["specifier"] = "==1.2.3" }, new TomlTable { ["name"] = "baz" } }; + var result = UvLock.ParseDependenciesArray(arr); + result.Should().Contain(d => d.Name == "bar" && d.Specifier == "==1.2.3"); + result.Should().Contain(d => d.Name == "baz" && d.Specifier == null); + result.Should().HaveCount(2); + } + + [TestMethod] + public void ParseDependenciesArray_NullOrNoValidDeps_ReturnsEmpty() + { + UvLock.ParseDependenciesArray(null).Should().BeEmpty(); + var arr = new TomlArray { 42, "foo", 3.14 }; + UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); + } + + [TestMethod] + public void ParseDependenciesArray_BranchCoverage_AllPaths() + { + // Path: dep is TomlTable but missing name + var arr = new TomlArray { new TomlTable { ["specifier"] = "==1.2.3" } }; + UvLock.ParseDependenciesArray(arr).Should().BeEmpty(); + } + + [TestMethod] + public void ParseMetadata_ParsesRequiresDistAndDev() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable { - var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; - UvLock.ParseMetadata(null, pkg); // Should not throw - var emptyTable = new TomlTable(); - UvLock.ParseMetadata(emptyTable, pkg); // Should not throw or set anything - pkg.MetadataRequiresDist.Should().BeEmpty(); - pkg.MetadataRequiresDev.Should().BeEmpty(); - } - - [TestMethod] - public void ParseMetadata_BranchCoverage_RequiresDistOnly() + ["requires-dist"] = new TomlArray { new TomlTable { ["name"] = "bar", ["specifier"] = ">=2.0.0" } }, + ["requires-dev"] = new TomlTable { ["dev"] = new TomlArray { new TomlTable { ["name"] = "baz" } } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar" && d.Specifier == ">=2.0.0"); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz" && d.Specifier == null); + } + + [TestMethod] + public void ParseMetadata_NullOrNoRelevantKeys_DoesNothing() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + UvLock.ParseMetadata(null, pkg); // Should not throw + var emptyTable = new TomlTable(); + UvLock.ParseMetadata(emptyTable, pkg); // Should not throw or set anything + pkg.MetadataRequiresDist.Should().BeEmpty(); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } + + [TestMethod] + public void ParseMetadata_BranchCoverage_RequiresDistOnly() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable { - var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; - var metadata = new TomlTable - { - ["requires-dist"] = new TomlArray { new TomlTable { ["name"] = "bar" } }, - }; - UvLock.ParseMetadata(metadata, pkg); - pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); - pkg.MetadataRequiresDev.Should().BeEmpty(); - } - - [TestMethod] - public void ParseMetadata_BranchCoverage_RequiresDevOnly() + ["requires-dist"] = new TomlArray { new TomlTable { ["name"] = "bar" } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().ContainSingle(d => d.Name == "bar"); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } + + [TestMethod] + public void ParseMetadata_BranchCoverage_RequiresDevOnly() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + var metadata = new TomlTable { - var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; - var metadata = new TomlTable - { - ["requires-dev"] = new TomlTable { ["dev"] = new TomlArray { new TomlTable { ["name"] = "baz" } } }, - }; - UvLock.ParseMetadata(metadata, pkg); - pkg.MetadataRequiresDist.Should().BeEmpty(); - pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); - } - - [TestMethod] - public void ParseMetadata_RequiresDevTableWithoutDevArray_DoesNotThrowOrSet() + ["requires-dev"] = new TomlTable { ["dev"] = new TomlArray { new TomlTable { ["name"] = "baz" } } }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDist.Should().BeEmpty(); + pkg.MetadataRequiresDev.Should().ContainSingle(d => d.Name == "baz"); + } + + [TestMethod] + public void ParseMetadata_RequiresDevTableWithoutDevArray_DoesNotThrowOrSet() + { + var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; + + // requires-dev exists but no "dev" key + var metadata = new TomlTable { - var pkg = new UvPackage { Name = "foo", Version = "1.0.0" }; - - // requires-dev exists but no "dev" key - var metadata = new TomlTable - { - ["requires-dev"] = new TomlTable { ["notdev"] = 42 }, - }; - UvLock.ParseMetadata(metadata, pkg); - pkg.MetadataRequiresDev.Should().BeEmpty(); - - // requires-dev exists, "dev" is not a TomlArray - metadata = new TomlTable - { - ["requires-dev"] = new TomlTable { ["dev"] = 42 }, - }; - UvLock.ParseMetadata(metadata, pkg); - pkg.MetadataRequiresDev.Should().BeEmpty(); - } - - [TestMethod] - public void ParsePackage_ParsesSourceRegistryAndVirtual() + ["requires-dev"] = new TomlTable { ["notdev"] = 42 }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDev.Should().BeEmpty(); + + // requires-dev exists, "dev" is not a TomlArray + metadata = new TomlTable { - var toml = """ + ["requires-dev"] = new TomlTable { ["dev"] = 42 }, + }; + UvLock.ParseMetadata(metadata, pkg); + pkg.MetadataRequiresDev.Should().BeEmpty(); + } + + [TestMethod] + public void ParsePackage_ParsesSourceRegistryAndVirtual() + { + var toml = """ [[package]] name = 'foo' version = '1.0.0' source = { registry = 'https://example.com/', virtual = '.' } """; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - var pkg = uvLock.Packages.First(); - pkg.Source.Should().NotBeNull(); - pkg.Source!.Registry.Should().Be("https://example.com/"); - pkg.Source.Virtual.Should().Be("."); - } - - [TestMethod] - public void ParsePackage_ParsesSource_RegistryOnly() - { - var toml = """ + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().Be("https://example.com/"); + pkg.Source.Virtual.Should().Be("."); + } + + [TestMethod] + public void ParsePackage_ParsesSource_RegistryOnly() + { + var toml = """ [[package]] name = 'foo' version = '1.0.0' source = { registry = 'https://example.com/' } """; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - var pkg = uvLock.Packages.First(); - pkg.Source.Should().NotBeNull(); - pkg.Source!.Registry.Should().Be("https://example.com/"); - pkg.Source.Virtual.Should().BeNull(); - } - - [TestMethod] - public void ParsePackage_ParsesSource_VirtualOnly() - { - var toml = """ + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().Be("https://example.com/"); + pkg.Source.Virtual.Should().BeNull(); + } + + [TestMethod] + public void ParsePackage_ParsesSource_VirtualOnly() + { + var toml = """ [[package]] name = 'foo' version = '1.0.0' source = { virtual = '.' } """; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - var pkg = uvLock.Packages.First(); - pkg.Source.Should().NotBeNull(); - pkg.Source!.Registry.Should().BeNull(); - pkg.Source.Virtual.Should().Be("."); - } - - [TestMethod] - public void ParsePackage_ParsesSource_Missing() - { - var toml = """ + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().NotBeNull(); + pkg.Source!.Registry.Should().BeNull(); + pkg.Source.Virtual.Should().Be("."); + } + + [TestMethod] + public void ParsePackage_ParsesSource_Missing() + { + var toml = """ [[package]] name = 'foo' version = '1.0.0' """; - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); - var uvLock = UvLock.Parse(ms); - uvLock.Packages.Should().ContainSingle(); - var pkg = uvLock.Packages.First(); - pkg.Source.Should().BeNull(); - } + using var ms = new MemoryStream(Encoding.UTF8.GetBytes(toml)); + var uvLock = UvLock.Parse(ms); + uvLock.Packages.Should().ContainSingle(); + var pkg = uvLock.Packages.First(); + pkg.Source.Should().BeNull(); } }