diff --git a/packages/http-client-csharp/docs/emitter.md b/packages/http-client-csharp/docs/emitter.md index 827da57427a..1cea78a066e 100644 --- a/packages/http-client-csharp/docs/emitter.md +++ b/packages/http-client-csharp/docs/emitter.md @@ -56,9 +56,9 @@ Set to `false` to skip generation of convenience methods. The default value is ` ### `unreferenced-types-handling` -**Type:** `"removeOrInternalize" | "internalize" | "keepAll"` +**Type:** `"internalize" | "keepAll"` -Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. +Defines the strategy on how to handle unreferenced types. The default value is `internalize`. ### `new-project` diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index 6978e355f9b..4d3df57e8bf 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -15,7 +15,7 @@ type ApiVersionSelection = string | Record; export interface CSharpEmitterOptions { "api-version"?: ApiVersionSelection; - "unreferenced-types-handling"?: "removeOrInternalize" | "internalize" | "keepAll"; + "unreferenced-types-handling"?: "internalize" | "keepAll"; "new-project"?: boolean; "save-inputs"?: boolean; debug?: boolean; @@ -61,10 +61,10 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType = }, "unreferenced-types-handling": { type: "string", - enum: ["removeOrInternalize", "internalize", "keepAll"], + enum: ["internalize", "keepAll"], nullable: true, description: - "Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`.", + "Defines the strategy on how to handle unreferenced types. The default value is `internalize`.", }, "new-project": { type: "boolean", diff --git a/packages/http-client-csharp/emitter/src/type/configuration.ts b/packages/http-client-csharp/emitter/src/type/configuration.ts index 2a2f3e2f2e8..11bce6cde71 100644 --- a/packages/http-client-csharp/emitter/src/type/configuration.ts +++ b/packages/http-client-csharp/emitter/src/type/configuration.ts @@ -3,7 +3,7 @@ export interface Configuration { "package-name": string | null; - "unreferenced-types-handling"?: "removeOrInternalize" | "internalize" | "keepAll"; + "unreferenced-types-handling"?: "internalize" | "keepAll"; "disable-xml-docs"?: boolean; "disable-roslyn-reduce"?: boolean; license?: { diff --git a/packages/http-client-csharp/emitter/test/Unit/options.test.ts b/packages/http-client-csharp/emitter/test/Unit/options.test.ts index be8603adeb0..9f62fae3d5a 100644 --- a/packages/http-client-csharp/emitter/test/Unit/options.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/options.test.ts @@ -124,7 +124,7 @@ describe("Configuration tests", async () => { } const customOptions: TestEmitterOptions = { "package-name": "custom-package", - "unreferenced-types-handling": "removeOrInternalize", + "unreferenced-types-handling": "internalize", "disable-xml-docs": true, "disable-roslyn-reduce": true, license: { @@ -141,7 +141,7 @@ describe("Configuration tests", async () => { const config = createConfiguration(customOptions, "rootNamespace", sdkContext); expect(config["package-name"]).toBe("custom-package"); - expect(config["unreferenced-types-handling"]).toBe("removeOrInternalize"); + expect(config["unreferenced-types-handling"]).toBe("internalize"); expect(config["disable-xml-docs"]).toBe(true); expect(config["disable-roslyn-reduce"]).toBe(true); expect(config.license).toEqual({ diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmTypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmTypeFactory.cs index 15596c96918..96a0ae4783c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmTypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmTypeFactory.cs @@ -269,7 +269,9 @@ public virtual MethodBodyStatement SerializeXmlValue( SerializationFormat format) => MrwSerializationTypeDefinition.SerializeXmlValueCore(valueType, value, xmlWriter, mrwOptionsParameter, format); - protected override ModelProvider? CreateModelCore(InputModelType model) => new ScmModelProvider(model); + // File models are represented by FileBinaryContent in CreateCSharpTypeCore, so there is no model to emit. + protected override ModelProvider? CreateModelCore(InputModelType model) + => model.IsFileType ? null : new ScmModelProvider(model); protected override ModelFactoryProvider CreateModelFactoryCore(IEnumerable models) => new ScmModelFactoryProvider(models); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ScmTypeFactoryTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ScmTypeFactoryTests.cs index 98f1b9a5fd0..4e70d32fb14 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ScmTypeFactoryTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ScmTypeFactoryTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; +using System.Reflection; using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index c013817a72e..da0b80a9409 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -90,6 +90,19 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), { // Ensure back-compatibility processing is done after all visitors have run outputType.ProcessTypeForBackCompatibility(); + } + + PostProcessTypeProviders(output.TypeProviders); + + LoggingHelpers.LogElapsedTime("All generated type providers post-processed"); + + var modelFactory = output.ModelFactory.Value; + foreach (var outputType in output.TypeProviders) + { + if (ReferenceEquals(outputType, modelFactory) && outputType.Methods.Count == 0) + { + continue; + } var writer = CodeModelGenerator.Instance.GetWriter(outputType); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); @@ -111,8 +124,6 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); - await generatedCodeWorkspace.PostProcessAsync(); - // Write the generated files to the output directory await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) { @@ -138,6 +149,21 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All files have been written to disk"); } + private static void PostProcessTypeProviders(IReadOnlyList typeProviders) + { + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.KeepAll) + { + return; + } + + var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + var postProcessor = new PostProcessor( + [.. CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep, .. CodeModelGenerator.Instance.AdditionalRootTypes], + modelFactoryFullName: modelFactory.Type.FullyQualifiedName, + additionalNonRootTypeNames: CodeModelGenerator.Instance.NonRootTypes); + postProcessor.Internalize(typeProviders); + } + internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs index 39ddf41b3ec..663e26c103a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs @@ -15,9 +15,8 @@ public class Configuration { public enum UnreferencedTypesHandlingOption { - RemoveOrInternalize = 0, - Internalize = 1, - KeepAll = 2 + Internalize = 0, + KeepAll = 1 } private const string GeneratedFolderName = "Generated"; @@ -83,7 +82,7 @@ private static class Options /// public LicenseInfo? LicenseInfo { get; } - internal static UnreferencedTypesHandlingOption UnreferencedTypesHandling { get; private set; } = UnreferencedTypesHandlingOption.RemoveOrInternalize; + internal static UnreferencedTypesHandlingOption UnreferencedTypesHandling { get; private set; } = UnreferencedTypesHandlingOption.Internalize; private string? _projectDirectory; internal string ProjectDirectory => _projectDirectory ??= Path.Combine(OutputDirectory, "src"); @@ -253,7 +252,7 @@ private static T ReadEnumOption(JsonElement root, string option) where T : st public static Enum? GetDefaultEnumOptionValue(string option) => option switch { - Options.UnreferencedTypesHandling => UnreferencedTypesHandlingOption.RemoveOrInternalize, + Options.UnreferencedTypesHandling => UnreferencedTypesHandlingOption.Internalize, _ => null }; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index c36686f637f..64d8f664e19 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -260,33 +260,6 @@ internal static Project AddDirectory(Project project, string directory, Func - /// This method invokes the postProcessor to do some post processing work - /// Depending on the configuration, it will either remove + internalize, just internalize or do nothing - /// - public async Task PostProcessAsync() - { - var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; - var nonRootTypes = CodeModelGenerator.Instance.NonRootTypes; - var postProcessor = new PostProcessor( - [.. CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep, .. CodeModelGenerator.Instance.AdditionalRootTypes], - modelFactoryFullName: modelFactory.Type.FullyQualifiedName, - additionalNonRootTypeNames: nonRootTypes); - - switch (Configuration.UnreferencedTypesHandling) - { - case Configuration.UnreferencedTypesHandlingOption.KeepAll: - break; - case Configuration.UnreferencedTypesHandlingOption.Internalize: - _project = await postProcessor.InternalizeAsync(_project); - break; - case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: - _project = await postProcessor.InternalizeAsync(_project); - _project = await postProcessor.RemoveAsync(_project); - break; - } - } - /// /// Resolves PackageReference items from the project's .csproj file and adds their assemblies /// as metadata references so that custom code referencing external NuGet types compiles correctly. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs index dc42f801732..8a6faa9aac4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs @@ -10,6 +10,8 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Simplification; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; namespace Microsoft.TypeSpec.Generator { @@ -113,6 +115,96 @@ private async Task GetTypeSymbolsAsync(Compilation compilation, protected virtual bool ShouldIncludeDocument(Document document) => !GeneratedCodeWorkspace.IsGeneratedTestDocument(document); + public void Internalize(IReadOnlyList typeProviders) + { + var allProviders = ProviderReferenceMapBuilder.GetAllProviders(typeProviders).ToArray(); + var candidateProviders = allProviders + .Where(IsPublicType) + .Where(provider => !IsExcludedProvider(provider)) + .ToArray(); + var rootProviders = candidateProviders.Where(IsRootProvider).ToArray(); + var referenceMap = new ProviderReferenceMapBuilder(typeProviders).BuildPublicReferenceMap(rootProviders); + var referencedProviders = VisitProvidersFromRoot(rootProviders, referenceMap).ToHashSet(); + var providersToInternalize = candidateProviders + .Where(provider => !referencedProviders.Contains(provider)) + .ToArray(); + + foreach (var provider in providersToInternalize) + { + provider.PreserveXmlDocs(); + provider.Update(modifiers: MakeInternal(provider.DeclarationModifiers)); + } + + RemoveMethodsFromModelFactory(providersToInternalize.Select(provider => provider.Name).ToHashSet()); + } + + private bool IsRootProvider(TypeProvider provider) + => IsClientProvider(provider) || provider.CustomCodeView != null || ShouldKeepProvider(provider, _typesToKeep); + + private bool IsExcludedProvider(TypeProvider provider) + => IsModelFactoryProvider(provider) || ShouldKeepProvider(provider, _additionalNonRootTypeNames); + + private bool IsModelFactoryProvider(TypeProvider provider) + => _modelFactoryFullName != null && provider.Type.FullyQualifiedName == _modelFactoryFullName; + + private static bool IsClientProvider(TypeProvider provider) + => provider.Name.EndsWith("Client", StringComparison.Ordinal); + + private static bool ShouldKeepProvider(TypeProvider provider, HashSet typesToKeep) + => typesToKeep.Contains(provider.Name) || typesToKeep.Contains(provider.Type.FullyQualifiedName); + + private static bool IsPublicType(TypeProvider provider) + => provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public); + + private static TypeSignatureModifiers MakeInternal(TypeSignatureModifiers modifiers) + => (modifiers & ~(TypeSignatureModifiers.Public | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Internal; + + private static IEnumerable VisitProvidersFromRoot( + IEnumerable rootProviders, + IReadOnlyDictionary> referenceMap) + { + var queue = new Queue(rootProviders); + var visited = new HashSet(); + while (queue.Count > 0) + { + var provider = queue.Dequeue(); + if (!visited.Add(provider)) + { + continue; + } + + yield return provider; + if (!referenceMap.TryGetValue(provider, out var references)) + { + continue; + } + + foreach (var reference in references) + { + queue.Enqueue(reference); + } + } + } + + private void RemoveMethodsFromModelFactory(HashSet namesToRemove) + { + if (_modelFactoryFullName == null || namesToRemove.Count == 0) + { + return; + } + + var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + if (modelFactory.Type.FullyQualifiedName != _modelFactoryFullName) + { + return; + } + + var methodsToKeep = modelFactory.Methods + .Where(method => !namesToRemove.Contains(method.Signature.Name)) + .ToArray(); + modelFactory.Update(methods: methodsToKeep); + } + /// /// This method marks the "not publicly" referenced types as internal if they are previously defined as public. It will do this job in the following steps: /// 1. This method will read all the public types defined in the given , and build a cache for those symbols @@ -133,12 +225,12 @@ public async Task InternalizeAsync(Project project) // first get all the declared symbols var definitions = await GetTypeSymbolsAsync(compilation, project, true); + // get the root symbols + var rootSymbols = await GetRootSymbolsAsync(project, definitions); // build the reference map var referenceMap = await new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( definitions.DeclaredSymbols, definitions.DeclaredNodesCache); - // get the root symbols - var rootSymbols = await GetRootSymbolsAsync(project, definitions); // traverse all the root and recursively add all the things we met var publicSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapBuilder.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapBuilder.cs new file mode 100644 index 00000000000..68e26bd167c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapBuilder.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal class ProviderReferenceMapBuilder + { + private readonly IReadOnlyList _providers; + + public ProviderReferenceMapBuilder(IReadOnlyList providers) + { + _providers = [.. GetAllProviders(providers)]; + } + + public IReadOnlyDictionary> BuildPublicReferenceMap(IEnumerable rootTypes) + { + var referenceMap = new Dictionary>(); + var visited = new HashSet(); + var queue = new Queue(rootTypes); + + while (queue.Count > 0) + { + var provider = queue.Dequeue(); + if (!visited.Add(provider)) + { + continue; + } + + var referencedTypes = BuildPublicApiReferences(provider.CanonicalView); + referenceMap[provider] = referencedTypes; + foreach (var referencedType in referencedTypes) + { + queue.Enqueue(referencedType); + } + } + + return referenceMap; + } + + public static IEnumerable GetAllProviders(IEnumerable providers) + { + foreach (var provider in providers) + { + yield return provider; + + foreach (var nestedType in GetAllProviders(provider.CanonicalView.NestedTypes)) + { + yield return nestedType; + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + yield return serializationProvider; + } + } + } + + private IReadOnlyList BuildPublicApiReferences(TypeProvider provider) + { + var referencedTypes = new HashSet(); + + AddType(provider.Type, referencedTypes); + AddType(provider.BaseType, referencedTypes); + foreach (var implementedType in provider.Implements) + { + AddType(implementedType, referencedTypes); + } + + foreach (var constructor in provider.Constructors.Where(static c => IsPublicApi(c.Signature.Modifiers))) + { + AddSignatureTypes(constructor.Signature, referencedTypes); + } + + foreach (var method in provider.Methods.Where(static m => IsPublicApi(m.Signature.Modifiers))) + { + AddSignatureTypes(method.Signature, referencedTypes); + AddType(method.Signature.ExplicitInterface, referencedTypes); + foreach (var genericArgument in method.Signature.GenericArguments ?? []) + { + AddType(genericArgument, referencedTypes); + } + } + + foreach (var property in provider.Properties.Where(static p => IsPublicApi(p.Modifiers))) + { + AddType(property.Type, referencedTypes); + AddType(property.ExplicitInterface, referencedTypes); + } + + foreach (var field in provider.Fields.Where(static f => IsPublicApi(f.Modifiers))) + { + AddType(field.Type, referencedTypes); + } + + foreach (var nestedType in provider.NestedTypes.Where(static t => IsPublicApi(t.DeclarationModifiers))) + { + AddType(nestedType.Type, referencedTypes); + } + + return [.. referencedTypes]; + } + + private void AddSignatureTypes(MethodSignatureBase signature, HashSet referencedTypes) + { + AddType(signature.ReturnType, referencedTypes); + foreach (var parameter in signature.Parameters) + { + AddType(parameter.Type, referencedTypes); + } + } + + private void AddType(CSharpType? type, HashSet referencedTypes) + { + if (type == null) + { + return; + } + + foreach (var provider in ResolveTypes(type)) + { + referencedTypes.Add(provider); + } + + AddType(type.BaseType, referencedTypes); + AddType(type.DeclaringType, referencedTypes); + foreach (var argument in type.Arguments) + { + AddType(argument, referencedTypes); + } + + if (type.IsUnion) + { + foreach (var unionItemType in type.UnionItemTypes) + { + AddType(unionItemType, referencedTypes); + } + } + } + + private IEnumerable ResolveTypes(CSharpType type) + { + if (type.IsFrameworkType) + { + return []; + } + + if (CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(type, out var provider) && provider != null) + { + return _providers.Where(candidate => ReferenceEquals(candidate, provider) || candidate.Type.AreNamesEqual(type)); + } + + return _providers.Where(provider => + provider.Type.AreNamesEqual(type) || provider.CanonicalView.Type.AreNamesEqual(type)); + } + + private static bool IsPublicApi(MethodSignatureModifiers modifiers) + => (modifiers.HasFlag(MethodSignatureModifiers.Public) || modifiers.HasFlag(MethodSignatureModifiers.Protected)) + && !modifiers.HasFlag(MethodSignatureModifiers.Private); + + private static bool IsPublicApi(FieldModifiers modifiers) + => (modifiers.HasFlag(FieldModifiers.Public) || modifiers.HasFlag(FieldModifiers.Protected)) + && !modifiers.HasFlag(FieldModifiers.Private); + + private static bool IsPublicApi(TypeSignatureModifiers modifiers) + => (modifiers.HasFlag(TypeSignatureModifiers.Public) || modifiers.HasFlag(TypeSignatureModifiers.Protected)) + && !modifiers.HasFlag(TypeSignatureModifiers.Private); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs index 49fe9723973..eb07aa4519f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/TypeProviderWriter.cs @@ -45,7 +45,7 @@ private bool IsPublicContext(TypeProvider provider) private void WriteType(CodeWriter writer) { - if (IsPublicContext(_provider)) + if (_provider.PreserveTypeXmlDocs || IsPublicContext(_provider)) { writer.WriteXmlDocsNoScope(_provider.XmlDocs); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index e850150e8ad..d92a9d31ebb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -143,6 +143,13 @@ public XmlDocProvider XmlDocs private set => _xmlDocs = value; } + internal bool PreserveTypeXmlDocs { get; private set; } + + internal void PreserveXmlDocs() + { + PreserveTypeXmlDocs = true; + } + public string? Deprecated { get => _deprecated; @@ -538,6 +545,7 @@ public virtual void Reset() _serializationProviders = null; _nestedTypes = null; _xmlDocs = null; + PreserveTypeXmlDocs = false; _declarationModifiers = null; _relativeFilePath = null; _customCodeView = new(() => BuildCustomCodeView()); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs index b0d402825e8..197bb9eb5e8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs @@ -156,7 +156,6 @@ public void DisableDocsForProperty() } [Test] - [TestCase("removeOrInternalize")] [TestCase("keepAll")] [TestCase("internalize")] public void UnreferencedTypeHandling(string input) @@ -170,7 +169,6 @@ public void UnreferencedTypeHandling(string input) MockHelpers.LoadMockGenerator(configuration: mockJson); var expected = input switch { - "removeOrInternalize" => Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, "keepAll" => Configuration.UnreferencedTypesHandlingOption.KeepAll, "internalize" => Configuration.UnreferencedTypesHandlingOption.Internalize, _ => throw new ArgumentException("Invalid input", nameof(input)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs index 28981148a4d..a3e1c2281db 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.ClientModel.Primitives; using System.Collections.Generic; using System.IO; @@ -8,6 +9,9 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; @@ -289,6 +293,45 @@ public async Task DoesNotRemoveValidAttributes() Assert.AreEqual(Helpers.GetExpectedFromFile().TrimEnd(), output, "The output should match the expected content."); } + [Test] + public void InternalizeUsesProviderPublicApiReferences() + { + MockHelpers.LoadMockGenerator(configuration: """{ "package-name": "Sample", "disable-xml-docs": false }"""); + + var request = new TestTypeProvider("RequestBody"); + var dependency = new TestTypeProvider("Dependency"); + var response = new TestTypeProvider("ResponseBody"); + response.PropertiesToBuild.Add(new PropertyProvider( + null, + MethodSignatureModifiers.Public, + dependency.Type, + "Dependency", + new AutoPropertyBody(false), + response)); + + var client = new TestTypeProvider("SampleClient"); + client.FieldsToBuild.Add(new FieldProvider(FieldModifiers.Private, request.Type, "_request", client)); + client.MethodsToBuild.Add(new MethodProvider( + new MethodSignature("Get", null, MethodSignatureModifiers.Public, response.Type, null, []), + MethodBodyStatement.Empty, + client)); + + var providers = new[] { client, request, response, dependency }; + + var postProcessor = new PostProcessor([]); + postProcessor.Internalize(providers); + + Assert.IsTrue(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(response.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(dependency.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(request.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(request.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + + var requestFile = new TypeProviderWriter(request).Write(); + StringAssert.Contains("/// The RequestBody. ", requestFile.Content); + StringAssert.Contains("internal partial class RequestBody", requestFile.Content); + } + private class TestPostProcessor : PostProcessor { private readonly string _rootFile; @@ -303,5 +346,36 @@ protected override Task IsRootDocument(Document document) return document.Name == _rootFile ? Task.FromResult(true) : Task.FromResult(false); } } + + private class TestTypeProvider : TypeProvider + { + private readonly string _name; + + public TestTypeProvider(string name = "Test") + { + _name = name; + } + + public List FieldsToBuild { get; } = []; + public List MethodsToBuild { get; } = []; + public List PropertiesToBuild { get; } = []; + + protected override string BuildName() => _name; + + protected override string BuildNamespace() => "Sample"; + + protected override FormattableString BuildDescription() => $"The {Name}."; + + protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs"); + + protected override TypeSignatureModifiers BuildDeclarationModifiers() + => TypeSignatureModifiers.Public | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; + + protected internal override FieldProvider[] BuildFields() => [.. FieldsToBuild]; + + protected internal override MethodProvider[] BuildMethods() => [.. MethodsToBuild]; + + protected internal override PropertyProvider[] BuildProperties() => [.. PropertiesToBuild]; + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs index d8ffdcd323c..eac3a6f1dd8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs @@ -340,7 +340,7 @@ public void AddConfiguredPluginDlls_NoPluginPaths_DoesNothing() new Dictionary(), "TestPackage", false, - Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + Configuration.UnreferencedTypesHandlingOption.Internalize, null, pluginPaths: null); @@ -358,7 +358,7 @@ public void AddConfiguredPluginDlls_InvalidDirectory_Throws() new Dictionary(), "TestPackage", false, - Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + Configuration.UnreferencedTypesHandlingOption.Internalize, null, pluginPaths: ["/nonexistent/path"]); @@ -385,7 +385,7 @@ public void AddConfiguredPluginDlls_DirectoryWithPreBuiltDlls_LoadsThem() new Dictionary(), "TestPackage", false, - Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + Configuration.UnreferencedTypesHandlingOption.Internalize, null, pluginPaths: [testDir]); @@ -422,7 +422,7 @@ namespace AutoBuildPlugin { public class Dummy { } }"); new Dictionary(), "TestPackage", false, - Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + Configuration.UnreferencedTypesHandlingOption.Internalize, null, pluginPaths: [testDir]); @@ -464,7 +464,7 @@ namespace Plugin2 { public class Dummy { } }"); new Dictionary(), "TestPackage", false, - Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + Configuration.UnreferencedTypesHandlingOption.Internalize, null, pluginPaths: [testDir1, testDir2]); diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/BinaryContentHelper.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/BinaryContentHelper.cs new file mode 100644 index 00000000000..b76cf24aa37 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/BinaryContentHelper.cs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Xml; + +namespace SampleTypeSpec +{ + internal static partial class BinaryContentHelper + { + /// + public static BinaryContent FromEnumerable(IEnumerable enumerable) + where T : notnull + { + Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); + content.JsonWriter.WriteStartArray(); + foreach (var item in enumerable) + { + content.JsonWriter.WriteObjectValue(item, ModelSerializationExtensions.WireOptions); + } + content.JsonWriter.WriteEndArray(); + + return content; + } + + /// + public static BinaryContent FromEnumerable(IEnumerable enumerable) + { + Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); + content.JsonWriter.WriteStartArray(); + foreach (var item in enumerable) + { + if (item == null) + { + content.JsonWriter.WriteNullValue(); + } + else + { +#if NET6_0_OR_GREATER + content.JsonWriter.WriteRawValue(item); +#else + using (JsonDocument document = JsonDocument.Parse(item)) + { + JsonSerializer.Serialize(content.JsonWriter, document.RootElement); + } +#endif + } + } + content.JsonWriter.WriteEndArray(); + + return content; + } + + /// + public static BinaryContent FromEnumerable(ReadOnlySpan span) + where T : notnull + { + Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); + content.JsonWriter.WriteStartArray(); + int i = 0; + for (; i < span.Length; i++) + { + content.JsonWriter.WriteObjectValue(span[i], ModelSerializationExtensions.WireOptions); + } + content.JsonWriter.WriteEndArray(); + + return content; + } + + /// + public static BinaryContent FromDictionary(IDictionary dictionary) + where TValue : notnull + { + Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); + content.JsonWriter.WriteStartObject(); + foreach (var item in dictionary) + { + content.JsonWriter.WritePropertyName(item.Key); + content.JsonWriter.WriteObjectValue(item.Value, ModelSerializationExtensions.WireOptions); + } + content.JsonWriter.WriteEndObject(); + + return content; + } + + /// + public static BinaryContent FromDictionary(IDictionary dictionary) + { + Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); + content.JsonWriter.WriteStartObject(); + foreach (var item in dictionary) + { + content.JsonWriter.WritePropertyName(item.Key); + if (item.Value == null) + { + content.JsonWriter.WriteNullValue(); + } + else + { +#if NET6_0_OR_GREATER + content.JsonWriter.WriteRawValue(item.Value); +#else + using (JsonDocument document = JsonDocument.Parse(item.Value)) + { + JsonSerializer.Serialize(content.JsonWriter, document.RootElement); + } +#endif + } + } + content.JsonWriter.WriteEndObject(); + + return content; + } + + /// + public static BinaryContent FromObject(object value) + { + Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); + content.JsonWriter.WriteObjectValue(value, ModelSerializationExtensions.WireOptions); + return content; + } + + /// + public static BinaryContent FromObject(BinaryData value) + { + Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); +#if NET6_0_OR_GREATER + content.JsonWriter.WriteRawValue(value); +#else + using (JsonDocument document = JsonDocument.Parse(value)) + { + JsonSerializer.Serialize(content.JsonWriter, document.RootElement); + } +#endif + return content; + } + + /// + /// + /// + public static BinaryContent FromEnumerable(IEnumerable enumerable, string rootNameHint, string childNameHint) + where T : notnull + { + using (MemoryStream stream = new MemoryStream(256)) + { + using (XmlWriter writer = XmlWriter.Create(stream, ModelSerializationExtensions.XmlWriterSettings)) + { + writer.WriteStartElement(rootNameHint); + foreach (var item in enumerable) + { + writer.WriteObjectValue(item, ModelSerializationExtensions.WireOptions, childNameHint); + } + writer.WriteEndElement(); + } + + if (stream.Position > int.MaxValue) + { + return BinaryContent.Create(BinaryData.FromStream(stream)); + } + else + { + return BinaryContent.Create(new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position))); + } + } + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/PipelineRequestHeadersExtensions.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/PipelineRequestHeadersExtensions.cs new file mode 100644 index 00000000000..69ddd4aee41 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/PipelineRequestHeadersExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Linq; + +namespace SampleTypeSpec +{ + internal static partial class PipelineRequestHeadersExtensions + { + /// + /// The name. + /// The value. + /// The delimiter. + public static void SetDelimited(this PipelineRequestHeaders headers, string name, IEnumerable value, string delimiter) + { + IEnumerable stringValues = value.Select(v => TypeFormatters.ConvertToString(v)); + headers.Set(name, string.Join(delimiter, stringValues)); + } + + /// + /// The name. + /// The value. + /// The delimiter. + /// The format. + public static void SetDelimited(this PipelineRequestHeaders headers, string name, IEnumerable value, string delimiter, SerializationFormat format) + { + IEnumerable stringValues = value.Select(v => TypeFormatters.ConvertToString(v, format)); + headers.Set(name, string.Join(delimiter, stringValues)); + } + + /// + /// The prefix to prepend to each header key. + /// The dictionary of headers to add. + public static void Add(this PipelineRequestHeaders headers, string prefix, IDictionary value) + { + foreach (var header in value) + { + headers.Add(prefix + header.Key, header.Value); + } + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/Utf8JsonBinaryContent.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/Utf8JsonBinaryContent.cs new file mode 100644 index 00000000000..f4586e305fc --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/Utf8JsonBinaryContent.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System.ClientModel; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace SampleTypeSpec +{ + internal partial class Utf8JsonBinaryContent : BinaryContent + { + private readonly MemoryStream _stream; + private readonly BinaryContent _content; + + public Utf8JsonBinaryContent() + { + _stream = new MemoryStream(); + _content = Create(_stream); + JsonWriter = new Utf8JsonWriter(_stream); + } + + /// Gets the JsonWriter. + public Utf8JsonWriter JsonWriter { get; } + + /// The stream containing the data to be written. + /// The cancellation token to use. + public override async Task WriteToAsync(Stream stream, CancellationToken cancellationToken = default) + { + await JsonWriter.FlushAsync().ConfigureAwait(false); + await _content.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); + } + + /// The stream containing the data to be written. + /// The cancellation token to use. + public override void WriteTo(Stream stream, CancellationToken cancellationToken = default) + { + JsonWriter.Flush(); + _content.WriteTo(stream, cancellationToken); + } + + /// + public override bool TryComputeLength(out long length) + { + length = JsonWriter.BytesCommitted + JsonWriter.BytesPending; + return true; + } + + public override void Dispose() + { + JsonWriter.Dispose(); + _content.Dispose(); + _stream.Dispose(); + } + } +} diff --git a/packages/http-client-csharp/readme.md b/packages/http-client-csharp/readme.md index 6dcfdea3555..ef647e8f07c 100644 --- a/packages/http-client-csharp/readme.md +++ b/packages/http-client-csharp/readme.md @@ -78,9 +78,9 @@ Set to `false` to skip generation of convenience methods. The default value is ` ### `unreferenced-types-handling` -**Type:** `"removeOrInternalize" | "internalize" | "keepAll"` +**Type:** `"internalize" | "keepAll"` -Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. +Defines the strategy on how to handle unreferenced types. The default value is `internalize`. ### `new-project`