diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs new file mode 100644 index 000000000..80b2bc31f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs @@ -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 properties = new List(); + foreach (var propertySymbol in _namedTypeSymbol.GetMembers().OfType()) + { + 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; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs index 2786fa12a..d51c4963c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs @@ -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; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Configuration.json b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Configuration.json index f34eb20cb..c510d225c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Configuration.json +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Configuration.json @@ -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" diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/EnumProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/EnumProviderTests.cs index b8156cca3..f90f8cae4 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/EnumProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/EnumProviderTests.cs @@ -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); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/NamedTypeSymbolProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/NamedTypeSymbolProviderTests.cs new file mode 100644 index 000000000..74ba83a45 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/NamedTypeSymbolProviderTests.cs @@ -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 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 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; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Snippets/ArgumentSnippetsTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Snippets/ArgumentSnippetsTests.cs index 173e3245f..74ec603a9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Snippets/ArgumentSnippetsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Snippets/ArgumentSnippetsTests.cs @@ -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)); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TypeFactoryTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TypeFactoryTests.cs index 2444f7892..7c612b3d1 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TypeFactoryTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TypeFactoryTests.cs @@ -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); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/TypeProviderWriterTests/TypeProviderWriter_WriteModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/TypeProviderWriterTests/TypeProviderWriter_WriteModel.cs index 09261709a..135ea17a8 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/TypeProviderWriterTests/TypeProviderWriter_WriteModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/TypeProviderWriterTests/TypeProviderWriter_WriteModel.cs @@ -3,20 +3,20 @@ #nullable disable using System; -using sample.namespace; +using Sample; -namespace sample.namespace.Models +namespace Sample.Models { /// Test model. public partial class TestModel { - /// Initializes a new instance of . + /// Initializes a new instance of . /// Required string, illustrating a reference type property. /// Required int, illustrating a value type property. /// is null. 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; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/TypeProviderWriterTests/TypeProviderWriter_WriteModelAsStruct.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/TypeProviderWriterTests/TypeProviderWriter_WriteModelAsStruct.cs index 025a97a56..cefc259e4 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/TypeProviderWriterTests/TypeProviderWriter_WriteModelAsStruct.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TestData/TypeProviderWriterTests/TypeProviderWriter_WriteModelAsStruct.cs @@ -3,20 +3,20 @@ #nullable disable using System; -using sample.namespace; +using Sample; -namespace sample.namespace.Models +namespace Sample.Models { /// Test model. public readonly partial struct TestModel { - /// Initializes a new instance of . + /// Initializes a new instance of . /// Required string, illustrating a reference type property. /// Required int, illustrating a value type property. /// is null. 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;