Skip to content

Commit 4bfc2c4

Browse files
author
andreas.hoffmann
committed
Generator supports allowNullAttribute for reference types.
1 parent 375d02e commit 4bfc2c4

6 files changed

Lines changed: 151 additions & 17 deletions

File tree

XmlSchemaClassGenerator.Tests/Compiler.cs

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static CompilationResult GenerateAssembly(Compilation compilation)
3333
};
3434
}
3535

36-
private static readonly ConcurrentDictionary<string, Assembly> Assemblies = new();
36+
private static readonly ConcurrentDictionary<string, Tuple<List<SyntaxTree>, Assembly>> Assemblies = new();
3737

3838
private static readonly string[] DependencyAssemblies = new[]
3939
{
@@ -55,21 +55,33 @@ public static CompilationResult GenerateAssembly(Compilation compilation)
5555
public static Assembly GetAssembly(string name)
5656
{
5757
Assemblies.TryGetValue(name, out var assembly);
58-
return assembly;
58+
return assembly.Item2;
5959
}
6060

61-
public static Assembly Generate(string name, string pattern, Generator generatorPrototype = null)
62-
{
63-
if (Assemblies.ContainsKey(name)) { return Assemblies[name]; }
61+
public static Assembly Generate(string name, string pattern, Generator generatorPrototype = null)
62+
{
63+
(_, var assembly) = GenerateVerbose(name, pattern, generatorPrototype);
64+
return assembly;
65+
}
66+
67+
public static (List<SyntaxTree>, Assembly) GenerateVerbose(string name, string pattern, Generator generatorPrototype = null)
68+
{
69+
if (Assemblies.TryGetValue(name, out var assembly)) { return (assembly.Item1, assembly.Item2); }
6470

6571
var files = Glob.ExpandNames(pattern).OrderByDescending(f => f);
6672

67-
return GenerateFiles(name, files, generatorPrototype);
73+
return GenerateFilesVerbose(name, files, generatorPrototype);
6874
}
6975

70-
public static Assembly GenerateFiles(string name, IEnumerable<string> files, Generator generatorPrototype = null)
71-
{
72-
if (Assemblies.ContainsKey(name)) { return Assemblies[name]; }
76+
public static Assembly GenerateFiles(string name, IEnumerable<string> files, Generator generatorPrototype = null)
77+
{
78+
(_, var assembly) = GenerateFilesVerbose(name, files, generatorPrototype);
79+
return assembly;
80+
}
81+
82+
public static (List<SyntaxTree>, Assembly) GenerateFilesVerbose(string name, IEnumerable<string> files, Generator generatorPrototype = null)
83+
{
84+
if (Assemblies.TryGetValue(name, out var assembly)) { return (assembly.Item1, assembly.Item2); }
7385

7486
generatorPrototype ??= new Generator
7587
{
@@ -123,28 +135,40 @@ public static Assembly GenerateFiles(string name, IEnumerable<string> files, Gen
123135

124136
gen.Generate(files);
125137

126-
return CompileFiles(name, output.Files);
138+
return CompileFilesVerbose(name, output.Files);
127139
}
128140

129-
public static Assembly CompileFiles(string name, IEnumerable<string> files)
130-
{
131-
return Compile(name, files.Select(f => File.ReadAllText(f)).ToArray());
141+
public static Assembly CompileFiles(string name, IEnumerable<string> files)
142+
{
143+
(_, var assembly) = CompileFilesVerbose(name, files);
144+
return assembly;
145+
}
146+
147+
public static (List<SyntaxTree>, Assembly) CompileFilesVerbose(string name, IEnumerable<string> files)
148+
{
149+
return CompileVerbose(name, files.Select(f => File.ReadAllText(f)).ToArray());
132150
}
133151

134152
private static readonly LanguageVersion MaxLanguageVersion = Enum
135153
.GetValues(typeof(LanguageVersion))
136154
.Cast<LanguageVersion>()
137155
.Max();
138156

139-
public static Assembly Compile(string name, params string[] contents)
157+
public static Assembly Compile(string name, params string[] contents)
140158
{
159+
(_, var assembly) = CompileVerbose(name, contents);
160+
return assembly;
161+
}
162+
163+
public static (List<SyntaxTree>, Assembly) CompileVerbose(string name, params string[] contents)
164+
{
141165
var trustedAssembliesPaths = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")).Split(Path.PathSeparator);
142166
var references = trustedAssembliesPaths
143167
.Where(p => DependencyAssemblies.Contains(Path.GetFileNameWithoutExtension(p)))
144168
.Select(p => MetadataReference.CreateFromFile(p))
145169
.ToList();
146170
var options = new CSharpParseOptions(kind: SourceCodeKind.Regular, languageVersion: MaxLanguageVersion);
147-
var syntaxTrees = contents.Select(c => CSharpSyntaxTree.ParseText(c, options));
171+
var syntaxTrees = contents.Select(c => CSharpSyntaxTree.ParseText(c, options)).ToList();
148172
var compilation = CSharpCompilation.Create(name, syntaxTrees)
149173
.AddReferences(references)
150174
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
@@ -155,9 +179,9 @@ public static Assembly Compile(string name, params string[] contents)
155179
Assert.True(result.Result.Success);
156180
Assert.NotNull(result.Assembly);
157181

158-
Assemblies[name] = result.Assembly;
182+
Assemblies[name] = Tuple.Create(syntaxTrees, result.Assembly);
159183

160-
return result.Assembly;
184+
return (syntaxTrees, result.Assembly);
161185
}
162186
}
163187
}

XmlSchemaClassGenerator.Tests/XmlTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Xml.XPath;
1515
using Ganss.IO;
1616
using Microsoft.CodeAnalysis;
17+
using Microsoft.CodeAnalysis.CSharp.Syntax;
1718
using Microsoft.Xml.XMLGen;
1819
using Xunit;
1920
using Xunit.Abstractions;
@@ -105,6 +106,7 @@ private static IEnumerable<string> ConvertXml(string name, string xsd, Generator
105106
const string DtsxPattern = "xsd/dtsx/dtsx2.xsd";
106107
const string WfsPattern = "xsd/wfs/schemas.opengis.net/wfs/2.0/wfs.xsd";
107108
const string EppPattern = "xsd/epp/*.xsd";
109+
const string NullableReferenceAttributesPattern = "xsd/nullablereferenceattributes/nullablereference.xsd";
108110

109111
// IATA test takes too long to perform every time
110112

@@ -2366,6 +2368,43 @@ void UnknownAttributeHandler(object sender, XmlAttributeEventArgs e)
23662368
*/
23672369
}
23682370

2371+
[Fact, TestPriority(1)]
2372+
[UseCulture("en-US")]
2373+
public void TestNullableReferenceAttributes()
2374+
{
2375+
var files = Glob.ExpandNames(NullableReferenceAttributesPattern).OrderByDescending(f => f);
2376+
var generator = new Generator
2377+
{
2378+
EnableNullableReferenceAttributes = true,
2379+
UseShouldSerializePattern = true,
2380+
NamespaceProvider = new NamespaceProvider
2381+
{
2382+
GenerateNamespace = key => "Test"
2383+
}
2384+
};
2385+
//Unfortunately, I did not find access to the Attribute "AllowNull" in the generated Assembly.
2386+
//It is just not accessible via "CustomAttributes".
2387+
//So I have to parse the Syntax-Tree to find it.
2388+
(var syntaxTrees, _) = Compiler.GenerateVerbose(nameof(TestNullableReferenceAttributes), NullableReferenceAttributesPattern, generator);
2389+
void assertNullable(string typename, bool nullable)
2390+
{
2391+
var root = (CompilationUnitSyntax)syntaxTrees.Single().GetRoot();
2392+
var ns = (NamespaceDeclarationSyntax)root.Members.Single();
2393+
var c1 = (ClassDeclarationSyntax)ns.Members.Single(
2394+
m => m is ClassDeclarationSyntax c && c.Identifier.ToString() == typename
2395+
);
2396+
var p = (PropertyDeclarationSyntax)c1.Members.Single(m => m is PropertyDeclarationSyntax n && n.Identifier.ToString() == "Text");
2397+
var hasAllowNullAttribute = p.AttributeLists.Any(d => d.Attributes.Any(a => a.GetText().ToString() == "System.Diagnostics.CodeAnalysis.AllowNullAttribute()"));
2398+
Assert.Equal(nullable, hasAllowNullAttribute);
2399+
}
2400+
assertNullable("ElementReferenceNullable", true);
2401+
assertNullable("ElementReferenceList", true);
2402+
assertNullable("ElementReferenceNonNullable", false);
2403+
assertNullable("AttributeReferenceNullable", true);
2404+
assertNullable("AttributeReferenceNonNullable", false);
2405+
assertNullable("AttributeValueNullableInt", false);
2406+
}
2407+
23692408
[Fact, TestPriority(1)]
23702409
public void TestNetex()
23712410
{
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<xs:schema targetNamespace="http://tempuri.org/XMLSchema.xsd"
3+
elementFormDefault="qualified"
4+
xmlns="http://tempuri.org/XMLSchema.xsd"
5+
xmlns:mstns="http://tempuri.org/XMLSchema.xsd"
6+
xmlns:xs="http://www.w3.org/2001/XMLSchema"
7+
>
8+
9+
<xs:element name="ElementReferenceNullable">
10+
<xs:complexType>
11+
<xs:sequence>
12+
<xs:element name="text" type="xs:string" minOccurs="0" />
13+
</xs:sequence>
14+
</xs:complexType>
15+
</xs:element>
16+
<xs:element name="ElementReferenceList">
17+
<xs:complexType>
18+
<xs:sequence>
19+
<xs:element name="text" type="xs:string" minOccurs="0" maxOccurs="unbounded" />
20+
</xs:sequence>
21+
</xs:complexType>
22+
</xs:element>
23+
<xs:element name="ElementReferenceNonNullable">
24+
<xs:complexType>
25+
<xs:sequence>
26+
<xs:element name="text" type="xs:string" minOccurs="1" />
27+
</xs:sequence>
28+
</xs:complexType>
29+
</xs:element>
30+
31+
<xs:element name="AttributeReferenceNullable">
32+
<xs:complexType>
33+
<xs:attribute name="text" type="xs:string" use="optional" />
34+
</xs:complexType>
35+
</xs:element>
36+
<xs:element name="AttributeReferenceNonNullable">
37+
<xs:complexType>
38+
<xs:attribute name="text" type="xs:string" use="required" />
39+
</xs:complexType>
40+
</xs:element>
41+
<xs:element name="AttributeValueNullableInt">
42+
<xs:complexType>
43+
<xs:attribute name="text" type="xs:int" use="optional" />
44+
</xs:complexType>
45+
</xs:element>
46+
</xs:schema>

XmlSchemaClassGenerator/Generator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ public bool GenerateNullables
115115
set { _configuration.GenerateNullables = value; }
116116
}
117117

118+
public bool EnableNullableReferenceAttributes
119+
{
120+
get { return _configuration.EnableNullableReferenceAttributes; }
121+
set { _configuration.EnableNullableReferenceAttributes = value; }
122+
}
123+
118124
public bool UseShouldSerializePattern
119125
{
120126
get { return _configuration.UseShouldSerializePattern; }

XmlSchemaClassGenerator/GeneratorConfiguration.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public GeneratorConfiguration()
7676
/// Use XElement instead of XmlElement for Any nodes?
7777
/// </summary>
7878
public bool UseXElementForAny { get; set; }
79+
/// <summary>
80+
/// Generate attributes for nullable references to avoid compiler-warnings in .NET Core and Standard with nullable-checks.
81+
/// </summary>
82+
public bool EnableNullableReferenceAttributes { get; set; }
7983

8084
private NamingScheme namingScheme;
8185

XmlSchemaClassGenerator/TypeModel.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,15 @@ private bool IsNullableValueType
750750
&& IsNullable && !(IsCollection || IsArray) && !IsList
751751
&& ((PropertyType is EnumModel) || (PropertyType is SimpleModel model && model.ValueType.IsValueType));
752752
}
753+
}
754+
755+
private bool IsNullableReferenceType
756+
{
757+
get
758+
{
759+
return DefaultValue == null
760+
&& IsNullable && (IsCollection || IsArray || IsList || PropertyType is ClassModel || PropertyType is SimpleModel model && !model.ValueType.IsValueType);
761+
}
753762
}
754763

755764
private bool IsNillableValueType
@@ -860,6 +869,7 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi
860869
var isArray = IsArray;
861870
var propertyType = PropertyType;
862871
var isNullableValueType = IsNullableValueType;
872+
var isNullableReferenceType = IsNullableReferenceType;
863873
var typeReference = TypeReference;
864874

865875
var requiresBackingField = withDataBinding || DefaultValue != null || IsCollection || isArray;
@@ -1114,6 +1124,11 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi
11141124
typeDeclaration.Members.Add(specifiedProperty);
11151125
}
11161126

1127+
if (isNullableReferenceType && Configuration.EnableNullableReferenceAttributes)
1128+
{
1129+
member.CustomAttributes.Add(new CodeAttributeDeclaration("System.Diagnostics.CodeAnalysis.AllowNullAttribute"));
1130+
}
1131+
11171132
var attributes = GetAttributes(isArray).ToArray();
11181133
member.CustomAttributes.AddRange(attributes);
11191134

0 commit comments

Comments
 (0)