Add NamedSymbolTypeProvider (#3746)
Fixes https://github.com/microsoft/typespec/issues/3744
This commit is contained in:
Родитель
4658a058e0
Коммит
737f244f0b
|
@ -0,0 +1,160 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.Generator.CSharp.Primitives;
|
||||
|
||||
namespace Microsoft.Generator.CSharp.Providers
|
||||
{
|
||||
public class NamedTypeSymbolProvider : TypeProvider
|
||||
{
|
||||
private INamedTypeSymbol _namedTypeSymbol;
|
||||
|
||||
public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol)
|
||||
{
|
||||
_namedTypeSymbol = namedTypeSymbol;
|
||||
}
|
||||
|
||||
public override string RelativeFilePath => throw new InvalidOperationException("This type should not be writting in generation");
|
||||
|
||||
public override string Name => _namedTypeSymbol.Name;
|
||||
|
||||
protected override string GetNamespace() => GetFullyQualifiedNameFromDisplayString(_namedTypeSymbol.ContainingNamespace);
|
||||
|
||||
protected override PropertyProvider[] BuildProperties()
|
||||
{
|
||||
List<PropertyProvider> properties = new List<PropertyProvider>();
|
||||
foreach (var propertySymbol in _namedTypeSymbol.GetMembers().OfType<IPropertySymbol>())
|
||||
{
|
||||
var propertyProvider = new PropertyProvider(
|
||||
$"{GetPropertySummary(propertySymbol)}",
|
||||
GetMethodSignatureModifiers(propertySymbol.DeclaredAccessibility),
|
||||
GetCSharpType(propertySymbol.Type),
|
||||
propertySymbol.Name,
|
||||
new AutoPropertyBody(propertySymbol.SetMethod is not null));
|
||||
properties.Add(propertyProvider);
|
||||
}
|
||||
return [.. properties];
|
||||
}
|
||||
|
||||
private static string? GetPropertySummary(IPropertySymbol propertySymbol)
|
||||
{
|
||||
var xmlDocumentation = propertySymbol.GetDocumentationCommentXml();
|
||||
if (!string.IsNullOrEmpty(xmlDocumentation))
|
||||
{
|
||||
var xDocument = XDocument.Parse(xmlDocumentation);
|
||||
var summaryElement = xDocument.Descendants("summary").FirstOrDefault();
|
||||
return summaryElement?.Value.Trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MethodSignatureModifiers GetMethodSignatureModifiers(Accessibility accessibility) => accessibility switch
|
||||
{
|
||||
Accessibility.Private => MethodSignatureModifiers.Private,
|
||||
Accessibility.Protected => MethodSignatureModifiers.Protected,
|
||||
Accessibility.Internal => MethodSignatureModifiers.Internal,
|
||||
Accessibility.Public => MethodSignatureModifiers.Public,
|
||||
_ => MethodSignatureModifiers.None
|
||||
};
|
||||
|
||||
private static CSharpType GetCSharpType(ITypeSymbol typeSymbol)
|
||||
{
|
||||
var fullyQualifiedName = GetFullyQualifiedName(typeSymbol);
|
||||
var pieces = fullyQualifiedName.Split('.');
|
||||
|
||||
//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))
|
||||
{
|
||||
return new CSharpType(
|
||||
typeSymbol.Name,
|
||||
string.Join('.', pieces.Take(pieces.Length - 1)),
|
||||
typeSymbol.IsValueType,
|
||||
typeSymbol.TypeKind == TypeKind.Enum,
|
||||
typeSymbol.NullableAnnotation == NullableAnnotation.Annotated,
|
||||
typeSymbol.ContainingType is not null ? GetCSharpType(typeSymbol.ContainingType) : null,
|
||||
typeSymbol is INamedTypeSymbol namedTypeSymbol ? namedTypeSymbol.TypeArguments.Select(GetCSharpType).ToArray() : null,
|
||||
typeSymbol.DeclaredAccessibility == Accessibility.Public,
|
||||
typeSymbol.BaseType is not null ? GetCSharpType(typeSymbol.BaseType) : null);
|
||||
}
|
||||
|
||||
var type = System.Type.GetType(fullyQualifiedName);
|
||||
if (type is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to convert ITypeSymbol: {fullyQualifiedName} to a CSharpType");
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private static string GetFullyQualifiedName(ITypeSymbol typeSymbol)
|
||||
{
|
||||
// Handle special cases for built-in types
|
||||
switch (typeSymbol.SpecialType)
|
||||
{
|
||||
case SpecialType.System_Object:
|
||||
return "System.Object";
|
||||
case SpecialType.System_Void:
|
||||
return "System.Void";
|
||||
case SpecialType.System_Boolean:
|
||||
return "System.Boolean";
|
||||
case SpecialType.System_Char:
|
||||
return "System.Char";
|
||||
case SpecialType.System_SByte:
|
||||
return "System.SByte";
|
||||
case SpecialType.System_Byte:
|
||||
return "System.Byte";
|
||||
case SpecialType.System_Int16:
|
||||
return "System.Int16";
|
||||
case SpecialType.System_UInt16:
|
||||
return "System.UInt16";
|
||||
case SpecialType.System_Int32:
|
||||
return "System.Int32";
|
||||
case SpecialType.System_UInt32:
|
||||
return "System.UInt32";
|
||||
case SpecialType.System_Int64:
|
||||
return "System.Int64";
|
||||
case SpecialType.System_UInt64:
|
||||
return "System.UInt64";
|
||||
case SpecialType.System_Decimal:
|
||||
return "System.Decimal";
|
||||
case SpecialType.System_Single:
|
||||
return "System.Single";
|
||||
case SpecialType.System_Double:
|
||||
return "System.Double";
|
||||
case SpecialType.System_String:
|
||||
return "System.String";
|
||||
case SpecialType.System_DateTime:
|
||||
return "System.DateTime";
|
||||
}
|
||||
|
||||
// Handle array types
|
||||
if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol)
|
||||
{
|
||||
var elementType = GetFullyQualifiedName(arrayTypeSymbol.ElementType);
|
||||
return elementType + "[]";
|
||||
}
|
||||
|
||||
// Handle generic types
|
||||
if (typeSymbol is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType)
|
||||
{
|
||||
var genericArguments = string.Join(",", namedTypeSymbol.TypeArguments.Select(GetFullyQualifiedName));
|
||||
var typeName = namedTypeSymbol.ConstructedFrom.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
return $"{typeName}[{genericArguments}]";
|
||||
}
|
||||
|
||||
// Default to fully qualified name
|
||||
return GetFullyQualifiedNameFromDisplayString(typeSymbol);
|
||||
}
|
||||
|
||||
private static string GetFullyQualifiedNameFromDisplayString(ISymbol typeSymbol)
|
||||
{
|
||||
const string globalPrefix = "global::";
|
||||
var fullyQualifiedName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
return fullyQualifiedName.StartsWith(globalPrefix, StringComparison.Ordinal) ? fullyQualifiedName.Substring(globalPrefix.Length) : fullyQualifiedName;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ namespace Microsoft.Generator.CSharp.Tests
|
|||
[Test]
|
||||
public void TestInitialize()
|
||||
{
|
||||
string ns = "sample.namespace";
|
||||
string ns = "Sample";
|
||||
string? unknownStringProperty = "unknownPropertyValue";
|
||||
bool? unknownBoolProp = false;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
{
|
||||
"output-folder": "./outputFolder",
|
||||
"project-folder": "./projectFolder",
|
||||
"namespace": "sample.namespace",
|
||||
"namespace": "Sample",
|
||||
"unknown-bool-property": false,
|
||||
"library-name": "sample-library",
|
||||
"unknown-string-property": "unknownPropertyValue"
|
||||
|
|
|
@ -225,7 +225,7 @@ namespace Microsoft.Generator.CSharp.Tests.Providers
|
|||
var result = writer.ToString(false);
|
||||
var builder = new StringBuilder();
|
||||
builder.Append($"e.ToSerialInt32()").Append(NewLine)
|
||||
.Append($"new global::sample.namespace.Models.MockInputEnum(1)");
|
||||
.Append($"new global::Sample.Models.MockInputEnum(1)");
|
||||
var expected = builder.ToString();
|
||||
|
||||
Assert.AreEqual(expected, result);
|
||||
|
@ -286,7 +286,7 @@ namespace Microsoft.Generator.CSharp.Tests.Providers
|
|||
var result = writer.ToString(false);
|
||||
var builder = new StringBuilder();
|
||||
builder.Append($"e.ToSerialSingle()").Append(NewLine)
|
||||
.Append($"new global::sample.namespace.Models.MockInputEnum(1F)");
|
||||
.Append($"new global::Sample.Models.MockInputEnum(1F)");
|
||||
var expected = builder.ToString();
|
||||
|
||||
Assert.AreEqual(expected, result);
|
||||
|
@ -347,7 +347,7 @@ namespace Microsoft.Generator.CSharp.Tests.Providers
|
|||
var result = writer.ToString(false);
|
||||
var builder = new StringBuilder();
|
||||
builder.Append($"e.ToString()").Append(NewLine)
|
||||
.Append($"new global::sample.namespace.Models.MockInputEnum(\"1\")");
|
||||
.Append($"new global::Sample.Models.MockInputEnum(\"1\")");
|
||||
var expected = builder.ToString();
|
||||
|
||||
Assert.AreEqual(expected, result);
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.Generator.CSharp.Primitives;
|
||||
using Microsoft.Generator.CSharp.Providers;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Microsoft.Generator.CSharp.Tests.Providers
|
||||
{
|
||||
public class NamedTypeSymbolProviderTests
|
||||
{
|
||||
private NamedTypeSymbolProvider _namedTypeSymbolProvider;
|
||||
private NamedSymbol _namedSymbol;
|
||||
|
||||
public NamedTypeSymbolProviderTests()
|
||||
{
|
||||
MockCodeModelPlugin.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 iNamedSymbol = GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "NamedSymbol");
|
||||
|
||||
_namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!);
|
||||
_namedSymbol = new NamedSymbol();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateName()
|
||||
{
|
||||
Assert.AreEqual(_namedSymbol.Name, _namedTypeSymbolProvider.Name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateNamespace()
|
||||
{
|
||||
Assert.AreEqual("Sample.Models", _namedTypeSymbolProvider.Type.Namespace);
|
||||
Assert.AreEqual(_namedSymbol.Type.Namespace, _namedTypeSymbolProvider.Type.Namespace);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateProperties()
|
||||
{
|
||||
Dictionary<string, PropertyProvider> properties = _namedTypeSymbolProvider.Properties.ToDictionary(p => p.Name);
|
||||
foreach (var expected in _namedSymbol.Properties)
|
||||
{
|
||||
var actual = properties[expected.Name];
|
||||
|
||||
Assert.IsTrue(properties.ContainsKey(expected.Name));
|
||||
Assert.AreEqual(expected.Name, actual.Name);
|
||||
Assert.AreEqual($"{expected.Description}.", actual.Description.ToString()); // the writer adds a period
|
||||
Assert.AreEqual(expected.Modifiers, actual.Modifiers);
|
||||
Assert.AreEqual(expected.Type, actual.Type);
|
||||
Assert.AreEqual(expected.Body.GetType(), actual.Body.GetType());
|
||||
Assert.AreEqual(expected.Body.HasSetter, actual.Body.HasSetter);
|
||||
}
|
||||
}
|
||||
|
||||
private class NamedSymbol : TypeProvider
|
||||
{
|
||||
public override string RelativeFilePath => ".";
|
||||
|
||||
public override string Name => "NamedSymbol";
|
||||
|
||||
protected override string GetNamespace() => CodeModelPlugin.Instance.Configuration.ModelNamespace;
|
||||
|
||||
protected override PropertyProvider[] BuildProperties()
|
||||
{
|
||||
return
|
||||
[
|
||||
new PropertyProvider($"IntProperty property", MethodSignatureModifiers.Public, typeof(int), "IntProperty", new AutoPropertyBody(true)),
|
||||
new PropertyProvider($"StringProperty property no setter", MethodSignatureModifiers.Public, typeof(string), "StringProperty", new AutoPropertyBody(false)),
|
||||
new PropertyProvider($"InternalStringProperty property no setter", MethodSignatureModifiers.Public, typeof(string), "InternalStringProperty", new AutoPropertyBody(false)),
|
||||
new PropertyProvider($"PropertyTypeProperty property", MethodSignatureModifiers.Public, new PropertyType().Type, "PropertyTypeProperty", new AutoPropertyBody(true)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private class PropertyType : TypeProvider
|
||||
{
|
||||
public override string RelativeFilePath => ".";
|
||||
|
||||
public override string Name => "PropertyType";
|
||||
|
||||
protected override PropertyProvider[] BuildProperties()
|
||||
{
|
||||
return
|
||||
[
|
||||
new PropertyProvider($"Foo property", MethodSignatureModifiers.Public, typeof(int), "Foo", new AutoPropertyBody(true)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
return GetSymbol(childNamespaceSymbol, name);
|
||||
}
|
||||
|
||||
foreach (INamedTypeSymbol symbol in namespaceSymbol.GetTypeMembers())
|
||||
{
|
||||
if (symbol.MetadataName == name)
|
||||
{
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ namespace Microsoft.Generator.CSharp.Tests.Snippets
|
|||
|
||||
|
||||
ArgumentSnippets.AssertNotNull(p).Write(writer);
|
||||
Assert.AreEqual("global::sample.namespace.Argument.AssertNotNull(p1, nameof(p1));\n", writer.ToString(false));
|
||||
Assert.AreEqual("global::Sample.Argument.AssertNotNull(p1, nameof(p1));\n", writer.ToString(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace Microsoft.Generator.CSharp.Tests
|
|||
new InputPrimitiveType(InputPrimitiveTypeKind.String),
|
||||
[new InputEnumTypeValue("value1", "value1", null), new InputEnumTypeValue("value2", "value2", null)],
|
||||
true);
|
||||
var expected = new CSharpType("SampleType", "sample.namespace.Models", true, true, false, null, null, true);
|
||||
var expected = new CSharpType("SampleType", "Sample.Models", true, true, false, null, null, true);
|
||||
|
||||
var actual = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(input);
|
||||
|
||||
|
@ -50,7 +50,7 @@ namespace Microsoft.Generator.CSharp.Tests
|
|||
[new InputEnumTypeValue("value1", "value1", null), new InputEnumTypeValue("value2", "value2", null)],
|
||||
true);
|
||||
var nullableInput = new InputNullableType(input);
|
||||
var expected = new CSharpType("SampleType", "sample.namespace.Models", true, true, true, null, null, true);
|
||||
var expected = new CSharpType("SampleType", "Sample.Models", true, true, true, null, null, true);
|
||||
|
||||
var actual = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(nullableInput);
|
||||
|
||||
|
@ -71,7 +71,7 @@ namespace Microsoft.Generator.CSharp.Tests
|
|||
new InputPrimitiveType(InputPrimitiveTypeKind.String),
|
||||
[new InputEnumTypeValue("value1", "value1", null), new InputEnumTypeValue("value2", "value2", null)],
|
||||
true);
|
||||
var expected = new CSharpType("SampleType", "sample.namespace.Models", true, true, false, null, null, true);
|
||||
var expected = new CSharpType("SampleType", "Sample.Models", true, true, false, null, null, true);
|
||||
|
||||
var enumProvider = CodeModelPlugin.Instance.TypeFactory.CreateEnum(input);
|
||||
|
||||
|
|
|
@ -3,20 +3,20 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using sample.namespace;
|
||||
using Sample;
|
||||
|
||||
namespace sample.namespace.Models
|
||||
namespace Sample.Models
|
||||
{
|
||||
/// <summary> Test model. </summary>
|
||||
public partial class TestModel
|
||||
{
|
||||
/// <summary> Initializes a new instance of <see cref="global::sample.namespace.Models.TestModel"/>. </summary>
|
||||
/// <summary> Initializes a new instance of <see cref="global::Sample.Models.TestModel"/>. </summary>
|
||||
/// <param name="requiredString"> Required string, illustrating a reference type property. </param>
|
||||
/// <param name="requiredInt"> Required int, illustrating a value type property. </param>
|
||||
/// <exception cref="global::System.ArgumentNullException"> <paramref name="requiredString"/> is null. </exception>
|
||||
public TestModel(string requiredString, int requiredInt)
|
||||
{
|
||||
global::sample.namespace.Argument.AssertNotNull(requiredString, nameof(requiredString));
|
||||
global::Sample.Argument.AssertNotNull(requiredString, nameof(requiredString));
|
||||
|
||||
RequiredString = requiredString;
|
||||
RequiredInt = requiredInt;
|
||||
|
|
|
@ -3,20 +3,20 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using sample.namespace;
|
||||
using Sample;
|
||||
|
||||
namespace sample.namespace.Models
|
||||
namespace Sample.Models
|
||||
{
|
||||
/// <summary> Test model. </summary>
|
||||
public readonly partial struct TestModel
|
||||
{
|
||||
/// <summary> Initializes a new instance of <see cref="global::sample.namespace.Models.TestModel"/>. </summary>
|
||||
/// <summary> Initializes a new instance of <see cref="global::Sample.Models.TestModel"/>. </summary>
|
||||
/// <param name="requiredString"> Required string, illustrating a reference type property. </param>
|
||||
/// <param name="requiredInt"> Required int, illustrating a value type property. </param>
|
||||
/// <exception cref="global::System.ArgumentNullException"> <paramref name="requiredString"/> is null. </exception>
|
||||
public TestModel(string requiredString, int requiredInt)
|
||||
{
|
||||
global::sample.namespace.Argument.AssertNotNull(requiredString, nameof(requiredString));
|
||||
global::Sample.Argument.AssertNotNull(requiredString, nameof(requiredString));
|
||||
|
||||
RequiredString = requiredString;
|
||||
RequiredInt = requiredInt;
|
||||
|
|
Загрузка…
Ссылка в новой задаче