- ClientModelPlugin.AdditionalMetadataReferences now includes
System.Memory.Data and System.Text.Json in order to allow Roslyn to
parse types from these assemblies found in custom code. BinaryData and
IJsonModel (which references Utf8JsonWriter) are common in customized
partial classes.
- AdditionalMetadataReferences were not added to the compilation for
custom code which would cause Roslyn to not load the additional
references correctly
- Even after fixing the above two issues, framework types that are not a
dependency of the generator, e.g. BinaryData, would not be found by
Type.GetType call in NamedTypeSymbolProvider. To work around this issue,
we will create a new CSharpType for such framework types using all of
the ISymbol information as inputs.
- Add handling for malformed xml docs
This commit is contained in:
JoshLove-msft 2024-09-10 22:39:45 -07:00 коммит произвёл GitHub
Родитель 2010bc4490
Коммит a5fc2ce620
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
9 изменённых файлов: 385 добавлений и 49 удалений

Просмотреть файл

@ -5,6 +5,7 @@ using System;
using System.ClientModel;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Text.Json;
using Microsoft.CodeAnalysis;
namespace Microsoft.Generator.CSharp.ClientModel
@ -21,7 +22,12 @@ namespace Microsoft.Generator.CSharp.ClientModel
public override ScmTypeFactory TypeFactory { get; }
public override IReadOnlyList<MetadataReference> AdditionalMetadataReferences => [MetadataReference.CreateFromFile(typeof(ClientResult).Assembly.Location)];
public override IReadOnlyList<MetadataReference> AdditionalMetadataReferences =>
[
MetadataReference.CreateFromFile(typeof(ClientResult).Assembly.Location),
MetadataReference.CreateFromFile(typeof(BinaryData).Assembly.Location),
MetadataReference.CreateFromFile(typeof(JsonSerializer).Assembly.Location)
];
[ImportingConstructor]
public ClientModelPlugin(GeneratorContext context)

Просмотреть файл

@ -176,7 +176,7 @@ namespace Microsoft.Generator.CSharp
}
project = project
.AddMetadataReferences(_assemblyMetadataReferences.Value)
.AddMetadataReferences(_assemblyMetadataReferences.Value.Concat(CodeModelPlugin.Instance.AdditionalMetadataReferences))
.WithCompilationOptions(new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary, metadataReferenceResolver: _metadataReferenceResolver.Value, nullableContextOptions: NullableContextOptions.Disable));

Просмотреть файл

@ -456,8 +456,37 @@ namespace Microsoft.Generator.CSharp.Primitives
return obj is CSharpType csType && Equals(csType, ignoreNullable: false);
}
public bool Equals(Type type) =>
IsFrameworkType && (type.IsGenericType ? type.GetGenericTypeDefinition() == FrameworkType && AreArgumentsEqual(type.GetGenericArguments()) : type == FrameworkType);
public bool Equals(Type type)
{
if (IsFrameworkType)
{
return type.IsGenericType
? type.GetGenericTypeDefinition() == FrameworkType && AreArgumentsEqual(type.GetGenericArguments())
: type == FrameworkType;
}
return Name == type.Name && Namespace == type.Namespace && IsValueType == type.IsValueType && AreArgumentsEqual(Arguments, type.GetGenericArguments())
&& IsEnum == type.IsEnum && IsStruct == type.IsValueType && IsPublic == type.IsPublic
&& ((type.IsEnum && _underlyingType! == type.GetEnumUnderlyingType()) || !type.IsEnum);
}
private static bool AreArgumentsEqual(IReadOnlyList<CSharpType> arguments, IReadOnlyList<Type> genericArguments)
{
if (arguments.Count != genericArguments.Count)
{
return false;
}
for (int i = 0; i < arguments.Count; i++)
{
if (!arguments[i].Equals(genericArguments[i]))
{
return false;
}
}
return true;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed override int GetHashCode()

Просмотреть файл

@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.Generator.CSharp.Primitives;
@ -22,7 +24,7 @@ namespace Microsoft.Generator.CSharp.Providers
private protected sealed override TypeProvider? GetCustomCodeView() => null;
protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writting in generation");
protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writing in generation");
protected override string BuildName() => _namedTypeSymbol.Name;
@ -107,7 +109,7 @@ namespace Microsoft.Generator.CSharp.Providers
methodSymbol.Name,
GetSymbolXmlDoc(methodSymbol, "summary"),
modifiers,
GetCSharpType(methodSymbol.ReturnType),
GetNullableCSharpType(methodSymbol.ReturnType),
GetSymbolXmlDoc(methodSymbol, "returns"),
[.. methodSymbol.Parameters.Select(p => ConvertToParameterProvider(methodSymbol, p))]);
@ -116,7 +118,7 @@ namespace Microsoft.Generator.CSharp.Providers
return [.. methods];
}
private static ParameterProvider ConvertToParameterProvider(IMethodSymbol methodSymbol, IParameterSymbol parameterSymbol)
private ParameterProvider ConvertToParameterProvider(IMethodSymbol methodSymbol, IParameterSymbol parameterSymbol)
{
return new ParameterProvider(
parameterSymbol.Name,
@ -141,13 +143,35 @@ namespace Microsoft.Generator.CSharp.Providers
var xmlDocumentation = propertySymbol.GetDocumentationCommentXml();
if (!string.IsNullOrEmpty(xmlDocumentation))
{
var xDocument = XDocument.Parse(xmlDocumentation);
XDocument xDocument = ParseXml(propertySymbol, xmlDocumentation);
var summaryElement = xDocument.Descendants(tag).FirstOrDefault();
return FormattableStringHelpers.FromString(summaryElement?.Value.Trim());
}
return null;
}
private static XDocument ParseXml(ISymbol docsSymbol, string xmlDocumentation)
{
XDocument xDocument;
try
{
xDocument = XDocument.Parse(xmlDocumentation);
}
catch (XmlException ex)
{
var files = new List<string>();
foreach (var reference in docsSymbol.DeclaringSyntaxReferences)
{
files.Add(reference.SyntaxTree.FilePath);
}
throw new InvalidOperationException($"Failed to parse XML documentation for {docsSymbol.Name}. " +
$"The malformed XML documentation is located in one or more of the following files: {string.Join(',', files)}", ex);
}
return xDocument;
}
private static string? GetParameterXmlDocumentation(IMethodSymbol methodSymbol, IParameterSymbol parameterSymbol)
{
var xmlDocumentation = methodSymbol.GetDocumentationCommentXml();
@ -157,7 +181,7 @@ namespace Microsoft.Generator.CSharp.Providers
return null;
}
var xmlDoc = XDocument.Parse(xmlDocumentation);
var xmlDoc = ParseXml(methodSymbol, xmlDocumentation);
var paramElement = xmlDoc.Descendants("param")
.FirstOrDefault(e => e.Attribute("name")?.Value == parameterSymbol.Name);
@ -182,41 +206,37 @@ namespace Microsoft.Generator.CSharp.Providers
_ => FieldModifiers.Public
};
private static CSharpType GetCSharpType(ITypeSymbol typeSymbol)
private CSharpType? GetNullableCSharpType(ITypeSymbol typeSymbol)
{
var fullyQualifiedName = GetFullyQualifiedName(typeSymbol);
var pieces = fullyQualifiedName.Split('.');
if (fullyQualifiedName == "System.Void")
{
return null;
}
return GetCSharpType(typeSymbol);
}
private CSharpType GetCSharpType(ITypeSymbol typeSymbol)
{
var fullyQualifiedName = GetFullyQualifiedName(typeSymbol);
var namedTypeSymbol = typeSymbol as INamedTypeSymbol;
//if fully qualified name is in the namespace of the library being emitted find it from the outputlibrary
if (fullyQualifiedName.StartsWith(CodeModelPlugin.Instance.Configuration.RootNamespace, StringComparison.Ordinal))
{
bool isValueType = typeSymbol.IsValueType;
bool isEnum = typeSymbol.TypeKind == TypeKind.Enum;
return new CSharpType(
typeSymbol.Name,
string.Join('.', pieces.Take(pieces.Length - 1)),
isValueType,
typeSymbol.NullableAnnotation == NullableAnnotation.Annotated,
typeSymbol.ContainingType is not null ? GetCSharpType(typeSymbol.ContainingType) : null,
namedTypeSymbol is not null ? [.. namedTypeSymbol.TypeArguments.Select(GetCSharpType)] : [],
typeSymbol.DeclaredAccessibility == Accessibility.Public,
isValueType && !isEnum,
baseType: typeSymbol.BaseType is not null ? GetCSharpType(typeSymbol.BaseType) : null,
underlyingEnumType: namedTypeSymbol is not null && namedTypeSymbol.EnumUnderlyingType is not null
? GetCSharpType(namedTypeSymbol.EnumUnderlyingType).FrameworkType
: null);
return ConstructCSharpTypeFromSymbol(typeSymbol, fullyQualifiedName, namedTypeSymbol);
}
Type? type = System.Type.GetType(fullyQualifiedName);
if (type is null)
{
throw new InvalidOperationException($"Unable to convert ITypeSymbol: {fullyQualifiedName} to a CSharpType");
if (typeSymbol.TypeKind == TypeKind.Error)
throw new InvalidOperationException($"Unable to convert ITypeSymbol: {fullyQualifiedName} to a CSharpType in {Name}");
return ConstructCSharpTypeFromSymbol(typeSymbol, fullyQualifiedName, namedTypeSymbol);
}
CSharpType result = new CSharpType(type);
if (namedTypeSymbol is not null && namedTypeSymbol.IsGenericType)
{
return result.MakeGenericType([.. namedTypeSymbol.TypeArguments.Select(GetCSharpType)]);
@ -225,6 +245,29 @@ namespace Microsoft.Generator.CSharp.Providers
return result;
}
private CSharpType ConstructCSharpTypeFromSymbol(
ITypeSymbol typeSymbol,
string fullyQualifiedName,
INamedTypeSymbol? namedTypeSymbol)
{
bool isValueType = typeSymbol.IsValueType;
bool isEnum = typeSymbol.TypeKind == TypeKind.Enum;
var pieces = fullyQualifiedName.Split('.');
return new CSharpType(
typeSymbol.Name,
string.Join('.', pieces.Take(pieces.Length - 1)),
isValueType,
typeSymbol.NullableAnnotation == NullableAnnotation.Annotated,
typeSymbol.ContainingType is not null ? GetCSharpType(typeSymbol.ContainingType) : null,
namedTypeSymbol is not null ? [.. namedTypeSymbol.TypeArguments.Select(GetCSharpType)] : [],
typeSymbol.DeclaredAccessibility == Accessibility.Public,
isValueType && !isEnum,
baseType: typeSymbol.BaseType is not null && typeSymbol.BaseType.TypeKind != TypeKind.Error ? GetCSharpType(typeSymbol.BaseType) : null,
underlyingEnumType: namedTypeSymbol is not null && namedTypeSymbol.EnumUnderlyingType is not null
? GetCSharpType(namedTypeSymbol.EnumUnderlyingType).FrameworkType
: null);
}
private static string GetFullyQualifiedName(ITypeSymbol typeSymbol)
{
// Handle special cases for built-in types

Просмотреть файл

@ -32,6 +32,26 @@ namespace Microsoft.Generator.CSharp.Tests.Primitives
Assert.IsTrue(cst1.Equals(cst2));
}
[TestCase(typeof(int))]
[TestCase(typeof(string))]
[TestCase(typeof(int[]))]
[TestCase(typeof(string[]))]
[TestCase(typeof(IDictionary<int, string>))]
[TestCase(typeof(int?))]
public void NonFrameworkTypeEqualsEquivalentFrameworkType(Type type)
{
var cSharpType = new CSharpType(
type.Name,
type.Namespace!,
type.IsValueType,
Nullable.GetUnderlyingType(type) != null,
null,
type.GetGenericArguments().Select(t => new CSharpType(t)).ToList(),
true,
type.IsValueType);
Assert.IsTrue(cSharpType.Equals(type));
}
[TestCase(typeof(int))]
[TestCase(typeof(IList<>))]
[TestCase(typeof(IList<int>))]

Просмотреть файл

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
namespace Microsoft.Generator.CSharp.Tests.Providers.NamedTypeSymbolProviders
{
public static class CompilationHelper
{
public static Compilation LoadCompilation(IEnumerable<TypeProvider> providers, IEnumerable<Type>? metadataReferenceTypes = default)
{
MockHelpers.LoadMockPlugin();
List<SyntaxTree> files = new List<SyntaxTree>();
foreach (var provider in providers)
{
files.Add(GetTree(provider));
}
return CSharpCompilation.Create(
assemblyName: "TestAssembly",
syntaxTrees: [.. files],
references: [.. metadataReferenceTypes?.Select(t => MetadataReference.CreateFromFile(t.Assembly.Location)) ?? [], MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]);
}
private static SyntaxTree GetTree(TypeProvider provider)
{
var writer = new TypeProviderWriter(provider);
var file = writer.Write();
return CSharpSyntaxTree.ParseText(file.Content, path: Path.Join(provider.RelativeFilePath, provider.Name + ".cs"));
}
internal static INamedTypeSymbol? GetSymbol(INamespaceSymbol namespaceSymbol, string name)
{
foreach (var childNamespaceSymbol in namespaceSymbol.GetNamespaceMembers())
{
return GetSymbol(childNamespaceSymbol, name);
}
foreach (INamedTypeSymbol symbol in namespaceSymbol.GetTypeMembers())
{
if (symbol.MetadataName == name)
{
return symbol;
}
}
return null;
}
}
}

Просмотреть файл

@ -0,0 +1,93 @@
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Linq;
using System.Text.Json;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Snippets;
using NUnit.Framework;
namespace Microsoft.Generator.CSharp.Tests.Providers.NamedTypeSymbolProviders
{
public class InterfaceTests
{
[Test]
public void ValidateMethods()
{
var model = new Model();
var compilation = CompilationHelper.LoadCompilation([model],
[
typeof(BinaryData),
typeof(JsonSerializer),
typeof(ClientResult)
]);
var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "Model");
var namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!);
var methods = namedTypeSymbolProvider.Methods;
Assert.AreEqual(1, methods.Count);
Assert.AreEqual("global::System.ClientModel.Primitives.IJsonModel<T>.Write", methods[0].Signature.Name);
Assert.AreEqual(2, methods[0].Signature.Parameters.Count);
Assert.IsTrue(methods[0].Signature.Parameters[0].Type.Equals(typeof(Utf8JsonWriter)));
Assert.IsTrue(methods[0].Signature.Parameters[1].Type.Equals(typeof(ModelReaderWriterOptions)));
Assert.IsNull(methods[0].Signature.ReturnType);
}
[Test]
public void VerifyPropertyCanBeLoaded()
{
var model = new Model();
var compilation = CompilationHelper.LoadCompilation([model], [typeof(BinaryData)]);
var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "Model");
var namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!);
Assert.AreEqual(2, namedTypeSymbolProvider.Properties.Count);
Assert.AreEqual("X", namedTypeSymbolProvider.Properties[0].Name);
Assert.IsTrue(namedTypeSymbolProvider.Properties[0].Type.Equals(typeof(int)));
Assert.AreEqual("Y", namedTypeSymbolProvider.Properties[1].Name);
Assert.IsTrue(namedTypeSymbolProvider.Properties[1].Type.Equals(typeof(BinaryData)));
}
[Test]
public void VerifyBaseTypeIsNull()
{
var model = new Model();
var compilation = CompilationHelper.LoadCompilation([model]);
var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "Model");
var namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!);
Assert.IsNull(namedTypeSymbolProvider.Type.BaseType);
}
private class Model : TypeProvider
{
protected override string BuildRelativeFilePath() => ".";
protected override string BuildName() => "Model";
protected override CSharpType[] BuildImplements()
{
return [new CSharpType(typeof(IJsonModel<>), Type)];
}
protected override MethodProvider[] BuildMethods()
{
var sig = new MethodSignature("Write", $"", MethodSignatureModifiers.None, null, $"", [ new ParameterProvider("writer", $"", typeof(Utf8JsonWriter)), new ParameterProvider("options", $"", typeof(ModelReaderWriterOptions)) ], null, null, null, new CSharpType(typeof(IJsonModel<>)));
return [new MethodProvider(sig, Snippet.ThrowExpression(Snippet.Null), this, null)];
}
protected override PropertyProvider[] BuildProperties()
{
return
[
new PropertyProvider($"", MethodSignatureModifiers.Public, new CSharpType(typeof(int)), "X",
new AutoPropertyBody(true), this),
new PropertyProvider($"", MethodSignatureModifiers.Public, new CSharpType(typeof(BinaryData)), "Y",
new AutoPropertyBody(true), this),
];
}
}
}
}

Просмотреть файл

@ -6,13 +6,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
using NUnit.Framework;
using static Microsoft.Generator.CSharp.Snippets.Snippet;
namespace Microsoft.Generator.CSharp.Tests.Providers
namespace Microsoft.Generator.CSharp.Tests.Providers.NamedTypeSymbolProviders
{
public class NamedTypeSymbolProviderTests
{
@ -21,18 +20,7 @@ namespace Microsoft.Generator.CSharp.Tests.Providers
public NamedTypeSymbolProviderTests()
{
MockHelpers.LoadMockPlugin();
List<SyntaxTree> files =
[
GetTree(new NamedSymbol()),
GetTree(new PropertyType())
];
var compilation = CSharpCompilation.Create(
assemblyName: "TestAssembly",
syntaxTrees: [.. files],
references: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]);
var compilation = CompilationHelper.LoadCompilation([new NamedSymbol(), new PropertyType()]);
var iNamedSymbol = GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "NamedSymbol");
_namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!);
@ -209,13 +197,6 @@ namespace Microsoft.Generator.CSharp.Tests.Providers
protected override string BuildName() => "PropertyType";
}
private static SyntaxTree GetTree(TypeProvider provider)
{
var writer = new TypeProviderWriter(provider);
var file = writer.Write();
return CSharpSyntaxTree.ParseText(file.Content);
}
internal static INamedTypeSymbol? GetSymbol(INamespaceSymbol namespaceSymbol, string name)
{
foreach (var childNamespaceSymbol in namespaceSymbol.GetNamespaceMembers())

Просмотреть файл

@ -0,0 +1,110 @@
using System;
using System.ClientModel.Primitives;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Xml;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Snippets;
using NUnit.Framework;
namespace Microsoft.Generator.CSharp.Tests.Providers.NamedTypeSymbolProviders
{
public class XmlDocsTests
{
[Test]
public void InvalidPropertyDocsThrows()
{
var model = new InvalidPropertyDocsModel();
var compilation = CompilationHelper.LoadCompilation(new[] { model });
var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, nameof(InvalidPropertyDocsModel));
var namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!);
var ex = Assert.Throws<InvalidOperationException>(() => _ = namedTypeSymbolProvider.Properties);
Assert.IsInstanceOf<XmlException>(ex!.InnerException);
StringAssert.Contains(
$"The malformed XML documentation is located in one or more of the following files: .{Path.DirectorySeparatorChar}{nameof(InvalidPropertyDocsModel)}.cs",
ex.Message);
}
[Test]
public void ValidDocsDoNotThrow()
{
var model = new ValidDocsModel();
var compilation = CompilationHelper.LoadCompilation(new[] { model });
var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, nameof(ValidDocsModel));
var namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!);
Assert.AreEqual(2, namedTypeSymbolProvider.Properties.Count);
Assert.AreEqual("X", namedTypeSymbolProvider.Properties[0].Name);
Assert.IsTrue(namedTypeSymbolProvider.Properties[0].Type.Equals(typeof(int)));
Assert.AreEqual("Y", namedTypeSymbolProvider.Properties[1].Name);
Assert.IsTrue(namedTypeSymbolProvider.Properties[1].Type.Equals(typeof(string)));
}
[Test]
public void InvalidParameterDocsThrows()
{
var model = new InvalidParameterDocsModel();
var compilation = CompilationHelper.LoadCompilation(new[] { model });
var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, nameof(InvalidParameterDocsModel));
var namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!);
var ex = Assert.Throws<InvalidOperationException>(() => _ = namedTypeSymbolProvider.Methods);
Assert.IsInstanceOf<XmlException>(ex!.InnerException);
StringAssert.Contains(
$"The malformed XML documentation is located in one or more of the following files: .{Path.DirectorySeparatorChar}{nameof(InvalidParameterDocsModel)}.cs",
ex.Message);
}
private class InvalidPropertyDocsModel : TypeProvider
{
protected override string BuildRelativeFilePath() => ".";
protected override string BuildName() => nameof(InvalidPropertyDocsModel);
protected override PropertyProvider[] BuildProperties()
{
return
[
new PropertyProvider($"This is an invalid description because it is missing closing slash <see cref=\"Y\">", MethodSignatureModifiers.Public, new CSharpType(typeof(int)), "X",
new AutoPropertyBody(true), this),
new PropertyProvider($"", MethodSignatureModifiers.Public, new CSharpType(typeof(string)), "Y",
new AutoPropertyBody(true), this),
];
}
}
private class InvalidParameterDocsModel : TypeProvider
{
protected override string BuildRelativeFilePath() => ".";
protected override string BuildName() => nameof(InvalidParameterDocsModel);
protected override MethodProvider[] BuildMethods()
{
var sig = new MethodSignature("Write", $"", MethodSignatureModifiers.Public, null, $"", [ new ParameterProvider("value", $"This is an invalid description because it is missing closing slash <see cref=\"Y\">", typeof(int)), new ParameterProvider("options", $"", typeof(string)) ]);
return [new MethodProvider(sig, Snippet.ThrowExpression(Snippet.Null), this, null)];
}
}
private class ValidDocsModel : TypeProvider
{
protected override string BuildRelativeFilePath() => ".";
protected override string BuildName() => nameof(ValidDocsModel);
protected override PropertyProvider[] BuildProperties()
{
return
[
new PropertyProvider($"This is a valid description <see cref=\"Y\"/>", MethodSignatureModifiers.Public, new CSharpType(typeof(int)), "X",
new AutoPropertyBody(true), this),
new PropertyProvider($"", MethodSignatureModifiers.Public, new CSharpType(typeof(string)), "Y",
new AutoPropertyBody(true), this),
];
}
}
}
}