diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md b/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md new file mode 100644 index 00000000000..e45ad2b2de0 --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-2026-06-19-08-41-23.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-csharp" +--- + +Improve C# generator post-processing reference-map parity and performance. diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-07-01-02-43-39.md b/.chronus/changes/mtg-hybrid-reference-map-2026-07-01-02-43-39.md new file mode 100644 index 00000000000..f84f3c0ca6e --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-2026-07-01-02-43-39.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-csharp" +--- + +Improve generated C# reference-map analysis by deriving generated body references from provider expression trees. diff --git a/.chronus/changes/mtg-hybrid-reference-map-2026-07-06-02-27-00.md b/.chronus/changes/mtg-hybrid-reference-map-2026-07-06-02-27-00.md new file mode 100644 index 00000000000..59962113994 --- /dev/null +++ b/.chronus/changes/mtg-hybrid-reference-map-2026-07-06-02-27-00.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-csharp" +--- + +Preserve generated SDK compatibility when using the optimized C# reference map. diff --git a/.chronus/changes/mtg-manual-name-reduction-experiment-2026-06-23-07-14-07.md b/.chronus/changes/mtg-manual-name-reduction-experiment-2026-06-23-07-14-07.md new file mode 100644 index 00000000000..f975121ea3e --- /dev/null +++ b/.chronus/changes/mtg-manual-name-reduction-experiment-2026-06-23-07-14-07.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-csharp" +--- + +Tune hybrid provider reference-map benchmark parity and performance. diff --git a/.chronus/config.yaml b/.chronus/config.yaml index 06dad2b4c85..e8f50624dc9 100644 --- a/.chronus/config.yaml +++ b/.chronus/config.yaml @@ -71,6 +71,7 @@ versionPolicies: changelog: ["@chronus/github/changelog", { repo: "microsoft/typespec" }] additionalPackages: + - packages/http-client-csharp - packages/http-client-python - packages/http-client-java diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs index 24c20fd2c0e..390dab4824f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs @@ -248,6 +248,11 @@ private static bool UseSingletonInstance(InputClient inputClient) protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs"); + protected override bool ShouldWriteTypeXmlDocs => CustomCodeView is null; + + protected override TypeSignatureModifiers BuildDeclarationModifiers() + => _clientProvider.DeclarationModifiers & (TypeSignatureModifiers.Public | TypeSignatureModifiers.Internal | TypeSignatureModifiers.Protected | TypeSignatureModifiers.Private); + protected override string BuildName() { if (UseSingletonInstance(_inputClient)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientPipelineExtensionsDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientPipelineExtensionsDefinition.cs index 02864633aca..a1a1c144489 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientPipelineExtensionsDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientPipelineExtensionsDefinition.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.ClientModel.Snippets; @@ -52,6 +53,9 @@ protected override MethodProvider[] BuildMethods() ]; } + protected override IReadOnlyList BuildBodyDependencyTypes() + => [ErrorResultSnippets.ErrorResultType]; + private MethodProvider ProcessHeadAsBoolMessage() { MethodSignature signature = GetProcessHeadAsBoolMessageSignature(false); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index f8ee8744e32..c6645b491f2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -43,6 +43,7 @@ private record ApiVersionFields(FieldProvider Field, PropertyProvider? Correspon private const string ClientSuffix = "Client"; private readonly FormattableString _publicCtorDescription; private readonly InputClient _inputClient; + protected override bool IsClientProvider => true; internal InputClient InputClient => _inputClient; private readonly InputAuth? _inputAuth; private readonly ParameterProvider _endpointParameter; @@ -426,6 +427,42 @@ private IReadOnlyList GetClientParameters() protected override string BuildName() => _inputClient.IsExactName ? _inputClient.Name : _inputClient.Name.ToIdentifierName(); + protected override IReadOnlyList BuildHelperDependencyTypes() + { + foreach (var method in Methods.OfType()) + { + if (method.BodyStatements != null) + { + return [new CancellationTokenExtensionsDefinition().Type, new ClientPipelineExtensionsDefinition().Type]; + } + } + + return []; + } + + protected override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new List(); + foreach (var method in Methods.OfType()) + { + if (method.BodyStatements == null) + { + continue; + } + + if (method.CollectionDefinition != null) + { + dependencies.Add(method.CollectionDefinition.Type); + } + + // Service method metadata can mention wire-only request/response models that are not + // emitted in the generated method signature or body. The graph builder and structured + // body scanner capture the generated types that are actually referenced. + } + + return dependencies; + } + protected override FieldProvider[] BuildFields() { List fields = [EndpointField]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs index 92e0cf23a67..4dc5c283cc7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientUriBuilderDefinition.cs @@ -28,6 +28,7 @@ internal sealed class ClientUriBuilderDefinition : TypeProvider private readonly FieldProvider _uriBuilderField; private readonly FieldProvider _pathAndQueryField; private readonly FieldProvider _pathLengthField; + protected override bool IncludeGeneratedBodyReferences => true; private PropertyProvider? _uriBuilderProperty; private PropertyProvider UriBuilderProperty => _uriBuilderProperty ??= new( diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs index ae617957bf5..590eaf2b935 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs @@ -217,6 +217,22 @@ private bool HasPagingOperationNameCollision(string operationName) protected override TypeSignatureModifiers BuildDeclarationModifiers() => TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; + protected override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new List { Client.Type, ResponseModelType, NextPagePropertyType }; + if (ItemModelType != null) + { + dependencies.Add(ItemModelType); + } + + foreach (var field in RequestFields) + { + dependencies.Add(field.Type); + } + + return dependencies; + } + protected override FieldProvider[] BuildFields() => [ClientField, .. RequestFields]; protected override CSharpType[] BuildImplements() => diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 5c287f5674c..6d6ea1348bb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -117,6 +117,10 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m protected override CSharpType? BuildBaseType() => _model.BaseType; + protected override IReadOnlyList BuildHelperDependencyTypes() => _rawDataField != null || _additionalProperties.Value.Length > 0 + ? [ChangeTrackingDictionaryType] + : []; + protected override SuppressionStatement[] BuildDisabledFileWarnings() { if (_model.CanonicalView.Properties.Any(p => ScmModelProvider.IsFileBinaryContentType(p.Type))) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs index 88cb97b16e7..398ffc329a3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MultipartFormDataSerializationDefinition.cs @@ -51,6 +51,12 @@ protected override string BuildRelativeFilePath() return Path.Combine("src", "Generated", "Models", $"{Name}.Serialization.Multipart.cs"); } + protected override IReadOnlyList BuildHelperDependencyTypes() => _model.Properties.Any( + prop => prop.WireInfo != null && !prop.WireInfo.IsRequired && + (prop.Type is { IsCollection: true, IsReadOnlyMemory: false } || prop.Type.IsDictionary)) + ? [OptionalType] + : []; + protected override SuppressionStatement[] BuildDisabledFileWarnings() => [new SuppressionStatement(null, Literal(ScmModelProvider.FileBinaryContentDiagnosticId), ScmModelProvider.ScmEvaluationTypeSuppressionJustification)]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 5d4b8b9b26a..504a09d2806 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -78,6 +78,43 @@ protected override FieldProvider[] BuildFields() return [.. pipelineMessage20xClassifiersFields]; } + protected override IReadOnlyList BuildHelperDependencyTypes() + { + var dependencies = new List { new ClientUriBuilderDefinition().Type }; + foreach (var serviceMethod in _inputClient.Methods) + { + foreach (var parameter in serviceMethod.Operation.Parameters) + { + if (parameter is not InputHeaderParameter and not InputQueryParameter) + { + continue; + } + + var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); + if (type?.IsDictionary == true) + { + AddDependency(dependencies, ChangeTrackingDictionaryType); + } + else if (type?.IsCollection == true) + { + AddDependency(dependencies, ChangeTrackingListType); + } + } + } + + return dependencies; + } + + private static void AddDependency(List dependencies, CSharpType dependency) + { + if (!dependencies.Any(existing => + existing.Name == dependency.Name && + existing.Namespace == dependency.Namespace)) + { + dependencies.Add(dependency); + } + } + protected override ScmMethodProvider[] BuildMethods() { List methods = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs index af294640060..e902afd7c79 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/SerializationFormatDefinition.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -45,6 +46,7 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs"); protected override string BuildName() => "SerializationFormat"; + protected override FormattableString BuildDescription() => $"The serialization format."; protected override TypeProvider[] BuildSerializationProviders() => []; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs index 588a2094b12..fdd8f72fba0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs @@ -26,7 +26,9 @@ public static MethodBodyStatement SetContent(this ScopedApi pip public static MethodBodyStatement SetHeaderDelimited(this HttpRequestApi pipelineRequest, string name, ValueExpression value, ValueExpression delimiter, ValueExpression? format = null) { ValueExpression[] parameters = format != null ? [Literal(name), value, delimiter, format] : [Literal(name), value, delimiter]; - return pipelineRequest.Property(nameof(PipelineRequest.Headers)).Invoke("SetDelimited", parameters).Terminate(); + return pipelineRequest.Property(nameof(PipelineRequest.Headers)) + .Invoke("SetDelimited", parameters, typeArguments: null, callAsAsync: false, extensionType: new PipelineRequestHeadersExtensionsDefinition().Type) + .Terminate(); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs new file mode 100644 index 00000000000..de336d6856e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/PostProcessing/ClientBodyDependencyPostProcessingTests.cs @@ -0,0 +1,446 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.PostProcessing +{ + public class ClientBodyDependencyPostProcessingTests + { + [Test] + public async Task OperationBodyParameterModelDoesNotBecomePublic() + { + var requestModel = InputFactory.Model("RequestBody"); + var parameter = InputFactory.BodyParameter("body", requestModel, isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Create", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertInternalModels([requestModel], [client], ["RequestBody"]); + } + + [Test] + public async Task OperationResponseBodyModelRemainsPublicAsRootOutputModel() + { + var responseModel = InputFactory.Model("ResponseBody"); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: responseModel)]); + var method = InputFactory.BasicServiceMethod( + "Get", + operation, + response: InputFactory.ServiceMethodResponse(InputPrimitiveType.String, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertPublicModels([responseModel], [client], ["ResponseBody"]); + } + + [Test] + public async Task NestedBodyModelGraphDoesNotBecomePublic() + { + var nestedModel = InputFactory.Model("NestedToolParameter"); + var toolModel = InputFactory.Model( + "ToolConfig", + properties: [InputFactory.Property("Parameter", nestedModel)]); + var parameter = InputFactory.BodyParameter("tool", toolModel, isRequired: true); + var operation = InputFactory.Operation("Configure", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Configure", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertInternalModels([toolModel, nestedModel], [client], ["ToolConfig", "NestedToolParameter"]); + } + + [Test] + public async Task NonDiscriminatorDerivedBodyModelDoesNotBecomePublicFromPublicBase() + { + var baseTool = InputFactory.Model("BaseTool"); + var concreteTool = InputFactory.Model( + "ConcreteTool", + properties: [InputFactory.Property("Name", InputPrimitiveType.String)], + baseModel: baseTool); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: baseTool)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(baseTool, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertMixedModels( + [baseTool, concreteTool], + [client], + publicModelNames: ["BaseTool"], + internalModelNames: ["ConcreteTool"]); + } + + [Test] + public async Task PublicModelSignatureDependencyIsPromotedToPublic() + { + var internalDependency = InputFactory.Model("InternalDependency", access: "internal"); + var responseModel = InputFactory.Model( + "ResponseBody", + properties: [InputFactory.Property("Dependency", internalDependency)]); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: responseModel)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(responseModel, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertPublicModels([responseModel, internalDependency], [client], ["ResponseBody", "InternalDependency"]); + } + + [Test] + public async Task AzureClientPublicMethodSignatureReferencesStayPublic() + { + var signatureModel = InputFactory.Model("SignatureModel", @namespace: "Azure.Sample.Models"); + var methodParameter = InputFactory.MethodParameter("signature", signatureModel, isRequired: true); + var operation = InputFactory.Operation( + "Create", + parameters: [InputFactory.BodyParameter("signature", signatureModel, isRequired: true)], + httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Create", operation, parameters: [methodParameter]); + var client = InputFactory.Client("SampleClient", clientNamespace: "Azure.Sample", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [signatureModel], + clients: [client], + customFiles: [], + expectedFiles: [], + publicModelNames: ["SignatureModel"], + packageName: "Azure.Sample"); + } + + [Test] + public async Task BasePreservedDerivedModelTraversesTransitiveDependencies() + { + var transitiveDependency = InputFactory.Model("TransitiveDependency"); + var dependency = InputFactory.Model( + "DerivedDependency", + properties: [InputFactory.Property("Transitive", transitiveDependency)]); + var baseModel = InputFactory.Model("BaseResult"); + var derivedModel = InputFactory.Model( + "DerivedResult", + properties: [InputFactory.Property("Dependency", dependency)], + baseModel: baseModel); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: baseModel)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(baseModel, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [baseModel, derivedModel, dependency, transitiveDependency], + clients: [client], + customFiles: [], + expectedFiles: [], + publicModelNames: ["BaseResult"], + internalModelNames: ["DerivedResult", "DerivedDependency", "TransitiveDependency"]); + } + + [Test] + public async Task PublicCustomCodeArraySignatureReferencesStayPublic() + { + var generatedModel = InputFactory.Model("GeneratedModel"); + + await GenerateAndAssertFiles( + enums: [], + models: [generatedModel], + clients: [], + customFiles: [ + (Path.Combine("src", "PublicCustomApi.cs"), """ + using Sample.Models; + + namespace Sample; + + public partial class PublicCustomApi + { + public GeneratedModel[] Items { get; } = System.Array.Empty(); + } + """) + ], + expectedFiles: [], + publicModelNames: ["GeneratedModel"]); + } + + [Test] + public async Task GeneratedRequestHeaderSetDelimitedReferenceKeepsExtensions() + { + var header = InputFactory.HeaderParameter("x-ms-custom", InputFactory.Array(InputPrimitiveType.String), isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [header]); + var method = InputFactory.BasicServiceMethod("Create", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [client], + customFiles: [], + expectedFiles: [Path.Combine("src", "Generated", "Internal", "PipelineRequestHeadersExtensions.cs")]); + } + + [Test] + public async Task BinaryDataBodyParameterDoesNotKeepBinaryContentHelpers() + { + var parameter = InputFactory.BodyParameter( + "content", + InputPrimitiveType.Base64, + isRequired: true, + contentTypes: ["application/octet-stream"], + defaultContentType: "application/octet-stream"); + var operation = InputFactory.Operation("Upload", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod( + "Upload", + operation, + parameters: [InputFactory.MethodParameter("content", InputPrimitiveType.Base64, isRequired: true)]); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [client], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Internal", "BinaryContentHelper.cs"), + Path.Combine("src", "Generated", "Internal", "Utf8JsonBinaryContent.cs") + ]); + } + + [Test] + public async Task CollectionBodyParameterKeepsBinaryContentHelpers() + { + var parameter = InputFactory.BodyParameter("items", InputFactory.Array(InputPrimitiveType.String), isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod( + "Create", + operation, + parameters: [InputFactory.MethodParameter("items", InputFactory.Array(InputPrimitiveType.String), isRequired: true)]); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [client], + customFiles: [], + expectedFiles: [ + Path.Combine("src", "Generated", "Internal", "BinaryContentHelper.cs"), + Path.Combine("src", "Generated", "Internal", "Utf8JsonBinaryContent.cs") + ]); + } + + [Test] + public async Task CustomOnlyRequestHeaderSetDelimitedReferenceKeepsExtensions() + { + await GenerateAndAssertFiles( + enums: [], + models: [], + clients: [], + customFiles: [ + (Path.Combine("src", "CustomHeaders.cs"), """ + using System.ClientModel.Primitives; + + namespace Sample; + + public static class CustomHeaders + { + public static void Add(PipelineRequestHeaders headers, string[] values) + => headers.SetDelimited("x-ms-custom", values, ","); + } + """) + ], + expectedFiles: [Path.Combine("src", "Generated", "Internal", "PipelineRequestHeadersExtensions.cs")]); + } + + [Test] + public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum() + { + var statusEnum = InputFactory.StringEnum( + "Status", + [("Succeeded", "succeeded"), ("Failed", "failed")], + clientNamespace: "Sample"); + var resultModel = InputFactory.Model( + "OperationResult", + properties: [InputFactory.Property("Status", statusEnum, isRequired: true)], + @namespace: "Sample"); + var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: resultModel)]); + var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(resultModel, [])); + var client = InputFactory.Client("TestClient", methods: [method], clientNamespace: "Sample"); + + await GenerateAndAssertFiles( + enums: [statusEnum], + models: [resultModel], + clients: [client], + customFiles: [ + (Path.Combine("src", "Custom", "Status.cs"), """ + namespace Sample; + + [CodeGenType("Status")] + public enum Status + { + Succeeded, + Failed + } + """) + ], + expectedFiles: [Path.Combine("src", "Generated", "Models", "Status.Serialization.cs")]); + } + + [Test] + public async Task CustomModelFactoryPartialDoesNotKeepBodyOnlyModelPublic() + { + var requestModel = InputFactory.Model("RequestBody"); + var parameter = InputFactory.BodyParameter("body", requestModel, isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [parameter], httpMethod: "POST"); + var method = InputFactory.BasicServiceMethod("Create", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [requestModel], + clients: [client], + customFiles: [ + (Path.Combine("src", "SampleModelFactory.cs"), """ + namespace Sample; + + [Microsoft.TypeSpec.Generator.Customizations.CodeGenType("SampleModelFactory")] + public static partial class SampleModelFactory + { + } + """) + ], + expectedFiles: [], + internalModelNames: ["RequestBody"]); + } + + private static async Task GenerateAndAssertInternalModels( + InputModelType[] models, + InputClient[] clients, + string[] modelNames) + => await GenerateAndAssertModels(models, clients, modelNames, shouldBePublic: false); + + private static async Task GenerateAndAssertPublicModels( + InputModelType[] models, + InputClient[] clients, + string[] modelNames) + => await GenerateAndAssertModels(models, clients, modelNames, shouldBePublic: true); + + private static async Task GenerateAndAssertMixedModels( + InputModelType[] models, + InputClient[] clients, + string[] publicModelNames, + string[] internalModelNames) + => await GenerateAndAssertModels(models, clients, publicModelNames, internalModelNames); + + private static async Task GenerateAndAssertModels( + InputModelType[] models, + InputClient[] clients, + string[] modelNames, + bool shouldBePublic) + => await GenerateAndAssertModels( + models, + clients, + shouldBePublic ? modelNames : [], + shouldBePublic ? [] : modelNames); + + private static async Task GenerateAndAssertModels( + InputModelType[] models, + InputClient[] clients, + string[] publicModelNames, + string[] internalModelNames) + { + await GenerateAndAssertFiles( + enums: [], + models: models, + clients: clients, + customFiles: [], + publicModelNames: publicModelNames, + internalModelNames: internalModelNames, + expectedFiles: []); + } + + private static async Task GenerateAndAssertFiles( + InputEnumType[] enums, + InputModelType[] models, + InputClient[] clients, + (string Path, string Content)[] customFiles, + string[] expectedFiles, + string[] unexpectedFiles = null!, + string[] publicModelNames = null!, + string[] internalModelNames = null!, + string packageName = "Sample") + { + publicModelNames ??= []; + internalModelNames ??= []; + unexpectedFiles ??= []; + + var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(outputPath); + try + { + foreach (var customFile in customFiles) + { + var customPath = Path.Combine(outputPath, customFile.Path); + Directory.CreateDirectory(Path.GetDirectoryName(customPath)!); + File.WriteAllText(customPath, customFile.Content); + } + + await MockHelpers.LoadMockGeneratorAsync( + inputEnums: () => enums, + inputModels: () => models, + clients: () => clients, + configuration: $$"""{ "package-name": "{{packageName}}", "disable-xml-docs": true }""", + outputPath: outputPath); + + await new CSharpGen().ExecuteAsync(); + + foreach (var modelName in publicModelNames) + { + var modelPath = Path.Combine(outputPath, "src", "Generated", "Models", $"{modelName}.cs"); + Assert.IsTrue(File.Exists(modelPath), $"Expected generated model file '{modelPath}'."); + var text = File.ReadAllText(modelPath); + StringAssert.Contains($"public partial class {modelName}", text, $"{modelName} should be public."); + } + + foreach (var modelName in internalModelNames) + { + var modelPath = Path.Combine(outputPath, "src", "Generated", "Models", $"{modelName}.cs"); + Assert.IsTrue(File.Exists(modelPath), $"Expected generated model file '{modelPath}'."); + var text = File.ReadAllText(modelPath); + StringAssert.Contains($"internal partial class {modelName}", text, $"{modelName} should be internal."); + StringAssert.DoesNotContain($"public partial class {modelName}", text, $"{modelName} should not be public."); + } + + var modelFactoryPath = Path.Combine(outputPath, "src", "Generated", "SampleModelFactory.cs"); + if (File.Exists(modelFactoryPath)) + { + var modelFactoryText = File.ReadAllText(modelFactoryPath); + foreach (var modelName in publicModelNames) + { + StringAssert.Contains($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should be generated."); + } + + foreach (var modelName in internalModelNames) + { + StringAssert.DoesNotContain($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should not be generated."); + } + } + + foreach (var expectedFile in expectedFiles) + { + var filePath = Path.Combine(outputPath, expectedFile); + Assert.IsTrue(File.Exists(filePath), $"Expected generated file '{filePath}'."); + } + + foreach (var unexpectedFile in unexpectedFiles) + { + var filePath = Path.Combine(outputPath, unexpectedFile); + Assert.IsFalse(File.Exists(filePath), $"Did not expect generated file '{filePath}'."); + } + } + finally + { + if (Directory.Exists(outputPath)) + { + Directory.Delete(outputPath, recursive: true); + } + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs index 72ee46c9658..e3877151fca 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderCustomizationTests.cs @@ -392,8 +392,7 @@ public async Task CanChangeClientAccessibility() // Find the client options provider var clientOptionsProvider = mockGenerator.Object.OutputLibrary.TypeProviders.SingleOrDefault(t => t is ClientOptionsProvider); Assert.IsNotNull(clientOptionsProvider); - // The client options were not customized - Assert.IsTrue(clientOptionsProvider!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(clientOptionsProvider!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); // ClientSettings should not be generated for internal clients Assert.IsNull(((ClientProvider)clientProvider).ClientSettings, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs index 9148f659e43..6c6e743ad89 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/TestHelpers/MockHelpers.cs @@ -34,7 +34,8 @@ public static async Task> LoadMockGeneratorAsync( Func>? apiVersions = null, string? configuration = null, Func? createCSharpTypeCore = null, - Func? createCSharpTypeCoreFallback = null) + Func? createCSharpTypeCoreFallback = null, + string? outputPath = null) { var mockGenerator = LoadMockGenerator( inputLiterals: inputLiterals, @@ -44,13 +45,13 @@ public static async Task> LoadMockGeneratorAsync( apiVersions: apiVersions, configuration: configuration, createCSharpTypeCore: createCSharpTypeCore, - createCSharpTypeCoreFallback: createCSharpTypeCoreFallback); + createCSharpTypeCoreFallback: createCSharpTypeCoreFallback, + outputPath: outputPath); var compilationResult = compilation == null ? null : await compilation(); var lastContractCompilationResult = lastContractCompilation == null ? null : await lastContractCompilation(); - var sourceInputModel = new Mock(() => new SourceInputModel(compilationResult, lastContractCompilationResult)) { CallBase = true }; - mockGenerator.Setup(p => p.SourceInputModel).Returns(sourceInputModel.Object); + mockGenerator.SetupProperty(p => p.SourceInputModel, new SourceInputModel(compilationResult, lastContractCompilationResult)); return mockGenerator; } @@ -76,7 +77,8 @@ public static Mock LoadMockGenerator( Func? createOutputLibrary = null, bool includeXmlDocs = false, Func? createCSharpTypeCoreFallback = null, - Func? createModelCore = null) + Func? createModelCore = null, + string? outputPath = null) { IReadOnlyList inputNsApiVersions = apiVersions?.Invoke() ?? []; IReadOnlyList inputNsLiterals = inputLiterals?.Invoke() ?? []; @@ -150,7 +152,7 @@ public static Mock LoadMockGenerator( { configuration = "{\"disable-xml-docs\": false, \"package-name\": \"Sample.Namespace\"}"; } - object?[] parameters = [_configFilePath, configuration]; + object?[] parameters = [outputPath ?? _configFilePath, configuration]; var config = loadMethod?.Invoke(null, parameters); var mockGeneratorContext = new Mock(config!); var mockGeneratorInstance = new Mock(mockGeneratorContext.Object) { CallBase = true }; @@ -186,8 +188,7 @@ public static Mock LoadMockGenerator( mockGeneratorInstance.Setup(p => p.OutputLibrary).Returns(createOutputLibrary); } - var sourceInputModel = new Mock(() => new SourceInputModel(null, null)) { CallBase = true }; - mockGeneratorInstance.Setup(p => p.SourceInputModel).Returns(sourceInputModel.Object); + mockGeneratorInstance.SetupProperty(p => p.SourceInputModel, new SourceInputModel(null, null)); codeModelInstance!.SetValue(null, mockGeneratorInstance.Object); clientModelInstance!.SetValue(null, mockGeneratorInstance.Object); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs new file mode 100644 index 00000000000..35344ec5216 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/FullGenerationBenchmark.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.TypeSpec.Generator.Perf +{ + public class FullGenerationBenchmark + { + private const string ProfileEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_STEPS"; + private const string ProfileOutputDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_DIR"; + private const string ShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW"; + private const string UseShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_USE_SHADOW"; + private const string ShadowReportEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_SHADOW_REPORT"; + + private bool _profileSteps; + + [Params(false, true)] + public bool UseProviderReferenceMap { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + _profileSteps = string.Equals( + Environment.GetEnvironmentVariable(ProfileEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + } + + [Benchmark] + public async Task GenerateSampleTypeSpecProject() + { + var postProcessingProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; + var generationProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; + GeneratedCodeWorkspace.PostProcessingProfile = postProcessingProfile; + CSharpGen.GenerationProfile = generationProfile; + + var benchmarkDirectory = CreateBenchmarkInputDirectory(); + var previousShadow = Environment.GetEnvironmentVariable(ShadowEnvironmentVariable); + var previousUseShadow = Environment.GetEnvironmentVariable(UseShadowEnvironmentVariable); + var previousShadowReport = Environment.GetEnvironmentVariable(ShadowReportEnvironmentVariable); + var stopwatch = Stopwatch.StartNew(); + try + { + SetProviderReferenceMapEnvironment(); + CodeModelGenerator.Instance = new BenchmarkCodeModelGenerator(benchmarkDirectory); + CodeModelGenerator.Instance.Configure(); + + var csharpGen = new CSharpGen(); + await csharpGen.ExecuteAsync(); + + return Directory.GetFiles(benchmarkDirectory, "*", SearchOption.AllDirectories) + .Where(static path => !path.EndsWith("tspCodeModel.json", StringComparison.Ordinal) && + !path.EndsWith("Configuration.json", StringComparison.Ordinal)) + .Sum(static path => (int)new FileInfo(path).Length); + } + finally + { + stopwatch.Stop(); + if (generationProfile != null) + { + WriteProfile( + generationProfile, + $"full-generation-profile-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", + $"Full generation invocation elapsed ms: {stopwatch.Elapsed.TotalMilliseconds:F3}{Environment.NewLine}" + + $"Input directory: {benchmarkDirectory}{Environment.NewLine}"); + } + + if (postProcessingProfile != null) + { + WriteProfile( + postProcessingProfile, + $"full-generation-post-processing-profile-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", + $"Full generation post-processing profile{Environment.NewLine}" + + $"Input directory: {benchmarkDirectory}{Environment.NewLine}"); + } + + CSharpGen.GenerationProfile = null; + GeneratedCodeWorkspace.PostProcessingProfile = null; + Environment.SetEnvironmentVariable(ShadowEnvironmentVariable, previousShadow); + Environment.SetEnvironmentVariable(UseShadowEnvironmentVariable, previousUseShadow); + Environment.SetEnvironmentVariable(ShadowReportEnvironmentVariable, previousShadowReport); + TryDeleteDirectory(benchmarkDirectory); + } + } + + private void SetProviderReferenceMapEnvironment() + { + Environment.SetEnvironmentVariable(ShadowEnvironmentVariable, UseProviderReferenceMap ? "true" : null); + Environment.SetEnvironmentVariable(UseShadowEnvironmentVariable, UseProviderReferenceMap ? "true" : null); + Environment.SetEnvironmentVariable(ShadowReportEnvironmentVariable, null); + } + + private static void WriteProfile(GeneratedCodeWorkspacePostProcessingProfile profile, string fileName, string header) + { + var profileDirectory = GetProfileOutputDirectory(); + Directory.CreateDirectory(profileDirectory); + File.WriteAllText(Path.Combine(profileDirectory, fileName), header + profile.GetSummary()); + } + + private static string CreateBenchmarkInputDirectory() + { + var sourceDirectory = FindFullGenerationInputDirectory(); + var benchmarkDirectory = Path.Combine(Path.GetTempPath(), "typespec-full-generation-benchmark", Guid.NewGuid().ToString("N")); + CopyDirectory(sourceDirectory, benchmarkDirectory); + return benchmarkDirectory; + } + + private static string FindFullGenerationInputDirectory() + { + const string relativePath = "packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec"; + + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var inputDirectory = Path.Combine(directory.FullName, relativePath); + if (File.Exists(Path.Combine(inputDirectory, "tspCodeModel.json")) && + File.Exists(Path.Combine(inputDirectory, "Configuration.json"))) + { + return inputDirectory; + } + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException($"Could not find '{relativePath}' from '{AppContext.BaseDirectory}'."); + } + + private static void CopyDirectory(string sourceDirectory, string destinationDirectory) + { + Directory.CreateDirectory(destinationDirectory); + foreach (var sourceFile in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories)) + { + var relativePath = Path.GetRelativePath(sourceDirectory, sourceFile); + var destinationFile = Path.Combine(destinationDirectory, relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!); + File.Copy(sourceFile, destinationFile, overwrite: true); + } + } + + private static void TryDeleteDirectory(string directory) + { + try + { + if (Directory.Exists(directory)) + { + Directory.Delete(directory, recursive: true); + } + } + catch + { + // Best-effort cleanup for benchmark temp output. + } + } + + private static string GetProfileOutputDirectory() + { + var configuredPath = Environment.GetEnvironmentVariable(ProfileOutputDirectoryEnvironmentVariable); + return string.IsNullOrWhiteSpace(configuredPath) + ? Path.Combine(Path.GetTempPath(), "typespec-post-processing-profiles") + : Path.GetFullPath(configuredPath); + } + + private sealed class BenchmarkCodeModelGenerator : CodeModelGenerator + { + public BenchmarkCodeModelGenerator(string outputPath) + : base(new GeneratorContext(Configuration.Load(outputPath))) + { + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs new file mode 100644 index 00000000000..24b5394e2cd --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/perf/PostProcessingBenchmark.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.TypeSpec.Generator.Primitives; + +namespace Microsoft.TypeSpec.Generator.Perf +{ + public class PostProcessingBenchmark + { + private const string GeneratedDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_GENERATED_DIR"; + private const string ProfileEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_STEPS"; + private const string ProfileOutputDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_DIR"; + private static readonly Regex NamespaceDeclarationRegex = new( + @"\bnamespace\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)", + RegexOptions.Compiled); + + [Params(1, 5)] + public int CorpusMultiplier { get; set; } + + private (string Name, string Content)[] _generatedFiles = []; + private bool _profileSteps; + + [GlobalSetup] + public void GlobalSetup() + { + InitializeGenerator(); + + var generatedDirectory = FindGeneratedDirectory(); + var sourceFiles = Directory.GetFiles(generatedDirectory, "*.cs", SearchOption.AllDirectories) + .OrderBy(static path => path, StringComparer.Ordinal) + .ToArray(); + + if (sourceFiles.Length == 0) + { + throw new InvalidOperationException($"No generated C# files found under '{generatedDirectory}'."); + } + + var declaredNamespaces = GetDeclaredNamespaces(sourceFiles); + _generatedFiles = BuildCorpus(generatedDirectory, sourceFiles, declaredNamespaces); + _profileSteps = string.Equals( + Environment.GetEnvironmentVariable(ProfileEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + } + + [Benchmark] + public async Task ProcessSampleTypeSpecGeneratedFiles() + { + var profile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null; + GeneratedCodeWorkspace.PostProcessingProfile = profile; + + var stopwatch = Stopwatch.StartNew(); + GeneratedCodeWorkspace.Initialize(); + var workspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: false); + + try + { + foreach (var file in _generatedFiles) + { + await workspace.AddGeneratedFile(new CodeFile(file.Content, file.Name)); + } + + var totalLength = 0; + await foreach (var file in workspace.GetGeneratedFilesAsync()) + { + totalLength += file.Text.Length; + } + + return totalLength; + } + finally + { + stopwatch.Stop(); + if (profile != null) + { + WriteProfile( + profile, + $"post-processing-profile-RoslynSimplifier-x{CorpusMultiplier}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv", + $"Reduction strategy: RoslynSimplifier{Environment.NewLine}" + + $"Corpus multiplier: {CorpusMultiplier}{Environment.NewLine}" + + $"File count: {_generatedFiles.Length}{Environment.NewLine}" + + $"Benchmark invocation elapsed ms: {stopwatch.Elapsed.TotalMilliseconds:F3}{Environment.NewLine}"); + } + + GeneratedCodeWorkspace.PostProcessingProfile = null; + } + } + + private static void WriteProfile(GeneratedCodeWorkspacePostProcessingProfile profile, string fileName, string header) + { + var profileDirectory = GetProfileOutputDirectory(); + Directory.CreateDirectory(profileDirectory); + File.WriteAllText(Path.Combine(profileDirectory, fileName), header + profile.GetSummary()); + } + + private static string GetProfileOutputDirectory() + { + var configuredPath = Environment.GetEnvironmentVariable(ProfileOutputDirectoryEnvironmentVariable); + return string.IsNullOrWhiteSpace(configuredPath) + ? Path.Combine(Path.GetTempPath(), "typespec-post-processing-profiles") + : Path.GetFullPath(configuredPath); + } + + private (string Name, string Content)[] BuildCorpus(string generatedDirectory, string[] sourceFiles, IReadOnlyList declaredNamespaces) + { + var generatedFiles = new List<(string Name, string Content)>(sourceFiles.Length * CorpusMultiplier); + for (var i = 0; i < CorpusMultiplier; i++) + { + var namespaceSuffix = CorpusMultiplier == 1 ? string.Empty : $".BenchmarkCopy{i}"; + var folderPrefix = CorpusMultiplier == 1 ? string.Empty : $"BenchmarkCopy{i}"; + foreach (var path in sourceFiles) + { + var relativePath = Path.GetRelativePath(generatedDirectory, path); + var content = File.ReadAllText(path); + if (CorpusMultiplier > 1) + { + content = MakeNamespacesUnique(content, declaredNamespaces, namespaceSuffix); + } + + generatedFiles.Add((Path.Combine(folderPrefix, relativePath), content)); + } + } + + return generatedFiles.ToArray(); + } + + private static IReadOnlyList GetDeclaredNamespaces(string[] sourceFiles) + { + var declaredNamespaces = sourceFiles + .SelectMany(static path => NamespaceDeclarationRegex.Matches(File.ReadAllText(path))) + .Select(static match => match.Groups[1].Value) + .Distinct(StringComparer.Ordinal) + .ToArray(); + + return declaredNamespaces + .Where(ns => !declaredNamespaces.Any(candidate => + !string.Equals(ns, candidate, StringComparison.Ordinal) && + ns.StartsWith(candidate + ".", StringComparison.Ordinal))) + .OrderByDescending(static ns => ns.Length) + .ToArray(); + } + + private static string MakeNamespacesUnique(string content, IReadOnlyList declaredNamespaces, string namespaceSuffix) + { + foreach (var declaredNamespace in declaredNamespaces) + { + var escapedNamespace = Regex.Escape(declaredNamespace); + content = content.Replace($"global::{declaredNamespace}.", $"global::{declaredNamespace}{namespaceSuffix}.", StringComparison.Ordinal); + content = Regex.Replace( + content, + $@"(? GetMetadataReferencePaths() + { + HashSet referencePaths = new(StringComparer.OrdinalIgnoreCase); + + if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trustedPlatformAssemblies) + { + foreach (var referencePath in trustedPlatformAssemblies.Split(Path.PathSeparator)) + { + if (referencePaths.Add(referencePath)) + { + yield return referencePath; + } + } + } + + foreach (var referencePath in Directory.GetFiles(AppContext.BaseDirectory, "*.dll", SearchOption.TopDirectoryOnly)) + { + if (referencePaths.Add(referencePath)) + { + yield return referencePath; + } + } + } + + private sealed class BenchmarkCodeModelGenerator : CodeModelGenerator + { + public BenchmarkCodeModelGenerator(string outputPath) + : base(new GeneratorContext(Configuration.Load(outputPath, "{\"package-name\":\"Sample.TypeSpec\",\"disable-xml-docs\":false}"))) + { + } + } + } +} 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..1d9fd7e4ed2 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 @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -17,8 +18,20 @@ internal sealed class CSharpGen { private const string ConfigurationFileName = "Configuration.json"; private const string CodeModelFileName = "tspCodeModel.json"; + private const string RawRequestUriBuilderExtensionsFileName = "RawRequestUriBuilderExtensions.cs"; + private const string SerializationFormatFileName = "SerializationFormat.cs"; + private const string TypeFormattersFileName = "TypeFormatters.cs"; - private static readonly string[] _filesToKeep = [ConfigurationFileName, CodeModelFileName]; + private static readonly string[] _filesToKeep = + [ + ConfigurationFileName, + CodeModelFileName, + RawRequestUriBuilderExtensionsFileName, + SerializationFormatFileName, + TypeFormattersFileName + ]; + + internal static GeneratedCodeWorkspacePostProcessingProfile? GenerationProfile { get; set; } /// /// Executes the generator task with the instance. @@ -27,25 +40,28 @@ public async Task ExecuteAsync() { CodeModelGenerator.Instance.Emitter.Info("Starting code generation"); CodeModelGenerator.Instance.Stopwatch.Start(); + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); var outputPath = CodeModelGenerator.Instance.Configuration.OutputDirectory; var generatedSourceOutputPath = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory; // Resolve PackageReference items from the .csproj so custom code referencing // external NuGet types (e.g., Azure.Storage.Common) compiles correctly. - await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); + await MeasureGenerationStepAsync("Generation.AddPackageReferencesFromProject", GeneratedCodeWorkspace.AddPackageReferencesFromProject); // Pre-walk the input library and resolve any external types that point at NuGet packages. // This populates ExternalTypeReferenceResolver's cache and registers each resolved assembly // as an additional metadata reference *before* the generated/custom code workspaces are // constructed, so their cached Roslyn projects pick the references up. - await ExternalTypeReferenceResolver.ResolveAllAsync(); + await MeasureGenerationStepAsync("Generation.ResolveExternalTypeReferences", ExternalTypeReferenceResolver.ResolveAllAsync); // Initialize the workspace project AFTER all metadata references have been added so the // eagerly-cached project sees them. GeneratedCodeWorkspace.Initialize(); - GeneratedCodeWorkspace customCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: true); + GeneratedCodeWorkspace customCodeWorkspace = await MeasureGenerationStepAsync( + "Generation.CreateCustomCodeWorkspace", + () => GeneratedCodeWorkspace.Create(isCustomCodeProject: true)); // The generated attributes need to be added into the workspace before loading the custom code. Otherwise, // Roslyn doesn't load the attributes completely and we are unable to get the attribute arguments. @@ -55,89 +71,208 @@ public async Task ExecuteAsync() generateAttributeTasks.Add(customCodeWorkspace.AddInMemoryFile(attributeProvider)); } - await Task.WhenAll(generateAttributeTasks); + await MeasureGenerationStepAsync("Generation.AddCustomizationAttributeProviders", () => Task.WhenAll(generateAttributeTasks)); - CodeModelGenerator.Instance.SourceInputModel = new SourceInputModel( - await customCodeWorkspace.GetCompilationAsync(), - await GeneratedCodeWorkspace.LoadBaselineContract(), - GeneratedCodeWorkspace.LoadApiCompatBaseline()); + CodeModelGenerator.Instance.SourceInputModel = await MeasureGenerationStepAsync( + "Generation.CreateSourceInputModel", + async () => new SourceInputModel( + await customCodeWorkspace.GetCompilationAsync(), + await GeneratedCodeWorkspace.LoadBaselineContract(), + GeneratedCodeWorkspace.LoadApiCompatBaseline())); - GeneratedCodeWorkspace generatedCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: false); + GeneratedCodeWorkspace generatedCodeWorkspace = await MeasureGenerationStepAsync( + "Generation.CreateGeneratedCodeWorkspace", + () => GeneratedCodeWorkspace.Create(isCustomCodeProject: false)); - var output = CodeModelGenerator.Instance.OutputLibrary; + var output = MeasureGenerationStep("Generation.GetOutputLibrary", () => CodeModelGenerator.Instance.OutputLibrary); Directory.CreateDirectory(Path.Combine(generatedSourceOutputPath, "Models")); List generateFilesTasks = new(); // Build all TypeProviders - foreach (var type in output.TypeProviders) + MeasureGenerationStep("Generation.BuildTypeProviders", () => { - type.EnsureBuilt(); - } + foreach (var type in output.TypeProviders) + { + type.EnsureBuilt(); + } + }); LoggingHelpers.LogElapsedTime("All generated type providers built"); // visit the entire library before generating files - foreach (var visitor in CodeModelGenerator.Instance.Visitors) + MeasureGenerationStep("Generation.ApplyVisitors", () => { - visitor.VisitLibrary(output); - } + foreach (var visitor in CodeModelGenerator.Instance.Visitors) + { + visitor.VisitLibrary(output); + } + }); - FilterAllCustomizedMembers(output); + MeasureGenerationStep("Generation.FilterCustomizedMembers", () => FilterAllCustomizedMembers(output)); LoggingHelpers.LogElapsedTime("All visitors have been applied"); - foreach (var outputType in output.TypeProviders) + MeasureGenerationStep("Generation.ProcessTypeBackCompatibility", () => { - // Ensure back-compatibility processing is done after all visitors have run - outputType.ProcessTypeForBackCompatibility(); + foreach (var outputType in output.TypeProviders) + { + // Ensure back-compatibility processing is done after all visitors have run + outputType.ProcessTypeForBackCompatibility(); + } + }); - var writer = CodeModelGenerator.Instance.GetWriter(outputType); - generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap) + { + MeasureGenerationStep("Generation.ApplyPreWriteAccessibility", () => generatedCodeWorkspace.ApplyPreWriteAccessibility(output.TypeProviders)); + } - foreach (var serialization in outputType.SerializationProviders) + MeasureGenerationStep("Generation.WriteTypeProviders", () => + { + foreach (var outputType in output.TypeProviders) { - writer = CodeModelGenerator.Instance.GetWriter(serialization); + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && !ProviderReferenceMapAnalyzer.ShouldWriteProvider(outputType)) + { + continue; + } + + if (outputType is ModelFactoryProvider && outputType.Methods.Count == 0) + { + continue; + } + + var writer = CodeModelGenerator.Instance.GetWriter(outputType); generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + + foreach (var serialization in outputType.SerializationProviders) + { + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap && !ProviderReferenceMapAnalyzer.ShouldWriteProvider(serialization)) + { + continue; + } + + writer = CodeModelGenerator.Instance.GetWriter(serialization); + generateFilesTasks.Add(generatedCodeWorkspace.AddGeneratedFile(writer.Write())); + } } - } + }); // Add all the generated files to the workspace - await Task.WhenAll(generateFilesTasks); + await MeasureGenerationStepAsync("Generation.AddGeneratedFilesToWorkspace", () => Task.WhenAll(generateFilesTasks)); + + if (ProviderReferenceMapShadowAnalyzer.UseShadowMap) + { + MeasureGenerationStep("Generation.ProviderReferenceMapAnalysis", () => generatedCodeWorkspace.AnalyzeProviderReferenceMap(output.TypeProviders)); + ProviderReferenceMapAnalyzer.RestorePreWriteModelFactoryMethods(); + } LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); // Delete any old generated files - DeleteDirectory(generatedSourceOutputPath, _filesToKeep); + MeasureGenerationStep("Generation.DeleteOldGeneratedFiles", () => DeleteDirectory(generatedSourceOutputPath, GetFilesToKeep())); LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); - await generatedCodeWorkspace.PostProcessAsync(); + await MeasureGenerationStepAsync("Generation.PostProcessAsync", generatedCodeWorkspace.PostProcessAsync); + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); // Write the generated files to the output directory - await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) + await MeasureGenerationStepAsync("Generation.WriteGeneratedFilesToDisk", async () => { - if (string.IsNullOrEmpty(file.Text)) + var generatedFiles = new List<(string Name, string Text)>(); + await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) { - continue; + if (string.IsNullOrEmpty(file.Text)) + { + continue; + } + + generatedFiles.Add((file.Name, file.Text)); } - var filename = Path.Combine(outputPath, file.Name); - CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); - Directory.CreateDirectory(Path.GetDirectoryName(filename)!); - await File.WriteAllTextAsync(filename, file.Text); - } + + foreach (var file in generatedFiles) + { + var filename = Path.Combine(outputPath, file.Name); + CodeModelGenerator.Instance.Emitter.Info($"Writing {Path.GetFullPath(filename)}"); + Directory.CreateDirectory(Path.GetDirectoryName(filename)!); + await File.WriteAllTextAsync(filename, file.Text); + } + }); // Write additional output files (e.g. configuration schemas, .targets files) - await CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath); + await MeasureGenerationStepAsync("Generation.WriteAdditionalFiles", () => CodeModelGenerator.Instance.WriteAdditionalFiles(outputPath)); // Write project scaffolding files (after additional files so schema existence can be checked) if (CodeModelGenerator.Instance.IsNewProject) { - await CodeModelGenerator.Instance.TypeFactory.CreateNewProjectScaffolding().Execute(); + await MeasureGenerationStepAsync( + "Generation.WriteProjectScaffolding", + () => CodeModelGenerator.Instance.TypeFactory.CreateNewProjectScaffolding().Execute()); } LoggingHelpers.LogElapsedTime("All files have been written to disk"); } + private static void MeasureGenerationStep(string stepName, Action action) + { + MeasureGenerationStep( + stepName, + () => + { + action(); + return 0; + }); + } + + private static T MeasureGenerationStep(string stepName, Func action) + { + var profile = GenerationProfile; + if (profile == null) + { + return action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + + private static Task MeasureGenerationStepAsync(string stepName, Func action) => MeasureGenerationStepAsync( + stepName, + async () => + { + await action(); + return 0; + }); + + private static async Task MeasureGenerationStepAsync(string stepName, Func> action) + { + var profile = GenerationProfile; + if (profile == null) + { + return await action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return await action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + internal static void FilterAllCustomizedMembers(OutputLibrary output) { foreach (var typeProvider in output.TypeProviders) @@ -163,6 +298,30 @@ private static void FilterCustomizedMembers(TypeProvider typeProvider) typeProvider.Fields); } + private static string[] GetFilesToKeep() + { + return _filesToKeep + .Concat( + CodeModelGenerator.Instance.AdditionalRootTypes + .Concat(CodeModelGenerator.Instance.NonRootTypes) + .Select(GetFileNameForType)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + + private static string GetFileNameForType(string typeName) + { + var simpleNameStart = typeName.LastIndexOf('.') + 1; + var simpleName = simpleNameStart > 0 ? typeName[simpleNameStart..] : typeName; + var genericArityStart = simpleName.IndexOf('`'); + if (genericArityStart >= 0) + { + simpleName = simpleName[..genericArityStart]; + } + + return simpleName.EndsWith(".cs", StringComparison.Ordinal) ? simpleName : $"{simpleName}.cs"; + } + /// /// Clears the output directory specified by . If is not null, /// the specified files in the output directory will not be deleted. @@ -177,9 +336,10 @@ private static void DeleteDirectory(string path, string[] filesToKeep) return; } + var fileNamesToKeep = filesToKeep.ToHashSet(StringComparer.Ordinal); foreach (var file in directoryInfo.GetFiles("*", SearchOption.AllDirectories)) { - if (!filesToKeep.Contains(file.Name)) + if (!ShouldKeepGeneratedFile(file, fileNamesToKeep)) { file.Delete(); } @@ -203,5 +363,10 @@ private static void DeleteDirectory(string path, string[] filesToKeep) directoryInfo.Delete(); } } + + private static bool ShouldKeepGeneratedFile(FileInfo file, HashSet fileNamesToKeep) + => fileNamesToKeep.Contains(file.Name) || + file.Extension.Equals(".cs", StringComparison.Ordinal) && + file.Name.EndsWith("Extensions.cs", StringComparison.Ordinal); } } 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..843e2e10db2 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 @@ -10,7 +10,9 @@ using System.Threading.Tasks; using Microsoft.Build.Construction; using Microsoft.CodeAnalysis; +using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Simplification; using Microsoft.TypeSpec.Generator.Primitives; @@ -18,7 +20,6 @@ using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Utilities; using NuGet.Configuration; -using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; namespace Microsoft.TypeSpec.Generator { @@ -38,6 +39,8 @@ internal class GeneratedCodeWorkspace private static readonly Lazy _metadataReferenceResolver = new(() => new WorkspaceMetadataReferenceResolver()); private static Task? _cachedProject; + internal static GeneratedCodeWorkspacePostProcessingProfile? PostProcessingProfile { get; set; } + private static readonly string[] _generatedFolders = [GeneratedFolder]; private static readonly string[] _sharedFolders = [SharedFolder]; @@ -84,6 +87,16 @@ public async Task AddInMemoryFile(TypeProvider type) await UpdateProject(document); } + internal void AnalyzeProviderReferenceMap(IReadOnlyList providers) + { + ProviderReferenceMapAnalyzer.Analyze(providers); + } + + internal void ApplyPreWriteAccessibility(IReadOnlyList providers) + { + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers); + } + private async Task UpdateProject(Document document) { var root = await document.GetSyntaxRootAsync(); @@ -103,7 +116,7 @@ internal static SyntaxTree GetTree(TypeProvider provider) public async IAsyncEnumerable<(string Name, string Text)> GetGeneratedFilesAsync() { - List> documents = new List>(); + List docs = new List(); var memberRemover = new MemberRemoverRewriter(); foreach (Document document in _project.Documents) { @@ -112,9 +125,12 @@ internal static SyntaxTree GetTree(TypeProvider provider) continue; } - documents.Add(ProcessDocument(document, memberRemover)); + docs.Add(document); } - var docs = await Task.WhenAll(documents); + + docs = PostProcessingProfile == null + ? [.. await Task.WhenAll(docs.Select(document => ProcessDocument(document, memberRemover)))] + : await ProcessDocumentsSequentiallyAsync(docs, memberRemover); LoggingHelpers.LogElapsedTime("Roslyn post processing complete"); @@ -130,36 +146,101 @@ internal static SyntaxTree GetTree(TypeProvider provider) } } - private async Task ProcessDocument(Document document, MemberRemoverRewriter memberRemover) + private async Task> ProcessDocumentsSequentiallyAsync(List documents, MemberRemoverRewriter memberRemover) { - var root = await document.GetSyntaxRootAsync(); - var semanticModel = await document.GetSemanticModelAsync(); + List processedDocuments = new(documents.Count); + foreach (var document in documents) + { + processedDocuments.Add(await ProcessDocument(document, memberRemover)); + } - if (semanticModel == null || root == null) + return processedDocuments; + } + + private async Task ProcessDocument(Document document, MemberRemoverRewriter memberRemover) + { + var totalStopwatch = PostProcessingProfile == null ? null : Stopwatch.StartNew(); + try { + var root = await MeasurePostProcessingStepAsync("GetSyntaxRootAsync", () => document.GetSyntaxRootAsync()); + var semanticModel = await MeasurePostProcessingStepAsync("GetSemanticModelAsync", () => document.GetSemanticModelAsync()); + + if (semanticModel == null || root == null) + { + return document; + } + + root = MeasurePostProcessingStep("MemberRemoverRewriter", () => memberRemover.Visit(root)); + + foreach (var rewriter in CodeModelGenerator.Instance.Rewriters) + { + rewriter.SemanticModel = semanticModel; + root = MeasurePostProcessingStep($"CustomRewriter.{rewriter.GetType().Name}", () => rewriter.Visit(root)); + } + document = document.WithSyntaxRoot(root); + + if (!CodeModelGenerator.Instance.Configuration.DisableRoslynReduce) + { + document = await MeasurePostProcessingStepAsync("Roslyn.Simplifier.ReduceAsync", () => Simplifier.ReduceAsync(document)); + } + + // Reformat if any custom rewriters have been applied + if (CodeModelGenerator.Instance.Rewriters.Count > 0) + { + document = await MeasurePostProcessingStepAsync("Formatter.FormatAsync", () => Formatter.FormatAsync(document)); + } return document; } + finally + { + if (totalStopwatch != null) + { + totalStopwatch.Stop(); + PostProcessingProfile?.Add("ProcessDocument.Total", totalStopwatch.Elapsed, 0); + } + } + } - root = memberRemover.Visit(root); + private static T MeasurePostProcessingStep(string stepName, Func action) + { + var profile = PostProcessingProfile; + if (profile == null) + { + return action(); + } - foreach (var rewriter in CodeModelGenerator.Instance.Rewriters) + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try { - rewriter.SemanticModel = semanticModel; - root = rewriter.Visit(root); + return action(); } - document = document.WithSyntaxRoot(root); + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } - if (!CodeModelGenerator.Instance.Configuration.DisableRoslynReduce) + private static async Task MeasurePostProcessingStepAsync(string stepName, Func> action) + { + var profile = PostProcessingProfile; + if (profile == null) { - document = await Simplifier.ReduceAsync(document); + return await action(); } - // Reformat if any custom rewriters have been applied - if (CodeModelGenerator.Instance.Rewriters.Count > 0) + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try { - document = await Formatter.FormatAsync(document); + return await action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); } - return document; } public static bool IsGeneratedDocument(Document document) => document.Folders.Contains(GeneratedFolder); @@ -278,11 +359,17 @@ public async Task PostProcessAsync() case Configuration.UnreferencedTypesHandlingOption.KeepAll: break; case Configuration.UnreferencedTypesHandlingOption.Internalize: - _project = await postProcessor.InternalizeAsync(_project); + if (!ProviderReferenceMapAnalyzer.PreWriteAccessibilityApplied) + { + _project = await MeasurePostProcessingStepAsync("PostProcess.InternalizeAsync", () => postProcessor.InternalizeAsync(_project)); + } break; case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: - _project = await postProcessor.InternalizeAsync(_project); - _project = await postProcessor.RemoveAsync(_project); + if (!ProviderReferenceMapAnalyzer.PreWriteAccessibilityApplied) + { + _project = await MeasurePostProcessingStepAsync("PostProcess.InternalizeAsync", () => postProcessor.InternalizeAsync(_project)); + } + _project = await MeasurePostProcessingStepAsync("PostProcess.RemoveAsync", () => postProcessor.RemoveAsync(_project)); break; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs new file mode 100644 index 00000000000..74a9a58168a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspacePostProcessingProfile.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.TypeSpec.Generator +{ + internal sealed class GeneratedCodeWorkspacePostProcessingProfile + { + private readonly object _syncRoot = new(); + private readonly Dictionary _steps = new(StringComparer.Ordinal); + + public void Add(string stepName, TimeSpan elapsed, long allocatedBytes) + { + lock (_syncRoot) + { + ref var summary = ref CollectionsMarshal.GetValueRefOrAddDefault(_steps, stepName, out _); + summary.Count++; + summary.ElapsedTicks += elapsed.Ticks; + summary.AllocatedBytes += allocatedBytes; + } + } + + public string GetSummary() + { + KeyValuePair[] steps; + lock (_syncRoot) + { + steps = _steps.ToArray(); + } + + var totalTicks = steps + .Where(static step => step.Key != "ProcessDocument.Total") + .Sum(static step => step.Value.ElapsedTicks); + var builder = new StringBuilder(); + builder.AppendLine("Post-processing step profile:"); + builder.AppendLine("Step, Count, Total ms, Avg ms, Percent of measured steps, Allocated bytes, Avg allocated bytes"); + + foreach (var step in steps.OrderByDescending(static step => step.Value.ElapsedTicks)) + { + var elapsedMs = TimeSpan.FromTicks(step.Value.ElapsedTicks).TotalMilliseconds; + var averageMs = elapsedMs / step.Value.Count; + var averageAllocatedBytes = step.Value.AllocatedBytes / step.Value.Count; + var percentage = totalTicks == 0 || step.Key == "ProcessDocument.Total" + ? 0 + : step.Value.ElapsedTicks * 100.0 / totalTicks; + builder.AppendLine($"{step.Key}, {step.Value.Count}, {elapsedMs:F3}, {averageMs:F3}, {percentage:F1}%, {step.Value.AllocatedBytes}, {averageAllocatedBytes}"); + } + + return builder.ToString(); + } + + private struct StepSummary + { + public int Count; + public long ElapsedTicks; + public long AllocatedBytes; + } + } +} 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..48731ffeabd 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 @@ -9,7 +9,9 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.TypeSpec.Generator { @@ -20,6 +22,8 @@ internal class PostProcessor private readonly HashSet _typesToKeep; private INamedTypeSymbol? _modelFactorySymbol; + private static GeneratedCodeWorkspacePostProcessingProfile? Profile => GeneratedCodeWorkspace.PostProcessingProfile; + public PostProcessor( HashSet typesToKeep, string? modelFactoryFullName = null, @@ -125,46 +129,126 @@ protected virtual bool ShouldIncludeDocument(Document document) => /// The processed . is immutable, therefore this should usually be a new instance public async Task InternalizeAsync(Project project) { - var compilation = await project.GetCompilationAsync(); + var compilation = await MeasureAsync("PostProcessor.Internalize.GetCompilationAsync", () => project.GetCompilationAsync()); if (compilation == null) { return project; } + var referenceMapResult = ProviderReferenceMapAnalyzer.LatestResult; + // first get all the declared symbols - var definitions = await GetTypeSymbolsAsync(compilation, project, true); - // 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); - - var symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); - - var nodesToInternalize = new Dictionary(); - foreach (var symbol in symbolsToInternalize) - { - foreach (var node in definitions.DeclaredNodesCache[symbol]) + var definitions = await MeasureAsync("PostProcessor.Internalize.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, publicOnly: referenceMapResult == null)); + IEnumerable symbolsToInternalize; + IEnumerable symbolsToPublicize = []; + if (referenceMapResult != null) + { + symbolsToInternalize = Measure("PostProcessor.Internalize.UseShadowCandidates", () => + GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.InternalizeCandidates).ToArray()); + symbolsToPublicize = Measure("PostProcessor.Internalize.UseShadowPublicizeCandidates", () => + GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.PublicizeCandidates).ToArray()); + } + else + { + // build the reference map + var referenceMap = + await MeasureAsync( + "PostProcessor.Internalize.BuildPublicReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildPublicReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DeclaredNodesCache)); + // get the root symbols + var rootSymbols = await MeasureAsync("PostProcessor.Internalize.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); + // traverse all the root and recursively add all the things we met + var publicSymbols = Measure("PostProcessor.Internalize.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray()); + + symbolsToInternalize = definitions.DeclaredSymbols.Except(publicSymbols); + } + + var nodesToInternalize = Measure("PostProcessor.Internalize.CollectNodes", () => + { + var nodes = new Dictionary(); + foreach (var symbol in symbolsToInternalize) { - nodesToInternalize[node] = project.GetDocumentId(node.SyntaxTree)!; + foreach (var node in definitions.DeclaredNodesCache[symbol]) + { + nodes[node] = project.GetDocumentId(node.SyntaxTree)!; + } } - } - foreach (var (model, documentId) in nodesToInternalize) + return nodes; + }); + + var nodesToPublicize = Measure("PostProcessor.Internalize.CollectPublicizeNodes", () => { - project = MarkInternal(project, model, documentId); + var nodes = new Dictionary(); + foreach (var symbol in symbolsToPublicize) + { + foreach (var node in definitions.DeclaredNodesCache[symbol]) + { + nodes[node] = project.GetDocumentId(node.SyntaxTree)!; + } + } + + return nodes; + }); + + project = Measure("PostProcessor.Internalize.ApplyAccessibilityChanges", () => + ApplyAccessibilityChanges(project, nodesToInternalize, nodesToPublicize)); + project = await MeasureAsync( + "PostProcessor.Internalize.InternalizePublicNestedTypesInInternalTypesAsync", + () => InternalizePublicNestedTypesInInternalTypesAsync(project)); + + var modelNamesToRemove = nodesToInternalize.Keys.Select(item => item.Identifier.Text); + if (referenceMapResult != null) + { + modelNamesToRemove = modelNamesToRemove.Concat(referenceMapResult.RemoveCandidates.Select(GetSimpleName)); } + project = await MeasureAsync( + "PostProcessor.Internalize.RemoveMethodsFromModelFactoryAsync", + () => RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet())); + + return project; + } + + private static async Task InternalizePublicNestedTypesInInternalTypesAsync(Project project) + { + foreach (var document in project.Documents.ToArray()) + { + if (!GeneratedCodeWorkspace.IsGeneratedDocument(document)) + { + continue; + } - var modelNamesToRemove = - nodesToInternalize.Keys.Select(item => item.Identifier.Text); - project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + continue; + } + + var nestedPublicTypes = root.DescendantNodes() + .OfType() + .Where(static declaration => declaration.Modifiers.Any(SyntaxKind.PublicKeyword) && + declaration.Ancestors().OfType().Any(static parent => parent.Modifiers.Any(SyntaxKind.InternalKeyword))) + .ToArray(); + if (nestedPublicTypes.Length == 0) + { + continue; + } + + var newRoot = root.ReplaceNodes(nestedPublicTypes, static (originalNode, _) => + ChangeAccessibility(originalNode, SyntaxKind.InternalKeyword)).WithAdditionalAnnotations(Simplifier.Annotation); + project = document.WithSyntaxRoot(newRoot).Project; + } return project; } + private static string GetSimpleName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); + } + private async Task RemoveMethodsFromModelFactoryAsync(Project project, TypeSymbols definitions, HashSet namesToRemove) @@ -238,46 +322,70 @@ private async Task RemoveMethodsFromModelFactoryAsync(Project project, /// The processed . is immutable, therefore this should usually be a new instance public async Task RemoveAsync(Project project) { - var compilation = await project.GetCompilationAsync(); + var compilation = await MeasureAsync("PostProcessor.Remove.GetCompilationAsync", () => project.GetCompilationAsync()); if (compilation == null) { return project; } // find all the declarations, including non-public declared - var definitions = await GetTypeSymbolsAsync(compilation, project, false); - // build reference map - var referenceMap = - await new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( - definitions.DeclaredSymbols, definitions.DocumentsCache); - // get root symbols - var rootSymbols = await GetRootSymbolsAsync(project, definitions); - // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal - // helpers that are required by the model factory. - if (_modelFactorySymbol != null) + var definitions = await MeasureAsync("PostProcessor.Remove.GetTypeSymbolsAsync", () => GetTypeSymbolsAsync(compilation, project, false)); + IEnumerable symbolsToRemove; + HashSet referencedSet; + if (ProviderReferenceMapAnalyzer.LatestResult is { } referenceMapResult) { - rootSymbols.Add(_modelFactorySymbol); + symbolsToRemove = Measure("PostProcessor.Remove.UseShadowCandidates", () => + GetSymbolsByName(definitions.DeclaredSymbols, referenceMapResult.RemoveCandidates).ToArray()); + referencedSet = Measure("PostProcessor.Remove.BuildShadowReferencedSet", () => + new HashSet(definitions.DeclaredSymbols.Except(symbolsToRemove), SymbolEqualityComparer.Default)); } - // traverse the map to determine the declarations that we are about to remove, starting from root nodes - var referencedSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); + else + { + // build reference map + var referenceMap = + await MeasureAsync( + "PostProcessor.Remove.BuildAllReferenceMapAsync", + () => new ReferenceMapBuilder(compilation, project).BuildAllReferenceMapAsync( + definitions.DeclaredSymbols, definitions.DocumentsCache)); + // get root symbols + var rootSymbols = await MeasureAsync("PostProcessor.Remove.GetRootSymbolsAsync", () => GetRootSymbolsAsync(project, definitions)); + // include model factory as a root symbol when doing the remove pass so that we are sure to include any internal + // helpers that are required by the model factory. + if (_modelFactorySymbol != null) + { + rootSymbols.Add(_modelFactorySymbol); + } + // traverse the map to determine the declarations that we are about to remove, starting from root nodes + var referencedSymbols = Measure("PostProcessor.Remove.VisitSymbolsFromRoot", () => VisitSymbolsFromRootAsync(rootSymbols, referenceMap).ToArray().AsEnumerable()); - referencedSymbols = AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols); - var referencedSet = new HashSet(referencedSymbols, SymbolEqualityComparer.Default); + referencedSymbols = Measure("PostProcessor.Remove.AddSampleSymbols", () => AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols)); + referencedSet = Measure("PostProcessor.Remove.BuildReferencedSet", () => new HashSet(referencedSymbols, SymbolEqualityComparer.Default)); - var symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); + symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); + } - var nodesToRemove = new List(); - foreach (var symbol in symbolsToRemove) + var nodesToRemove = Measure("PostProcessor.Remove.CollectNodes", () => { - if (referencedSet.Contains(GetBase(symbol))) + var nodes = new List(); + foreach (var symbol in symbolsToRemove) { - continue; + if (referencedSet.Contains(GetBase(symbol))) + { + continue; + } + nodes.AddRange(definitions.DeclaredNodesCache[symbol]); } - nodesToRemove.AddRange(definitions.DeclaredNodesCache[symbol]); - } + + return nodes; + }); // remove them one by one - project = await RemoveModelsAsync(project, nodesToRemove); + var referencedTypeNames = referencedSet + .Select(static symbol => symbol.GetFullyQualifiedName()) + .ToHashSet(StringComparer.Ordinal); + project = await MeasureAsync( + "PostProcessor.Remove.RemoveModelsAsync", + () => RemoveModelsAsync(project, nodesToRemove, referencedTypeNames)); return project; } @@ -289,7 +397,6 @@ private INamedTypeSymbol GetBase(INamedTypeSymbol symbol) { return symbol; } - return GetBase(baseType); } @@ -329,7 +436,6 @@ private static IEnumerable VisitSymbolsFromRootAsync( { continue; } - visited.Add(definition); // add this definition to the result yield return definition; @@ -352,42 +458,103 @@ private static IEnumerable GetReferencedTypes(T definition, return Enumerable.Empty(); } - private Project MarkInternal(Project project, BaseTypeDeclarationSyntax declarationNode, DocumentId documentId) + private static IEnumerable GetSymbolsByName(IEnumerable symbols, HashSet names) { - var newNode = ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); - var tree = declarationNode.SyntaxTree; - var document = project.GetDocument(documentId)!; - var newRoot = tree.GetRoot().ReplaceNode(declarationNode, newNode) - .WithAdditionalAnnotations(Simplifier.Annotation); - document = document.WithSyntaxRoot(newRoot); - return document.Project; + foreach (var symbol in symbols) + { + if (names.Contains(symbol.GetFullyQualifiedName())) + { + yield return symbol; + } + } } - private async Task RemoveModelsAsync(Project project, - IEnumerable unusedModels) + private Project ApplyAccessibilityChanges( + Project project, + IReadOnlyDictionary nodesToInternalize, + IReadOnlyDictionary nodesToPublicize) { - // accumulate the definitions from the same document together - var documents = new Dictionary>(); + var changesByDocument = new Dictionary>(); + AddAccessibilityChanges(changesByDocument, nodesToInternalize, SyntaxKind.InternalKeyword); + AddAccessibilityChanges(changesByDocument, nodesToPublicize, SyntaxKind.PublicKeyword); - foreach (var model in unusedModels) + foreach (var (documentId, changes) in changesByDocument) + { + var document = project.GetDocument(documentId)!; + var root = changes.Keys.First().SyntaxTree.GetRoot(); + var newRoot = root.ReplaceNodes( + changes.Keys, + (originalNode, _) => changes.TryGetValue(originalNode, out var targetAccessibility) + ? ChangeAccessibility(originalNode, targetAccessibility) + : originalNode) + .WithAdditionalAnnotations(Simplifier.Annotation); + document = document.WithSyntaxRoot(newRoot); + project = document.Project; + } + + return project; + } + + private static void AddAccessibilityChanges( + Dictionary> changesByDocument, + IReadOnlyDictionary nodes, + SyntaxKind targetAccessibility) + { + foreach (var (node, documentId) in nodes) { - var document = project.GetDocument(model.SyntaxTree); - Debug.Assert(document != null); - if (!documents.ContainsKey(document)) + if (!changesByDocument.TryGetValue(documentId, out var changes)) { - documents.Add(document, new HashSet()); + changes = new Dictionary(); + changesByDocument[documentId] = changes; } - documents[document].Add(model); + changes[node] = targetAccessibility; } + } + + private static BaseTypeDeclarationSyntax ChangeAccessibility(BaseTypeDeclarationSyntax declarationNode, SyntaxKind targetAccessibility) + { + return targetAccessibility == SyntaxKind.PublicKeyword + ? ChangeModifier(declarationNode, SyntaxKind.InternalKeyword, SyntaxKind.PublicKeyword) + : ChangeModifier(declarationNode, SyntaxKind.PublicKeyword, SyntaxKind.InternalKeyword); + } - foreach (var models in documents.Values) + private async Task RemoveModelsAsync(Project project, + IEnumerable unusedModels, + HashSet referencedTypeNames) + { + // accumulate the definitions from the same document together + var documents = Measure("PostProcessor.Remove.RemoveModelsAsync.GroupByDocument", () => { - project = await RemoveModelsFromDocumentAsync(project, models); - } + var groupedDocuments = new Dictionary>(); + foreach (var model in unusedModels) + { + var document = project.GetDocument(model.SyntaxTree); + Debug.Assert(document != null); + if (!groupedDocuments.ContainsKey(document)) + groupedDocuments.Add(document, new HashSet()); + + groupedDocuments[document].Add(model); + } + + return groupedDocuments; + }); + + project = await MeasureAsync("PostProcessor.Remove.RemoveModelsAsync.RemoveModelsFromDocuments", async () => + { + var updatedProject = project; + foreach (var models in documents.Values) + { + updatedProject = await RemoveModelsFromDocumentAsync(updatedProject, models); + } + + return updatedProject; + }); // remove what are now invalid references due to the models being removed - project = await RemoveInvalidRefs(project); + project = await MeasureAsync( + "PostProcessor.Remove.RemoveModelsAsync.RemoveInvalidRefs", + () => RemoveInvalidRefs(project, referencedTypeNames)); return project; } @@ -419,7 +586,6 @@ private async Task RemoveModelsFromDocumentAsync(Project project, { return project; } - var root = await tree.GetRootAsync(); root = root.RemoveNodes(models, SyntaxRemoveOptions.KeepNoTrivia); @@ -438,25 +604,59 @@ private async Task RemoveModelsFromDocumentAsync(Project project, return document.Project; } - private async Task RemoveInvalidRefs(Project project) + private async Task RemoveInvalidRefs(Project project, HashSet referencedTypeNames) { var solution = project.Solution; // Process each document for invalid usings - foreach (var documentId in project.DocumentIds) + solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidUsings", async () => { - solution = await RemoveInvalidUsings(solution, documentId); - } + var updatedSolution = solution; + foreach (var documentId in project.DocumentIds) + { + if (IsModelFactoryDocument(project.GetDocument(documentId))) + { + continue; + } + + updatedSolution = await RemoveInvalidUsings(updatedSolution, documentId); + } + + return updatedSolution; + }); // Process each document for invalid attributes (with fresh semantic models) - foreach (var documentId in project.DocumentIds) + solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidAttributes", async () => { - solution = await RemoveInvalidAttributes(solution, documentId); - } + var updatedSolution = solution; + foreach (var documentId in project.DocumentIds) + { + updatedSolution = await RemoveInvalidAttributes(updatedSolution, documentId, referencedTypeNames); + } + + return updatedSolution; + }); + + // Process each document for invalid XML documentation references (with fresh semantic models) + solution = await MeasureAsync("PostProcessor.Remove.RemoveInvalidRefs.RemoveInvalidXmlDocReferences", async () => + { + var updatedSolution = solution; + foreach (var documentId in project.DocumentIds) + { + updatedSolution = await RemoveInvalidXmlDocReferences(updatedSolution, documentId); + } + + return updatedSolution; + }); return solution.GetProject(project.Id)!; } + private bool IsModelFactoryDocument(Document? document) + => document != null && + _modelFactoryFullName != null && + document.Name.EndsWith("ModelFactory.cs", StringComparison.Ordinal); + private async Task RemoveInvalidUsings(Solution solution, DocumentId documentId) { var document = solution.GetDocument(documentId)!; @@ -486,7 +686,7 @@ private async Task RemoveInvalidUsings(Solution solution, DocumentId d return solution; } - private async Task RemoveInvalidAttributes(Solution solution, DocumentId documentId) + private async Task RemoveInvalidAttributes(Solution solution, DocumentId documentId, HashSet referencedTypeNames) { var document = solution.GetDocument(documentId)!; var root = await document.GetSyntaxRootAsync(); @@ -507,6 +707,15 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && model.GetTypeInfo(typeOfExpr.Type).Type?.TypeKind == TypeKind.Error) == true)) .ToHashSet(); + foreach (var attr in attributes) + { + if (IsInternalRecordBuildableAttribute(attr) || + await ShouldRemoveUnreferencedInternalBuildableAttribute(solution, model, attr, referencedTypeNames)) + { + invalidAttributes.Add(attr); + } + } + if (invalidAttributes.Count > 0) { cu = cu.RemoveNodes(invalidAttributes, SyntaxRemoveOptions.KeepNoTrivia)!; @@ -562,6 +771,200 @@ arg.Expression is TypeOfExpressionSyntax typeOfExpr && return solution; } + private static bool IsInternalRecordBuildableAttribute(AttributeListSyntax attributeList) + { + if (attributeList.Attributes.Count != 1 || + !IsModelReaderWriterBuildableAttribute(attributeList.Attributes[0])) + { + return false; + } + + var typeName = attributeList.Attributes[0].ArgumentList?.Arguments + .Select(static argument => argument.Expression) + .OfType() + .Select(static typeOfExpression => typeOfExpression.Type.ToString().Split('.').Last()) + .FirstOrDefault(); + + return typeName?.StartsWith("Update", StringComparison.Ordinal) == true && typeName.EndsWith("Record", StringComparison.Ordinal) || + typeName?.EndsWith("PatchUpdate", StringComparison.Ordinal) == true; + } + + private static async Task ShouldRemoveUnreferencedInternalBuildableAttribute( + Solution solution, + SemanticModel model, + AttributeListSyntax attributeList, + HashSet referencedTypeNames) + { + if (attributeList.Attributes.Count != 1) + { + return false; + } + + var attribute = attributeList.Attributes[0]; + if (model.GetSymbolInfo(attribute).Symbol?.ContainingType.Name != "ModelReaderWriterBuildableAttribute" && + !IsModelReaderWriterBuildableAttribute(attribute)) + { + return false; + } + + var typeOfExpression = attribute.ArgumentList?.Arguments + .Select(static argument => argument.Expression) + .OfType() + .FirstOrDefault(); + if (typeOfExpression == null || + model.GetTypeInfo(typeOfExpression.Type).Type is not INamedTypeSymbol typeSymbol || + typeSymbol.DeclaredAccessibility != Accessibility.Internal) + { + return false; + } + + if (typeSymbol.BaseType is { SpecialType: not SpecialType.System_Object }) + { + return false; + } + + if (typeSymbol.Name.EndsWith("PatchUpdate", StringComparison.Ordinal) || + typeSymbol.Name.StartsWith("Update", StringComparison.Ordinal) && typeSymbol.Name.EndsWith("Record", StringComparison.Ordinal)) + { + return true; + } + + if (referencedTypeNames.Contains(typeSymbol.GetFullyQualifiedName())) + { + return false; + } + + foreach (var referencedSymbol in await SymbolFinder.FindReferencesAsync(typeSymbol, solution)) + { + foreach (var location in referencedSymbol.Locations) + { + if (!location.Location.IsInSource) + { + continue; + } + + var document = location.Document; + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + continue; + } + + var node = root.FindNode(location.Location.SourceSpan); + if (node.AncestorsAndSelf().OfType().Any()) + { + continue; + } + + if (IsWithinTypeDeclaration(typeSymbol, node)) + { + continue; + } + + return false; + } + } + + return true; + } + + private static bool IsModelReaderWriterBuildableAttribute(AttributeSyntax attribute) + { + var name = attribute.Name.ToString(); + return name.EndsWith("ModelReaderWriterBuildable", StringComparison.Ordinal) || + name.EndsWith("ModelReaderWriterBuildableAttribute", StringComparison.Ordinal) || + name.Contains(".ModelReaderWriterBuildableAttribute", StringComparison.Ordinal); + } + + private static bool IsWithinTypeDeclaration(INamedTypeSymbol typeSymbol, SyntaxNode node) + { + foreach (var syntaxReference in typeSymbol.DeclaringSyntaxReferences) + { + if (syntaxReference.SyntaxTree == node.SyntaxTree && syntaxReference.Span.Contains(node.Span)) + { + return true; + } + } + return false; + } + + private static T Measure(string stepName, Func action) + { + var profile = Profile; + if (profile == null) + { + return action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + + private static async Task MeasureAsync(string stepName, Func> action) + { + var profile = Profile; + if (profile == null) + { + return await action(); + } + + var allocatedBytes = GC.GetTotalAllocatedBytes(precise: false); + var stopwatch = Stopwatch.StartNew(); + try + { + return await action(); + } + finally + { + stopwatch.Stop(); + profile.Add(stepName, stopwatch.Elapsed, GC.GetTotalAllocatedBytes(precise: false) - allocatedBytes); + } + } + + private async Task RemoveInvalidXmlDocReferences(Solution solution, DocumentId documentId) + { + var document = solution.GetDocument(documentId)!; + var root = await document.GetSyntaxRootAsync(); + var model = await document.GetSemanticModelAsync(); + + if (root == null || model == null) + { + return solution; + } + + var invalidSeeElements = root.DescendantTrivia(descendIntoTrivia: true) + .SelectMany(static trivia => trivia.GetStructure()?.DescendantNodes().OfType() ?? []) + .Where(element => string.Equals(element.Name.LocalName.ValueText, "see", StringComparison.Ordinal)) + .Where(element => element.Attributes.OfType().Any(attribute => model.GetSymbolInfo(attribute.Cref).Symbol == null)) + .ToArray(); + + if (invalidSeeElements.Length == 0) + { + return solution; + } + + var text = await document.GetTextAsync(); + var source = text.ToString(); + foreach (var element in invalidSeeElements) + { + var cref = element.Attributes.OfType().First().Cref.ToString(); + var colonIndex = cref.IndexOf(':'); + var replacement = colonIndex >= 0 ? cref.Substring(colonIndex + 1) : cref; + source = source.Replace(element.ToFullString(), replacement, StringComparison.Ordinal); + } + + return solution.WithDocumentText(documentId, SourceText.From(source, text.Encoding)); + } + private async Task> GetRootSymbolsAsync(Project project, TypeSymbols modelSymbols) { var result = new HashSet(SymbolEqualityComparer.Default); @@ -574,7 +977,6 @@ private async Task> GetRootSymbolsAsync(Project projec { continue; } - if (await IsRootDocument(document)) { result.Add(symbol); @@ -660,7 +1062,6 @@ private static void AddInList(Dictionary dicti { newList = collectionConstructor(); } - newList.Add(value); dictionary.Add(key, newList); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs new file mode 100644 index 00000000000..e5b1ae9eae0 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowAnalyzer.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.TypeSpec.Generator +{ + internal static class ProviderReferenceMapShadowAnalyzer + { + private const string UseShadowEnvironmentVariable = "TYPESPEC_PROVIDER_REFERENCE_MAP_USE_SHADOW"; + + public static bool UseShadowMap => string.Equals( + Environment.GetEnvironmentVariable(UseShadowEnvironmentVariable), + "true", + StringComparison.OrdinalIgnoreCase); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs new file mode 100644 index 00000000000..6750579e24c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ProviderReferenceMapShadowResult.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.TypeSpec.Generator +{ + internal sealed record ProviderReferenceMapShadowResult( + ProjectId ProjectId, + HashSet InternalizeCandidates, + HashSet PublicizeCandidates, + HashSet RemoveCandidates) + { + } +} 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..cf2921b951e 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 @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; @@ -30,6 +33,8 @@ public virtual CodeFile Write() WriteType(writer); } + PreserveExistingModelFactoryUsings(writer); + foreach (var suppression in _provider.DisabledFileWarnings) { suppression.RestoreStatement.Write(writer); @@ -37,6 +42,50 @@ public virtual CodeFile Write() return new CodeFile(writer.ToString(), _provider.RelativeFilePath); } + private void PreserveExistingModelFactoryUsings(CodeWriter writer) + { + if (_provider is not ModelFactoryProvider) + { + return; + } + + var existingFilePath = Path.Combine(CodeModelGenerator.Instance.Configuration.OutputDirectory, _provider.RelativeFilePath); + if (!File.Exists(existingFilePath)) + { + return; + } + + foreach (var @namespace in ReadTopLevelUsingNamespaces(existingFilePath)) + { + writer.UseNamespace(@namespace); + } + } + + private static IEnumerable ReadTopLevelUsingNamespaces(string filePath) + { + foreach (var line in File.ReadLines(filePath)) + { + var trimmedLine = line.Trim(); + if (trimmedLine.Length == 0 || + trimmedLine.StartsWith("//", StringComparison.Ordinal) || + trimmedLine.StartsWith("#", StringComparison.Ordinal)) + { + continue; + } + + if (trimmedLine.StartsWith("using ", StringComparison.Ordinal) && + trimmedLine.EndsWith(";", StringComparison.Ordinal) && + !trimmedLine.StartsWith("using static ", StringComparison.Ordinal) && + !trimmedLine.Contains('=')) + { + yield return trimmedLine.Substring("using ".Length, trimmedLine.Length - "using ".Length - 1); + continue; + } + + break; + } + } + private bool IsPublicContext(TypeProvider provider) { return provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) && @@ -45,7 +94,7 @@ private bool IsPublicContext(TypeProvider provider) private void WriteType(CodeWriter writer) { - if (IsPublicContext(_provider)) + if (_provider.PreserveTypeXmlDocs || _provider.ShouldWriteTypeXmlDocs || IsPublicContext(_provider)) { writer.WriteXmlDocsNoScope(_provider.XmlDocs); } @@ -207,6 +256,11 @@ private void WriteConstructors(CodeWriter writer) private void WriteMethods(CodeWriter writer) { + if (_provider is ModelFactoryProvider { PreserveLeadingMethodSeparator: true } && _provider.Methods.Count > 0) + { + writer.WriteLine(); + } + for (int i = 0; i < _provider.Methods.Count; i++) { writer.WriteMethod(_provider.Methods[i]); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index c30d3a00bff..fef9e4b4c95 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -29,6 +29,10 @@ protected internal ModelFactoryProvider(IEnumerable models) _models = models; } + internal bool PreserveLeadingMethodSeparator { get; private set; } + + internal void PreserveLeadingMethodSeparatorForBackCompat() => PreserveLeadingMethodSeparator = true; + protected override string BuildName() => string.Concat(CodeModelGenerator.Instance.TypeFactory.ServiceName, ModelFactorySuffix); protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs index ed3a45d54e8..637a0def083 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs @@ -22,6 +22,7 @@ internal sealed class NamedTypeSymbolProvider : TypeProvider { private INamedTypeSymbol _namedTypeSymbol; private readonly Compilation _compilation; + private string? _metadataName; private TypeProvider? _baseTypeProvider; public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation) @@ -30,6 +31,23 @@ public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation com _compilation = compilation; } + internal string MetadataName + { + get + { + if (_metadataName != null) + { + return _metadataName; + } + + var ns = _namedTypeSymbol.ContainingNamespace.GetFullyQualifiedNameFromDisplayString(); + _metadataName = string.IsNullOrEmpty(ns) ? _namedTypeSymbol.MetadataName : $"{ns}.{_namedTypeSymbol.MetadataName}"; + return _metadataName; + } + } + + internal string MetadataSimpleName => _namedTypeSymbol.Name; + private protected sealed override NamedTypeSymbolProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null; private protected sealed override TypeProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null; @@ -321,6 +339,359 @@ [.. methodSymbol.Parameters.Select(p => ConvertToParameterProvider(methodSymbol, return [.. methods]; } + protected internal override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new HashSet(); + foreach (var syntaxReference in _namedTypeSymbol.DeclaringSyntaxReferences) + { + var syntax = syntaxReference.GetSyntax(); + if (IsGeneratedSourceFile(syntax.SyntaxTree.FilePath)) + { + continue; + } + + AddBodyDependencyTypes(syntax, dependencies); + } + + return [.. dependencies]; + } + + protected internal override IReadOnlyList BuildSignatureDependencyTypes() + { + var dependencies = new HashSet(); + foreach (var syntaxReference in _namedTypeSymbol.DeclaringSyntaxReferences) + { + var syntax = syntaxReference.GetSyntax(); + if (IsGeneratedSourceFile(syntax.SyntaxTree.FilePath) || + syntax is not TypeDeclarationSyntax typeDeclaration || + !IsPublic(typeDeclaration.Modifiers)) + { + continue; + } + + var semanticModel = _compilation.GetSemanticModel(typeDeclaration.SyntaxTree); + var namespaceCandidates = GetNamespaceCandidates(typeDeclaration); + AddSyntaxTypeReferences(typeDeclaration.BaseList, dependencies, semanticModel, namespaceCandidates); + foreach (var member in typeDeclaration.Members) + { + if (IsPublicApiMember(member)) + { + AddPublicSignatureDependencyTypes(member, dependencies); + } + } + } + + return [.. dependencies]; + } + + private void AddBodyDependencyTypes(SyntaxNode syntax, HashSet dependencies) + { + var semanticModel = _compilation.GetSemanticModel(syntax.SyntaxTree); + AddSyntaxTypeReferences(syntax, dependencies, semanticModel, GetNamespaceCandidates(syntax)); + + foreach (var invocation in syntax.DescendantNodes().OfType()) + { + if (GetInvocationName(invocation) == "SetDelimited") + { + dependencies.Add(CreateUnresolvedDependencyType("SetDelimited")); + } + } + } + + private void AddPublicSignatureDependencyTypes(MemberDeclarationSyntax member, HashSet dependencies) + { + var semanticModel = _compilation.GetSemanticModel(member.SyntaxTree); + var namespaceCandidates = GetNamespaceCandidates(member); + switch (member) + { + case MethodDeclarationSyntax method: + AddSyntaxTypeReferences(method.ReturnType, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(method.ParameterList, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(method.ConstraintClauses, dependencies, semanticModel, namespaceCandidates); + break; + case ConstructorDeclarationSyntax constructor: + AddSyntaxTypeReferences(constructor.ParameterList, dependencies, semanticModel, namespaceCandidates); + break; + case ConversionOperatorDeclarationSyntax conversion: + AddSyntaxTypeReferences(conversion.Type, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(conversion.ParameterList, dependencies, semanticModel, namespaceCandidates); + break; + case OperatorDeclarationSyntax @operator: + AddSyntaxTypeReferences(@operator.ReturnType, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(@operator.ParameterList, dependencies, semanticModel, namespaceCandidates); + break; + case PropertyDeclarationSyntax property: + AddSyntaxTypeReferences(property.Type, dependencies, semanticModel, namespaceCandidates); + break; + case IndexerDeclarationSyntax indexer: + AddSyntaxTypeReferences(indexer.Type, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(indexer.ParameterList, dependencies, semanticModel, namespaceCandidates); + break; + case FieldDeclarationSyntax field: + AddSyntaxTypeReferences(field.Declaration.Type, dependencies, semanticModel, namespaceCandidates); + break; + case EventFieldDeclarationSyntax eventField: + AddSyntaxTypeReferences(eventField.Declaration.Type, dependencies, semanticModel, namespaceCandidates); + break; + case EventDeclarationSyntax @event: + AddSyntaxTypeReferences(@event.Type, dependencies, semanticModel, namespaceCandidates); + break; + case DelegateDeclarationSyntax @delegate: + AddSyntaxTypeReferences(@delegate.ReturnType, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(@delegate.ParameterList, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(@delegate.ConstraintClauses, dependencies, semanticModel, namespaceCandidates); + break; + case BaseTypeDeclarationSyntax type: + AddSyntaxTypeReferences(type.BaseList, dependencies, semanticModel, namespaceCandidates); + break; + } + } + + private static void AddSyntaxTypeReferences(SyntaxNode? node, HashSet dependencies, SemanticModel semanticModel, IReadOnlyList namespaceCandidates) + { + if (node == null) + { + return; + } + + foreach (var type in node.DescendantNodesAndSelf().OfType()) + { + if (type.IsPartOfStructuredTrivia()) + { + continue; + } + + AddSyntaxTypeReference(type, dependencies, semanticModel, namespaceCandidates); + } + } + + private static void AddSyntaxTypeReferences(IEnumerable nodes, HashSet dependencies, SemanticModel semanticModel, IReadOnlyList namespaceCandidates) + { + foreach (var node in nodes) + { + AddSyntaxTypeReferences(node, dependencies, semanticModel, namespaceCandidates); + } + } + + private static bool IsPublicApiMember(MemberDeclarationSyntax member) + => member switch + { + EventDeclarationSyntax @event => IsPublic(@event.Modifiers), + EventFieldDeclarationSyntax @event => IsPublic(@event.Modifiers), + BaseFieldDeclarationSyntax field => IsPublic(field.Modifiers), + BaseMethodDeclarationSyntax method => IsPublic(method.Modifiers), + BasePropertyDeclarationSyntax property => IsPublic(property.Modifiers), + DelegateDeclarationSyntax @delegate => IsPublic(@delegate.Modifiers), + BaseTypeDeclarationSyntax type => IsPublic(type.Modifiers), + _ => false + }; + + private static bool IsPublic(SyntaxTokenList modifiers) + => modifiers.Any(static modifier => + modifier.IsKind(SyntaxKind.PublicKeyword) || + modifier.IsKind(SyntaxKind.ProtectedKeyword)); + + private static bool IsGeneratedSourceFile(string filePath) => + filePath.Contains("/Generated/", StringComparison.Ordinal) || + filePath.Contains("\\Generated\\", StringComparison.Ordinal); + + private static CSharpType CreateUnresolvedDependencyType(string name, int genericArgumentCount = 0) + => new( + name, + string.Empty, + isValueType: false, + isNullable: false, + declaringType: null, + args: [.. Enumerable.Range(0, genericArgumentCount).Select(static _ => CreateUnresolvedDependencyType(string.Empty))], + isPublic: false, + isStruct: false); + + private static void AddSyntaxTypeReference(TypeSyntax type, HashSet dependencies, SemanticModel semanticModel, IReadOnlyList namespaceCandidates) + { + if (TryAddSemanticTypeReference(type, dependencies, semanticModel)) + { + return; + } + + if (!IsSyntacticTypeReference(type)) + { + return; + } + + switch (type) + { + case IdentifierNameSyntax identifier: + AddUnresolvedDependencyType(dependencies, identifier.Identifier.ValueText, namespaceCandidates); + break; + case GenericNameSyntax genericName: + AddUnresolvedDependencyType(dependencies, genericName.Identifier.ValueText, namespaceCandidates, genericName.TypeArgumentList.Arguments.Count); + foreach (var argument in genericName.TypeArgumentList.Arguments) + { + AddSyntaxTypeReference(argument, dependencies, semanticModel, namespaceCandidates); + } + break; + case QualifiedNameSyntax qualifiedName: + AddQualifiedUnresolvedDependencyType(dependencies, qualifiedName); + AddSyntaxTypeReference(qualifiedName.Right, dependencies, semanticModel, namespaceCandidates); + break; + case AliasQualifiedNameSyntax aliasQualifiedName: + AddSyntaxTypeReference(aliasQualifiedName.Name, dependencies, semanticModel, namespaceCandidates); + break; + case ArrayTypeSyntax arrayType: + AddSyntaxTypeReference(arrayType.ElementType, dependencies, semanticModel, namespaceCandidates); + break; + case NullableTypeSyntax nullableType: + AddSyntaxTypeReference(nullableType.ElementType, dependencies, semanticModel, namespaceCandidates); + break; + case PointerTypeSyntax pointerType: + AddSyntaxTypeReference(pointerType.ElementType, dependencies, semanticModel, namespaceCandidates); + break; + case TupleTypeSyntax tupleType: + foreach (var element in tupleType.Elements) + { + AddSyntaxTypeReference(element.Type, dependencies, semanticModel, namespaceCandidates); + } + break; + } + } + + private static bool TryAddSemanticTypeReference(TypeSyntax type, HashSet dependencies, SemanticModel semanticModel) + { + var typeSymbol = semanticModel.GetTypeInfo(type).Type ?? + semanticModel.GetTypeInfo(type).ConvertedType ?? + (semanticModel.GetSymbolInfo(type).Symbol as INamedTypeSymbol); + if (typeSymbol is not INamedTypeSymbol namedTypeSymbol || + namedTypeSymbol.TypeKind == TypeKind.Error || + namedTypeSymbol.SpecialType == SpecialType.System_Void) + { + return false; + } + + dependencies.Add(namedTypeSymbol.GetCSharpType()); + return true; + } + + private static void AddUnresolvedDependencyType(HashSet dependencies, string name, IReadOnlyList namespaceCandidates, int genericArgumentCount = 0) + { + if (string.Equals(name, "var", StringComparison.Ordinal) || + string.Equals(name, "dynamic", StringComparison.Ordinal)) + { + return; + } + + dependencies.Add(CreateUnresolvedDependencyType(name, genericArgumentCount)); + foreach (var ns in namespaceCandidates) + { + dependencies.Add(CreateDependencyType(name, ns, genericArgumentCount)); + } + } + + private static void AddQualifiedUnresolvedDependencyType(HashSet dependencies, QualifiedNameSyntax qualifiedName) + { + var fullName = qualifiedName.ToString(); + var lastDot = fullName.LastIndexOf('.'); + if (lastDot <= 0 || lastDot == fullName.Length - 1) + { + return; + } + + dependencies.Add(CreateDependencyType(fullName.Substring(lastDot + 1), fullName.Substring(0, lastDot))); + } + + private static CSharpType CreateDependencyType(string name, string ns, int genericArgumentCount = 0) + => new( + name, + ns, + isValueType: false, + isNullable: false, + declaringType: null, + args: [.. Enumerable.Range(0, genericArgumentCount).Select(static _ => CreateUnresolvedDependencyType(string.Empty))], + isPublic: false, + isStruct: false); + + private static IReadOnlyList GetNamespaceCandidates(SyntaxNode node) + { + var namespaces = new HashSet(StringComparer.Ordinal); + for (var current = node; current != null; current = current.Parent) + { + switch (current) + { + case BaseNamespaceDeclarationSyntax namespaceDeclaration: + namespaces.Add(namespaceDeclaration.Name.ToString()); + break; + } + } + + if (node.SyntaxTree.GetRoot() is CompilationUnitSyntax compilationUnit) + { + foreach (var usingDirective in compilationUnit.Usings) + { + if (usingDirective.Alias == null && !usingDirective.StaticKeyword.IsKind(SyntaxKind.StaticKeyword) && usingDirective.Name != null) + { + namespaces.Add(usingDirective.Name.ToString()); + } + } + } + + return [.. namespaces]; + } + + private static bool IsSyntacticTypeReference(TypeSyntax type) + { + var parent = type.Parent; + return parent switch + { + ArrayTypeSyntax arrayType => arrayType.ElementType == type && IsSyntacticTypeReference(arrayType), + NullableTypeSyntax nullableType => nullableType.ElementType == type && IsSyntacticTypeReference(nullableType), + PointerTypeSyntax pointerType => pointerType.ElementType == type && IsSyntacticTypeReference(pointerType), + TupleElementSyntax tupleElement => tupleElement.Type == type, + TypeArgumentListSyntax typeArgumentList => typeArgumentList.Arguments.Contains(type), + QualifiedNameSyntax qualifiedName => qualifiedName.Right == type && IsSyntacticTypeReference(qualifiedName), + AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name == type && IsSyntacticTypeReference(aliasQualifiedName), + SimpleBaseTypeSyntax simpleBaseType => simpleBaseType.Type == type, + ParameterSyntax parameter => parameter.Type == type, + VariableDeclarationSyntax variableDeclaration => variableDeclaration.Type == type, + PropertyDeclarationSyntax property => property.Type == type, + IndexerDeclarationSyntax indexer => indexer.Type == type, + MethodDeclarationSyntax method => method.ReturnType == type, + LocalFunctionStatementSyntax localFunction => localFunction.ReturnType == type, + DelegateDeclarationSyntax @delegate => @delegate.ReturnType == type, + OperatorDeclarationSyntax @operator => @operator.ReturnType == type, + ConversionOperatorDeclarationSyntax conversion => conversion.Type == type, + TypeConstraintSyntax typeConstraint => typeConstraint.Type == type, + ObjectCreationExpressionSyntax objectCreation => objectCreation.Type == type, + MemberAccessExpressionSyntax memberAccess => memberAccess.Expression == type && LooksLikeTypeName(type), + CastExpressionSyntax cast => cast.Type == type, + DefaultExpressionSyntax @default => @default.Type == type, + SizeOfExpressionSyntax sizeOf => sizeOf.Type == type, + TypeOfExpressionSyntax typeOf => typeOf.Type == type, + DeclarationExpressionSyntax declaration => declaration.Type == type, + _ => false + }; + } + + private static bool LooksLikeTypeName(TypeSyntax type) + => type switch + { + IdentifierNameSyntax identifier => IsUppercaseIdentifier(identifier.Identifier.ValueText), + GenericNameSyntax genericName => IsUppercaseIdentifier(genericName.Identifier.ValueText), + QualifiedNameSyntax qualifiedName => LooksLikeTypeName(qualifiedName.Right), + AliasQualifiedNameSyntax aliasQualifiedName => LooksLikeTypeName(aliasQualifiedName.Name), + _ => false + }; + + private static bool IsUppercaseIdentifier(string name) + => name.Length > 0 && char.IsUpper(name[0]); + + private static string? GetInvocationName(InvocationExpressionSyntax invocation) + => invocation.Expression switch + { + IdentifierNameSyntax identifier => identifier.Identifier.ValueText, + MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText, + GenericNameSyntax genericName => genericName.Identifier.ValueText, + _ => null + }; + private static bool IsPartialMethodDeclaration(IMethodSymbol methodSymbol) { foreach (var syntaxReference in methodSymbol.DeclaringSyntaxReferences) 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..810366b61e1 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,15 @@ public XmlDocProvider XmlDocs private set => _xmlDocs = value; } + internal bool PreserveTypeXmlDocs { get; private set; } + + protected internal virtual bool ShouldWriteTypeXmlDocs => false; + + internal void PreserveXmlDocs() + { + PreserveTypeXmlDocs = true; + } + public string? Deprecated { get => _deprecated; @@ -292,6 +301,28 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable SerializationProviders => _serializationProviders ??= BuildSerializationProviders(); + private IReadOnlyList? _helperDependencyTypes; + internal IReadOnlyList HelperDependencyTypes => _helperDependencyTypes ??= BuildHelperDependencyTypes(); + protected internal virtual IReadOnlyList BuildHelperDependencyTypes() => []; + + protected CSharpType ChangeTrackingDictionaryType => new ChangeTrackingDictionaryDefinition().Type; + + protected CSharpType ChangeTrackingListType => new ChangeTrackingListDefinition().Type; + + protected CSharpType OptionalType => new OptionalDefinition().Type; + + private IReadOnlyList? _bodyDependencyTypes; + internal IReadOnlyList BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); + protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; + + private IReadOnlyList? _signatureDependencyTypes; + internal IReadOnlyList SignatureDependencyTypes => _signatureDependencyTypes ??= BuildSignatureDependencyTypes(); + protected internal virtual IReadOnlyList BuildSignatureDependencyTypes() => []; + + protected internal virtual bool IsClientProvider => false; + + protected internal virtual bool IncludeGeneratedBodyReferences => false; + private IReadOnlyList? _attributes; public IReadOnlyList Attributes @@ -538,6 +569,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/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs new file mode 100644 index 00000000000..4891da18938 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs @@ -0,0 +1,405 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static void AddGeneratedBodyReferences(IReadOnlyList providers, ProviderReferenceGraph graph) + { + foreach (var (provider, isSerializationProvider) in GetBodyReferenceProviders(providers)) + { + if (IsModelFactoryProvider(provider) || + !IsGeneratedBodyReferenceCandidate(provider, isSerializationProvider)) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!graph.Nodes.Contains(providerName)) + { + continue; + } + + AddProviderBodyDependencyTypes( + graph.References[providerName], + GetNonEnumStructuredBodyReferenceTypes(provider, graph.Nodes), + graph.Nodes); + AddProviderBodyDependencyTypes(graph.References[providerName], provider.BodyDependencyTypes, graph.Nodes); + AddProviderInfrastructureReferences(graph.References[providerName], provider, isSerializationProvider, graph.Nodes); + } + } + + private static void AddProviderInfrastructureReferences(HashSet references, TypeProvider provider, bool isSerializationProvider, HashSet nodes) + { + AddMatchingName(references, "ProviderConstants", nodes); + AddMatchingName(references, "TypeFormatters", nodes); + + if (provider.SerializationProviders.Count > 0) + { + AddSerializationExtensionReferences(references, provider, nodes); + } + + if (isSerializationProvider) + { + AddMatchingName(references, "Optional", nodes); + AddMatchingName(references, "ModelSerializationExtensions", nodes); + AddSerializationExtensionReferences(references, provider, nodes); + } + + foreach (var method in provider.Methods) + { + AddMethodInfrastructureReferences(references, method, nodes); + } + } + + private static void AddSerializationExtensionReferences(HashSet references, TypeProvider provider, HashSet nodes) + { + AddSerializationExtensionReferences(references, provider.Type, nodes); + AddSerializationExtensionReferences(references, provider.BaseType, nodes); + foreach (var implementedType in provider.Implements) + { + AddSerializationExtensionReferences(references, implementedType, nodes); + } + + foreach (var property in provider.Properties) + { + AddSerializationExtensionReferences(references, property.Type, nodes); + } + + foreach (var field in provider.Fields) + { + AddSerializationExtensionReferences(references, field.Type, nodes); + } + + foreach (var constructor in provider.Constructors) + { + AddSerializationExtensionReferences(references, constructor.Signature.ReturnType, nodes); + foreach (var parameter in constructor.Signature.Parameters) + { + AddSerializationExtensionReferences(references, parameter.Type, nodes); + } + } + + foreach (var method in provider.Methods) + { + AddSerializationExtensionReferences(references, method.Signature.ReturnType, nodes); + foreach (var parameter in method.Signature.Parameters) + { + AddSerializationExtensionReferences(references, parameter.Type, nodes); + } + } + } + + private static void AddSerializationExtensionReferences(HashSet references, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + if (string.IsNullOrEmpty(type.Namespace)) + { + AddMatchingName(references, $"{type.Name}Extensions", nodes); + } + else + { + AddExactMetadataNameMatch(references, $"{type.Namespace}.{type.Name}Extensions", nodes); + } + foreach (var argument in type.Arguments) + { + AddSerializationExtensionReferences(references, argument, nodes); + } + } + + private static void AddMethodInfrastructureReferences(HashSet references, MethodProvider method, HashSet nodes) + { + AddReturnTypeInfrastructureReferences(references, method.Signature.ReturnType, nodes); + } + + private static void AddReturnTypeInfrastructureReferences(HashSet references, CSharpType? returnType, HashSet nodes) + { + var type = UnwrapTask(returnType); + if (type == null) + { + return; + } + + var typeName = StripGenericArity(type.Name); + if (string.Equals(typeName, "Pageable", StringComparison.Ordinal)) + { + AddMatchingName(references, "PageableWrapper", nodes); + } + else if (string.Equals(typeName, "AsyncPageable", StringComparison.Ordinal)) + { + AddMatchingName(references, "AsyncPageableWrapper", nodes); + } + else if (string.Equals(typeName, "ArmOperation", StringComparison.Ordinal)) + { + AddMatchingNamesWithSimpleNameSuffix(references, "ArmOperation", nodes); + AddMatchingNamesWithSimpleNameSuffix(references, "OperationSource", nodes); + if (type.Arguments.Count > 0) + { + AddMatchingName(references, $"{BuildOperationSourceTypeName(type.Arguments[0])}OperationSource", nodes); + } + } + } + + private static CSharpType? UnwrapTask(CSharpType? type) + { + var typeName = type == null ? null : StripGenericArity(type.Name); + if ((string.Equals(typeName, "Task", StringComparison.Ordinal) || + string.Equals(typeName, "ValueTask", StringComparison.Ordinal)) && + type?.Arguments.Count > 0) + { + return type.Arguments[0]; + } + + return type; + } + + private static string BuildOperationSourceTypeName(CSharpType type) + { + var argumentNames = string.Join("", type.Arguments.Select(BuildOperationSourceTypeName)); + return $"{type.Name}{(argumentNames.Length > 0 ? "Of" : string.Empty)}{argumentNames}"; + } + + private static IReadOnlyList GetNonEnumStructuredBodyReferenceTypes(TypeProvider provider, HashSet nodes) + { + var references = new List(); + foreach (var dependency in CollectStructuredBodyReferenceTypes(provider)) + { + if (!IsEnumProviderDependency(dependency, nodes)) + { + references.Add(dependency); + } + } + + return references; + } + + private static IReadOnlyList CollectStructuredBodyReferenceTypes(TypeProvider provider) + { + var references = new HashSet(); + var visited = new HashSet(ReferenceEqualityComparer.Instance); + + foreach (var field in provider.Fields) + { + CollectStructuredBodyReferenceTypes(field.InitializationValue, references, visited); + } + + foreach (var property in provider.Properties) + { + CollectStructuredBodyReferenceTypes(property.Body, references, visited); + } + + foreach (var constructor in provider.Constructors) + { + CollectStructuredBodyReferenceTypes(constructor.BodyExpression, references, visited); + CollectStructuredBodyReferenceTypes(constructor.BodyStatements, references, visited); + } + + foreach (var method in provider.Methods) + { + if (method.IsMethodSuppressed()) + { + continue; + } + + CollectStructuredBodyReferenceTypes(method.BodyExpression, references, visited); + CollectStructuredBodyReferenceTypes(method.BodyStatements, references, visited); + } + + return [.. references]; + } + + private static void CollectStructuredBodyReferenceTypes(object? value, HashSet references, HashSet visited) + { + switch (value) + { + case null: + case string: + case FormattableString: + return; + } + + if (!value.GetType().IsValueType && !visited.Add(value)) + { + return; + } + + switch (value) + { + case CSharpType type: + references.Add(type); + return; + case Type type: + references.Add(type); + return; + case ParameterProvider parameter: + references.Add(parameter.Type); + CollectStructuredBodyReferenceTypes(parameter.DefaultValue, references, visited); + CollectStructuredBodyReferenceTypes(parameter.InitializationValue, references, visited); + return; + case MethodSignatureBase signature: + CollectStructuredBodyReferenceTypes(signature.ReturnType, references, visited); + CollectStructuredBodyReferenceTypes(signature.Parameters, references, visited); + return; + case KeyValuePair positionalArgument: + CollectStructuredBodyReferenceTypes(positionalArgument.Value, references, visited); + return; + case FieldProvider field: + references.Add(field.Type); + CollectStructuredBodyReferenceTypes(field.InitializationValue, references, visited); + return; + } + + if (IsStructuredBodyReferenceObject(value)) + { + foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.GetIndexParameters().Length > 0) + { + continue; + } + + CollectStructuredBodyReferenceTypes(property.GetValue(value), references, visited); + } + + return; + } + + if (value is not IEnumerable values) + { + return; + } + + foreach (var item in values) + { + CollectStructuredBodyReferenceTypes(item, references, visited); + } + } + + private static bool IsEnumProviderDependency(CSharpType dependency, HashSet nodes) + { + var providerName = GetProviderTypeName(dependency); + if (!nodes.Contains(providerName)) + { + return false; + } + + foreach (var provider in CodeModelGenerator.Instance.OutputLibrary.TypeProviders) + { + if (provider is EnumProvider && + string.Equals(GetProviderTypeName(provider.Type), providerName, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static bool IsStructuredBodyReferenceObject(object value) => + value is ValueExpression || + value is MethodBodyStatement || + value is PropertyBody; + + private static void AddProviderBodyDependencyTypes( + HashSet references, + IReadOnlyList dependencies, + HashSet nodes, + bool includeSimpleNameReferences = false, + bool includeUnqualifiedSimpleNameReferences = false, + bool includeExtensionReferences = true) + { + foreach (var dependency in dependencies) + { + AddProviderBodyDependencyType(references, dependency, nodes, includeSimpleNameReferences, includeUnqualifiedSimpleNameReferences, includeExtensionReferences); + } + } + + private static void AddProviderBodyDependencyType( + HashSet references, + CSharpType? dependency, + HashSet nodes, + bool includeSimpleNameReferences, + bool includeUnqualifiedSimpleNameReferences, + bool includeExtensionReferences) + { + if (dependency == null) + { + return; + } + + AddTypeReference(references, dependency, nodes); + if (includeSimpleNameReferences && + !string.IsNullOrEmpty(dependency.Namespace) && + dependency.Arguments.Count == 0) + { + AddMatchingName(references, dependency.Name, nodes); + } + else if (includeUnqualifiedSimpleNameReferences && + string.IsNullOrEmpty(dependency.Namespace) && + dependency.Arguments.Count == 0) + { + AddUnambiguousMatchingName(references, dependency.Name, nodes); + } + if (includeExtensionReferences) + { + if (string.IsNullOrEmpty(dependency.Namespace)) + { + AddMatchingName(references, $"{dependency.Name}Extensions", nodes); + } + else + { + AddExactMetadataNameMatch(references, $"{dependency.Namespace}.{dependency.Name}Extensions", nodes); + } + } + + foreach (var argument in dependency.Arguments) + { + AddProviderBodyDependencyType(references, argument, nodes, includeSimpleNameReferences, includeUnqualifiedSimpleNameReferences, includeExtensionReferences); + } + } + + private static IReadOnlyList<(TypeProvider Provider, bool IsSerializationProvider)> GetBodyReferenceProviders(IReadOnlyList providers) + { + var bodyReferenceProviders = new List<(TypeProvider Provider, bool IsSerializationProvider)>(); + foreach (var provider in providers) + { + bodyReferenceProviders.Add((provider, false)); + foreach (var serializationProvider in provider.SerializationProviders) + { + bodyReferenceProviders.Add((serializationProvider, true)); + } + } + + return bodyReferenceProviders; + } + + private static bool IsGeneratedBodyReferenceCandidate(TypeProvider provider, bool isSerializationProvider) + { + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) + { + return true; + } + + return IsClientProvider(provider) || + isSerializationProvider || + provider.IncludeGeneratedBodyReferences || + provider.HelperDependencyTypes.Count > 0 || + provider.BodyDependencyTypes.Count > 0; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs new file mode 100644 index 00000000000..67475070797 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs @@ -0,0 +1,358 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static HashSet GetCustomInternalBoundaryNodes( + ProviderReferenceGraph publicGraph, + HashSet customInternalDeclarations) + { + var boundaryNodes = new HashSet(StringComparer.Ordinal); + foreach (var node in publicGraph.Nodes) + { + if (!publicGraph.References.TryGetValue(node, out var references)) + { + continue; + } + + if (references.Overlaps(customInternalDeclarations)) + { + boundaryNodes.Add(node); + } + } + + return boundaryNodes; + } + + private static HashSet GetPublicizeDeclaredNodes( + IReadOnlyList generatedProviders, + HashSet nodes, + HashSet internalizeDeclaredNodes) + { + var publicizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, nodes, publicOnly: false); + return publicizeDeclaredNodes; + } + + private static HashSet GetPublicApiTraversalNodes( + HashSet internalizeDeclaredNodes, + HashSet publicizeDeclaredNodes, + HashSet generatedInternalDeclarations, + HashSet generatedImplementationInternalDeclarations) + { + var traversalNodes = new HashSet(StringComparer.Ordinal); + foreach (var node in internalizeDeclaredNodes) + { + if (generatedInternalDeclarations.Contains(node) || + generatedImplementationInternalDeclarations.Contains(node)) + { + continue; + } + + traversalNodes.Add(node); + } + + foreach (var node in publicizeDeclaredNodes) + { + if (!generatedImplementationInternalDeclarations.Contains(node)) + { + traversalNodes.Add(node); + } + } + + return traversalNodes; + } + + private static HashSet GetInternalizeCandidates( + HashSet internalizeDeclaredNodes, + HashSet publicizeReachable, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet customInternalBoundaryNodes, + HashSet customPublicRoots, + HashSet publicizeRoots, + HashSet nodes, + IReadOnlyDictionary> references) + { + var candidates = new HashSet(StringComparer.Ordinal); + foreach (var node in internalizeDeclaredNodes) + { + var isNonRootKept = IsKeptName(node, CodeModelGenerator.Instance.NonRootTypes, nodes); + if (!publicizeReachable.Contains(node) || + customInternalDeclarations.Contains(node) || + generatedInternalDeclarations.Contains(node) && !customPublicRoots.Contains(node) || + customInternalBoundaryNodes.Contains(node) && (!publicizeRoots.Contains(node) || isNonRootKept) || + isNonRootKept && + references.TryGetValue(node, out var nodeReferences) && + (nodeReferences.Overlaps(customInternalDeclarations) || + nodeReferences.Overlaps(generatedInternalDeclarations))) + { + candidates.Add(node); + } + } + + // If a public non-root type exposes something that must remain internal, make the + // exposing type internal too. That avoids generating public APIs with internal types. + var addedCandidate = true; + while (addedCandidate) + { + addedCandidate = false; + foreach (var node in internalizeDeclaredNodes) + { + var isNonRootKept = IsKeptName(node, CodeModelGenerator.Instance.NonRootTypes, nodes); + if (candidates.Contains(node) || + publicizeRoots.Contains(node) && !isNonRootKept || + !references.TryGetValue(node, out var nodeReferences) || + !nodeReferences.Overlaps(candidates)) + { + continue; + } + + candidates.Add(node); + addedCandidate = true; + } + } + + // Non-root keep entries preserve the declared type/file, but deliberately do not + // root their dependencies. They can still be internalized when needed to avoid + // exposing custom/internal types through a public surface. + RemoveKeptNonRootNames(candidates, nodes, references, customInternalDeclarations, generatedInternalDeclarations, customInternalBoundaryNodes); + return candidates; + } + + private static void AddNestedInternalizeCandidates( + IReadOnlyList generatedProviders, + HashSet candidates, + HashSet nodes) + { + var addedCandidate = true; + while (addedCandidate) + { + addedCandidate = false; + foreach (var provider in generatedProviders) + { + var declaringType = provider.DeclaringTypeProvider?.Type; + if (declaringType == null || + !candidates.Contains(GetProviderTypeName(declaringType))) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (nodes.Contains(providerName) && candidates.Add(providerName)) + { + addedCandidate = true; + } + } + } + } + + private static void AddInternalOnlyDependencyCandidates( + HashSet internalizeDeclaredNodes, + HashSet candidates, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet explicitPublicRoots, + IReadOnlyDictionary> references, + HashSet generatedImplementationInternalDeclarations) + { + var addedCandidate = true; + while (addedCandidate) + { + addedCandidate = false; + foreach (var node in internalizeDeclaredNodes) + { + if (candidates.Contains(node) || explicitPublicRoots.Contains(node)) + { + continue; + } + + var hasPredecessor = false; + var allPredecessorsInternalized = true; + foreach (var (owner, children) in references) + { + if (string.Equals(owner, node, StringComparison.Ordinal) || + generatedImplementationInternalDeclarations.Contains(owner) || + !children.Contains(node)) + { + continue; + } + + hasPredecessor = true; + if (!candidates.Contains(owner) && + !customInternalDeclarations.Contains(owner) && + !generatedInternalDeclarations.Contains(owner)) + { + allPredecessorsInternalized = false; + break; + } + } + + if (hasPredecessor && allPredecessorsInternalized && candidates.Add(node)) + { + addedCandidate = true; + } + } + } + } + + private static HashSet GetPublicizeCandidates( + HashSet publicizeDeclaredNodes, + HashSet publicizeReachable, + HashSet customInternalDeclarations, + HashSet customInternalBoundaryNodes, + HashSet internalizeHelperRoots, + HashSet publicizeRootExclusions, + HashSet generatedInternalDeclarations, + HashSet publicizeRoots, + Dictionary> publicApiReferences, + Dictionary> internalizeReferences, + HashSet generatedImplementationInternalDeclarations) + { + var candidates = new HashSet(StringComparer.Ordinal); + foreach (var node in publicizeDeclaredNodes) + { + if (customInternalDeclarations.Contains(node) || + customInternalBoundaryNodes.Contains(node) || + internalizeHelperRoots.Contains(node) || + publicizeRootExclusions.Contains(node) || + !publicizeReachable.Contains(node)) + { + continue; + } + + if (generatedInternalDeclarations.Contains(node) && + !publicizeRoots.Contains(node) && + !HasPublicApiPredecessor(node, publicApiReferences, publicizeReachable, generatedImplementationInternalDeclarations)) + { + continue; + } + + if (!publicizeRoots.Contains(node) && + !HasPublicApiPredecessor(node, internalizeReferences, publicizeReachable, generatedImplementationInternalDeclarations)) + { + continue; + } + + candidates.Add(node); + } + + return candidates; + } + + private static HashSet GetRemovalCandidates( + IReadOnlyList providers, + IReadOnlyList generatedProviders, + ProviderReferenceGraph graph, + HashSet customRemovalRoots, + HashSet generatedDiscriminatorBaseNames) + { + var removeRoots = GetRootNames( + providers, + graph.Nodes, + helperRoots: [], + includeModelFactory: true, + includeAdditionalRoots: true, + includeUnionVariantRoots: true, + includeModelFactorySignatureRoots: true, + publicClientRootsOnly: false); + + removeRoots.UnionWith(customRemovalRoots); + AddMatchingNamesWithSimpleNameSuffix(removeRoots, "ReferenceType", graph.Nodes); + AddKeptNonRootNames(removeRoots, graph.Nodes); + AddCustomCodeExtensionRoots(removeRoots, generatedProviders, graph.Nodes); + AddCustomizationBackedExtensionRoots(removeRoots, graph.Nodes); + AddCustomRequestHeaderExtensionsRoot(removeRoots, generatedProviders, graph.Nodes); + RemoveUnusedRequestHeaderExtensionsRoot(removeRoots, graph.References, providers); + + var removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + AddDerivedModelReferences(providers, graph.Nodes, graph.References, removeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + removeReachableWithoutHelpers = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachableWithoutHelpers); + + var removeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, removeReachableWithoutHelpers, graph.References); + removeRoots.UnionWith(removeHelperRoots); + + var removeReachable = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachable); + + var removeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: false); + removeDeclaredNodes.ExceptWith(removeReachable); + return removeDeclaredNodes; + } + + private static void AddKeptNonRootNames(HashSet roots, HashSet nodes) + { + var nonRootTypes = CodeModelGenerator.Instance.NonRootTypes; + foreach (var node in nodes) + { + if (IsKeptName(node, nonRootTypes, nodes)) + { + roots.Add(node); + } + } + } + + private static void RemoveKeptNonRootNames( + HashSet candidates, + HashSet nodes, + IReadOnlyDictionary> references, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet customInternalBoundaryNodes) + { + var nonRootTypes = CodeModelGenerator.Instance.NonRootTypes; + foreach (var node in nodes) + { + if (customInternalDeclarations.Contains(node) || + generatedInternalDeclarations.Contains(node) || + customInternalBoundaryNodes.Contains(node)) + { + continue; + } + + if (IsKeptName(node, nonRootTypes, nodes) && + !HasCandidateReference(node, candidates, references)) + { + candidates.Remove(node); + } + } + } + + private static bool HasCandidateReference( + string node, + HashSet candidates, + IReadOnlyDictionary> references) + { + if (references.TryGetValue(node, out var nodeReferences)) + { + foreach (var reference in nodeReferences) + { + if (!string.Equals(reference, node, StringComparison.Ordinal) && + candidates.Contains(reference)) + { + return true; + } + } + } + + foreach (var (source, sourceReferences) in references) + { + if (!string.Equals(source, node, StringComparison.Ordinal) && + candidates.Contains(source) && + sourceReferences.Contains(node)) + { + return true; + } + } + + return false; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs new file mode 100644 index 00000000000..334e8acd93e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs @@ -0,0 +1,542 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static HashSet GetCustomCodeGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + AddCustomCodeViewGeneratedTypeRoot(roots, customCodeView, generatedTypeNames); + AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: false); + } + + return roots; + } + + private static HashSet GetCustomCodePublicGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + AddCustomCodeViewGeneratedTypeRoot(roots, customCodeView, generatedTypeNames); + AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: true); + } + + return roots; + } + + private static IEnumerable GetCustomCodeViews(IReadOnlyList providers) + { + var visited = new HashSet(StringComparer.Ordinal); + var modelFactoryCustomCodeView = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value.CustomCodeView; + if (modelFactoryCustomCodeView != null && visited.Add(GetCustomCodeViewIdentity(modelFactoryCustomCodeView))) + { + yield return modelFactoryCustomCodeView; + } + + foreach (var provider in providers) + { + var customCodeView = provider.CustomCodeView; + if (customCodeView == null || !visited.Add(GetCustomCodeViewIdentity(customCodeView))) + { + continue; + } + + yield return customCodeView; + } + + foreach (var customTypeProvider in CodeModelGenerator.Instance.SourceInputModel.GetCustomizationTypeProviders()) + { + if (visited.Add(GetCustomCodeViewIdentity(customTypeProvider))) + { + yield return customTypeProvider; + } + } + } + + private static string GetCustomCodeViewIdentity(TypeProvider customCodeView) => + customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider + ? namedTypeSymbolProvider.MetadataName + : GetProviderTypeName(customCodeView.Type); + + private static void AddCustomRequestHeaderExtensionsRoot(HashSet roots, IReadOnlyList providers, HashSet nodes) + { + if (!HasCustomRequestHeaderExtensionsReference(providers)) + { + return; + } + + AddMatchingNamesWithSimpleNameSuffix(roots, "RequestHeaderExtensions", nodes); + AddMatchingNamesWithSimpleNameSuffix(roots, "RequestHeadersExtensions", nodes); + } + + private static void AddCustomCodeExtensionRoots(HashSet roots, IReadOnlyList providers, HashSet nodes) + { + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + AddMatchingName(roots, $"{GetCustomCodeViewSimpleName(customCodeView)}Extensions", nodes); + } + } + + private static string GetCustomCodeViewSimpleName(TypeProvider customCodeView) => + customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider + ? namedTypeSymbolProvider.MetadataSimpleName + : customCodeView.Type.Name; + + private static void AddCustomCodeViewGeneratedTypeRoot(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames) + { + if (customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider) + { + AddExactMetadataNameMatch(roots, namedTypeSymbolProvider.MetadataName, generatedTypeNames); + return; + } + + AddTypeReference(roots, customCodeView.Type, generatedTypeNames); + } + + private static void AddCustomizationBackedExtensionRoots(HashSet roots, HashSet nodes) + { + foreach (var node in nodes) + { + var simpleName = GetSimpleName(node); + if (!simpleName.EndsWith("Extensions", StringComparison.Ordinal)) + { + continue; + } + + var namespaceName = GetNamespaceName(node); + if (namespaceName == null) + { + continue; + } + + var customTypeName = simpleName.Substring(0, simpleName.Length - "Extensions".Length); + if (CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(namespaceName, customTypeName) != null) + { + roots.Add(node); + } + } + } + + private static void AddCustomCodeViewRoots(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames, bool publicOnly) + { + AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); + AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames, includeSimpleNameReferences: true, includeUnqualifiedSimpleNameReferences: true, includeExtensionReferences: !publicOnly); + if (!publicOnly) + { + AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); + AddCustomCodeViewNamespaceBodyDependencyTypes(roots, customCodeView, generatedTypeNames); + AddAttributes(roots, customCodeView.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); + AddMatchingName(roots, $"{GetCustomCodeViewSimpleName(customCodeView)}Extensions", generatedTypeNames); + } + + foreach (var implementedType in customCodeView.Implements) + { + AddTypeReference(roots, implementedType, generatedTypeNames); + } + + foreach (var constructor in customCodeView.Constructors) + { + if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(roots, constructor.Signature, generatedTypeNames, serializationProviderNamesByType: null, includeAttributes: !publicOnly); + } + + foreach (var method in customCodeView.Methods) + { + if (publicOnly && !IsPublic(method.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(roots, method.Signature, generatedTypeNames, serializationProviderNamesByType: null, includeAttributes: !publicOnly); + } + + foreach (var property in customCodeView.Properties) + { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + + AddTypeReference(roots, property.Type, generatedTypeNames); + AddTypeReference(roots, property.ExplicitInterface, generatedTypeNames); + if (!publicOnly) + { + AddAttributes(roots, property.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); + } + } + + foreach (var field in customCodeView.Fields) + { + if (publicOnly && !IsPublic(field.Modifiers)) + { + continue; + } + + AddTypeReference(roots, field.Type, generatedTypeNames); + if (!publicOnly) + { + AddAttributes(roots, field.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); + } + } + } + + private static void AddCustomCodeViewNamespaceBodyDependencyTypes(HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames) + { + var customNamespace = customCodeView.Type.Namespace; + if (string.IsNullOrEmpty(customNamespace)) + { + return; + } + + var customMemberNames = GetCustomCodeViewMemberNames(customCodeView); + foreach (var dependency in customCodeView.BodyDependencyTypes) + { + AddNamespaceBodyDependencyType(roots, dependency, customNamespace, customMemberNames, generatedTypeNames); + } + } + + private static void AddNamespaceBodyDependencyType(HashSet roots, CSharpType? dependency, string customNamespace, HashSet customMemberNames, HashSet generatedTypeNames) + { + if (dependency == null) + { + return; + } + + if (string.IsNullOrEmpty(dependency.Namespace) && + dependency.Arguments.Count == 0 && + !customMemberNames.Contains(dependency.Name)) + { + AddExactMetadataNameMatch(roots, $"{customNamespace}.{dependency.Name}", generatedTypeNames); + } + + foreach (var argument in dependency.Arguments) + { + AddNamespaceBodyDependencyType(roots, argument, customNamespace, customMemberNames, generatedTypeNames); + } + } + + private static HashSet GetCustomCodeViewMemberNames(TypeProvider customCodeView) + { + var memberNames = new HashSet(StringComparer.Ordinal); + foreach (var property in customCodeView.Properties) + { + memberNames.Add(property.Name); + } + + foreach (var field in customCodeView.Fields) + { + memberNames.Add(field.Name); + } + + foreach (var method in customCodeView.Methods) + { + memberNames.Add(method.Signature.Name); + } + + return memberNames; + } + + private static HashSet GetApiBaselineGeneratedTypeRoots(HashSet generatedTypeNames) + { + var roots = new HashSet(StringComparer.Ordinal); + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + if (string.IsNullOrEmpty(projectDirectory)) + { + return roots; + } + + var apiDirectory = Path.GetFullPath(Path.Combine(projectDirectory, "..", "api")); + if (!Directory.Exists(apiDirectory)) + { + return roots; + } + + var apiText = string.Join("\n", Directory.GetFiles(apiDirectory, "*.cs", SearchOption.AllDirectories).Select(File.ReadAllText)); + var apiDeclaredTypeNames = GetApiDeclaredTypeNames(apiText); + foreach (var fullName in generatedTypeNames) + { + var simpleName = StripGenericArity(GetSimpleName(fullName)); + var normalizedFullName = StripGenericArity(fullName); + if (!ContainsApiTypeReference(apiText, apiDeclaredTypeNames, normalizedFullName, simpleName)) + { + continue; + } + + roots.Add(fullName); + } + + return roots; + } + + private static HashSet GetApiDeclaredTypeNames(string apiText) + { + var declaredTypeNames = new HashSet(StringComparer.Ordinal); + string? currentNamespace = null; + foreach (var line in apiText.Split('\n')) + { + var namespaceMatch = Regex.Match(line, @"^namespace\s+([\w.]+)\s*\{?\s*$"); + if (namespaceMatch.Success) + { + currentNamespace = namespaceMatch.Groups[1].Value; + continue; + } + + if (currentNamespace == null) + { + continue; + } + + var declarationMatch = Regex.Match(line, @"^ \S.*?\b(class|struct|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)(?!\s*<)(?!\w)"); + if (declarationMatch.Success) + { + declaredTypeNames.Add($"{currentNamespace}.{declarationMatch.Groups[2].Value}"); + } + } + + return declaredTypeNames; + } + + private static bool ContainsApiTypeReference(string apiText, HashSet apiDeclaredTypeNames, string fullName, string simpleName) + { + var fullNamePattern = $@"(? GetCustomCodeInternalGeneratedTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + { + var declarations = new HashSet(StringComparer.Ordinal); + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + if (!customCodeView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) + { + continue; + } + + if (customCodeView is NamedTypeSymbolProvider namedTypeSymbolProvider) + { + AddExactMetadataNameMatch(declarations, namedTypeSymbolProvider.MetadataName, generatedTypeNames); + } + else + { + AddTypeReference(declarations, customCodeView.Type, generatedTypeNames); + } + } + + return declarations; + } + + private static void AddExactMetadataNameMatch(HashSet target, string metadataName, HashSet generatedTypeNames) + { + var normalizedName = NormalizeMetadataTypeName(metadataName); + if (!string.IsNullOrEmpty(normalizedName) && generatedTypeNames.Contains(normalizedName)) + { + target.Add(normalizedName); + } + } + + private static string NormalizeMetadataTypeName(string metadataName) + { + var arrayIndex = metadataName.IndexOf('[', StringComparison.Ordinal); + if (arrayIndex > 0) + { + metadataName = metadataName.Substring(0, arrayIndex); + } + + return metadataName; + } + + private static HashSet GetGeneratedPersistableModelProxyTypeNames(IReadOnlyList providers, HashSet generatedTypeNames) + { + var proxyTypes = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (provider.Attributes.Any(static attribute => IsAttributeNamed(attribute, "PersistableModelProxy"))) + { + AddTypeReference(proxyTypes, provider.Type, generatedTypeNames); + } + } + + return proxyTypes; + } + + private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + { + var declarations = GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Internal); + declarations.UnionWith(GetGeneratedTypeDeclarationsByExistingSourceAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Internal)); + return declarations; + } + + private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Public); + + private static HashSet GetGeneratedPublicTypeDeclarationsFromLastContract(IReadOnlyList providers, HashSet generatedTypeNames) + => HasApiBaselineDirectory() + ? new HashSet(StringComparer.Ordinal) + : GetGeneratedPublicTypeDeclarations(providers, generatedTypeNames); + + private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessibility( + IReadOnlyList providers, + HashSet generatedTypeNames, + TypeSignatureModifiers accessibility) + { + var declarations = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + var lastContractView = CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContractIncludingInternal( + provider.Type.Namespace, + provider.Type.Name, + provider.DeclaringTypeProvider?.Type.Name); + if (lastContractView?.DeclarationModifiers.HasFlag(accessibility) != true) + { + continue; + } + + AddTypeReference(declarations, provider.Type, generatedTypeNames); + } + + return declarations; + } + + private static HashSet GetGeneratedTypeDeclarationsByExistingSourceAccessibility( + IReadOnlyList providers, + HashSet generatedTypeNames, + TypeSignatureModifiers accessibility) + { + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + if (string.IsNullOrEmpty(projectDirectory)) + { + return []; + } + + var declarations = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (!generatedTypeNames.Contains(GetProviderTypeName(provider.Type)) || + !ExistingSourceHasAccessibility(projectDirectory, provider, accessibility)) + { + continue; + } + + AddTypeReference(declarations, provider.Type, generatedTypeNames); + } + + return declarations; + } + + private static bool ExistingSourceHasAccessibility(string projectDirectory, TypeProvider provider, TypeSignatureModifiers accessibility) + { + var relativeFilePath = provider.DeclaringTypeProvider?.RelativeFilePath ?? provider.RelativeFilePath; + var path = Path.GetFullPath(Path.Combine(projectDirectory, relativeFilePath)); + if (!File.Exists(path)) + { + return false; + } + + var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetCompilationUnitRoot(); + foreach (var declaration in root.DescendantNodes().OfType()) + { + if (!string.Equals(declaration.Identifier.ValueText, provider.Type.Name, StringComparison.Ordinal) || + !IsDeclaringTypeMatch(declaration, provider.DeclaringTypeProvider)) + { + continue; + } + + return HasAccessibility(declaration.Modifiers, accessibility); + } + + return false; + } + + private static bool IsDeclaringTypeMatch(BaseTypeDeclarationSyntax declaration, TypeProvider? declaringTypeProvider) + { + var declaringType = declaration.Parent as BaseTypeDeclarationSyntax; + if (declaringTypeProvider == null) + { + return declaringType == null; + } + + return declaringType != null && + string.Equals(declaringType.Identifier.ValueText, declaringTypeProvider.Type.Name, StringComparison.Ordinal); + } + + private static bool HasAccessibility(SyntaxTokenList modifiers, TypeSignatureModifiers accessibility) => + accessibility switch + { + TypeSignatureModifiers.Internal => modifiers.Any(SyntaxKind.InternalKeyword), + TypeSignatureModifiers.Public => modifiers.Any(SyntaxKind.PublicKeyword), + _ => false + }; + + private static HashSet GetGeneratedImplementationInternalTypeDeclarations( + IReadOnlyList providers, + HashSet generatedInternalDeclarations) + { + var implementationDeclarations = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)) + { + continue; + } + + AddTypeReference(implementationDeclarations, provider.Type, generatedInternalDeclarations); + } + + foreach (var name in generatedInternalDeclarations) + { + if (GetSimpleName(name).StartsWith("Internal", StringComparison.Ordinal)) + { + implementationDeclarations.Add(name); + } + } + + return implementationDeclarations; + } + + private static bool IsGeneratedInternalImplementation(TypeProvider provider) + => provider.RelativeFilePath.Contains( + $"{Path.DirectorySeparatorChar}Generated{Path.DirectorySeparatorChar}Internal{Path.DirectorySeparatorChar}", + StringComparison.Ordinal); + + private static HashSet GetSimpleNames(HashSet names) + { + var simpleNames = new HashSet(StringComparer.Ordinal); + foreach (var name in names) + { + simpleNames.Add(GetSimpleName(name)); + } + + return simpleNames; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs new file mode 100644 index 00000000000..0d2536e18c1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) + { + // Each generated provider becomes a node, and provider metadata supplies the edges: + // inheritance, signatures, properties, fields, nested/serialization providers, attributes, + // and selected implementation dependencies. This avoids parsing generated C# just to + // rediscover generated-to-generated references. + var serializationProviderNamesByType = GetSerializationProviderNamesByType(generatedProviders); + IReadOnlyDictionary? serializationReferenceNamesByType = publicOnly ? null : serializationProviderNamesByType; + var nodes = new HashSet(StringComparer.Ordinal); + var references = new Dictionary>(StringComparer.Ordinal); + foreach (var provider in generatedProviders) + { + var providerName = GetProviderTypeName(provider.Type); + if (nodes.Add(providerName)) + { + references.Add(providerName, new HashSet(StringComparer.Ordinal)); + } + } + + foreach (var provider in generatedProviders) + { + var current = GetProviderTypeName(provider.Type); + AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType); + + if (publicOnly && !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + if (!publicOnly && IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) + { + continue; + } + + // Model factory signatures mention many models. The existing Roslyn post-processor + // removes factory methods for unreachable models, so model factory should only + // contribute helper dependencies, not model reachability edges. + if (IsModelFactoryProvider(provider)) + { + continue; + } + + foreach (var implementedType in provider.Implements) + { + AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType); + } + + if (!publicOnly) + { + foreach (var nestedType in provider.NestedTypes) + { + AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType); + } + } + + if (!publicOnly) + { + foreach (var serializationProvider in provider.SerializationProviders) + { + AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType); + } + } + + foreach (var property in provider.Properties) + { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + + AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType); + AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType); + if (!publicOnly) + { + AddAttributes(references[current], property.Attributes, nodes, serializationReferenceNamesByType, includeArguments: false); + } + } + + foreach (var field in provider.Fields) + { + if (publicOnly && !field.Modifiers.HasFlag(FieldModifiers.Public)) + { + continue; + } + + AddTypeReference(references[current], field.Type, nodes, serializationReferenceNamesByType); + if (!publicOnly) + { + AddAttributes(references[current], field.Attributes, nodes, serializationReferenceNamesByType, includeArguments: false); + } + } + + foreach (var constructor in provider.Constructors) + { + if (publicOnly && !IsPublic(constructor.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(references[current], constructor.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly, includeAttributeArguments: false); + } + + foreach (var method in provider.Methods) + { + if (method.IsMethodSuppressed()) + { + continue; + } + + if (publicOnly && !IsPublic(method.Signature.Modifiers)) + { + continue; + } + + AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly, includeAttributeArguments: false); + if (!publicOnly) + { + AddTypeReference(references[current], GetCollectionDefinitionType(method), nodes, serializationReferenceNamesByType); + } + } + } + + return new ProviderReferenceGraph(nodes, references); + } + + private static Dictionary GetSerializationProviderNamesByType(IReadOnlyList generatedProviders) + { + var namesByType = new Dictionary>(StringComparer.Ordinal); + foreach (var provider in generatedProviders) + { + if (provider.SerializationProviders.Count == 0) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!namesByType.TryGetValue(providerName, out var serializationProviderNames)) + { + serializationProviderNames = new HashSet(StringComparer.Ordinal); + namesByType.Add(providerName, serializationProviderNames); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + serializationProviderNames.Add(GetProviderTypeName(serializationProvider.Type)); + } + } + + var result = new Dictionary(StringComparer.Ordinal); + foreach (var (providerName, serializationProviderNames) in namesByType) + { + result.Add(providerName, [.. serializationProviderNames]); + } + + return result; + } + + private static CSharpType? GetCollectionDefinitionType(MethodProvider method) + { + var property = method.GetType().GetProperty("CollectionDefinition"); + return property?.GetValue(method) is TypeProvider collectionDefinition + ? collectionDefinition.Type + : null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs new file mode 100644 index 00000000000..e07ee8960a2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs @@ -0,0 +1,617 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static bool IsKept(CSharpType type, HashSet roots, HashSet nodes) + { + var providerName = GetProviderTypeName(type); + return IsKeptName(providerName, type.Name, roots, nodes); + } + + private static bool IsKeptName(string providerName, HashSet roots, HashSet nodes) + { + return IsKeptName(providerName, StripGenericArity(GetSimpleName(providerName)), roots, nodes); + } + + private static bool IsKeptName(string providerName, string simpleName, HashSet roots, HashSet nodes) + { + if (roots.Contains(providerName) && nodes.Contains(providerName)) + { + return true; + } + + if (!roots.Contains(simpleName)) + { + return false; + } + + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + return simpleNameLookup.TryGetValue(simpleName, out var matches) && + matches.Length == 1 && + string.Equals(matches[0], providerName, StringComparison.Ordinal); + } + + private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => + IsClientProvider(provider) && + (!publicOnly || !HasApiBaselineDirectory() && provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + + private static bool IsClientProvider(TypeProvider provider) + { + for (var type = provider.GetType(); type != null && type != typeof(TypeProvider); type = type.BaseType) + { + if (string.Equals(type.Name, "ClientProvider", StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static bool IsAdditionalRootProvider(TypeProvider provider, HashSet roots, HashSet nodes) + { + if (provider.DeclaringTypeProvider != null || !IsKept(provider.Type, roots, nodes)) + { + return false; + } + + return provider is not ModelProvider && provider is not EnumProvider; + } + + private static bool HasApiBaselineDirectory() + { + var projectDirectory = CodeModelGenerator.Instance.Configuration.ProjectDirectory; + if (string.IsNullOrEmpty(projectDirectory)) + { + return false; + } + + var directory = new DirectoryInfo(Path.GetFullPath(projectDirectory)); + while (directory != null) + { + if (Directory.Exists(Path.Combine(directory.FullName, "api")) || + directory.Parent != null && Directory.Exists(Path.Combine(directory.Parent.FullName, "api"))) + { + return true; + } + + directory = directory.Parent; + } + + return false; + } + + private static bool IsModelFactoryProvider(TypeProvider provider) + => provider is ModelFactoryProvider; + + private static IEnumerable GetExistingGeneratedHelperRoots( + IReadOnlyList generatedProviders, + HashSet generatedInternalDeclarations) + { + var helperNames = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(generatedProviders)) + { + if (!IsGeneratedInternalHelperDeclaration(provider)) + { + continue; + } + + var name = GetProviderTypeName(provider.Type); + if (generatedInternalDeclarations.Contains(name)) + { + helperNames.Add(name); + } + } + + AddSiblingExtensionRoot(helperNames, generatedInternalDeclarations, "CancellationTokenExtensions", "RequestContextExtensions"); + + return helperNames; + } + + private static bool IsGeneratedInternalHelperDeclaration(TypeProvider provider) + { + if (provider is ModelProvider || + provider is EnumProvider || + IsModelFactoryProvider(provider) || + provider.DeclaringTypeProvider != null || + provider.SerializationProviders.Count > 0) + { + return false; + } + + return !string.Equals(provider.RelativeFilePath, Path.Combine("src", "Generated", "Models", $"{provider.Name}.cs"), StringComparison.Ordinal); + } + + private static HashSet GetHelperRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet reachableTypes, + IReadOnlyDictionary>? references = null) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + var isModelFactory = IsModelFactoryProvider(provider); + if (!reachableTypes.Contains(providerName) && !isModelFactory) + { + continue; + } + + AddHelperDependencies(roots, provider.HelperDependencyTypes, nodes, references == null ? null : references[providerName]); + + foreach (var property in provider.Properties) + { + AddInitializationHelperRoot(roots, property.Type, nodes); + AddParameterValidationHelperRoot(roots, property.AsParameter, nodes); + } + + foreach (var field in provider.Fields) + { + AddParameterValidationHelperRoot(roots, field.AsParameter, nodes); + } + + foreach (var constructor in provider.Constructors) + { + foreach (var parameter in constructor.Signature.Parameters) + { + AddParameterValidationHelperRoot(roots, parameter, nodes); + } + } + + foreach (var method in provider.Methods) + { + // Only factory methods for reachable models can instantiate collection helpers. + if (isModelFactory && + (method.Signature.ReturnType == null || !reachableTypes.Contains(GetProviderTypeName(method.Signature.ReturnType)))) + { + continue; + } + + foreach (var parameter in method.Signature.Parameters) + { + AddParameterValidationHelperRoot(roots, parameter, nodes); + if (isModelFactory) + { + AddModelFactoryCollectionInitializationHelperRoot(roots, parameter.Type, nodes); + } + } + } + } + + AddSiblingExtensionRoot(roots, nodes, "CancellationTokenExtensions", "RequestContextExtensions"); + return roots; + } + + private static void AddSiblingExtensionRoot(HashSet roots, HashSet nodes, string sourceName, string siblingName) + { + var siblingRoots = new List(); + foreach (var root in roots) + { + if (!string.Equals(GetSimpleName(root), sourceName, StringComparison.Ordinal)) + { + continue; + } + + var namespaceName = GetNamespaceName(root); + if (namespaceName == null) + { + continue; + } + + var sibling = $"{namespaceName}.{siblingName}"; + if (nodes.Contains(sibling)) + { + siblingRoots.Add(sibling); + } + } + + roots.UnionWith(siblingRoots); + } + + private static void AddParameterValidationHelperRoot(HashSet roots, ParameterProvider parameter, HashSet nodes) + { + if (parameter.Validation != ParameterValidationType.None) + { + AddMatchingName(roots, "Argument", nodes); + } + } + + private static void AddHelperDependencies( + HashSet roots, + IReadOnlyList dependencies, + HashSet nodes, + HashSet? referencedNames) + { + foreach (var dependency in dependencies) + { + if (referencedNames == null) + { + AddTypeReference(roots, dependency, nodes); + continue; + } + + var matches = new HashSet(StringComparer.Ordinal); + AddTypeReference(matches, dependency, nodes); + foreach (var match in matches) + { + if (referencedNames.Contains(match)) + { + roots.Add(match); + } + } + } + } + + private static void RemoveUnusedRequestHeaderExtensionsRoot( + HashSet roots, + IReadOnlyDictionary> references, + IReadOnlyList providers) + { + var hasCustomReference = HasCustomRequestHeaderExtensionsReference(providers); + if (hasCustomReference) + { + return; + } + + var unusedRequestHeaderExtensions = new List(); + foreach (var root in roots) + { + if (IsRequestHeadersExtensionsRoot(root) && + !HasExternalReference(root, references)) + { + unusedRequestHeaderExtensions.Add(root); + } + } + + roots.ExceptWith(unusedRequestHeaderExtensions); + } + + private static bool HasExternalReference(string root, IReadOnlyDictionary> references) + { + foreach (var (source, sourceReferences) in references) + { + if (!string.Equals(source, root, StringComparison.Ordinal) && + sourceReferences.Contains(root)) + { + return true; + } + } + + return false; + } + + private static bool IsRequestHeadersExtensionsRoot(string root) => + root.EndsWith(".RequestHeaderExtensions", StringComparison.Ordinal) || + root.EndsWith(".RequestHeadersExtensions", StringComparison.Ordinal); + + private static bool HasCustomRequestHeaderExtensionsReference(IReadOnlyList providers) + { + foreach (var customCodeView in GetCustomCodeViews(providers)) + { + if (customCodeView is NamedTypeSymbolProvider) + { + if (HasRequestHeaderExtensionsDependency(customCodeView.HelperDependencyTypes) || + HasRequestHeaderExtensionsDependency(customCodeView.BodyDependencyTypes) || + HasRequestHeaderExtensionsDependency(customCodeView.SignatureDependencyTypes)) + { + return true; + } + + continue; + } + + if (HasRequestHeaderExtensionsDependency(customCodeView.HelperDependencyTypes) || + HasRequestHeaderExtensionsDependency(customCodeView.BodyDependencyTypes) || + HasRequestHeaderExtensionsMethodDependency(customCodeView.Methods) || + HasRequestHeaderExtensionsPropertyDependency(customCodeView.Properties) || + HasRequestHeaderExtensionsFieldDependency(customCodeView.Fields)) + { + return true; + } + } + + return false; + } + + private static bool HasRequestHeaderExtensionsDependency(IEnumerable dependencies) + { + foreach (var dependency in dependencies) + { + if (IsRequestHeaderExtensionsDependency(dependency)) + { + return true; + } + } + + return false; + } + + private static bool HasRequestHeaderExtensionsMethodDependency(IReadOnlyList methods) + { + foreach (var method in methods) + { + if (IsRequestHeaderExtensionsDependency(method.Signature.ReturnType)) + { + return true; + } + + foreach (var parameter in method.Signature.Parameters) + { + if (IsRequestHeaderExtensionsDependency(parameter.Type)) + { + return true; + } + } + } + + return false; + } + + private static bool HasRequestHeaderExtensionsPropertyDependency(IReadOnlyList properties) + { + foreach (var property in properties) + { + if (IsRequestHeaderExtensionsDependency(property.Type)) + { + return true; + } + } + + return false; + } + + private static bool HasRequestHeaderExtensionsFieldDependency(IReadOnlyList fields) + { + foreach (var field in fields) + { + if (IsRequestHeaderExtensionsDependency(field.Type)) + { + return true; + } + } + + return false; + } + + private static bool IsRequestHeaderExtensionsDependency(string name) + => string.Equals(name, "RequestHeaderExtensions", StringComparison.Ordinal) || + string.Equals(name, "SetDelimited", StringComparison.Ordinal); + + private static bool IsRequestHeaderExtensionsDependency(CSharpType? type) + { + if (type == null) + { + return false; + } + + if (IsRequestHeaderExtensionsDependency(type.Name)) + { + return true; + } + + foreach (var argument in type.Arguments) + { + if (IsRequestHeaderExtensionsDependency(argument)) + { + return true; + } + } + + return false; + } + + private static void AddInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + var initializationType = type.PropertyInitializationType; + if (!string.Equals(initializationType.FullyQualifiedName, type.FullyQualifiedName, StringComparison.Ordinal)) + { + AddMatchingName(roots, initializationType.Name, nodes); + } + + if (type is { IsList: true, IsReadOnlyMemory: false }) + { + AddTypeReference(roots, CodeModelGenerator.Instance.TypeFactory.ListInitializationType, nodes); + } + + if (type.IsDictionary) + { + AddTypeReference(roots, CodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType, nodes); + } + + foreach (var argument in type.Arguments) + { + AddInitializationHelperRoot(roots, argument, nodes); + } + } + + private static void AddModelFactoryCollectionInitializationHelperRoot(HashSet roots, CSharpType? type, HashSet nodes) + { + if (type == null) + { + return; + } + + if (type is { IsList: true, IsReadOnlyMemory: false }) + { + AddTypeReference(roots, CodeModelGenerator.Instance.TypeFactory.ListInitializationType, nodes); + } + + if (type.IsDictionary) + { + AddTypeReference(roots, CodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType, nodes); + } + + foreach (var argument in type.Arguments) + { + AddModelFactoryCollectionInitializationHelperRoot(roots, argument, nodes); + } + } + + private static void AddMatchingName(HashSet target, string name, HashSet nodes) + { + if (nodes.Contains(name)) + { + target.Add(name); + return; + } + + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + if (!simpleNameLookup.TryGetValue(name, out var matches)) + { + return; + } + + foreach (var match in matches) + { + target.Add(match); + } + } + + private static void AddUnambiguousMatchingName(HashSet target, string name, HashSet nodes) + { + if (nodes.Contains(name)) + { + target.Add(name); + return; + } + + var simpleNameLookup = _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup); + if (simpleNameLookup.TryGetValue(name, out var matches) && matches.Length == 1) + { + target.Add(matches[0]); + } + } + + private static void AddMatchingNamesWithSimpleNameSuffix(HashSet target, string suffix, HashSet nodes) + { + foreach (var node in nodes) + { + if (GetSimpleName(node).EndsWith(suffix, StringComparison.Ordinal)) + { + target.Add(node); + } + } + } + + private static Dictionary BuildSimpleNameLookup(HashSet nodes) + { + var lookup = new Dictionary>(StringComparer.Ordinal); + foreach (var node in nodes) + { + if (IsNestedNode(node, nodes)) + { + continue; + } + + var simpleName = StripGenericArity(GetSimpleName(node)); + if (!lookup.TryGetValue(simpleName, out var matchingNodes)) + { + matchingNodes = []; + lookup.Add(simpleName, matchingNodes); + } + + matchingNodes.Add(node); + } + + var result = new Dictionary(StringComparer.Ordinal); + foreach (var (simpleName, matchingNodes) in lookup) + { + result.Add(simpleName, [.. matchingNodes]); + } + + return result; + } + + private static bool IsNestedNode(string node, HashSet nodes) + { + var dotIndex = node.IndexOf('.'); + while (dotIndex >= 0) + { + if (nodes.Contains(node.Substring(0, dotIndex))) + { + return true; + } + + dotIndex = node.IndexOf('.', dotIndex + 1); + } + + return false; + } + + private static HashSet GetReachableTypes(HashSet roots, IReadOnlyDictionary> references) + { + return GetReachableTypes(roots, references, expandableNodes: null); + } + + private static HashSet GetReachableTypes( + HashSet roots, + IReadOnlyDictionary> references, + HashSet? expandableNodes) + { + var reachable = new HashSet(StringComparer.Ordinal); + var queue = new Queue(roots); + while (queue.Count > 0) + { + var current = queue.Dequeue(); + if (!reachable.Add(current)) + { + continue; + } + + if (expandableNodes != null && !expandableNodes.Contains(current)) + { + continue; + } + + if (!references.TryGetValue(current, out var children)) + { + continue; + } + + foreach (var child in children) + { + queue.Enqueue(child); + } + } + + return reachable; + } + + private static bool HasPublicApiPredecessor( + string name, + IReadOnlyDictionary> references, + HashSet publicizeReachable, + HashSet generatedImplementationInternalDeclarations) + { + foreach (var (owner, children) in references) + { + if (!publicizeReachable.Contains(owner) || + string.Equals(owner, name, StringComparison.Ordinal) || + generatedImplementationInternalDeclarations.Contains(owner) || + !children.Contains(name)) + { + continue; + } + + return true; + } + + return false; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs new file mode 100644 index 00000000000..5674657f48d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static Dictionary> CloneReferences(IReadOnlyDictionary> references) + { + var clone = new Dictionary>(StringComparer.Ordinal); + foreach (var (name, referencedNames) in references) + { + clone.Add(name, new HashSet(referencedNames, StringComparer.Ordinal)); + } + + return clone; + } + + private static void AddDerivedModelReferences( + IReadOnlyList providers, + HashSet nodes, + Dictionary> references, + HashSet publicBaseModels, + HashSet generatedDiscriminatorBaseNames) + { + var modelProviders = new List(); + var discriminatorProviders = new List(); + var discriminatorBaseNames = new HashSet(StringComparer.Ordinal); + foreach (var provider in providers) + { + if (provider is not ModelProvider modelProvider || + !modelProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + modelProviders.Add(modelProvider); + + if (modelProvider.DiscriminatorProperty != null) + { + discriminatorBaseNames.Add(GetProviderTypeName(modelProvider.Type)); + } + + if (!modelProvider.IsUnknownDiscriminatorModel && + (modelProvider.DiscriminatorProperty != null || modelProvider.DiscriminatorValue != null)) + { + discriminatorProviders.Add(modelProvider); + } + } + + discriminatorBaseNames.UnionWith(generatedDiscriminatorBaseNames); + var addedReference = true; + while (addedReference) + { + addedReference = false; + foreach (var provider in discriminatorProviders) + { + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName) || + !publicBaseModels.Contains(providerName)) + { + continue; + } + + foreach (var derivedModel in provider.DerivedModels) + { + if (derivedModel.IsUnknownDiscriminatorModel || + !derivedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var before = references[providerName].Count; + AddTypeReference(references[providerName], derivedModel.Type, nodes); + var derivedName = GetProviderTypeName(derivedModel.Type); + if (nodes.Contains(derivedName) && publicBaseModels.Add(derivedName) || references[providerName].Count != before) + { + addedReference = true; + } + } + } + + foreach (var provider in modelProviders) + { + if (provider.IsUnknownDiscriminatorModel || + !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName)) + { + continue; + } + + var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); + if (baseTypeName == null || + !discriminatorBaseNames.Contains(baseTypeName) || + !nodes.Contains(baseTypeName) || + !publicBaseModels.Contains(baseTypeName)) + { + continue; + } + + var before = references[baseTypeName].Count; + references[baseTypeName].Add(providerName); + if (publicBaseModels.Add(providerName) || references[baseTypeName].Count != before) + { + addedReference = true; + } + } + } + } + + private static void AddBasePreservedReferences( + IReadOnlyList providers, + HashSet nodes, + IReadOnlyDictionary> references, + HashSet reachableTypes) + { + var basePreservedRoots = new HashSet(StringComparer.Ordinal); + var addedRoot = true; + while (addedRoot) + { + addedRoot = false; + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + if (!nodes.Contains(providerName) || reachableTypes.Contains(providerName) || basePreservedRoots.Contains(providerName)) + { + continue; + } + + var baseTypeName = provider.BaseType == null ? null : GetProviderTypeName(provider.BaseType); + if (baseTypeName == null || !reachableTypes.Contains(baseTypeName)) + { + continue; + } + + if (basePreservedRoots.Add(providerName)) + { + addedRoot = true; + } + } + + if (addedRoot) + { + reachableTypes.UnionWith(GetReachableTypes(basePreservedRoots, references)); + } + } + } + + private static IReadOnlyList GetGeneratedProviders(IReadOnlyList providers) + { + var generatedProviders = new List(); + foreach (var provider in providers) + { + AddGeneratedProvider(generatedProviders, provider); + } + + return generatedProviders; + } + + private static void AddGeneratedProvider(List generatedProviders, TypeProvider provider) + { + generatedProviders.Add(provider); + foreach (var nestedType in provider.NestedTypes) + { + AddGeneratedProvider(generatedProviders, nestedType); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddGeneratedProvider(generatedProviders, serializationProvider); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs new file mode 100644 index 00000000000..e56982cf089 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static HashSet GetRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet helperRoots, + bool includeModelFactory, + bool includeAdditionalRoots, + bool includeUnionVariantRoots, + bool includeModelFactorySignatureRoots, + bool publicClientRootsOnly) + { + var generator = CodeModelGenerator.Instance; + var roots = new HashSet(StringComparer.Ordinal); + var modelFactoryName = GetProviderTypeName(generator.OutputLibrary.ModelFactory.Value.Type); + + foreach (var provider in providers) + { + var name = GetProviderTypeName(provider.Type); + if (IsClientProviderRoot(provider, publicClientRootsOnly) || + includeAdditionalRoots && IsAdditionalRootProvider(provider, generator.AdditionalRootTypes, nodes) || + includeModelFactory && string.Equals(name, modelFactoryName, StringComparison.Ordinal) || + includeModelFactory && helperRoots.Contains(name)) + { + roots.Add(name); + } + } + + if (includeModelFactorySignatureRoots) + { + AddLastContractModelFactorySignatureRoots(providers, roots, nodes); + } + + if (!includeUnionVariantRoots) + { + return roots; + } + + AddUnionVariantRoots(roots, providers, nodes); + + return roots; + } + + private static void AddLastContractModelFactorySignatureRoots(IReadOnlyList providers, HashSet roots, HashSet nodes) + { + foreach (var provider in providers) + { + if (!IsModelFactoryProvider(provider)) + { + continue; + } + + foreach (var method in provider.LastContractView?.Methods ?? []) + { + if (!method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) || + IsImplementationOnlyModelFactoryMethod(method)) + { + continue; + } + + AddTypeReference(roots, method.Signature.ReturnType, nodes); + foreach (var parameter in method.Signature.Parameters) + { + AddTypeReference(roots, parameter.Type, nodes); + } + } + } + } + + private static void AddUnionVariantRoots(HashSet roots, IReadOnlyList providers, HashSet nodes) + { + var unionVariantTypesToKeep = CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep; + foreach (var provider in GetGeneratedProviders(providers)) + { + if (provider is not ModelProvider || + !unionVariantTypesToKeep.Contains(provider.Type.Name) || + string.Equals(provider.Type.Namespace, "TypeSpec.Http", StringComparison.Ordinal)) + { + continue; + } + + AddMatchingName(roots, GetProviderTypeName(provider.Type), nodes); + } + } + + private static bool ShouldUseUnionVariantFallbackRoots() => + !HasApiBaselineDirectory() && + CodeModelGenerator.Instance.SourceInputModel.LastContract == null; + + private static bool IsImplementationOnlyModelFactoryMethod(MethodProvider method) + { + var returnType = method.Signature.ReturnType; + if (returnType == null) + { + return true; + } + + var returnTypeName = GetSimpleName(GetProviderTypeName(returnType)); + return returnTypeName.StartsWith("Paged", StringComparison.Ordinal) || + returnTypeName.EndsWith("Request", StringComparison.Ordinal); + } + + private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) + { + if (namesToRemove.Count == 0) + { + return; + } + + var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + _preWriteModelFactory = modelFactory; + _preWriteModelFactoryMethods ??= [.. modelFactory.Methods]; + var removedLeadingMethod = false; + var methodsToKeep = new List(); + for (int i = 0; i < modelFactory.Methods.Count; i++) + { + var method = modelFactory.Methods[i]; + if (!namesToRemove.Contains(method.Signature.Name)) + { + methodsToKeep.Add(method); + continue; + } + + if (i == 0) + { + removedLeadingMethod = true; + } + } + + if (removedLeadingMethod && methodsToKeep.Count > 0) + { + modelFactory.PreserveLeadingMethodSeparatorForBackCompat(); + } + + modelFactory.Update(methods: methodsToKeep); + } + + private static HashSet GetPostProcessorDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) + { + var generator = CodeModelGenerator.Instance; + var declaredNodes = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (IsModelFactoryProvider(provider)) + { + continue; + } + + if (publicOnly && !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + var name = GetProviderTypeName(provider.Type); + if (!nodes.Contains(name)) + { + continue; + } + + declaredNodes.Add(name); + } + + return declaredNodes; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs new file mode 100644 index 00000000000..3833ab0350e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static void AddSignatureReferences( + HashSet references, + MethodSignatureBase signature, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType, + bool includeAttributes = true, + bool includeAttributeArguments = true) + { + AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType); + if (includeAttributes) + { + AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); + } + + foreach (var parameter in signature.Parameters) + { + AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType); + if (includeAttributes) + { + AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); + } + } + + if (signature is MethodSignature methodSignature) + { + AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType); + if (methodSignature.GenericArguments != null) + { + foreach (var genericArgument in methodSignature.GenericArguments) + { + AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType); + } + } + + if (methodSignature.GenericParameterConstraints != null) + { + foreach (var constraint in methodSignature.GenericParameterConstraints) + { + AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType); + } + } + } + + if (signature is ConstructorSignature constructorSignature) + { + AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType); + } + } + + private static void AddAttributes( + HashSet references, + IReadOnlyList attributes, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType, + bool includeArguments) + { + foreach (var attribute in attributes) + { + AddTypeReference(references, attribute.Type, nodes, serializationProviderNamesByType); + if (!includeArguments) + { + continue; + } + + foreach (var argument in attribute.Arguments) + { + AddAttributeArgumentReference(references, argument, nodes, serializationProviderNamesByType); + } + + foreach (var (_, argument) in attribute.PositionalArguments) + { + AddAttributeArgumentReference(references, argument, nodes, serializationProviderNamesByType); + } + } + } + + private static bool IsAttributeNamed(AttributeStatement attribute, string name) + => string.Equals(attribute.Type.Name, name, StringComparison.Ordinal) || + string.Equals(attribute.Type.Name, $"{name}Attribute", StringComparison.Ordinal); + + private static void AddAttributeArgumentReference( + HashSet references, + ValueExpression argument, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType) + { + if (argument is TypeOfExpression typeOf) + { + AddTypeReference(references, typeOf.Type, nodes, serializationProviderNamesByType); + AddMatchingName(references, typeOf.Type.Name, nodes); + } + } + + private static void AddTypeReference( + HashSet references, + CSharpType? type, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType = null) + { + if (type == null) + { + return; + } + + if (type.IsArray) + { + AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType); + return; + } + + var providerTypeName = GetProviderTypeName(type); + if (nodes.Contains(providerTypeName)) + { + references.Add(providerTypeName); + if (serializationProviderNamesByType != null && serializationProviderNamesByType.TryGetValue(providerTypeName, out var serializationProviderNames)) + { + foreach (var serializationProviderName in serializationProviderNames) + { + references.Add(serializationProviderName); + } + } + } + + AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType); + AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType); + foreach (var argument in type.Arguments) + { + AddTypeReference(references, argument, nodes, serializationProviderNamesByType); + } + } + + private static string GetSimpleName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.Substring(lastDot + 1); + } + + private static string? GetNamespaceName(string fullyQualifiedName) + { + var lastDot = fullyQualifiedName.LastIndexOf('.'); + return lastDot < 0 ? null : fullyQualifiedName.Substring(0, lastDot); + } + + private static string GetProviderTypeName(CSharpType type) + { + var name = type.Arguments.Count > 0 && !type.Name.Contains('`', StringComparison.Ordinal) + ? $"{type.Name}`{type.Arguments.Count}" + : type.Name; + if (type.DeclaringType is { } declaringType) + { + return $"{GetProviderTypeName(declaringType)}.{name}"; + } + + return string.IsNullOrEmpty(type.Namespace) ? name : $"{type.Namespace}.{name}"; + } + + private static string StripGenericArity(string name) + { + var tick = name.IndexOf('`'); + return tick < 0 ? name : name.Substring(0, tick); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs new file mode 100644 index 00000000000..5a96b69a868 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static ProviderReferenceMapResult? _latestResult; + private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); + private static TypeProvider? _preWriteModelFactory; + private static MethodProvider[]? _preWriteModelFactoryMethods; + + public static ProviderReferenceMapResult? LatestResult => _latestResult; + public static bool PreWriteAccessibilityApplied { get; private set; } + + public static bool ShouldWriteProvider(TypeProvider provider) => + _latestResult?.RemoveCandidates.Contains(GetProviderTypeName(provider.Type)) != true; + + public static void ResetPreWriteAccessibility() + { + RestorePreWriteModelFactoryMethods(); + _latestResult = null; + PreWriteAccessibilityApplied = false; + } + + public static void ApplyPreWriteAccessibility(IReadOnlyList providers) + { + PreWriteAccessibilityApplied = false; + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.KeepAll) + { + return; + } + + // Accessibility has to be adjusted before files are written. Roslyn can remove files + // later, but it cannot safely change provider declarations or model factory signatures. + var (internalizeCandidates, publicizeCandidates) = GetPreWriteAccessibilityCandidates(providers); + foreach (var provider in GetGeneratedProviders(providers)) + { + var providerName = GetProviderTypeName(provider.Type); + if (internalizeCandidates.Contains(providerName)) + { + if (provider.DeclaringTypeProvider is null) + { + provider.PreserveXmlDocs(); + } + provider.Update(modifiers: MakeInternal(provider.DeclarationModifiers)); + } + else if (publicizeCandidates.Contains(providerName) && !IsGeneratedInternalImplementation(provider)) + { + provider.Update(modifiers: MakePublic(provider.DeclarationModifiers)); + } + } + + RemoveMethodsFromModelFactory(GetSimpleNames(internalizeCandidates)); + PreWriteAccessibilityApplied = true; + } + + public static void RestorePreWriteModelFactoryMethods() + { + if (_preWriteModelFactory == null || _preWriteModelFactoryMethods == null) + { + return; + } + + _preWriteModelFactory.Update(methods: _preWriteModelFactoryMethods); + _preWriteModelFactory = null; + _preWriteModelFactoryMethods = null; + } + + public static void Analyze(IReadOnlyList providers) + { + var generatedProviders = GetGeneratedProviders(providers); + + // Build two graphs from provider metadata: the full implementation graph for removal, + // and the public-surface graph for accessibility decisions. + var graph = BuildGraph(generatedProviders); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true); + + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); + var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); + customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); + var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarationsFromLastContract(generatedProviders, graph.Nodes); + customPublicRoots.UnionWith(generatedPublicDeclarations); + var customCodeRemovalRoots = GetCustomCodeGeneratedTypeRoots(generatedProviders, graph.Nodes); + var customRemovalRoots = new HashSet(customCodeRemovalRoots, StringComparer.Ordinal); + customRemovalRoots.UnionWith(apiBaselineGeneratedTypeRoots); + customRemovalRoots.UnionWith(generatedPublicDeclarations); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); + customRemovalRoots.UnionWith(GetExistingGeneratedHelperRoots(generatedProviders, generatedInternalDeclarations)); + + // Helper types are rooted after an initial reachability pass so unused infrastructure + // such as change-tracking dictionaries can still be removed when no reachable type needs them. + var generatedDiscriminatorBaseNames = GetGeneratedPersistableModelProxyTypeNames(generatedProviders, publicGraph.Nodes); + var (internalizeCandidates, publicizeCandidates, _) = GetAccessibilityCandidates( + providers, + generatedProviders, + graph, + publicGraph, + customPublicRoots, + customInternalDeclarations, + generatedInternalDeclarations, + generatedDiscriminatorBaseNames); + + var removeCandidates = new HashSet(StringComparer.Ordinal); + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize) + { + // Body-only generated dependencies are needed to avoid deleting helper files, but they do + // not contribute to public API reachability for internalization. + AddGeneratedBodyReferences(providers, graph); + removeCandidates = GetRemovalCandidates( + providers, + generatedProviders, + graph, + customRemovalRoots, + generatedDiscriminatorBaseNames); + } + + _latestResult = new ProviderReferenceMapResult( + internalizeCandidates, + publicizeCandidates, + removeCandidates); + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize) + { + RemoveMethodsFromModelFactory(GetSimpleNames(removeCandidates)); + } + } + + private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates) GetPreWriteAccessibilityCandidates(IReadOnlyList providers) + { + var generatedProviders = GetGeneratedProviders(providers); + var graph = BuildGraph(generatedProviders); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true); + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); + var apiBaselineGeneratedTypeRoots = GetApiBaselineGeneratedTypeRoots(graph.Nodes); + customPublicRoots.UnionWith(apiBaselineGeneratedTypeRoots); + var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarationsFromLastContract(generatedProviders, graph.Nodes); + customPublicRoots.UnionWith(generatedPublicDeclarations); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); + var generatedDiscriminatorBaseNames = new HashSet(StringComparer.Ordinal); + + var (internalizeCandidates, publicizeCandidates, _) = GetAccessibilityCandidates( + providers, + generatedProviders, + graph, + publicGraph, + customPublicRoots, + customInternalDeclarations, + generatedInternalDeclarations, + generatedDiscriminatorBaseNames); + + return (internalizeCandidates, publicizeCandidates); + } + + private static (HashSet InternalizeCandidates, HashSet PublicizeCandidates, HashSet InternalizeHelperRoots) GetAccessibilityCandidates( + IReadOnlyList providers, + IReadOnlyList generatedProviders, + ProviderReferenceGraph graph, + ProviderReferenceGraph publicGraph, + HashSet customPublicRoots, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet generatedDiscriminatorBaseNames) + { + var internalizeReferences = CloneReferences(publicGraph.References); + + // Start from public client and custom/public API roots. Anything public-reachable can + // stay public unless it crosses a custom/internal boundary. + var internalizeRoots = GetRootNames(providers, graph.Nodes, helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, includeUnionVariantRoots: false, includeModelFactorySignatureRoots: true, publicClientRootsOnly: true); + if (ShouldUseUnionVariantFallbackRoots()) + { + AddUnionVariantRoots(internalizeRoots, providers, graph.Nodes); + } + + var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); + internalizeRoots.UnionWith(customPublicRoots); + var internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, internalizeReachableWithoutHelpers, generatedDiscriminatorBaseNames); + internalizeReachableWithoutHelpers = GetReachableTypes(internalizeRoots, internalizeReferences); + var publicizeRoots = new HashSet(internalizeRoots, StringComparer.Ordinal); + var publicApiReferences = CloneReferences(publicGraph.References); + var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers, graph.References); + internalizeRoots.UnionWith(internalizeHelperRoots); + var internalizeDeclaredNodes = GetPostProcessorDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); + var customInternalBoundaryNodes = GetCustomInternalBoundaryNodes(publicGraph, customInternalDeclarations); + var publicizeDeclaredNodes = GetPublicizeDeclaredNodes(generatedProviders, graph.Nodes, internalizeDeclaredNodes); + var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedProviders, generatedInternalDeclarations); + var publicApiTraversalNodes = GetPublicApiTraversalNodes( + internalizeDeclaredNodes, + publicizeDeclaredNodes, + generatedInternalDeclarations, + generatedImplementationInternalDeclarations); + var publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); + var internalizeCandidates = GetInternalizeCandidates( + internalizeDeclaredNodes, + publicizeReachable, + customInternalDeclarations, + generatedInternalDeclarations, + customInternalBoundaryNodes, + customPublicRoots, + publicizeRoots, + graph.Nodes, + internalizeReferences); + AddNestedInternalizeCandidates(generatedProviders, internalizeCandidates, graph.Nodes); + AddInternalOnlyDependencyCandidates( + internalizeDeclaredNodes, + internalizeCandidates, + customInternalDeclarations, + generatedInternalDeclarations, + customPublicRoots, + internalizeReferences, + generatedImplementationInternalDeclarations); + AddNestedInternalizeCandidates(generatedProviders, internalizeCandidates, graph.Nodes); + publicizeRoots.ExceptWith(internalizeCandidates); + publicizeReachable = GetReachableTypes(publicizeRoots, internalizeReferences, publicApiTraversalNodes); + var publicizeRootExclusions = GetRootNames( + providers, + graph.Nodes, + helperRoots: [], + includeModelFactory: true, + includeAdditionalRoots: true, + includeUnionVariantRoots: true, + includeModelFactorySignatureRoots: false, + publicClientRootsOnly: true); + var publicizeCandidates = GetPublicizeCandidates( + publicizeDeclaredNodes, + publicizeReachable, + customInternalDeclarations, + customInternalBoundaryNodes, + internalizeHelperRoots, + publicizeRootExclusions, + generatedInternalDeclarations, + publicizeRoots, + publicApiReferences, + internalizeReferences, + generatedImplementationInternalDeclarations); + return (internalizeCandidates, publicizeCandidates, internalizeHelperRoots); + } + + private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); + private static bool IsPublic(FieldModifiers modifiers) => modifiers.HasFlag(FieldModifiers.Public); + + private static TypeSignatureModifiers MakeInternal(TypeSignatureModifiers modifiers) + => (modifiers & ~(TypeSignatureModifiers.Public | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Internal; + + private static TypeSignatureModifiers MakePublic(TypeSignatureModifiers modifiers) + => (modifiers & ~(TypeSignatureModifiers.Internal | TypeSignatureModifiers.Private | TypeSignatureModifiers.Protected)) | TypeSignatureModifiers.Public; + + private sealed record ProviderReferenceGraph( + HashSet Nodes, + Dictionary> References); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapResult.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapResult.cs new file mode 100644 index 00000000000..eafe1d9d546 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapResult.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.TypeSpec.Generator +{ + internal sealed record ProviderReferenceMapResult( + HashSet InternalizeCandidates, + HashSet PublicizeCandidates, + HashSet RemoveCandidates) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index a329166ee4b..17433758fe5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -22,6 +22,7 @@ public class SourceInputModel public ApiCompatBaseline ApiCompatBaseline { get; } private readonly Lazy> _nameMap; + private readonly Lazy> _customizationTypeProviders; public SourceInputModel(Compilation? customization, Compilation? lastContract) : this(customization, lastContract, ApiCompatBaseline.Empty) @@ -35,6 +36,7 @@ public SourceInputModel(Compilation? customization, Compilation? lastContract, A ApiCompatBaseline = apiCompatBaseline ?? ApiCompatBaseline.Empty; _nameMap = new(PopulateNameMap); + _customizationTypeProviders = new(PopulateCustomizationTypeProviders); } private IReadOnlyDictionary PopulateNameMap() @@ -70,6 +72,35 @@ private IReadOnlyDictionary PopulateNameMap() return FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); } + internal TypeProvider? FindForTypeInLastContractIncludingInternal(string ns, string name, string? declaringTypeName = null) + { + return FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName); + } + + private IReadOnlyList PopulateCustomizationTypeProviders() + { + var providers = new List(); + if (Customization == null) + { + return providers; + } + + foreach (IModuleSymbol module in Customization.Assembly.Modules) + { + foreach (var type in SourceInputHelper.GetSymbols(module.GlobalNamespace)) + { + if (type is INamedTypeSymbol namedTypeSymbol) + { + providers.Add(new NamedTypeSymbolProvider(namedTypeSymbol, Customization)); + } + } + } + + return providers; + } + + internal IReadOnlyList GetCustomizationTypeProviders() => _customizationTypeProviders.Value; + private TypeProvider? FindTypeInCompilation( Compilation? compilation, string ns, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index cd70e60ea9a..01a43a52297 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -200,11 +200,6 @@ protected internal TypeFactory() if (modelProvider != null) { - if (model.Access == "public") - { - CodeModelGenerator.Instance.AddTypeToKeep(modelProvider); - } - CSharpTypeMap[modelProvider.Type] = modelProvider; TypeProvidersByName[modelProvider.Type.Name] = modelProvider; } 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..4800fb26abe 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 @@ -60,7 +60,42 @@ public async Task RemovesInvalidUsings() CollectionAssert.Contains(usings, "System"); } + [Test] + public async Task RemovesInvalidUsingsKeepsFileHeader() + { + MockHelpers.LoadMockGenerator(); + var workspace = new AdhocWorkspace(); + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + name: "TestProj", + assemblyName: "TestProj", + language: LanguageNames.CSharp) + .WithMetadataReferences(new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + var project = workspace.AddProject(projectInfo); + var folder = Helpers.GetAssetFileOrDirectoryPath(false); + project = AddGeneratedDocument( + project, + "RootClient.cs", + "src", + "Generated", + "RootClient.cs", + File.ReadAllText(Path.Join(folder, "RootClient.cs"))); + var postProcessor = new TestPostProcessor("RootClient.cs"); + + var resultProject = await postProcessor.RemoveAsync(project); + var doc = resultProject.Documents.Single(d => d.Name == "RootClient.cs"); + var text = (await doc.GetTextAsync()).ToString(); + + StringAssert.StartsWith("// Copyright (c) Microsoft Corporation. All rights reserved.", text); + StringAssert.Contains("// ", text); + StringAssert.Contains("#nullable disable", text); + StringAssert.DoesNotContain("using Missing.Namespace;", text); + } [Test] public async Task DoesNotRemoveValidUsings() { @@ -289,11 +324,249 @@ public async Task DoesNotRemoveValidAttributes() Assert.AreEqual(Helpers.GetExpectedFromFile().TrimEnd(), output, "The output should match the expected content."); } + [Test] + public async Task ModelFactoryMethodDoesNotKeepModelPublic() + { + MockHelpers.LoadMockGenerator(); + var workspace = new AdhocWorkspace(); + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + name: "TestProj", + assemblyName: "TestProj", + language: LanguageNames.CSharp) + .WithMetadataReferences(new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + + var project = workspace.AddProject(projectInfo); + project = AddGeneratedDocument(project, "RootClient.cs", "src", "Generated", "RootClient.cs", """ +namespace Sample +{ + public partial class RootClient + { + } +} +"""); + project = AddGeneratedDocument(project, "UnusedModel.cs", "src", "Generated", "Models", "UnusedModel.cs", """ +namespace Sample.Models +{ + public partial class UnusedModel + { + } +} +"""); + project = AddGeneratedDocument(project, "SampleModelFactory.cs", "src", "Generated", "SampleModelFactory.cs", """ +namespace Sample +{ + public static partial class SampleModelFactory + { + public static Sample.Models.UnusedModel UnusedModel() => new Sample.Models.UnusedModel(); + } +} +"""); + + var postProcessor = new TestPostProcessor("RootClient.cs", modelFactoryFullName: "Sample.SampleModelFactory"); + + var resultProject = await postProcessor.InternalizeAsync(project); + var modelDoc = resultProject.Documents.Single(d => d.Name == "UnusedModel.cs"); + var modelRoot = await modelDoc.GetSyntaxRootAsync(); + var modelDeclaration = modelRoot!.DescendantNodes().OfType().Single(); + Assert.IsTrue(modelDeclaration.Modifiers.Any(m => m.Text == "internal")); + Assert.IsFalse(modelDeclaration.Modifiers.Any(m => m.Text == "public")); + + var modelFactoryDoc = resultProject.Documents.SingleOrDefault(d => d.Name == "SampleModelFactory.cs"); + if (modelFactoryDoc != null) + { + var modelFactoryRoot = await modelFactoryDoc.GetSyntaxRootAsync(); + Assert.IsFalse(modelFactoryRoot!.DescendantNodes().OfType().Any(), + "Model factory methods for internalized models should be removed."); + } + } + + [TestCase("RequestBodyModel")] + [TestCase("ResponseBodyModel")] + [TestCase("NestedToolConfig")] + [TestCase("BodyOnlyValueKind")] + public async Task BodyOnlyGeneratedTypesDoNotBecomePublic(string bodyOnlyTypeName) + { + var project = CreateProjectWithRootClient(); + var typeDeclaration = bodyOnlyTypeName == "BodyOnlyValueKind" + ? $$""" +namespace Sample.Models +{ + public enum {{bodyOnlyTypeName}} + { + Default + } +} +""" + : $$""" +namespace Sample.Models +{ + public partial class {{bodyOnlyTypeName}} + { + } +} +"""; + project = AddGeneratedDocument(project, $"{bodyOnlyTypeName}.cs", "src", "Generated", "Models", $"{bodyOnlyTypeName}.cs", typeDeclaration); + var factoryExpression = bodyOnlyTypeName == "BodyOnlyValueKind" + ? $"Sample.Models.{bodyOnlyTypeName}.Default" + : $"new Sample.Models.{bodyOnlyTypeName}()"; + project = AddGeneratedDocument(project, "SampleModelFactory.cs", "src", "Generated", "SampleModelFactory.cs", $$""" +namespace Sample +{ + public static partial class SampleModelFactory + { + public static Sample.Models.{{bodyOnlyTypeName}} {{bodyOnlyTypeName}}() => {{factoryExpression}}; + } +} +"""); + + var resultProject = await new TestPostProcessor("RootClient.cs", modelFactoryFullName: "Sample.SampleModelFactory").InternalizeAsync(project); + + await AssertGeneratedTypeInternal(resultProject, $"{bodyOnlyTypeName}.cs", bodyOnlyTypeName); + await AssertModelFactoryMethodRemoved(resultProject, bodyOnlyTypeName); + } + + [Test] + public async Task BodyOnlyNestedGraphDoesNotBecomePublic() + { + var project = CreateProjectWithRootClient(); + project = AddGeneratedDocument(project, "ToolConfig.cs", "src", "Generated", "Models", "ToolConfig.cs", """ +namespace Sample.Models +{ + public partial class ToolConfig + { + public ToolParameter Parameter { get; set; } + } +} +"""); + project = AddGeneratedDocument(project, "ToolParameter.cs", "src", "Generated", "Models", "ToolParameter.cs", """ +namespace Sample.Models +{ + public partial class ToolParameter + { + } +} +"""); + project = AddGeneratedDocument(project, "SampleModelFactory.cs", "src", "Generated", "SampleModelFactory.cs", """ +namespace Sample +{ + public static partial class SampleModelFactory + { + public static Sample.Models.ToolConfig ToolConfig() => new Sample.Models.ToolConfig(); + public static Sample.Models.ToolParameter ToolParameter() => new Sample.Models.ToolParameter(); + } +} +"""); + + var resultProject = await new TestPostProcessor("RootClient.cs", modelFactoryFullName: "Sample.SampleModelFactory").InternalizeAsync(project); + + await AssertGeneratedTypeInternal(resultProject, "ToolConfig.cs", "ToolConfig"); + await AssertGeneratedTypeInternal(resultProject, "ToolParameter.cs", "ToolParameter"); + await AssertModelFactoryMethodRemoved(resultProject, "ToolConfig"); + await AssertModelFactoryMethodRemoved(resultProject, "ToolParameter"); + } + + [Test] + public async Task ClientOptionsNestedEnumDoesNotBecomePublicWhenOptionsIsNotRooted() + { + var project = CreateProjectWithRootClient("public SampleClientOptions Options { get; }"); + project = AddGeneratedDocument(project, "SampleClientOptions.cs", "src", "Generated", "SampleClientOptions.cs", """ +namespace Sample +{ + public partial class SampleClientOptions + { + public enum ServiceVersion + { + V1 + } + } +} +"""); + + var resultProject = await new TestPostProcessor("RootClient.cs").InternalizeAsync(project); + + await AssertGeneratedTypePublic(resultProject, "SampleClientOptions.cs", "SampleClientOptions"); + var optionsDoc = resultProject.Documents.Single(d => d.Name == "SampleClientOptions.cs"); + var root = await optionsDoc.GetSyntaxRootAsync(); + var nestedEnum = root!.DescendantNodes().OfType().Single(); + Assert.IsFalse(nestedEnum.Modifiers.Any(m => m.Text == "public")); + } + + private static Project AddGeneratedDocument(Project project, string name, string folder1, string folder2, string fileName, string text) + => project.AddDocument(name, text, folders: [folder1, folder2], filePath: Path.Join(folder1, folder2, fileName)).Project; + + private static Project AddGeneratedDocument(Project project, string name, string folder1, string folder2, string folder3, string fileName, string text) + => project.AddDocument(name, text, folders: [folder1, folder2, folder3], filePath: Path.Join(folder1, folder2, folder3, fileName)).Project; + + private static Project CreateProjectWithRootClient(string members = "") + { + MockHelpers.LoadMockGenerator(); + var workspace = new AdhocWorkspace(); + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + name: "TestProj", + assemblyName: "TestProj", + language: LanguageNames.CSharp) + .WithMetadataReferences(new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + + var project = workspace.AddProject(projectInfo); + return AddGeneratedDocument(project, "RootClient.cs", "src", "Generated", "RootClient.cs", $$""" +namespace Sample +{ + public partial class RootClient + { + {{members}} + } +} +"""); + } + + private static async Task AssertGeneratedTypeInternal(Project project, string documentName, string typeName) + { + var declaration = await GetTypeDeclaration(project, documentName, typeName); + Assert.IsTrue(declaration.Modifiers.Any(m => m.Text == "internal"), $"{typeName} should be internal."); + Assert.IsFalse(declaration.Modifiers.Any(m => m.Text == "public"), $"{typeName} should not be public."); + } + + private static async Task AssertGeneratedTypePublic(Project project, string documentName, string typeName) + { + var declaration = await GetTypeDeclaration(project, documentName, typeName); + Assert.IsTrue(declaration.Modifiers.Any(m => m.Text == "public"), $"{typeName} should be public."); + } + + private static async Task GetTypeDeclaration(Project project, string documentName, string typeName) + { + var document = project.Documents.Single(d => d.Name == documentName); + var root = await document.GetSyntaxRootAsync(); + return root!.DescendantNodes().OfType().Single(t => t.Identifier.Text == typeName); + } + + private static async Task AssertModelFactoryMethodRemoved(Project project, string methodName) + { + var modelFactoryDoc = project.Documents.SingleOrDefault(d => d.Name == "SampleModelFactory.cs"); + if (modelFactoryDoc == null) + { + return; + } + + var modelFactoryRoot = await modelFactoryDoc.GetSyntaxRootAsync(); + Assert.IsFalse(modelFactoryRoot!.DescendantNodes().OfType().Any(m => m.Identifier.Text == methodName), + $"Model factory method {methodName} should be removed."); + } + private class TestPostProcessor : PostProcessor { private readonly string _rootFile; - public TestPostProcessor(string rootFile, IEnumerable? nonRootTypes = null) : base([], additionalNonRootTypeNames: nonRootTypes) + public TestPostProcessor(string rootFile, IEnumerable? additionalRootTypeNames = null, IEnumerable? nonRootTypes = null, string? modelFactoryFullName = null) : base((additionalRootTypeNames ?? []).ToHashSet(), modelFactoryFullName: modelFactoryFullName, additionalNonRootTypeNames: nonRootTypes) { _rootFile = rootFile; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs new file mode 100644 index 00000000000..572d6f590ac --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsingsKeepsFileHeader/RootClient.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using Missing.Namespace; + +namespace Sample +{ + public partial class RootClient + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoriesCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoriesCustomizationTests.cs index 1bdf4020167..c35f1968d76 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoriesCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoriesCustomizationTests.cs @@ -177,6 +177,32 @@ public async Task OmitsModelFactoryMethodIfParamTypeInternal() Assert.IsNull(modelFactory); } + // This test validates that a derived model customized to be internal does not get a + // public model factory method just because its base model remains public. + [Test] + public async Task OmitsModelFactoryMethodIfDerivedModelTypeInternal() + { + var baseModel = InputFactory.Model( + "baseModel", + properties: [InputFactory.Property("BaseProp", InputPrimitiveType.String)]); + var derivedModel = InputFactory.Model( + "derivedModel", + properties: [InputFactory.Property("DerivedProp", InputPrimitiveType.String)], + baseModel: baseModel); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [baseModel, derivedModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + var csharpGen = new CSharpGen(); + + await csharpGen.ExecuteAsync(); + + var modelFactory = mockGenerator.Object.OutputLibrary.TypeProviders.SingleOrDefault(t => t is ModelFactoryProvider); + Assert.IsNotNull(modelFactory); + CollectionAssert.Contains(modelFactory!.Methods.Select(m => m.Signature.Name), "BaseModel"); + CollectionAssert.DoesNotContain(modelFactory.Methods.Select(m => m.Signature.Name), "DerivedModel"); + } + [TestCase(true)] [TestCase(false)] public async Task CanCustomizeModelFullConstructor(bool extraParameters) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoriesCustomizationTests/OmitsModelFactoryMethodIfDerivedModelTypeInternal/DerivedModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoriesCustomizationTests/OmitsModelFactoryMethodIfDerivedModelTypeInternal/DerivedModel.cs new file mode 100644 index 00000000000..bdb2034f5f0 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoriesCustomizationTests/OmitsModelFactoryMethodIfDerivedModelTypeInternal/DerivedModel.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Sample.Models +{ + internal partial class DerivedModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ClientCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ClientCustomizationTests.cs index 7f3ea5cd1ff..d96e98a9d8e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ClientCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ClientCustomizationTests.cs @@ -405,7 +405,7 @@ private class ClientTypeProvider : TypeProvider public MethodProvider[] MethodProviders { get; set; } = []; public ConstructorProvider[] ConstructorProviders { get; set; } = []; - protected override string BuildRelativeFilePath() => "."; + protected override string BuildRelativeFilePath() => $"{Name}.cs"; protected override string BuildName() => "MockInputClient"; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 9f9945a2360..a30bd15aa29 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -1524,7 +1524,7 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public void PublicModelsAreIncludedInAdditionalRootTypes() + public void PublicModelsAreNotIncludedInAdditionalRootTypes() { var inputModel = InputFactory.Model( "MockInputModel", @@ -1537,7 +1537,7 @@ public void PublicModelsAreIncludedInAdditionalRootTypes() Assert.IsNotNull(modelProvider); var rootTypes = CodeModelGenerator.Instance.AdditionalRootTypes; - Assert.IsTrue(rootTypes.Contains("Sample.Models.MockInputModel")); + Assert.IsFalse(rootTypes.Contains("Sample.Models.MockInputModel")); } [Test] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs index 9ceee883bd4..4715686fce7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; @@ -370,6 +371,54 @@ public async Task ValidatePartialMethodWithBodyIsNotDetectedAsPartialDeclaration Assert.IsFalse(doIt.IsPartialMethod, "Partial methods with bodies should not be treated as customization signals."); } + [Test] + public void BodyDependenciesIncludeUsingNamespaceCandidatesForUnresolvedTypeSyntax() + { + var tree = CSharpSyntaxTree.ParseText(""" + using Sample.Models; + + namespace Sample + { + public partial class CustomClient + { + public void Test(object response) + { + ReferencedModel result = (ReferencedModel)response; + } + } + } + """); + var compilation = CSharpCompilation.Create( + "TestAssembly", + [tree], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var symbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "CustomClient")!; + var provider = new NamedTypeSymbolProvider(symbol, compilation); + + Assert.IsTrue(provider.BodyDependencyTypes.Any(type => type.FullyQualifiedName == "Sample.Models.ReferencedModel")); + } + + [Test] + public void MetadataNamePreservesGenericArity() + { + var tree = CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + public partial class CustomModel + { + } + } + """); + var compilation = CSharpCompilation.Create( + "TestAssembly", + [tree], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var symbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "CustomModel`1")!; + var provider = new NamedTypeSymbolProvider(symbol, compilation); + + Assert.AreEqual("Sample.Models.CustomModel`1", provider.MetadataName); + } + // Operator signatures parsed from a customization partial must compare equal to the corresponding generated signatures. [Test] public async Task ValidateOperatorSignaturesMatchGenerated() diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/DocumentationModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/DocumentationModelFactory.cs index 8918ac1946c..4440dc1b6a0 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/DocumentationModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/DocumentationModelFactory.cs @@ -8,7 +8,6 @@ namespace Documentation { public static partial class DocumentationModelFactory { - public static BulletPointsModel BulletPointsModel(BulletPointsEnum prop = default) => throw null; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/ParametersBasicModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/ParametersBasicModelFactory.cs index 06d44d34bc1..c17dda5eaec 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/ParametersBasicModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/ParametersBasicModelFactory.cs @@ -3,7 +3,6 @@ #nullable disable using Parameters.Basic._ExplicitBody; -using Parameters.Basic._ImplicitBody; namespace Parameters.Basic { diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/ParametersSpreadModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/ParametersSpreadModelFactory.cs index 775c933bc6b..4e494c2f2ec 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/ParametersSpreadModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/ParametersSpreadModelFactory.cs @@ -2,8 +2,6 @@ #nullable disable -using System.Collections.Generic; -using Parameters.Spread._Alias; using Parameters.Spread._Model; namespace Parameters.Spread diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/PayloadMultiPartModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/PayloadMultiPartModelFactory.cs index 6d036b01c86..e5c87b994a6 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/PayloadMultiPartModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/PayloadMultiPartModelFactory.cs @@ -2,7 +2,6 @@ #nullable disable -using System; using System.ClientModel; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/PayloadPageableModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/PayloadPageableModelFactory.cs index 0b66e798a05..1b7b8eefa6f 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/PayloadPageableModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/PayloadPageableModelFactory.cs @@ -2,18 +2,12 @@ #nullable disable -using System; -using System.Collections.Generic; -using Payload.Pageable._PageSize; -using Payload.Pageable._ServerDrivenPagination; using Payload.Pageable._ServerDrivenPagination.AlternateInitialVerb; -using Payload.Pageable._ServerDrivenPagination.ContinuationToken; namespace Payload.Pageable { public static partial class PayloadPageableModelFactory { - public static Pet Pet(string id = default, string name = default) => throw null; public static XmlPet XmlPet(string id = default, string name = default) => throw null; diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/SpecialWordsModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/SpecialWordsModelFactory.cs index 1abb2114c9a..43aca8db259 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/SpecialWordsModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/SpecialWordsModelFactory.cs @@ -2,10 +2,8 @@ #nullable disable -using System.Collections.Generic; using SpecialWords._ModelProperties; using SpecialWords._Models; -using SpecialWords._ReservedOperationBodyParams; namespace SpecialWords {