From 02e2605fd0e6de13d0034db643ff6c61be9331b4 Mon Sep 17 00:00:00 2001 From: Michael Ganss Date: Mon, 11 Dec 2023 17:27:01 +0100 Subject: [PATCH] Fix #456 --- XmlSchemaClassGenerator.Console/Program.cs | 3 +- .../XmlSchemaClassGenerator.Tests.csproj | 2 +- XmlSchemaClassGenerator.Tests/XmlTests.cs | 105 ++++++++++++------ .../CollectionSettersMode.cs | 8 +- XmlSchemaClassGenerator/TypeModel.cs | 18 ++- 5 files changed, 90 insertions(+), 46 deletions(-) diff --git a/XmlSchemaClassGenerator.Console/Program.cs b/XmlSchemaClassGenerator.Console/Program.cs index f9c38d4c..40782aee 100644 --- a/XmlSchemaClassGenerator.Console/Program.cs +++ b/XmlSchemaClassGenerator.Console/Program.cs @@ -120,7 +120,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l { "cit|collectionImplementationType=", "the default collection type implementation to use (default is null)", v => collectionImplementationType = v == null ? null : Type.GetType(v, true) }, { "csm|collectionSettersMode=", @"generate a private, public or public setters without backing field initialization for collections -(default is Private; can be: {Private, Public, PublicWithoutConstructorInitialization})", +(default is Private; can be: {Private, Public, PublicWithoutConstructorInitialization, Init})", v => { collectionSettersMode = v switch @@ -129,6 +129,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l "pu" or "Public" => CollectionSettersMode.Public, "puwci" or "PublicWithoutConstructorInitialization" => CollectionSettersMode.PublicWithoutConstructorInitialization, + "in" or "Init" => CollectionSettersMode.Init, _ => CollectionSettersMode.Private }; }}, diff --git a/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj b/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj index 60746d12..bc00e48d 100644 --- a/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj +++ b/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj @@ -12,7 +12,7 @@ false false false - latest + preview opencover ../coverage.xml [XmlSchemaClassGenerator]* diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index f9f0c47c..6ced8594 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -23,14 +23,9 @@ namespace XmlSchemaClassGenerator.Tests { [TestCaseOrderer("XmlSchemaClassGenerator.Tests.PriorityOrderer", "XmlSchemaClassGenerator.Tests")] - public class XmlTests + public class XmlTests(ITestOutputHelper output) { - private readonly ITestOutputHelper Output; - - public XmlTests(ITestOutputHelper output) - { - Output = output; - } + private readonly ITestOutputHelper Output = output; private static IEnumerable ConvertXml(string name, IEnumerable xsds, Generator generatorPrototype = null) { @@ -300,7 +295,8 @@ public void TestListWithPublicPropertySetters() [Fact] public void TestListWithPublicPropertySettersWithoutConstructors() { - var assembly = Compiler.Generate("ListPublicWithoutConstructorInitialization", ListPattern, new Generator { + var assembly = Compiler.Generate("ListPublicWithoutConstructorInitialization", ListPattern, new Generator + { GenerateNullables = true, IntegerDataType = typeof(int), DataAnnotationMode = DataAnnotationMode.All, @@ -317,10 +313,45 @@ public void TestListWithPublicPropertySettersWithoutConstructors() var myClassType = assembly.GetType("List.MyClass"); Assert.NotNull(myClassType); var iListType = typeof(Collection<>); - var collectionPropertyInfos = myClassType.GetProperties().Where(p => p.PropertyType.IsGenericType && iListType.IsAssignableFrom(p.PropertyType.GetGenericTypeDefinition())).OrderBy(p=>p.Name).ToList(); - var publicCollectionPropertyInfos = collectionPropertyInfos.Where(p => p.SetMethod.IsPublic).OrderBy(p=>p.Name).ToList(); + var collectionPropertyInfos = myClassType.GetProperties().Where(p => p.PropertyType.IsGenericType && iListType.IsAssignableFrom(p.PropertyType.GetGenericTypeDefinition())).OrderBy(p => p.Name).ToList(); + var publicCollectionPropertyInfos = collectionPropertyInfos.Where(p => p.SetMethod.IsPublic).OrderBy(p => p.Name).ToList(); + Assert.NotEmpty(collectionPropertyInfos); + Assert.Equal(collectionPropertyInfos, publicCollectionPropertyInfos); + var myClassInstance = Activator.CreateInstance(myClassType); + foreach (var collectionPropertyInfo in publicCollectionPropertyInfos) + { + Assert.Null(collectionPropertyInfo.GetValue(myClassInstance)); + } + } + + [Fact] + public void TestListWithInitPropertySettersWithoutConstructors() + { + var assembly = Compiler.Generate("ListInitWithoutConstructorInitialization", ListPattern, new Generator + { + GenerateNullables = true, + IntegerDataType = typeof(int), + DataAnnotationMode = DataAnnotationMode.All, + GenerateDesignerCategoryAttribute = false, + GenerateComplexTypesForCollections = true, + EntityFramework = false, + GenerateInterfaces = true, + NamespacePrefix = "List", + GenerateDescriptionAttribute = true, + TextValuePropertyName = "Value", + CollectionSettersMode = CollectionSettersMode.Init + }); + Assert.NotNull(assembly); + var myClassType = assembly.GetType("List.MyClass"); + Assert.NotNull(myClassType); + var iListType = typeof(Collection<>); + var collectionPropertyInfos = myClassType.GetProperties().Where(p => p.PropertyType.IsGenericType && iListType.IsAssignableFrom(p.PropertyType.GetGenericTypeDefinition())).OrderBy(p => p.Name).ToList(); + var publicCollectionPropertyInfos = collectionPropertyInfos.Where(p => p.SetMethod.IsPublic).OrderBy(p => p.Name).ToList(); Assert.NotEmpty(collectionPropertyInfos); Assert.Equal(collectionPropertyInfos, publicCollectionPropertyInfos); + var requiredCustomModifiers = collectionPropertyInfos.Select(p => p.SetMethod.ReturnParameter.GetRequiredCustomModifiers()).ToList(); + Assert.Equal(collectionPropertyInfos.Count, requiredCustomModifiers.Count); + Assert.All(requiredCustomModifiers, m => Assert.Contains(typeof(System.Runtime.CompilerServices.IsExternalInit), m)); var myClassInstance = Activator.CreateInstance(myClassType); foreach (var collectionPropertyInfo in publicCollectionPropertyInfos) { @@ -393,14 +424,16 @@ public void TestListWithPublicPropertySettersWithoutConstructorsSpecified() } } - public static IEnumerable TestSimpleData() { + public static TheoryData TestSimpleData() { + var theoryData = new TheoryData(); foreach (var referenceMode in new[] { CodeTypeReferenceOptions.GlobalReference, /*CodeTypeReferenceOptions.GenericTypeParameter*/ }) foreach (var namingScheme in new[] { NamingScheme.Direct, NamingScheme.PascalCase }) foreach (var collectionType in new[] { typeof(Collection<>), /*typeof(Array)*/ }) { - yield return new object[] { referenceMode, namingScheme, collectionType }; + theoryData.Add(referenceMode, namingScheme, collectionType); } + return theoryData; } @@ -686,28 +719,28 @@ void UnknownAttributeHandler(object sender, XmlAttributeEventArgs e) private static readonly XmlQualifiedName AnyType = new("anyType", XmlSchema.Namespace); - public static IEnumerable Classes => new List - { - new object[] { "ApartmentBuy" }, - new object[] { "ApartmentRent" }, - new object[] { "AssistedLiving" }, - new object[] { "CompulsoryAuction" }, - new object[] { "GarageBuy" }, - new object[] { "GarageRent" }, - new object[] { "Gastronomy" }, - new object[] { "HouseBuy" }, - new object[] { "HouseRent" }, - new object[] { "HouseType" }, - new object[] { "Industry" }, - new object[] { "Investment" }, - new object[] { "LivingBuySite" }, - new object[] { "LivingRentSite" }, - new object[] { "Office" }, - new object[] { "SeniorCare" }, - new object[] { "ShortTermAccommodation" }, - new object[] { "SpecialPurpose" }, - new object[] { "Store" }, - new object[] { "TradeSite" }, + public static TheoryData Classes => new() + { + "ApartmentBuy", + "ApartmentRent", + "AssistedLiving", + "CompulsoryAuction", + "GarageBuy", + "GarageRent", + "Gastronomy", + "HouseBuy", + "HouseRent", + "HouseType", + "Industry", + "Investment", + "LivingBuySite", + "LivingRentSite", + "Office", + "SeniorCare", + "ShortTermAccommodation", + "SpecialPurpose", + "Store", + "TradeSite", }; [Theory, TestPriority(2)] @@ -772,7 +805,7 @@ public void TestCustomNamespaces() { diXsd, "DiagramElement" } }; - List customNamespaceConfig = new(); + List customNamespaceConfig = []; foreach (var ns in xsdToCsharpNsMap) customNamespaceConfig.Add(string.Format(customNsPattern, ns.Key, ns.Value)); @@ -1478,7 +1511,7 @@ public void DoNotGenerateIntermediaryClassForArrayElements() var fooType = assembly.DefinedTypes.Single(t => t.FullName == "Test.Foo"); Assert.NotNull(fooType); - Assert.True(fooType.FullName == "Test.Foo"); + Assert.Equal("Test.Foo", fooType.FullName); } [Fact] diff --git a/XmlSchemaClassGenerator/CollectionSettersMode.cs b/XmlSchemaClassGenerator/CollectionSettersMode.cs index c4f81dd4..a0db5eac 100644 --- a/XmlSchemaClassGenerator/CollectionSettersMode.cs +++ b/XmlSchemaClassGenerator/CollectionSettersMode.cs @@ -18,8 +18,12 @@ public enum CollectionSettersMode /// Public, /// - /// All collection setters are public and baking collections fields not initialized in constructors + /// All collection setters are public and backing collections fields are not initialized in constructors /// - PublicWithoutConstructorInitialization + PublicWithoutConstructorInitialization, + /// + /// All collections setters are init-only + /// + Init } } diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index cdc19c07..dc1333bb 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -548,16 +548,15 @@ public void SetSchemaNameAndNamespace(TypeModel owningTypeModel, IXmlSchemaNode : xs.QualifiedName.Namespace; } - internal static string GetAccessors(CodeMemberField backingField = null, bool withDataBinding = false, PropertyValueTypeCode typeCode = PropertyValueTypeCode.Other, bool privateSetter = false) + internal static string GetAccessors(CodeMemberField backingField = null, bool withDataBinding = false, PropertyValueTypeCode typeCode = PropertyValueTypeCode.Other, string setter = "set") { - var privateString = privateSetter ? "private " : string.Empty; return backingField == null ? " { get; set; }" : CodeUtilities.NormalizeNewlines($@" {{ get {{ return {backingField.Name}; }} - {privateString}set + {setter} {{{(typeCode, withDataBinding) switch { (PropertyValueTypeCode.ValueType, true) => $@" @@ -730,7 +729,13 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi } var propertyValueTypeCode = IsCollection || isArray ? PropertyValueTypeCode.Array : propertyType.GetPropertyValueTypeCode(); - member.Name += GetAccessors(backingField, withDataBinding, propertyValueTypeCode, IsPrivateSetter); + var setter = Configuration.CollectionSettersMode switch + { + CollectionSettersMode.Private when IsEnumerable => "private set", + CollectionSettersMode.Init when IsEnumerable => "init", + _ => "set" + }; + member.Name += GetAccessors(backingField, withDataBinding, propertyValueTypeCode, setter); } else { @@ -869,7 +874,7 @@ [new CodeMethodReturnStatement(valueExpression)], var countProperty = collectionType == typeof(Array) ? nameof(Array.Length) : nameof(List.Count); var countReference = new CodePropertyReferenceExpression(listReference, countProperty); var notZeroExpression = new CodeBinaryOperatorExpression(countReference, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(0)); - if (Configuration.CollectionSettersMode is CollectionSettersMode.PublicWithoutConstructorInitialization or CollectionSettersMode.Public) + if (Configuration.CollectionSettersMode is CollectionSettersMode.PublicWithoutConstructorInitialization or CollectionSettersMode.Public or CollectionSettersMode.Init) { var notNullExpression = new CodeBinaryOperatorExpression(listReference, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(null)); notZeroExpression = new CodeBinaryOperatorExpression(notNullExpression, CodeBinaryOperatorType.BooleanAnd, notZeroExpression); @@ -927,7 +932,8 @@ [new CodeMethodReturnStatement(valueExpression)], member.CustomAttributes.AddRange(attributes); // initialize List<> - if (isEnumerable && Configuration.CollectionSettersMode != CollectionSettersMode.PublicWithoutConstructorInitialization) + if (isEnumerable && (Configuration.CollectionSettersMode != CollectionSettersMode.PublicWithoutConstructorInitialization) + && (Configuration.CollectionSettersMode != CollectionSettersMode.Init)) { var constructor = typeDeclaration.Members.OfType().FirstOrDefault();