diff --git a/CompositeKey.slnx b/CompositeKey.slnx index 8dcb9fa..1c5d121 100644 --- a/CompositeKey.slnx +++ b/CompositeKey.slnx @@ -8,6 +8,7 @@ + diff --git a/src/CompositeKey.Analyzers.Common.UnitTests/CompositeKey.Analyzers.Common.UnitTests.csproj b/src/CompositeKey.Analyzers.Common.UnitTests/CompositeKey.Analyzers.Common.UnitTests.csproj new file mode 100644 index 0000000..bebdef8 --- /dev/null +++ b/src/CompositeKey.Analyzers.Common.UnitTests/CompositeKey.Analyzers.Common.UnitTests.csproj @@ -0,0 +1,40 @@ + + + + net9.0;net8.0 + true + + false + true + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/CompositeKey.Analyzers.Common.UnitTests/TestHelpers/CompilationTestHelper.cs b/src/CompositeKey.Analyzers.Common.UnitTests/TestHelpers/CompilationTestHelper.cs new file mode 100644 index 0000000..a67b240 --- /dev/null +++ b/src/CompositeKey.Analyzers.Common.UnitTests/TestHelpers/CompilationTestHelper.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CompositeKey.Analyzers.Common.UnitTests.TestHelpers; + +/// +/// Test helper for creating Roslyn compilation objects for testing validation logic. +/// +public static class CompilationTestHelper +{ + private static readonly Assembly SystemRuntimeAssembly = Assembly.Load(new AssemblyName("System.Runtime")); + private static readonly CSharpParseOptions DefaultParseOptions = new(LanguageVersion.CSharp11); + + /// + /// Creates a C# compilation from source code. + /// + public static CSharpCompilation CreateCompilation(string source, string assemblyName = "TestAssembly") + { + List references = + [ + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Guid).Assembly.Location), + MetadataReference.CreateFromFile(SystemRuntimeAssembly.Location), + MetadataReference.CreateFromFile(typeof(CompositeKeyAttribute).Assembly.Location), + ]; + + return CSharpCompilation.Create( + assemblyName, + [CSharpSyntaxTree.ParseText(source, DefaultParseOptions)], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + /// + /// Gets the semantic model and type declaration syntax for the first type in the compilation. + /// + public static (SemanticModel SemanticModel, TypeDeclarationSyntax TypeDeclaration, INamedTypeSymbol TypeSymbol) + GetFirstTypeInfo(CSharpCompilation compilation, string typeName) + { + var syntaxTree = compilation.SyntaxTrees.First(); + var semanticModel = compilation.GetSemanticModel(syntaxTree); + + var typeDeclaration = syntaxTree.GetRoot() + .DescendantNodes() + .OfType() + .First(t => t.Identifier.ValueText == typeName); + + var typeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration) + ?? throw new InvalidOperationException($"Could not get type symbol for {typeName}"); + + return (semanticModel, typeDeclaration, typeSymbol); + } + + /// + /// Gets the CompositeKeyConstructorAttribute type symbol from the compilation. + /// + public static INamedTypeSymbol? GetCompositeKeyConstructorAttributeSymbol(CSharpCompilation compilation) + { + return compilation.GetTypeByMetadataName("CompositeKey.CompositeKeyConstructorAttribute"); + } +} diff --git a/src/CompositeKey.Analyzers.Common.UnitTests/Validation/TypeValidationTests.cs b/src/CompositeKey.Analyzers.Common.UnitTests/Validation/TypeValidationTests.cs new file mode 100644 index 0000000..4b287b0 --- /dev/null +++ b/src/CompositeKey.Analyzers.Common.UnitTests/Validation/TypeValidationTests.cs @@ -0,0 +1,572 @@ +using CompositeKey.Analyzers.Common.Diagnostics; +using CompositeKey.Analyzers.Common.UnitTests.TestHelpers; +using CompositeKey.Analyzers.Common.Validation; + +namespace CompositeKey.Analyzers.Common.UnitTests.Validation; + +public static class TypeValidationTests +{ + public class ValidateTypeForCompositeKeyTests + { + [Fact] + public void ValidRecord_ShouldReturnSuccess() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + [CompositeKey("{Id}")] + public partial record TestKey(Guid Id); + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (semanticModel, typeDeclaration, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + var result = TypeValidation.ValidateTypeForCompositeKey( + typeSymbol, + typeDeclaration, + semanticModel, + attributeSymbol, + CancellationToken.None); + + // Assert + result.IsSuccess.ShouldBeTrue(); + result.Constructor.ShouldNotBeNull(); + result.TargetTypeDeclarations.ShouldNotBeNull(); + result.TargetTypeDeclarations.ShouldContain("public partial record TestKey"); + } + + [Theory] + [InlineData("class", "TestClass")] + [InlineData("struct", "TestStruct")] + [InlineData("interface", "TestInterface")] + public void NonRecordType_ShouldReturnFailureWithUnsupportedCompositeType(string typeKind, string typeName) + { + // Arrange + string source = typeKind switch + { + "interface" => $$""" + using System; + using CompositeKey; + + [CompositeKey("{Id}")] + public partial {{typeKind}} {{typeName}} + { + Guid Id { get; set; } + } + """, + _ => $$""" + using System; + using CompositeKey; + + [CompositeKey("{Id}")] + public partial {{typeKind}} {{typeName}} + { + public Guid Id { get; set; } + } + """ + }; + + var compilation = CompilationTestHelper.CreateCompilation(source); + var (semanticModel, typeDeclaration, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, typeName); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + var result = TypeValidation.ValidateTypeForCompositeKey( + typeSymbol, + typeDeclaration, + semanticModel, + attributeSymbol, + CancellationToken.None); + + // Assert + result.IsSuccess.ShouldBeFalse(); + result.Descriptor.ShouldBe(DiagnosticDescriptors.UnsupportedCompositeType); + result.MessageArgs.ShouldNotBeNull(); + result.MessageArgs![0].ShouldBe(typeName); + } + + [Fact] + public void NonPartialRecord_ShouldReturnFailureWithCompositeTypeMustBePartial() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + [CompositeKey("{Id}")] + public record TestKey(Guid Id); + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (semanticModel, typeDeclaration, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + var result = TypeValidation.ValidateTypeForCompositeKey( + typeSymbol, + typeDeclaration, + semanticModel, + attributeSymbol, + CancellationToken.None); + + // Assert + result.IsSuccess.ShouldBeFalse(); + result.Descriptor.ShouldBe(DiagnosticDescriptors.CompositeTypeMustBePartial); + result.MessageArgs.ShouldNotBeNull(); + result.MessageArgs![0].ShouldBe("TestKey"); + } + + [Fact] + public void RecordWithMultiplePublicConstructorsAndNoParameterless_ShouldReturnFailureWithNoObviousDefaultConstructor() + { + // Arrange - Multiple constructors but none parameterless and none attributed + const string Source = """ + using System; + using CompositeKey; + + [CompositeKey("{Id}")] + public partial record TestKey + { + public Guid Id { get; set; } + public string Name { get; set; } + + public TestKey(Guid id) { Id = id; Name = ""; } + public TestKey(Guid id, string name) { Id = id; Name = name; } + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (semanticModel, typeDeclaration, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + var result = TypeValidation.ValidateTypeForCompositeKey( + typeSymbol, + typeDeclaration, + semanticModel, + attributeSymbol, + CancellationToken.None); + + // Assert + result.IsSuccess.ShouldBeFalse(); + result.Descriptor.ShouldBe(DiagnosticDescriptors.NoObviousDefaultConstructor); + result.MessageArgs.ShouldNotBeNull(); + result.MessageArgs![0].ShouldBe("TestKey"); + } + + [Fact] + public void NestedPartialRecord_ShouldReturnSuccessWithAllDeclarations() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial class Outer + { + [CompositeKey("{Id}")] + public partial record InnerKey(Guid Id); + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (semanticModel, typeDeclaration, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "InnerKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + var result = TypeValidation.ValidateTypeForCompositeKey( + typeSymbol, + typeDeclaration, + semanticModel, + attributeSymbol, + CancellationToken.None); + + // Assert + result.IsSuccess.ShouldBeTrue(); + result.TargetTypeDeclarations.ShouldNotBeNull(); + result.TargetTypeDeclarations.Count.ShouldBe(2); + result.TargetTypeDeclarations.ShouldContain("public partial record InnerKey"); + result.TargetTypeDeclarations.ShouldContain("public partial class Outer"); + } + + [Fact] + public void RecordStruct_ShouldReturnSuccessIfValid() + { + // Arrange - Record structs are supported + const string Source = """ + using System; + using CompositeKey; + + [CompositeKey("{Id}")] + public partial record struct TestRecordStruct(Guid Id); + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (semanticModel, typeDeclaration, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestRecordStruct"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + var result = TypeValidation.ValidateTypeForCompositeKey( + typeSymbol, + typeDeclaration, + semanticModel, + attributeSymbol, + CancellationToken.None); + + // Assert + // Record structs are valid composite key types + result.IsSuccess.ShouldBeTrue(); + result.Constructor.ShouldNotBeNull(); + result.TargetTypeDeclarations.ShouldNotBeNull(); + } + + [Fact] + public void NestedTypeWithNonPartialParent_ShouldReturnFailureWithCompositeTypeMustBePartial() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public class NonPartialOuter + { + [CompositeKey("{Id}")] + public partial record InnerKey(Guid Id); + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (semanticModel, typeDeclaration, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "InnerKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + var result = TypeValidation.ValidateTypeForCompositeKey( + typeSymbol, + typeDeclaration, + semanticModel, + attributeSymbol, + CancellationToken.None); + + // Assert + result.IsSuccess.ShouldBeFalse(); + result.Descriptor.ShouldBe(DiagnosticDescriptors.CompositeTypeMustBePartial); + result.MessageArgs.ShouldNotBeNull(); + result.MessageArgs![0].ShouldBe("InnerKey"); + } + } + + public class TryGetObviousOrExplicitlyMarkedConstructorTests + { + [Fact] + public void RecordWithPrimaryConstructor_ShouldReturnPrimaryConstructor() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey(Guid Id, string Name); + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + attributeSymbol, + out var constructor); + + // Assert + result.ShouldBeTrue(); + constructor.ShouldNotBeNull(); + constructor.Parameters.Length.ShouldBe(2); + } + + [Fact] + public void RecordWithParameterlessConstructor_ShouldReturnParameterlessConstructor() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey + { + public Guid Id { get; set; } + public TestKey() { } + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + attributeSymbol, + out var constructor); + + // Assert + result.ShouldBeTrue(); + constructor.ShouldNotBeNull(); + constructor.Parameters.Length.ShouldBe(0); + } + + [Fact] + public void RecordWithMultipleParameterizedConstructors_ShouldReturnFalse() + { + // Arrange - Multiple constructors, no parameterless, no attributed + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey + { + public Guid Id { get; set; } + public string Name { get; set; } + + public TestKey(Guid id) { Id = id; Name = ""; } + public TestKey(Guid id, string name) { Id = id; Name = name; } + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + attributeSymbol, + out var constructor); + + // Assert + result.ShouldBeFalse(); + constructor.ShouldBeNull(); + } + + [Fact] + public void RecordWithSinglePublicConstructor_ShouldReturnThatConstructor() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey + { + public Guid Id { get; set; } + public string Name { get; set; } + + public TestKey(Guid id, string name) + { + Id = id; + Name = name; + } + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + attributeSymbol, + out var constructor); + + // Assert + result.ShouldBeTrue(); + constructor.ShouldNotBeNull(); + constructor.Parameters.Length.ShouldBe(2); + } + + [Fact] + public void RecordWithExplicitAttributeMarkedConstructor_ShouldReturnAttributedConstructor() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey + { + public Guid Id { get; set; } + public string Name { get; set; } + + public TestKey() { } + + [CompositeKeyConstructor] + public TestKey(Guid id, string name) + { + Id = id; + Name = name; + } + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + attributeSymbol, + out var constructor); + + // Assert + result.ShouldBeTrue(); + constructor.ShouldNotBeNull(); + constructor.Parameters.Length.ShouldBe(2); + constructor.GetAttributes().Any(a => a.AttributeClass?.Name == "CompositeKeyConstructorAttribute").ShouldBeTrue(); + } + + [Fact] + public void RecordWithMultipleAttributedConstructors_ShouldReturnFalse() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey + { + public Guid Id { get; set; } + public string Name { get; set; } + + [CompositeKeyConstructor] + public TestKey(Guid id) + { + Id = id; + Name = ""; + } + + [CompositeKeyConstructor] + public TestKey(Guid id, string name) + { + Id = id; + Name = name; + } + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + attributeSymbol, + out var constructor); + + // Assert + result.ShouldBeFalse(); + constructor.ShouldBeNull(); + } + + [Fact] + public void RecordWithCopyConstructor_ShouldIgnoreCopyConstructor() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey(Guid Id) + { + // Copy constructor should be ignored + public TestKey(TestKey original) : this(original.Id) { } + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + attributeSymbol, + out var constructor); + + // Assert + result.ShouldBeTrue(); + constructor.ShouldNotBeNull(); + // Should return primary constructor, not copy constructor + constructor.Parameters.Length.ShouldBe(1); + constructor.Parameters[0].Type.Name.ShouldBe("Guid"); + } + + [Fact] + public void RecordWithNullAttributeSymbol_ShouldStillWork() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey(Guid Id); + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + + // Act - Pass null for attribute symbol + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + null, + out var constructor); + + // Assert + result.ShouldBeTrue(); + constructor.ShouldNotBeNull(); + constructor.Parameters.Length.ShouldBe(1); + } + + [Fact] + public void RecordWithMultipleConstructorsIncludingParameterless_ShouldPreferParameterless() + { + // Arrange + const string Source = """ + using System; + using CompositeKey; + + public partial record TestKey + { + public Guid Id { get; set; } + public string Name { get; set; } + + public TestKey() { } + public TestKey(Guid id) { Id = id; Name = ""; } + public TestKey(Guid id, string name) { Id = id; Name = name; } + } + """; + + var compilation = CompilationTestHelper.CreateCompilation(Source); + var (_, _, typeSymbol) = CompilationTestHelper.GetFirstTypeInfo(compilation, "TestKey"); + var attributeSymbol = CompilationTestHelper.GetCompositeKeyConstructorAttributeSymbol(compilation); + + // Act + bool result = TypeValidation.TryGetObviousOrExplicitlyMarkedConstructor( + typeSymbol, + attributeSymbol, + out var constructor); + + // Assert + result.ShouldBeTrue(); + constructor.ShouldNotBeNull(); + // Should prefer the parameterless constructor + constructor.Parameters.Length.ShouldBe(0); + } + } +} diff --git a/src/CompositeKey.Analyzers.Common.UnitTests/packages.lock.json b/src/CompositeKey.Analyzers.Common.UnitTests/packages.lock.json new file mode 100644 index 0000000..9e002f8 --- /dev/null +++ b/src/CompositeKey.Analyzers.Common.UnitTests/packages.lock.json @@ -0,0 +1,645 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + }, + "JunitXml.TestLogger": { + "type": "Direct", + "requested": "[6.1.0, )", + "resolved": "6.1.0", + "contentHash": "a3ciawoHOzqcry7yS5z9DerNyF9QZi6fEZZJPILSy6Noj6+r8Ydma+cENA6wvivXDCblpXxw72wWT9QApNy/0w==" + }, + "Microsoft.CodeAnalysis": { + "type": "Direct", + "requested": "[4.8.0, )", + "resolved": "4.8.0", + "contentHash": "g5eTgZVyBr4k1zxvJeVrJ1nDvBHrDt7XX2Uo7UWRoF9GdzOv9od4WtOeL1/e86ifgwX/H7H1Vs5u2OCdv0HYpQ==", + "dependencies": { + "Microsoft.CodeAnalysis.CSharp.Workspaces": "[4.8.0]", + "Microsoft.CodeAnalysis.VisualBasic.Workspaces": "[4.8.0]" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.14.1, )", + "resolved": "17.14.1", + "contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==", + "dependencies": { + "Microsoft.CodeCoverage": "17.14.1", + "Microsoft.TestPlatform.TestHost": "17.14.1" + } + }, + "Shouldly": { + "type": "Direct", + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", + "dependencies": { + "DiffEngine": "11.3.0", + "EmptyFiles": "4.4.0" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.9.3, )", + "resolved": "2.9.3", + "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", + "dependencies": { + "xunit.analyzers": "1.18.0", + "xunit.assert": "2.9.3", + "xunit.core": "[2.9.3]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.3, )", + "resolved": "3.1.3", + "contentHash": "go7e81n/UI3LeNqoJIJ3thkS4JfJtiQnDbAxLh09JkJqoHthnfbLS5p68s4/Bm12B9umkoYSB5SaDr68hZNleg==" + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "11.3.0", + "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "dependencies": { + "EmptyFiles": "4.4.0", + "System.Management": "6.0.1" + } + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.VisualBasic": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "kfHPh/etcWypMDYfHxgfitgJMhi986OFCICb76RPcA1Toordf6bBYEJytWr2L5CNdkXFWuw5qTkrlsktBav4VA==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.VisualBasic.Workspaces": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "4fNpQX8LRV0ZCfB6+rr9s61zdhNpN6Bgow/kmvsO2Gm5KtzbOUPijbUZex26wJwRHyW+ZYoatTRd449A7+D3Wg==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.VisualBasic": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Bcl.AsyncInterfaces": "7.0.0", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "System.Composition": "7.0.0", + "System.IO.Pipelines": "7.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ==", + "dependencies": { + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.14.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Convention": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0", + "System.Composition.TypedParts": "7.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", + "dependencies": { + "System.Composition.Runtime": "7.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.18.0", + "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]", + "xunit.extensibility.execution": "[2.9.3]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", + "dependencies": { + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]" + } + }, + "compositekey": { + "type": "Project" + }, + "compositekey.analyzers.common": { + "type": "Project" + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "CentralTransitive", + "requested": "[4.8.0, )", + "resolved": "4.8.0", + "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" + } + } + }, + "net9.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + }, + "JunitXml.TestLogger": { + "type": "Direct", + "requested": "[6.1.0, )", + "resolved": "6.1.0", + "contentHash": "a3ciawoHOzqcry7yS5z9DerNyF9QZi6fEZZJPILSy6Noj6+r8Ydma+cENA6wvivXDCblpXxw72wWT9QApNy/0w==" + }, + "Microsoft.CodeAnalysis": { + "type": "Direct", + "requested": "[4.8.0, )", + "resolved": "4.8.0", + "contentHash": "g5eTgZVyBr4k1zxvJeVrJ1nDvBHrDt7XX2Uo7UWRoF9GdzOv9od4WtOeL1/e86ifgwX/H7H1Vs5u2OCdv0HYpQ==", + "dependencies": { + "Microsoft.CodeAnalysis.CSharp.Workspaces": "[4.8.0]", + "Microsoft.CodeAnalysis.VisualBasic.Workspaces": "[4.8.0]" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.14.1, )", + "resolved": "17.14.1", + "contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==", + "dependencies": { + "Microsoft.CodeCoverage": "17.14.1", + "Microsoft.TestPlatform.TestHost": "17.14.1" + } + }, + "Shouldly": { + "type": "Direct", + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", + "dependencies": { + "DiffEngine": "11.3.0", + "EmptyFiles": "4.4.0" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.9.3, )", + "resolved": "2.9.3", + "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", + "dependencies": { + "xunit.analyzers": "1.18.0", + "xunit.assert": "2.9.3", + "xunit.core": "[2.9.3]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.3, )", + "resolved": "3.1.3", + "contentHash": "go7e81n/UI3LeNqoJIJ3thkS4JfJtiQnDbAxLh09JkJqoHthnfbLS5p68s4/Bm12B9umkoYSB5SaDr68hZNleg==" + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "11.3.0", + "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "dependencies": { + "EmptyFiles": "4.4.0", + "System.Management": "6.0.1" + } + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.VisualBasic": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "kfHPh/etcWypMDYfHxgfitgJMhi986OFCICb76RPcA1Toordf6bBYEJytWr2L5CNdkXFWuw5qTkrlsktBav4VA==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.VisualBasic.Workspaces": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "4fNpQX8LRV0ZCfB6+rr9s61zdhNpN6Bgow/kmvsO2Gm5KtzbOUPijbUZex26wJwRHyW+ZYoatTRd449A7+D3Wg==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.VisualBasic": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Bcl.AsyncInterfaces": "7.0.0", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "System.Composition": "7.0.0", + "System.IO.Pipelines": "7.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ==", + "dependencies": { + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.14.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Convention": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0", + "System.Composition.TypedParts": "7.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", + "dependencies": { + "System.Composition.Runtime": "7.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.18.0", + "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]", + "xunit.extensibility.execution": "[2.9.3]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", + "dependencies": { + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]" + } + }, + "compositekey": { + "type": "Project" + }, + "compositekey.analyzers.common": { + "type": "Project" + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "CentralTransitive", + "requested": "[4.8.0, )", + "resolved": "4.8.0", + "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" + } + } + } + } +} \ No newline at end of file diff --git a/src/CompositeKey.Analyzers.Common/Validation/TypeValidation.cs b/src/CompositeKey.Analyzers.Common/Validation/TypeValidation.cs new file mode 100644 index 0000000..6863e8f --- /dev/null +++ b/src/CompositeKey.Analyzers.Common/Validation/TypeValidation.cs @@ -0,0 +1,150 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using CompositeKey.Analyzers.Common.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CompositeKey.Analyzers.Common.Validation; + +public static class TypeValidation +{ + public static TypeValidationResult ValidateTypeForCompositeKey( + INamedTypeSymbol targetTypeSymbol, + TypeDeclarationSyntax typeDeclarationSyntax, + SemanticModel semanticModel, + INamedTypeSymbol? compositeKeyConstructorAttributeType, + CancellationToken cancellationToken) + { + if (!targetTypeSymbol.IsRecord) + { + return TypeValidationResult.Failure(DiagnosticDescriptors.UnsupportedCompositeType, targetTypeSymbol.Name); + } + + if (!TryGetTargetTypeDeclarations(typeDeclarationSyntax, semanticModel, out var targetTypeDeclarations, cancellationToken)) + { + return TypeValidationResult.Failure(DiagnosticDescriptors.CompositeTypeMustBePartial, targetTypeSymbol.Name); + } + + if (!TryGetObviousOrExplicitlyMarkedConstructor(targetTypeSymbol, compositeKeyConstructorAttributeType, out var constructor)) + { + return TypeValidationResult.Failure(DiagnosticDescriptors.NoObviousDefaultConstructor, targetTypeSymbol.Name); + } + + return TypeValidationResult.Success(constructor, targetTypeDeclarations); + } + + public static bool TryGetObviousOrExplicitlyMarkedConstructor( + INamedTypeSymbol typeSymbol, + INamedTypeSymbol? compositeKeyConstructorAttributeType, + [NotNullWhen(true)] out IMethodSymbol? constructor) + { + constructor = null; + + var publicConstructors = typeSymbol.Constructors + .Where(c => !c.IsStatic && !(c.IsImplicitlyDeclared && typeSymbol.IsValueType && c.Parameters.Length == 0)) + .Where(c => !(c.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(c.Parameters[0].Type, typeSymbol))) + .ToArray(); + + var lonePublicConstructor = publicConstructors.Length == 1 ? publicConstructors[0] : null; + IMethodSymbol? constructorWithAttribute = null; + IMethodSymbol? publicParameterlessConstructor = null; + + foreach (var ctor in publicConstructors) + { + if (compositeKeyConstructorAttributeType != null && + ctor.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, compositeKeyConstructorAttributeType))) + { + if (constructorWithAttribute is not null) + return false; // Multiple constructors with attribute + + constructorWithAttribute = ctor; + } + else if (ctor.Parameters.Length == 0) + { + publicParameterlessConstructor = ctor; + } + } + + constructor = constructorWithAttribute ?? publicParameterlessConstructor ?? lonePublicConstructor; + return constructor is not null; + } + + private static bool TryGetTargetTypeDeclarations( + TypeDeclarationSyntax typeDeclarationSyntax, + SemanticModel semanticModel, + [NotNullWhen(true)] out List? targetTypeDeclarations, + CancellationToken cancellationToken) + { + targetTypeDeclarations = null; + + for (var current = typeDeclarationSyntax; current != null; current = current.Parent as TypeDeclarationSyntax) + { + var stringBuilder = new StringBuilder(); + + bool isPartialType = false; + foreach (var modifier in current.Modifiers) + { + stringBuilder.Append(modifier.Text); + stringBuilder.Append(' '); + + isPartialType |= modifier.IsKind(SyntaxKind.PartialKeyword); + } + + if (!isPartialType) + return false; + + stringBuilder.Append(GetTypeKindKeyword(current)); + stringBuilder.Append(' '); + + var typeSymbol = semanticModel.GetDeclaredSymbol(current, cancellationToken); + if (typeSymbol is null) + return false; + + stringBuilder.Append(typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); + + (targetTypeDeclarations ??= []).Add(stringBuilder.ToString()); + } + + return targetTypeDeclarations?.Count > 0; + } + + private static string GetTypeKindKeyword(TypeDeclarationSyntax typeDeclarationSyntax) => + typeDeclarationSyntax.Kind() switch + { + SyntaxKind.ClassDeclaration => "class", + SyntaxKind.InterfaceDeclaration => "interface", + SyntaxKind.StructDeclaration => "struct", + SyntaxKind.RecordDeclaration => "record", + SyntaxKind.RecordStructDeclaration => "record struct", + SyntaxKind.EnumDeclaration => "enum", + SyntaxKind.DelegateDeclaration => "delegate", + _ => throw new ArgumentOutOfRangeException(nameof(typeDeclarationSyntax)) + }; +} + +public record TypeValidationResult +{ + [MemberNotNullWhen(true, nameof(Constructor), nameof(TargetTypeDeclarations))] + [MemberNotNullWhen(false, nameof(Descriptor), nameof(MessageArgs))] + public required bool IsSuccess { get; init; } + + public DiagnosticDescriptor? Descriptor { get; init; } + public object?[]? MessageArgs { get; init; } + public IMethodSymbol? Constructor { get; init; } + public IReadOnlyList? TargetTypeDeclarations { get; init; } + + public static TypeValidationResult Success(IMethodSymbol constructor, IReadOnlyList targetTypeDeclarations) => new() + { + IsSuccess = true, + Constructor = constructor, + TargetTypeDeclarations = targetTypeDeclarations + }; + + public static TypeValidationResult Failure(DiagnosticDescriptor descriptor, params object?[]? messageArgs) => new() + { + IsSuccess = false, + Descriptor = descriptor, + MessageArgs = messageArgs + }; +} diff --git a/src/CompositeKey.SourceGeneration/SourceGenerator.Parser.cs b/src/CompositeKey.SourceGeneration/SourceGenerator.Parser.cs index b6e358a..45227ac 100644 --- a/src/CompositeKey.SourceGeneration/SourceGenerator.Parser.cs +++ b/src/CompositeKey.SourceGeneration/SourceGenerator.Parser.cs @@ -1,7 +1,6 @@ using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text; using CompositeKey.Analyzers.Common.Diagnostics; +using CompositeKey.Analyzers.Common.Validation; using CompositeKey.SourceGeneration.Core; using CompositeKey.SourceGeneration.Core.Extensions; using CompositeKey.SourceGeneration.Core.Tokenization; @@ -56,27 +55,27 @@ public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location return null; } - if (!targetTypeSymbol.IsRecord) - { - ReportDiagnostic(DiagnosticDescriptors.UnsupportedCompositeType, _location, targetTypeSymbol.Name); - return null; - } + // Validate type structure using comprehensive shared validation + var validationResult = TypeValidation.ValidateTypeForCompositeKey( + targetTypeSymbol, + typeDeclarationSyntax, + semanticModel, + _knownTypeSymbols.CompositeKeyConstructorAttributeType, + cancellationToken); - if (!TryGetTargetTypeDeclarations(typeDeclarationSyntax, semanticModel, out var targetTypeDeclarations, cancellationToken)) + if (!validationResult.IsSuccess) { - ReportDiagnostic(DiagnosticDescriptors.CompositeTypeMustBePartial, _location, targetTypeSymbol.Name); + ReportDiagnostic(validationResult.Descriptor, _location, validationResult.MessageArgs); return null; } + // Use validated data from the validation result (guaranteed non-null due to MemberNotNullWhen on IsSuccess) + var targetTypeDeclarations = validationResult.TargetTypeDeclarations; + var constructor = validationResult.Constructor; + var compositeKeyAttributeValues = ParseCompositeKeyAttributeValues(targetTypeSymbol); Debug.Assert(compositeKeyAttributeValues is not null); - if (TryGetObviousOrExplicitlyMarkedConstructor(targetTypeSymbol) is not { } constructor) - { - ReportDiagnostic(DiagnosticDescriptors.NoObviousDefaultConstructor, _location, targetTypeSymbol.Name); - return null; - } - var constructorParameters = ParseConstructorParameters(constructor, out var constructionStrategy, out bool constructorSetsRequiredMembers); var properties = ParseProperties(targetTypeSymbol); var propertyInitializers = ParsePropertyInitializers(constructorParameters, properties.Select(p => p.Spec).ToList(), ref constructionStrategy, constructorSetsRequiredMembers); @@ -404,33 +403,6 @@ private ConstructorParameterSpec[] ParseConstructorParameters( return constructorParameters; } - private IMethodSymbol? TryGetObviousOrExplicitlyMarkedConstructor(INamedTypeSymbol typeSymbol) - { - var publicConstructors = typeSymbol.Constructors - .Where(c => !c.IsStatic && !(c.IsImplicitlyDeclared && typeSymbol.IsValueType && c.Parameters.Length == 0)) - .Where(c => !(c.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(c.Parameters[0].Type, typeSymbol))) - .ToArray(); - - var lonePublicConstructor = publicConstructors.Length == 1 ? publicConstructors[0] : null; - IMethodSymbol? constructorWithAttribute = null, publicParameterlessConstructor = null; - - foreach (var constructor in publicConstructors) - { - if (constructor.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _knownTypeSymbols.CompositeKeyConstructorAttributeType))) - { - if (constructorWithAttribute is not null) - return null; // Somehow we found a duplicate so let's just return null so the diagnostic is emitted - - constructorWithAttribute = constructor; - } - else if (constructor.Parameters.Length == 0) - { - publicParameterlessConstructor = constructor; - } - } - - return constructorWithAttribute ?? publicParameterlessConstructor ?? lonePublicConstructor; - } private CompositeKeyAttributeValues? ParseCompositeKeyAttributeValues(INamedTypeSymbol targetTypeSymbol) { @@ -481,55 +453,5 @@ private ConstructorParameterSpec[] ParseConstructorParameters( } } - private static bool TryGetTargetTypeDeclarations( - TypeDeclarationSyntax typeDeclarationSyntax, - SemanticModel semanticModel, - [NotNullWhen(true)] out List? targetTypeDeclarations, - CancellationToken cancellationToken) - { - targetTypeDeclarations = null; - - for (var current = typeDeclarationSyntax; current != null; current = current.Parent as TypeDeclarationSyntax) - { - StringBuilder stringBuilder = new(); - - bool isPartialType = false; - foreach (var modifier in current.Modifiers) - { - stringBuilder.Append(modifier.Text); - stringBuilder.Append(' '); - - isPartialType |= modifier.IsKind(SyntaxKind.PartialKeyword); - } - - if (!isPartialType) - return false; - - stringBuilder.Append(GetTypeKindKeyword(current)); - stringBuilder.Append(' '); - - var typeSymbol = semanticModel.GetDeclaredSymbol(current, cancellationToken); - Debug.Assert(typeSymbol is not null); - - stringBuilder.Append(typeSymbol!.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); - - (targetTypeDeclarations ??= []).Add(stringBuilder.ToString()); - } - - return targetTypeDeclarations?.Count > 0; - - static string GetTypeKindKeyword(TypeDeclarationSyntax typeDeclarationSyntax) => - typeDeclarationSyntax.Kind() switch - { - SyntaxKind.ClassDeclaration => "class", - SyntaxKind.InterfaceDeclaration => "interface", - SyntaxKind.StructDeclaration => "struct", - SyntaxKind.RecordDeclaration => "record", - SyntaxKind.RecordStructDeclaration => "record struct", - SyntaxKind.EnumDeclaration => "enum", - SyntaxKind.DelegateDeclaration => "delegate", - _ => throw new ArgumentOutOfRangeException(nameof(typeDeclarationSyntax)) - }; - } } }