From 4bfc2c46ebfbe5839d5afebd855ffe2f70be5efa Mon Sep 17 00:00:00 2001 From: "andreas.hoffmann" Date: Sat, 12 Mar 2022 15:08:08 +0100 Subject: [PATCH 1/5] Generator supports allowNullAttribute for reference types. --- XmlSchemaClassGenerator.Tests/Compiler.cs | 58 +++++++++++++------ XmlSchemaClassGenerator.Tests/XmlTests.cs | 39 +++++++++++++ .../nullablereference.xsd | 46 +++++++++++++++ XmlSchemaClassGenerator/Generator.cs | 6 ++ .../GeneratorConfiguration.cs | 4 ++ XmlSchemaClassGenerator/TypeModel.cs | 15 +++++ 6 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd diff --git a/XmlSchemaClassGenerator.Tests/Compiler.cs b/XmlSchemaClassGenerator.Tests/Compiler.cs index f7925a03..e0492624 100644 --- a/XmlSchemaClassGenerator.Tests/Compiler.cs +++ b/XmlSchemaClassGenerator.Tests/Compiler.cs @@ -33,7 +33,7 @@ public static CompilationResult GenerateAssembly(Compilation compilation) }; } - private static readonly ConcurrentDictionary Assemblies = new(); + private static readonly ConcurrentDictionary, Assembly>> Assemblies = new(); private static readonly string[] DependencyAssemblies = new[] { @@ -55,21 +55,33 @@ public static CompilationResult GenerateAssembly(Compilation compilation) public static Assembly GetAssembly(string name) { Assemblies.TryGetValue(name, out var assembly); - return assembly; + return assembly.Item2; } - public static Assembly Generate(string name, string pattern, Generator generatorPrototype = null) - { - if (Assemblies.ContainsKey(name)) { return Assemblies[name]; } + public static Assembly Generate(string name, string pattern, Generator generatorPrototype = null) + { + (_, var assembly) = GenerateVerbose(name, pattern, generatorPrototype); + return assembly; + } + + public static (List, Assembly) GenerateVerbose(string name, string pattern, Generator generatorPrototype = null) + { + if (Assemblies.TryGetValue(name, out var assembly)) { return (assembly.Item1, assembly.Item2); } var files = Glob.ExpandNames(pattern).OrderByDescending(f => f); - return GenerateFiles(name, files, generatorPrototype); + return GenerateFilesVerbose(name, files, generatorPrototype); } - public static Assembly GenerateFiles(string name, IEnumerable files, Generator generatorPrototype = null) - { - if (Assemblies.ContainsKey(name)) { return Assemblies[name]; } + public static Assembly GenerateFiles(string name, IEnumerable files, Generator generatorPrototype = null) + { + (_, var assembly) = GenerateFilesVerbose(name, files, generatorPrototype); + return assembly; + } + + public static (List, Assembly) GenerateFilesVerbose(string name, IEnumerable files, Generator generatorPrototype = null) + { + if (Assemblies.TryGetValue(name, out var assembly)) { return (assembly.Item1, assembly.Item2); } generatorPrototype ??= new Generator { @@ -123,12 +135,18 @@ public static Assembly GenerateFiles(string name, IEnumerable files, Gen gen.Generate(files); - return CompileFiles(name, output.Files); + return CompileFilesVerbose(name, output.Files); } - public static Assembly CompileFiles(string name, IEnumerable files) - { - return Compile(name, files.Select(f => File.ReadAllText(f)).ToArray()); + public static Assembly CompileFiles(string name, IEnumerable files) + { + (_, var assembly) = CompileFilesVerbose(name, files); + return assembly; + } + + public static (List, Assembly) CompileFilesVerbose(string name, IEnumerable files) + { + return CompileVerbose(name, files.Select(f => File.ReadAllText(f)).ToArray()); } private static readonly LanguageVersion MaxLanguageVersion = Enum @@ -136,15 +154,21 @@ public static Assembly CompileFiles(string name, IEnumerable files) .Cast() .Max(); - public static Assembly Compile(string name, params string[] contents) + public static Assembly Compile(string name, params string[] contents) { + (_, var assembly) = CompileVerbose(name, contents); + return assembly; + } + + public static (List, Assembly) CompileVerbose(string name, params string[] contents) + { var trustedAssembliesPaths = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")).Split(Path.PathSeparator); var references = trustedAssembliesPaths .Where(p => DependencyAssemblies.Contains(Path.GetFileNameWithoutExtension(p))) .Select(p => MetadataReference.CreateFromFile(p)) .ToList(); var options = new CSharpParseOptions(kind: SourceCodeKind.Regular, languageVersion: MaxLanguageVersion); - var syntaxTrees = contents.Select(c => CSharpSyntaxTree.ParseText(c, options)); + var syntaxTrees = contents.Select(c => CSharpSyntaxTree.ParseText(c, options)).ToList(); var compilation = CSharpCompilation.Create(name, syntaxTrees) .AddReferences(references) .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); @@ -155,9 +179,9 @@ public static Assembly Compile(string name, params string[] contents) Assert.True(result.Result.Success); Assert.NotNull(result.Assembly); - Assemblies[name] = result.Assembly; + Assemblies[name] = Tuple.Create(syntaxTrees, result.Assembly); - return result.Assembly; + return (syntaxTrees, result.Assembly); } } } diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 47e6eac4..9bed8e72 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -14,6 +14,7 @@ using System.Xml.XPath; using Ganss.IO; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Xml.XMLGen; using Xunit; using Xunit.Abstractions; @@ -105,6 +106,7 @@ private static IEnumerable ConvertXml(string name, string xsd, Generator const string DtsxPattern = "xsd/dtsx/dtsx2.xsd"; const string WfsPattern = "xsd/wfs/schemas.opengis.net/wfs/2.0/wfs.xsd"; const string EppPattern = "xsd/epp/*.xsd"; + const string NullableReferenceAttributesPattern = "xsd/nullablereferenceattributes/nullablereference.xsd"; // IATA test takes too long to perform every time @@ -2366,6 +2368,43 @@ void UnknownAttributeHandler(object sender, XmlAttributeEventArgs e) */ } + [Fact, TestPriority(1)] + [UseCulture("en-US")] + public void TestNullableReferenceAttributes() + { + var files = Glob.ExpandNames(NullableReferenceAttributesPattern).OrderByDescending(f => f); + var generator = new Generator + { + EnableNullableReferenceAttributes = true, + UseShouldSerializePattern = true, + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }; + //Unfortunately, I did not find access to the Attribute "AllowNull" in the generated Assembly. + //It is just not accessible via "CustomAttributes". + //So I have to parse the Syntax-Tree to find it. + (var syntaxTrees, _) = Compiler.GenerateVerbose(nameof(TestNullableReferenceAttributes), NullableReferenceAttributesPattern, generator); + void assertNullable(string typename, bool nullable) + { + var root = (CompilationUnitSyntax)syntaxTrees.Single().GetRoot(); + var ns = (NamespaceDeclarationSyntax)root.Members.Single(); + var c1 = (ClassDeclarationSyntax)ns.Members.Single( + m => m is ClassDeclarationSyntax c && c.Identifier.ToString() == typename + ); + var p = (PropertyDeclarationSyntax)c1.Members.Single(m => m is PropertyDeclarationSyntax n && n.Identifier.ToString() == "Text"); + var hasAllowNullAttribute = p.AttributeLists.Any(d => d.Attributes.Any(a => a.GetText().ToString() == "System.Diagnostics.CodeAnalysis.AllowNullAttribute()")); + Assert.Equal(nullable, hasAllowNullAttribute); + } + assertNullable("ElementReferenceNullable", true); + assertNullable("ElementReferenceList", true); + assertNullable("ElementReferenceNonNullable", false); + assertNullable("AttributeReferenceNullable", true); + assertNullable("AttributeReferenceNonNullable", false); + assertNullable("AttributeValueNullableInt", false); + } + [Fact, TestPriority(1)] public void TestNetex() { diff --git a/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd b/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd new file mode 100644 index 00000000..05eb6261 --- /dev/null +++ b/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XmlSchemaClassGenerator/Generator.cs b/XmlSchemaClassGenerator/Generator.cs index 0e790717..e0e8c215 100644 --- a/XmlSchemaClassGenerator/Generator.cs +++ b/XmlSchemaClassGenerator/Generator.cs @@ -115,6 +115,12 @@ public bool GenerateNullables set { _configuration.GenerateNullables = value; } } + public bool EnableNullableReferenceAttributes + { + get { return _configuration.EnableNullableReferenceAttributes; } + set { _configuration.EnableNullableReferenceAttributes = value; } + } + public bool UseShouldSerializePattern { get { return _configuration.UseShouldSerializePattern; } diff --git a/XmlSchemaClassGenerator/GeneratorConfiguration.cs b/XmlSchemaClassGenerator/GeneratorConfiguration.cs index fa71cfd0..0d4433ed 100644 --- a/XmlSchemaClassGenerator/GeneratorConfiguration.cs +++ b/XmlSchemaClassGenerator/GeneratorConfiguration.cs @@ -76,6 +76,10 @@ public GeneratorConfiguration() /// Use XElement instead of XmlElement for Any nodes? /// public bool UseXElementForAny { get; set; } + /// + /// Generate attributes for nullable references to avoid compiler-warnings in .NET Core and Standard with nullable-checks. + /// + public bool EnableNullableReferenceAttributes { get; set; } private NamingScheme namingScheme; diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index e0713f47..e3482df4 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -750,6 +750,15 @@ private bool IsNullableValueType && IsNullable && !(IsCollection || IsArray) && !IsList && ((PropertyType is EnumModel) || (PropertyType is SimpleModel model && model.ValueType.IsValueType)); } + } + + private bool IsNullableReferenceType + { + get + { + return DefaultValue == null + && IsNullable && (IsCollection || IsArray || IsList || PropertyType is ClassModel || PropertyType is SimpleModel model && !model.ValueType.IsValueType); + } } private bool IsNillableValueType @@ -860,6 +869,7 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi var isArray = IsArray; var propertyType = PropertyType; var isNullableValueType = IsNullableValueType; + var isNullableReferenceType = IsNullableReferenceType; var typeReference = TypeReference; var requiresBackingField = withDataBinding || DefaultValue != null || IsCollection || isArray; @@ -1114,6 +1124,11 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi typeDeclaration.Members.Add(specifiedProperty); } + if (isNullableReferenceType && Configuration.EnableNullableReferenceAttributes) + { + member.CustomAttributes.Add(new CodeAttributeDeclaration("System.Diagnostics.CodeAnalysis.AllowNullAttribute")); + } + var attributes = GetAttributes(isArray).ToArray(); member.CustomAttributes.AddRange(attributes); From 74715947b15f7e012818d87fe5e807af57187b42 Mon Sep 17 00:00:00 2001 From: "andreas.hoffmann" Date: Sat, 12 Mar 2022 15:08:08 +0100 Subject: [PATCH 2/5] Generator supports allowNullAttribute for reference types. --- XmlSchemaClassGenerator.Console/Program.cs | 5 +- XmlSchemaClassGenerator.Tests/Compiler.cs | 58 +++++++++++++------ XmlSchemaClassGenerator.Tests/XmlTests.cs | 39 +++++++++++++ .../nullablereference.xsd | 46 +++++++++++++++ XmlSchemaClassGenerator/Generator.cs | 6 ++ .../GeneratorConfiguration.cs | 4 ++ XmlSchemaClassGenerator/TypeModel.cs | 15 +++++ 7 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd diff --git a/XmlSchemaClassGenerator.Console/Program.cs b/XmlSchemaClassGenerator.Console/Program.cs index 15374ebf..1cc6c242 100644 --- a/XmlSchemaClassGenerator.Console/Program.cs +++ b/XmlSchemaClassGenerator.Console/Program.cs @@ -54,6 +54,7 @@ static void Main(string[] args) var uniqueTypeNamesAcrossNamespaces = false; var createGeneratedCodeAttributeVersion = true; var netCoreSpecificCode = false; + var nullableReferenceAttributes = false; var generateCommandLineArgs = true; var options = new OptionSet { @@ -126,7 +127,8 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l v => commentLanguages = v.Split(',').Select(l => l.Trim()).ToArray() }, { "un|uniqueTypeNames", "generate type names that are unique across namespaces (default is false)", v => uniqueTypeNamesAcrossNamespaces = v != null }, { "gc|generatedCodeAttribute", "add version information to GeneratedCodeAttribute (default is true)", v => createGeneratedCodeAttributeVersion = v != null }, - { "nc|netCore", "generate .NET Core specific code that might not work with .NET Framework (default is false)", v => netCoreSpecificCode = v != null }, + { "nc|netCore", "generate .NET Core specific code that might not work with .NET Framework (default is false)", v => netCoreSpecificCode = v != null }, + { "nr|nullableReferenceAttributes", "generate attributes for nullable references (default is false)", v => nullableReferenceAttributes = v != null }, { "ca|commandArgs", "generate a comment with the exact command line arguments that were used to generate the source code (default is true)", v => generateCommandLineArgs = v != null }, }; @@ -203,6 +205,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l UniqueTypeNamesAcrossNamespaces = uniqueTypeNamesAcrossNamespaces, CreateGeneratedCodeAttributeVersion = createGeneratedCodeAttributeVersion, NetCoreSpecificCode = netCoreSpecificCode, + EnableNullableReferenceAttributes = nullableReferenceAttributes, GenerateCommandLineArgumentsComment = generateCommandLineArgs, }; diff --git a/XmlSchemaClassGenerator.Tests/Compiler.cs b/XmlSchemaClassGenerator.Tests/Compiler.cs index f7925a03..e0492624 100644 --- a/XmlSchemaClassGenerator.Tests/Compiler.cs +++ b/XmlSchemaClassGenerator.Tests/Compiler.cs @@ -33,7 +33,7 @@ public static CompilationResult GenerateAssembly(Compilation compilation) }; } - private static readonly ConcurrentDictionary Assemblies = new(); + private static readonly ConcurrentDictionary, Assembly>> Assemblies = new(); private static readonly string[] DependencyAssemblies = new[] { @@ -55,21 +55,33 @@ public static CompilationResult GenerateAssembly(Compilation compilation) public static Assembly GetAssembly(string name) { Assemblies.TryGetValue(name, out var assembly); - return assembly; + return assembly.Item2; } - public static Assembly Generate(string name, string pattern, Generator generatorPrototype = null) - { - if (Assemblies.ContainsKey(name)) { return Assemblies[name]; } + public static Assembly Generate(string name, string pattern, Generator generatorPrototype = null) + { + (_, var assembly) = GenerateVerbose(name, pattern, generatorPrototype); + return assembly; + } + + public static (List, Assembly) GenerateVerbose(string name, string pattern, Generator generatorPrototype = null) + { + if (Assemblies.TryGetValue(name, out var assembly)) { return (assembly.Item1, assembly.Item2); } var files = Glob.ExpandNames(pattern).OrderByDescending(f => f); - return GenerateFiles(name, files, generatorPrototype); + return GenerateFilesVerbose(name, files, generatorPrototype); } - public static Assembly GenerateFiles(string name, IEnumerable files, Generator generatorPrototype = null) - { - if (Assemblies.ContainsKey(name)) { return Assemblies[name]; } + public static Assembly GenerateFiles(string name, IEnumerable files, Generator generatorPrototype = null) + { + (_, var assembly) = GenerateFilesVerbose(name, files, generatorPrototype); + return assembly; + } + + public static (List, Assembly) GenerateFilesVerbose(string name, IEnumerable files, Generator generatorPrototype = null) + { + if (Assemblies.TryGetValue(name, out var assembly)) { return (assembly.Item1, assembly.Item2); } generatorPrototype ??= new Generator { @@ -123,12 +135,18 @@ public static Assembly GenerateFiles(string name, IEnumerable files, Gen gen.Generate(files); - return CompileFiles(name, output.Files); + return CompileFilesVerbose(name, output.Files); } - public static Assembly CompileFiles(string name, IEnumerable files) - { - return Compile(name, files.Select(f => File.ReadAllText(f)).ToArray()); + public static Assembly CompileFiles(string name, IEnumerable files) + { + (_, var assembly) = CompileFilesVerbose(name, files); + return assembly; + } + + public static (List, Assembly) CompileFilesVerbose(string name, IEnumerable files) + { + return CompileVerbose(name, files.Select(f => File.ReadAllText(f)).ToArray()); } private static readonly LanguageVersion MaxLanguageVersion = Enum @@ -136,15 +154,21 @@ public static Assembly CompileFiles(string name, IEnumerable files) .Cast() .Max(); - public static Assembly Compile(string name, params string[] contents) + public static Assembly Compile(string name, params string[] contents) { + (_, var assembly) = CompileVerbose(name, contents); + return assembly; + } + + public static (List, Assembly) CompileVerbose(string name, params string[] contents) + { var trustedAssembliesPaths = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")).Split(Path.PathSeparator); var references = trustedAssembliesPaths .Where(p => DependencyAssemblies.Contains(Path.GetFileNameWithoutExtension(p))) .Select(p => MetadataReference.CreateFromFile(p)) .ToList(); var options = new CSharpParseOptions(kind: SourceCodeKind.Regular, languageVersion: MaxLanguageVersion); - var syntaxTrees = contents.Select(c => CSharpSyntaxTree.ParseText(c, options)); + var syntaxTrees = contents.Select(c => CSharpSyntaxTree.ParseText(c, options)).ToList(); var compilation = CSharpCompilation.Create(name, syntaxTrees) .AddReferences(references) .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); @@ -155,9 +179,9 @@ public static Assembly Compile(string name, params string[] contents) Assert.True(result.Result.Success); Assert.NotNull(result.Assembly); - Assemblies[name] = result.Assembly; + Assemblies[name] = Tuple.Create(syntaxTrees, result.Assembly); - return result.Assembly; + return (syntaxTrees, result.Assembly); } } } diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 47e6eac4..9bed8e72 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -14,6 +14,7 @@ using System.Xml.XPath; using Ganss.IO; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Xml.XMLGen; using Xunit; using Xunit.Abstractions; @@ -105,6 +106,7 @@ private static IEnumerable ConvertXml(string name, string xsd, Generator const string DtsxPattern = "xsd/dtsx/dtsx2.xsd"; const string WfsPattern = "xsd/wfs/schemas.opengis.net/wfs/2.0/wfs.xsd"; const string EppPattern = "xsd/epp/*.xsd"; + const string NullableReferenceAttributesPattern = "xsd/nullablereferenceattributes/nullablereference.xsd"; // IATA test takes too long to perform every time @@ -2366,6 +2368,43 @@ void UnknownAttributeHandler(object sender, XmlAttributeEventArgs e) */ } + [Fact, TestPriority(1)] + [UseCulture("en-US")] + public void TestNullableReferenceAttributes() + { + var files = Glob.ExpandNames(NullableReferenceAttributesPattern).OrderByDescending(f => f); + var generator = new Generator + { + EnableNullableReferenceAttributes = true, + UseShouldSerializePattern = true, + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }; + //Unfortunately, I did not find access to the Attribute "AllowNull" in the generated Assembly. + //It is just not accessible via "CustomAttributes". + //So I have to parse the Syntax-Tree to find it. + (var syntaxTrees, _) = Compiler.GenerateVerbose(nameof(TestNullableReferenceAttributes), NullableReferenceAttributesPattern, generator); + void assertNullable(string typename, bool nullable) + { + var root = (CompilationUnitSyntax)syntaxTrees.Single().GetRoot(); + var ns = (NamespaceDeclarationSyntax)root.Members.Single(); + var c1 = (ClassDeclarationSyntax)ns.Members.Single( + m => m is ClassDeclarationSyntax c && c.Identifier.ToString() == typename + ); + var p = (PropertyDeclarationSyntax)c1.Members.Single(m => m is PropertyDeclarationSyntax n && n.Identifier.ToString() == "Text"); + var hasAllowNullAttribute = p.AttributeLists.Any(d => d.Attributes.Any(a => a.GetText().ToString() == "System.Diagnostics.CodeAnalysis.AllowNullAttribute()")); + Assert.Equal(nullable, hasAllowNullAttribute); + } + assertNullable("ElementReferenceNullable", true); + assertNullable("ElementReferenceList", true); + assertNullable("ElementReferenceNonNullable", false); + assertNullable("AttributeReferenceNullable", true); + assertNullable("AttributeReferenceNonNullable", false); + assertNullable("AttributeValueNullableInt", false); + } + [Fact, TestPriority(1)] public void TestNetex() { diff --git a/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd b/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd new file mode 100644 index 00000000..05eb6261 --- /dev/null +++ b/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XmlSchemaClassGenerator/Generator.cs b/XmlSchemaClassGenerator/Generator.cs index 0e790717..e0e8c215 100644 --- a/XmlSchemaClassGenerator/Generator.cs +++ b/XmlSchemaClassGenerator/Generator.cs @@ -115,6 +115,12 @@ public bool GenerateNullables set { _configuration.GenerateNullables = value; } } + public bool EnableNullableReferenceAttributes + { + get { return _configuration.EnableNullableReferenceAttributes; } + set { _configuration.EnableNullableReferenceAttributes = value; } + } + public bool UseShouldSerializePattern { get { return _configuration.UseShouldSerializePattern; } diff --git a/XmlSchemaClassGenerator/GeneratorConfiguration.cs b/XmlSchemaClassGenerator/GeneratorConfiguration.cs index fa71cfd0..0d4433ed 100644 --- a/XmlSchemaClassGenerator/GeneratorConfiguration.cs +++ b/XmlSchemaClassGenerator/GeneratorConfiguration.cs @@ -76,6 +76,10 @@ public GeneratorConfiguration() /// Use XElement instead of XmlElement for Any nodes? /// public bool UseXElementForAny { get; set; } + /// + /// Generate attributes for nullable references to avoid compiler-warnings in .NET Core and Standard with nullable-checks. + /// + public bool EnableNullableReferenceAttributes { get; set; } private NamingScheme namingScheme; diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index e0713f47..e3482df4 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -750,6 +750,15 @@ private bool IsNullableValueType && IsNullable && !(IsCollection || IsArray) && !IsList && ((PropertyType is EnumModel) || (PropertyType is SimpleModel model && model.ValueType.IsValueType)); } + } + + private bool IsNullableReferenceType + { + get + { + return DefaultValue == null + && IsNullable && (IsCollection || IsArray || IsList || PropertyType is ClassModel || PropertyType is SimpleModel model && !model.ValueType.IsValueType); + } } private bool IsNillableValueType @@ -860,6 +869,7 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi var isArray = IsArray; var propertyType = PropertyType; var isNullableValueType = IsNullableValueType; + var isNullableReferenceType = IsNullableReferenceType; var typeReference = TypeReference; var requiresBackingField = withDataBinding || DefaultValue != null || IsCollection || isArray; @@ -1114,6 +1124,11 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi typeDeclaration.Members.Add(specifiedProperty); } + if (isNullableReferenceType && Configuration.EnableNullableReferenceAttributes) + { + member.CustomAttributes.Add(new CodeAttributeDeclaration("System.Diagnostics.CodeAnalysis.AllowNullAttribute")); + } + var attributes = GetAttributes(isArray).ToArray(); member.CustomAttributes.AddRange(attributes); From 1693d5b12ad6a8c4f1fc0fff2b6e21f99871ce43 Mon Sep 17 00:00:00 2001 From: "andreas.hoffmann" Date: Sat, 12 Mar 2022 23:39:45 +0100 Subject: [PATCH 3/5] Show user input if there are issues with the namespaces. --- XmlSchemaClassGenerator/CodeUtilities.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XmlSchemaClassGenerator/CodeUtilities.cs b/XmlSchemaClassGenerator/CodeUtilities.cs index 8abc4673..e76e4d78 100644 --- a/XmlSchemaClassGenerator/CodeUtilities.cs +++ b/XmlSchemaClassGenerator/CodeUtilities.cs @@ -351,7 +351,7 @@ public static KeyValuePair ParseNamespace(string nsArg, st var parts = nsArg.Split(new[] { '=' }, 2); if (parts.Length != 2) { - throw new ArgumentException("XML and C# namespaces should be separated by '='."); + throw new ArgumentException("XML and C# namespaces should be separated by '='. You entered: " + nsArg); } var xmlNs = parts[0]; From 032f1e49642d185831a316cdc32e5aa07ebc7e64 Mon Sep 17 00:00:00 2001 From: "andreas.hoffmann" Date: Tue, 15 Mar 2022 20:51:47 +0100 Subject: [PATCH 4/5] Added MaybeNullAttribute and fixed tests with the correct check. Changes to Compiler.cs are not necessary anymore. --- XmlSchemaClassGenerator.Tests/Compiler.cs | 59 +++++++---------------- XmlSchemaClassGenerator.Tests/XmlTests.cs | 34 ++++++------- XmlSchemaClassGenerator/TypeModel.cs | 3 +- 3 files changed, 37 insertions(+), 59 deletions(-) diff --git a/XmlSchemaClassGenerator.Tests/Compiler.cs b/XmlSchemaClassGenerator.Tests/Compiler.cs index e0492624..95453d4a 100644 --- a/XmlSchemaClassGenerator.Tests/Compiler.cs +++ b/XmlSchemaClassGenerator.Tests/Compiler.cs @@ -33,7 +33,7 @@ public static CompilationResult GenerateAssembly(Compilation compilation) }; } - private static readonly ConcurrentDictionary, Assembly>> Assemblies = new(); + private static readonly ConcurrentDictionary Assemblies = new(); private static readonly string[] DependencyAssemblies = new[] { @@ -55,33 +55,21 @@ public static CompilationResult GenerateAssembly(Compilation compilation) public static Assembly GetAssembly(string name) { Assemblies.TryGetValue(name, out var assembly); - return assembly.Item2; - } - - public static Assembly Generate(string name, string pattern, Generator generatorPrototype = null) - { - (_, var assembly) = GenerateVerbose(name, pattern, generatorPrototype); - return assembly; + return assembly; } - public static (List, Assembly) GenerateVerbose(string name, string pattern, Generator generatorPrototype = null) - { - if (Assemblies.TryGetValue(name, out var assembly)) { return (assembly.Item1, assembly.Item2); } + public static Assembly Generate(string name, string pattern, Generator generatorPrototype = null) + { + if (Assemblies.ContainsKey(name)) { return Assemblies[name]; } var files = Glob.ExpandNames(pattern).OrderByDescending(f => f); - return GenerateFilesVerbose(name, files, generatorPrototype); + return GenerateFiles(name, files, generatorPrototype); } - public static Assembly GenerateFiles(string name, IEnumerable files, Generator generatorPrototype = null) - { - (_, var assembly) = GenerateFilesVerbose(name, files, generatorPrototype); - return assembly; - } - - public static (List, Assembly) GenerateFilesVerbose(string name, IEnumerable files, Generator generatorPrototype = null) - { - if (Assemblies.TryGetValue(name, out var assembly)) { return (assembly.Item1, assembly.Item2); } + public static Assembly GenerateFiles(string name, IEnumerable files, Generator generatorPrototype = null) + { + if (Assemblies.ContainsKey(name)) { return Assemblies[name]; } generatorPrototype ??= new Generator { @@ -125,6 +113,7 @@ public static (List, Assembly) GenerateFilesVerbose(string name, IEn UniqueTypeNamesAcrossNamespaces = generatorPrototype.UniqueTypeNamesAcrossNamespaces, CreateGeneratedCodeAttributeVersion = generatorPrototype.CreateGeneratedCodeAttributeVersion, NetCoreSpecificCode = generatorPrototype.NetCoreSpecificCode, + EnableNullableReferenceAttributes = generatorPrototype.EnableNullableReferenceAttributes, NamingScheme = generatorPrototype.NamingScheme }; @@ -135,18 +124,12 @@ public static (List, Assembly) GenerateFilesVerbose(string name, IEn gen.Generate(files); - return CompileFilesVerbose(name, output.Files); + return CompileFiles(name, output.Files); } - public static Assembly CompileFiles(string name, IEnumerable files) - { - (_, var assembly) = CompileFilesVerbose(name, files); - return assembly; - } - - public static (List, Assembly) CompileFilesVerbose(string name, IEnumerable files) - { - return CompileVerbose(name, files.Select(f => File.ReadAllText(f)).ToArray()); + public static Assembly CompileFiles(string name, IEnumerable files) + { + return Compile(name, files.Select(f => File.ReadAllText(f)).ToArray()); } private static readonly LanguageVersion MaxLanguageVersion = Enum @@ -154,21 +137,15 @@ public static (List, Assembly) CompileFilesVerbose(string name, IEnu .Cast() .Max(); - public static Assembly Compile(string name, params string[] contents) + public static Assembly Compile(string name, params string[] contents) { - (_, var assembly) = CompileVerbose(name, contents); - return assembly; - } - - public static (List, Assembly) CompileVerbose(string name, params string[] contents) - { var trustedAssembliesPaths = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")).Split(Path.PathSeparator); var references = trustedAssembliesPaths .Where(p => DependencyAssemblies.Contains(Path.GetFileNameWithoutExtension(p))) .Select(p => MetadataReference.CreateFromFile(p)) .ToList(); var options = new CSharpParseOptions(kind: SourceCodeKind.Regular, languageVersion: MaxLanguageVersion); - var syntaxTrees = contents.Select(c => CSharpSyntaxTree.ParseText(c, options)).ToList(); + var syntaxTrees = contents.Select(c => CSharpSyntaxTree.ParseText(c, options)); var compilation = CSharpCompilation.Create(name, syntaxTrees) .AddReferences(references) .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); @@ -179,9 +156,9 @@ public static (List, Assembly) CompileVerbose(string name, params st Assert.True(result.Result.Success); Assert.NotNull(result.Assembly); - Assemblies[name] = Tuple.Create(syntaxTrees, result.Assembly); + Assemblies[name] = result.Assembly; - return (syntaxTrees, result.Assembly); + return result.Assembly; } } } diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 9bed8e72..f32c41d8 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -2,6 +2,7 @@ using System.CodeDom; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -2382,27 +2383,26 @@ public void TestNullableReferenceAttributes() GenerateNamespace = key => "Test" } }; - //Unfortunately, I did not find access to the Attribute "AllowNull" in the generated Assembly. - //It is just not accessible via "CustomAttributes". - //So I have to parse the Syntax-Tree to find it. - (var syntaxTrees, _) = Compiler.GenerateVerbose(nameof(TestNullableReferenceAttributes), NullableReferenceAttributesPattern, generator); + var assembly = Compiler.Generate(nameof(TestNullableReferenceAttributes), NullableReferenceAttributesPattern, generator); void assertNullable(string typename, bool nullable) { - var root = (CompilationUnitSyntax)syntaxTrees.Single().GetRoot(); - var ns = (NamespaceDeclarationSyntax)root.Members.Single(); - var c1 = (ClassDeclarationSyntax)ns.Members.Single( - m => m is ClassDeclarationSyntax c && c.Identifier.ToString() == typename - ); - var p = (PropertyDeclarationSyntax)c1.Members.Single(m => m is PropertyDeclarationSyntax n && n.Identifier.ToString() == "Text"); - var hasAllowNullAttribute = p.AttributeLists.Any(d => d.Attributes.Any(a => a.GetText().ToString() == "System.Diagnostics.CodeAnalysis.AllowNullAttribute()")); + Type c = assembly.GetType(typename); + var property = c.GetProperty("Text"); + var setParameter = property.SetMethod.GetParameters(); + var getReturnParameter = property.GetMethod.ReturnParameter; + var allowNullableAttribute = setParameter.Single().CustomAttributes.SingleOrDefault(a => a.AttributeType == typeof(AllowNullAttribute)); + var maybeNullAttribute = getReturnParameter.CustomAttributes.SingleOrDefault(a => a.AttributeType == typeof(MaybeNullAttribute)); + var hasAllowNullAttribute = allowNullableAttribute != null; + var hasMaybeNullAttribute = maybeNullAttribute != null; Assert.Equal(nullable, hasAllowNullAttribute); + Assert.Equal(nullable, hasMaybeNullAttribute); } - assertNullable("ElementReferenceNullable", true); - assertNullable("ElementReferenceList", true); - assertNullable("ElementReferenceNonNullable", false); - assertNullable("AttributeReferenceNullable", true); - assertNullable("AttributeReferenceNonNullable", false); - assertNullable("AttributeValueNullableInt", false); + assertNullable("Test.ElementReferenceNullable", true); + assertNullable("Test.ElementReferenceList", true); + assertNullable("Test.ElementReferenceNonNullable", false); + assertNullable("Test.AttributeReferenceNullable", true); + assertNullable("Test.AttributeReferenceNonNullable", false); + assertNullable("Test.AttributeValueNullableInt", false); } [Fact, TestPriority(1)] diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index e3482df4..80b31f77 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -1127,7 +1127,8 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi if (isNullableReferenceType && Configuration.EnableNullableReferenceAttributes) { member.CustomAttributes.Add(new CodeAttributeDeclaration("System.Diagnostics.CodeAnalysis.AllowNullAttribute")); - } + member.CustomAttributes.Add(new CodeAttributeDeclaration("System.Diagnostics.CodeAnalysis.MaybeNullAttribute")); + } var attributes = GetAttributes(isArray).ToArray(); member.CustomAttributes.AddRange(attributes); From 08deb9cd07bfd342b6b4cb193c26d4a43e2ff05d Mon Sep 17 00:00:00 2001 From: "andreas.hoffmann" Date: Wed, 16 Mar 2022 16:27:42 +0100 Subject: [PATCH 5/5] Better description for nullableReferenceAttributes --- XmlSchemaClassGenerator.Console/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XmlSchemaClassGenerator.Console/Program.cs b/XmlSchemaClassGenerator.Console/Program.cs index 1cc6c242..1cdb594d 100644 --- a/XmlSchemaClassGenerator.Console/Program.cs +++ b/XmlSchemaClassGenerator.Console/Program.cs @@ -128,7 +128,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l { "un|uniqueTypeNames", "generate type names that are unique across namespaces (default is false)", v => uniqueTypeNamesAcrossNamespaces = v != null }, { "gc|generatedCodeAttribute", "add version information to GeneratedCodeAttribute (default is true)", v => createGeneratedCodeAttributeVersion = v != null }, { "nc|netCore", "generate .NET Core specific code that might not work with .NET Framework (default is false)", v => netCoreSpecificCode = v != null }, - { "nr|nullableReferenceAttributes", "generate attributes for nullable references (default is false)", v => nullableReferenceAttributes = v != null }, + { "nr|nullableReferenceAttributes", "generate attributes for nullable reference types (default is false)", v => nullableReferenceAttributes = v != null }, { "ca|commandArgs", "generate a comment with the exact command line arguments that were used to generate the source code (default is true)", v => generateCommandLineArgs = v != null }, };