From 980c6ad9a74b2edc540c6fefc368d112c50e9322 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 14 Jul 2026 01:38:22 +0000 Subject: [PATCH 1/8] perf(csharp): use provider reference map for generation Replace Roslyn-based generated-source reference analysis with provider metadata while preserving generated contracts and customization dependencies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../BinaryContentHelperDefinition.cs | 3 + .../src/Providers/ClientOptionsProvider.cs | 28 + .../src/Providers/ClientProvider.cs | 36 + .../Providers/CollectionResultDefinition.cs | 16 + .../ModelReaderWriterContextDefinition.cs | 85 +- .../ModelSerializationExtensionsDefinition.cs | 3 + .../MrwSerializationTypeDefinition.Xml.cs | 6 +- .../MrwSerializationTypeDefinition.cs | 74 +- ...ultipartFormDataSerializationDefinition.cs | 3 + .../src/Providers/PipelineRequestProvider.cs | 4 +- .../src/Providers/RestClientProvider.cs | 184 ++- .../SerializationFormatDefinition.cs | 2 + .../src/Snippets/HttpRequestApiSnippets.cs | 4 +- .../Providers/ClientOptionsProviderTests.cs | 22 + .../ClientProviderCustomizationTests.cs | 3 +- ...ModelReaderWriterContextDefinitionTests.cs | 295 +++- .../ModelWithProjectedProperty.cs | 12 + .../SampleContext.cs | 9 + .../SystemObjectModelSerializationTests.cs | 34 - .../RestClientProviderTests.cs | 200 ++- .../TestClient.cs | 20 + .../ClientBodyDependencyReferenceMapTests.cs | 969 +++++++++++++ .../test/TestHelpers/MockHelpers.cs | 17 +- .../src/CSharpGen.cs | 109 +- .../src/CodeModelGenerator.cs | 2 +- .../PostProcessing/GeneratedCodeWorkspace.cs | 29 +- .../src/PostProcessing/PostProcessor.cs | 669 --------- .../src/PostProcessing/ReferenceMap.cs | 70 - .../src/PostProcessing/ReferenceMapBuilder.cs | 306 ---- .../src/Primitives/TypeProviderWriter.cs | 50 +- .../src/Providers/CanonicalTypeProvider.cs | 34 +- .../src/Providers/ModelFactoryProvider.cs | 4 + .../src/Providers/NamedTypeSymbolProvider.cs | 402 ++++++ .../src/Providers/TypeProvider.cs | 82 +- ...iderReferenceMapAnalyzer.BodyReferences.cs | 406 ++++++ ...ProviderReferenceMapAnalyzer.Candidates.cs | 396 ++++++ ...derReferenceMapAnalyzer.CustomCodeRoots.cs | 382 +++++ ...oviderReferenceMapAnalyzer.GraphBuilder.cs | 183 +++ .../ProviderReferenceMapAnalyzer.Helpers.cs | 450 ++++++ ...ReferenceMapAnalyzer.ReferenceTraversal.cs | 194 +++ ...viderReferenceMapAnalyzer.RootSelection.cs | 184 +++ ...renceMapAnalyzer.TypeReferenceCollector.cs | 193 +++ .../ProviderReferenceMapAnalyzer.cs | 378 +++++ .../ProviderReferenceMapResult.cs | 14 + .../ProviderReferenceMapSession.cs | 28 + .../src/SourceInput/SourceInputHelper.cs | 18 +- .../src/SourceInput/SourceInputModel.cs | 101 +- .../src/StartUp/GeneratorHandler.cs | 8 +- .../src/Statements/XmlDocStatement.cs | 2 + .../src/TypeFactory.cs | 39 +- .../src/Utilities/TypeSymbolExtensions.cs | 61 +- .../test/CSharpGenTests.cs | 24 + .../test/OutputLibraryVisitorTests.cs | 63 + .../test/PostProcessing/PostProcessorTests.cs | 307 ---- .../DoesNotRemoveValidAttributes.cs | 11 - .../DoesNotRemoveValidAttributes.cs | 15 - .../DoesNotRemoveValidAttributes/Model.cs | 6 - .../DoesNotRemoveValidAttributes/RootClass.cs | 7 - .../DoesNotRemoveValidUsings.cs | 11 - .../DoesNotRemoveValidUsings/Model.cs | 6 - .../RemovesInvalidAttributes.cs | 10 - .../RemovesInvalidAttributes/Model.cs | 6 - .../RemovesInvalidAttributes.cs | 15 - .../RemovesInvalidAttributes/RootClass.cs | 7 - ...nvalidAttributesAndKeepsValidAttributes.cs | 11 - .../Model.cs | 6 - ...nvalidAttributesAndKeepsValidAttributes.cs | 16 - .../RootClass.cs | 7 - ...AttributesAndKeepsValidAttributesNoDocs.cs | 8 - .../Model.cs | 6 - ...AttributesAndKeepsValidAttributesNoDocs.cs | 13 - .../RootClass.cs | 7 - .../RemovesInvalidUsings/Model.cs | 6 - .../RemovesInvalidUsings.cs | 11 - .../EnumProviders/EnumProviderTests.cs | 4 +- .../ModelFactoriesCustomizationTests.cs | 26 + .../DerivedModel.cs | 9 + .../ClientCustomizationTests.cs | 2 +- .../ModelProviders/ModelProviderTests.cs | 15 +- .../NamedTypeSymbolProviderTests.cs | 63 + .../CustomClient.cs | 12 + .../CustomModel.cs | 6 + .../CustomApi.cs | 9 + .../CustomApi.cs | 12 + .../CustomApi.cs | 9 + .../NestedTypes.cs | 19 + .../test/Providers/TypeProviderTests.cs | 2 +- .../ProviderReferenceMapAnalyzerTests.cs | 1246 +++++++++++++++++ .../LastContract.cs | 17 + .../test/StartUp/GeneratorHandlerTests.cs | 8 +- .../test/TestHelpers/TestOutputLibrary.cs | 5 + .../test/TypeFactoryTests.cs | 11 + .../GenericContainer.cs | 6 + .../Utilities/TypeSymbolExtensionsTests.cs | 41 + .../test/Writers/TypeProviderWriterTests.cs | 36 + .../Models/ParametersBasicContext.cs | 1 - .../Generated/ParametersBasicModelFactory.cs | 1 - .../Models/ParametersSpreadContext.cs | 1 - .../Generated/ParametersSpreadModelFactory.cs | 2 - .../Models/PayloadMultiPartContext.cs | 1 - .../Generated/PayloadMultiPartModelFactory.cs | 1 - .../Models/PayloadPageableContext.cs | 3 - .../Generated/PayloadPageableModelFactory.cs | 5 - .../Generated/Models/SpecialWordsContext.cs | 1 - .../src/Generated/SpecialWordsModelFactory.cs | 2 - 105 files changed, 7155 insertions(+), 1823 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/CustomProjectionPropertiesDoNotAddBuildableTypes/ModelWithProjectedProperty.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/CustomizedBuildableAttributesAreNotRegenerated/SampleContext.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeOrderPreservedFromLastContractViewWithNamedBody/TestClient.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ReferenceMap/ClientBodyDependencyReferenceMapTests.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ReferenceMap.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ReferenceMapBuilder.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapResult.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapSession.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/DoesNotRemoveValidAttributes.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/Model.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/RootClass.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidUsings/DoesNotRemoveValidUsings.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidUsings/Model.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/Model.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/RemovesInvalidAttributes.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/RootClass.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/Model.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/RemovesInvalidAttributesAndKeepsValidAttributes.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/RootClass.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/Model.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/RootClass.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsings/Model.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsings/RemovesInvalidUsings.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoriesCustomizationTests/OmitsModelFactoryMethodIfDerivedModelTypeInternal/DerivedModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/BodyDependenciesIncludeUsingNamespaceCandidatesForUnresolvedTypeSyntax/CustomClient.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/MetadataNamePreservesGenericArity/CustomModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/PublicInterfaceMemberSignatureDependenciesAreIncluded/CustomApi.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/PublicNestedMemberSignatureDependenciesAreIncluded/CustomApi.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/SourceInputHelperYieldsNestedSymbols/CustomApi.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/SourceInputLookupUsesFullNestedDeclaringTypeName/NestedTypes.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/ModelFactorySignatureRootsDoNotDependOnModelName/LastContract.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TestData/TypeSymbolExtensionsTests/TypeParameterDoesNotResolveContainingGenericType/GenericContainer.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/BinaryContentHelperDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/BinaryContentHelperDefinition.cs index a2ee813e298..936a8501435 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/BinaryContentHelperDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/BinaryContentHelperDefinition.cs @@ -35,6 +35,9 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs"); + protected override IReadOnlyList BuildBodyDependencyTypes() => + [ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.Type]; + protected override MethodProvider[] BuildMethods() { var methods = new List 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..82c5820812c 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 @@ -29,6 +29,7 @@ public class ClientOptionsProvider : TypeProvider private readonly ClientProvider _clientProvider; private readonly Dictionary? _serviceVersionsEnums; private static ClientOptionsProvider? _singletonInstance; + private static readonly List _singletonClientProviders = []; internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientProvider) { @@ -80,9 +81,14 @@ public static ClientOptionsProvider CreateClientOptionsProvider(InputClient inpu // Use singleton instance if (_singletonInstance == null) { + _singletonClientProviders.Clear(); // Create singleton with namespace-based naming _singletonInstance = new ClientOptionsProvider(inputClient, clientProvider); } + if (!_singletonClientProviders.Contains(clientProvider)) + { + _singletonClientProviders.Add(clientProvider); + } return _singletonInstance; } @@ -248,6 +254,28 @@ private static bool UseSingletonInstance(InputClient inputClient) protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs"); + // TODO https://github.com/microsoft/typespec/issues/11181: Remove this once generated source parity no longer depends on internal ClientOptions XML docs. + protected override bool ShouldWriteTypeXmlDocs => CustomCodeView is null; + + protected override TypeSignatureModifiers BuildDeclarationModifiers() + { + if (this == _singletonInstance) + { + TypeSignatureModifiers singletonAccessibility = TypeSignatureModifiers.None; + foreach (var clientProvider in _singletonClientProviders) + { + singletonAccessibility |= GetAccessibilityModifiers(clientProvider.DeclarationModifiers); + } + + return singletonAccessibility; + } + + return GetAccessibilityModifiers(_clientProvider.DeclarationModifiers); + } + + private static TypeSignatureModifiers GetAccessibilityModifiers(TypeSignatureModifiers modifiers) + => modifiers & (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/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 947b717b5ec..46b67fe6146 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 @@ -426,6 +426,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 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/CollectionResultDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/CollectionResultDefinition.cs index 62f28d86c8d..b734d9a6529 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/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 947302cbe17..fe07fe05573 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; @@ -32,14 +33,22 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override CSharpType BuildBaseType() => typeof(ModelReaderWriterContext); + protected override bool ShouldAnalyzeAttributesInReferenceMap => false; + protected override IReadOnlyList BuildAttributes() { var attributes = new Dictionary(); + var customizedBuildableTypes = GetCustomizedBuildableTypes(); // Add ModelReaderWriterBuildableAttribute for all IPersistableModel types (HashSet buildableTypes, HashSet buildableProviders) = CollectBuildableTypes(); foreach (var type in buildableTypes) { + if (customizedBuildableTypes.Contains(GetTypeIdentity(type))) + { + continue; + } + // Use the full attribute type name to ensure proper compilation var attributeType = new CSharpType(typeof(ModelReaderWriterBuildableAttribute)); var attributeStatement = new AttributeStatement(attributeType, TypeOf(type)); @@ -56,6 +65,11 @@ protected override IReadOnlyList BuildAttributes() } foreach (var provider in buildableProviders) { + if (!ShouldWriteProvider(provider) || customizedBuildableTypes.Contains(GetTypeIdentity(provider.Type))) + { + continue; + } + // Use the full attribute type name to ensure proper compilation var attributeType = new CSharpType(typeof(ModelReaderWriterBuildableAttribute)); var attributeStatement = new AttributeStatement(attributeType, TypeOf(provider.Type)); @@ -76,6 +90,39 @@ protected override IReadOnlyList BuildAttributes() return attributes.OrderBy(a => GetSimpleTypeName(a.Key)).Select(kvp => kvp.Value).ToList(); } + private HashSet GetCustomizedBuildableTypes() + { + var customizedTypes = new HashSet(StringComparer.Ordinal); + foreach (var attribute in CustomCodeView?.Attributes ?? []) + { + if (!string.Equals( + attribute.Type.FullyQualifiedName, + typeof(ModelReaderWriterBuildableAttribute).FullName, + StringComparison.Ordinal)) + { + continue; + } + + foreach (var argument in attribute.Arguments) + { + if (argument is TypeOfExpression typeOf) + { + customizedTypes.Add(GetTypeIdentity(typeOf.Type)); + } + } + } + + return customizedTypes; + } + + private static string GetTypeIdentity(CSharpType type) + { + var name = type.FullyQualifiedName.TrimStart('.'); + return type.Arguments.Count == 0 + ? name + : $"{name}<{string.Join(",", type.Arguments.Select(GetTypeIdentity))}>"; + } + /// /// Collects all types that implement IPersistableModel, including all models and their properties /// that are also IPersistableModel types, recursively without duplicates. @@ -87,7 +134,6 @@ protected override IReadOnlyList BuildAttributes() var visitedBaseProviders = new HashSet(ReferenceEqualityComparer.Instance); var buildableProviders = new HashSet(s_typeProviderNameComparer); var buildableTypes = new HashSet(s_cSharpTypeNameComparer); - // Base-model traversal can encounter equivalent provider instances that are not reference-equal to // the output-library roots, so keep the output-library provider set name-comparable. var contextEligibleOutputProviders = new HashSet( @@ -97,8 +143,13 @@ protected override IReadOnlyList BuildAttributes() // Process each output-library provider recursively to discover types from methods and properties. foreach (var provider in contextEligibleOutputProviders) { + if (!ShouldWriteProvider(provider)) + { + continue; + } + // Only output-library providers get standalone context entries. - if (ImplementsModelReaderWriter(provider)) + if (ShouldAddStandaloneBuildableProvider(provider)) { buildableProviders.Add(provider); } @@ -190,9 +241,10 @@ private void CollectBuildableTypesRecursiveCore( { // Traverse base model properties for discoverable types, but do not add the base model // itself as a context entry unless it was in the output-library seed set. - if (visitedBaseProviders.Add(modelProvider.BaseModelProvider)) + var baseModelProvider = modelProvider.BaseModelProvider; + if (visitedBaseProviders.Add(baseModelProvider)) { - CollectBuildableTypesRecursiveCore(modelProvider.BaseModelProvider, visitedTypes, visitedTypeProviders, visitedBaseProviders, buildableProviders, buildableTypes); + CollectBuildableTypesRecursiveCore(baseModelProvider, visitedTypes, visitedTypeProviders, visitedBaseProviders, buildableProviders, buildableTypes); } } else @@ -220,6 +272,11 @@ private void CollectBuildableTypesFromFrameworkType( try { + if (!IsResolvableBuildableType(frameworkType)) + { + return; + } + buildableTypes.Add(frameworkType.FrameworkType); var type = frameworkType.FrameworkType; var properties = type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); @@ -426,6 +483,26 @@ private static bool ImplementsModelReaderWriter(TypeProvider typeProvider) return false; } + private static bool ShouldAddStandaloneBuildableProvider(TypeProvider provider) + { + var isResolvable = IsResolvableBuildableType(provider.Type); + var implementsMrw = ImplementsModelReaderWriter(provider); + var hasWritableSerialization = HasWritableModelReaderWriterSerialization(provider); + return isResolvable && implementsMrw && hasWritableSerialization; + } + + private static bool HasWritableModelReaderWriterSerialization(TypeProvider provider) + { + if (provider is not ModelProvider) + { + return true; + } + + return provider.SerializationProviders + .OfType() + .Any(ShouldWriteProvider); + } + private static void AddAttributeForType( Dictionary attributes, AttributeStatement attributeStatement, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.cs index f6f6cebbd49..5d1b2102001 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.cs @@ -108,6 +108,9 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override string BuildName() => "ModelSerializationExtensions"; + protected override IReadOnlyList BuildBodyDependencyTypes() => + [new TypeFormattersDefinition().Type]; + protected override FieldProvider[] BuildFields() { return [WireOptionsField, _jsonDocumentOptionsField, .. BuildXmlFields()]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Xml.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Xml.cs index 5d76b8f44a1..2b50fff372e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Xml.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Xml.cs @@ -67,7 +67,7 @@ private MethodProvider BuildXmlModelWriteCoreMethod() MethodSignatureModifiers modifiers = _isStruct ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Internal | MethodSignatureModifiers.Virtual; - if (_shouldOverrideXmlMethods) + if (_shouldOverrideMethods) { modifiers = MethodSignatureModifiers.Internal | MethodSignatureModifiers.Override; } @@ -81,7 +81,7 @@ private MethodProvider BuildXmlModelWriteCoreMethod() private MethodBodyStatement[] BuildXmlModelWriteCoreMethodBody() { - var categorizedProperties = _shouldOverrideXmlMethods + var categorizedProperties = _shouldOverrideMethods ? CategorizedXmlProperties : AllCategorizedXmlProperties; var statements = new List @@ -90,7 +90,7 @@ private MethodBodyStatement[] BuildXmlModelWriteCoreMethodBody() MethodBodyStatement.EmptyLine }; - if (_shouldOverrideXmlMethods) + if (_shouldOverrideMethods) { statements.Add(Base.Invoke(XmlModelWriteCoreMethodName, _xmlWriterParameter, _serializationOptionsParameter).Terminate()); } 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 8202a2af405..c93e46c6173 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 @@ -53,16 +53,10 @@ public partial class MrwSerializationTypeDefinition : TypeProvider private readonly ScopedApi _mrwOptionsParameterSnippet; private readonly ScopedApi _jsonElementParameterSnippet; private readonly ScopedApi _isNotEqualToWireConditionSnippet; - // These interface types depend on _model.Type. Build them lazily so we do not cache a - // CSharpType before delayed base model resolution has updated the model's inheritance. - private CSharpType? _jsonModelTInterfaceValue; - private CSharpType _jsonModelTInterface => _jsonModelTInterfaceValue ??= new CSharpType(typeof(IJsonModel<>), SerializationInterfaceType.Type); - private CSharpType? _jsonModelObjectInterface; - private CSharpType? JsonModelObjectInterface => _isStruct ? _jsonModelObjectInterface ??= (CSharpType)typeof(IJsonModel) : null; - private CSharpType? _persistableModelTInterfaceValue; - private CSharpType _persistableModelTInterface => _persistableModelTInterfaceValue ??= new CSharpType(typeof(IPersistableModel<>), SerializationInterfaceType.Type); - private CSharpType? _persistableModelObjectInterface; - private CSharpType? PersistableModelObjectInterface => _isStruct ? _persistableModelObjectInterface ??= (CSharpType)typeof(IPersistableModel) : null; + private readonly CSharpType _jsonModelTInterface; + private readonly CSharpType? _jsonModelObjectInterface; + private readonly CSharpType _persistableModelTInterface; + private readonly CSharpType? _persistableModelObjectInterface; private readonly ModelProvider _model; private readonly InputModelType _inputModel; private readonly FieldProvider? _rawDataField; @@ -73,20 +67,10 @@ public partial class MrwSerializationTypeDefinition : TypeProvider private readonly bool _supportsXml; private ConstructorProvider? _serializationConstructor; // Flag to determine if the model should override the serialization methods - private bool? _shouldOverrideMethods; - private bool ShouldOverrideMethods => _shouldOverrideMethods ??= _model.BaseModelProvider != null && !_isStruct; - private bool? _shouldSkipSerializationMethodOverrides; - private bool ShouldSkipSerializationMethodOverrides => _shouldSkipSerializationMethodOverrides ??= ShouldSkipDerivedSerializationMethodOverrides(_model.BaseModelProvider); - private readonly bool _shouldOverrideXmlMethods; + private readonly bool _shouldOverrideMethods; + private readonly bool _shouldSkipDerivedSerializationMethodOverrides; private readonly Lazy _additionalProperties; - // Unknown discriminator models use their base model as the serialization interface type. - // This can also touch model.Type, so defer it until serialization method/interface emission. - private TypeProvider SerializationInterfaceType => _serializationInterfaceType ??= _inputModel.IsUnknownDiscriminatorModel - ? ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(_inputModel.BaseModel!)! - : _model; - private TypeProvider? _serializationInterfaceType; - private CSharpType RootType => _rootType ??= GetRootModelType(); private CSharpType? _rootType; @@ -100,10 +84,17 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m _isStruct = _model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Struct); _supportsXml = inputModel.Usage.HasFlag(InputModelTypeUsage.Xml); _supportsJson = inputModel.Usage.HasFlag(InputModelTypeUsage.Json) || !_supportsXml; - _shouldOverrideXmlMethods = _model.BaseModelProvider != null && !_isStruct; + // Initialize the serialization interfaces + var interfaceType = inputModel.IsUnknownDiscriminatorModel ? ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel.BaseModel!)! : _model; + _jsonModelTInterface = new CSharpType(typeof(IJsonModel<>), interfaceType.Type); + _jsonModelObjectInterface = _isStruct ? (CSharpType)typeof(IJsonModel) : null; + _persistableModelTInterface = new CSharpType(typeof(IPersistableModel<>), interfaceType.Type); + _persistableModelObjectInterface = _isStruct ? (CSharpType)typeof(IPersistableModel) : null; _rawDataField = _model.Fields.FirstOrDefault(f => f.Name == AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName); _additionalBinaryDataProperty = new(GetAdditionalBinaryDataPropertiesProp); _additionalProperties = new(() => [.. _model.Properties.Where(p => p.IsAdditionalProperties)]); + _shouldOverrideMethods = _model.BaseModelProvider != null && !_isStruct; + _shouldSkipDerivedSerializationMethodOverrides = ShouldSkipDerivedSerializationMethodOverrides(_model.BaseModelProvider); _utf8JsonWriterSnippet = _utf8JsonWriterParameter.As(); _mrwOptionsParameterSnippet = _serializationOptionsParameter.As(); _jsonElementParameterSnippet = _jsonElementDeserializationParam.As(); @@ -126,6 +117,13 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m protected override CSharpType? BuildBaseType() => _model.BaseType; + protected override IReadOnlyList BuildHelperDependencyTypes() => _rawDataField != null || _additionalProperties.Value.Length > 0 + ? [ScmCodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType] + : []; + + protected override IReadOnlyList BuildBodyDependencyTypes() => + [new OptionalDefinition().Type, ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.Type]; + protected override SuppressionStatement[] BuildDisabledFileWarnings() { if (_model.CanonicalView.Properties.Any(p => ScmModelProvider.IsFileBinaryContentType(p.Type))) @@ -438,19 +436,17 @@ protected override CSharpType[] BuildImplements() if (_supportsJson) { interfaces.Add(_jsonModelTInterface); - var jsonModelObjectInterface = JsonModelObjectInterface; - if (jsonModelObjectInterface != null) + if (_jsonModelObjectInterface != null) { - interfaces.Add(jsonModelObjectInterface); + interfaces.Add(_jsonModelObjectInterface); } } else if (_supportsXml) { interfaces.Add(_persistableModelTInterface); - var persistableModelObjectInterface = PersistableModelObjectInterface; - if (persistableModelObjectInterface != null) + if (_persistableModelObjectInterface != null) { - interfaces.Add(persistableModelObjectInterface); + interfaces.Add(_persistableModelObjectInterface); } } @@ -480,7 +476,7 @@ internal MethodProvider BuildJsonModelWriteMethodObjectDeclaration() var castToT = This.CastTo(_jsonModelTInterface); return new MethodProvider ( - new MethodSignature(nameof(IJsonModel.Write), null, MethodSignatureModifiers.None, null, null, [_utf8JsonWriterParameter, _serializationOptionsParameter], ExplicitInterface: JsonModelObjectInterface), + new MethodSignature(nameof(IJsonModel.Write), null, MethodSignatureModifiers.None, null, null, [_utf8JsonWriterParameter, _serializationOptionsParameter], ExplicitInterface: _jsonModelObjectInterface), castToT.Invoke(nameof(IJsonModel.Write), [_utf8JsonWriterParameter, _serializationOptionsParameter]), this ); @@ -495,7 +491,7 @@ internal MethodProvider BuildJsonModelCreateMethodObjectDeclaration() var castToT = This.CastTo(_jsonModelTInterface); return new MethodProvider ( - new MethodSignature(nameof(IJsonModel.Create), null, MethodSignatureModifiers.None, typeof(object), null, [_utf8JsonReaderParameter, _serializationOptionsParameter], ExplicitInterface: JsonModelObjectInterface), + new MethodSignature(nameof(IJsonModel.Create), null, MethodSignatureModifiers.None, typeof(object), null, [_utf8JsonReaderParameter, _serializationOptionsParameter], ExplicitInterface: _jsonModelObjectInterface), castToT.Invoke(nameof(IJsonModel.Create), [_utf8JsonReaderParameter.AsArgument(), _serializationOptionsParameter]), this ); @@ -511,7 +507,7 @@ internal MethodProvider BuildPersistableModelWriteMethodObjectDeclaration() var returnType = typeof(BinaryData); return new MethodProvider ( - new MethodSignature(nameof(IPersistableModel.Write), null, MethodSignatureModifiers.None, returnType, null, [_serializationOptionsParameter], ExplicitInterface: PersistableModelObjectInterface), + new MethodSignature(nameof(IPersistableModel.Write), null, MethodSignatureModifiers.None, returnType, null, [_serializationOptionsParameter], ExplicitInterface: _persistableModelObjectInterface), castToT.Invoke(nameof(IPersistableModel.Write), [_serializationOptionsParameter]), this ); @@ -527,7 +523,7 @@ internal MethodProvider BuildPersistableModelCreateMethodObjectDeclaration() var returnType = typeof(object); return new MethodProvider ( - new MethodSignature(nameof(IPersistableModel.Create), null, MethodSignatureModifiers.None, returnType, null, [_dataParameter, _serializationOptionsParameter], ExplicitInterface: PersistableModelObjectInterface), + new MethodSignature(nameof(IPersistableModel.Create), null, MethodSignatureModifiers.None, returnType, null, [_dataParameter, _serializationOptionsParameter], ExplicitInterface: _persistableModelObjectInterface), castToT.Invoke(nameof(IPersistableModel.Create), [_dataParameter, _serializationOptionsParameter]), this ); @@ -541,7 +537,7 @@ internal MethodProvider BuildJsonModelWriteCoreMethod() MethodSignatureModifiers modifiers = _isStruct ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (ShouldOverrideMethods) + if (_shouldOverrideMethods) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -563,7 +559,7 @@ internal MethodProvider BuildPersistableModelWriteCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (ShouldOverrideMethods && !ShouldSkipSerializationMethodOverrides) + if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -587,7 +583,7 @@ internal MethodProvider BuildPersistableModelCreateCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (ShouldOverrideMethods && !ShouldSkipSerializationMethodOverrides) + if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -635,7 +631,7 @@ internal MethodProvider BuildJsonModelCreateCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (ShouldOverrideMethods && !ShouldSkipSerializationMethodOverrides) + if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -807,7 +803,7 @@ internal MethodProvider BuildPersistableModelGetFormatFromOptionsObjectDeclarati // string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => ((IPersistableModel)this).GetFormatFromOptions(options); return new MethodProvider ( - new MethodSignature(nameof(IPersistableModel.GetFormatFromOptions), null, MethodSignatureModifiers.None, typeof(string), null, [_serializationOptionsParameter], ExplicitInterface: PersistableModelObjectInterface), + new MethodSignature(nameof(IPersistableModel.GetFormatFromOptions), null, MethodSignatureModifiers.None, typeof(string), null, [_serializationOptionsParameter], ExplicitInterface: _persistableModelObjectInterface), castToT.Invoke(nameof(IPersistableModel.GetFormatFromOptions), [_serializationOptionsParameter]), this ); @@ -1066,7 +1062,7 @@ private MethodBodyStatement[] BuildPersistableModelCreateCoreMethodBody() private MethodBodyStatement CallBaseJsonModelWriteCore(bool isDynamicModelWithNonDynamicBase) { // base.() - bool callBaseWriteMethod = ShouldOverrideMethods + bool callBaseWriteMethod = _shouldOverrideMethods && (_jsonPatchProperty is null || !isDynamicModelWithNonDynamicBase); return callBaseWriteMethod ? Base.Invoke(JsonModelWriteCoreMethodName, [_utf8JsonWriterParameter, _serializationOptionsParameter]).Terminate() 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..0581bf32615 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 @@ -54,6 +54,9 @@ protected override string BuildRelativeFilePath() protected override SuppressionStatement[] BuildDisabledFileWarnings() => [new SuppressionStatement(null, Literal(ScmModelProvider.FileBinaryContentDiagnosticId), ScmModelProvider.ScmEvaluationTypeSuppressionJustification)]; + protected override IReadOnlyList BuildBodyDependencyTypes() => + [new OptionalDefinition().Type, ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.Type]; + protected override ConstructorProvider[] BuildConstructors() { if (_hasJsonOrXmlUsage) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/PipelineRequestProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/PipelineRequestProvider.cs index 63c100116e0..12ee884ce7e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/PipelineRequestProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/PipelineRequestProvider.cs @@ -33,7 +33,9 @@ public override MethodBodyStatement SetHeaders(IReadOnlyList ar => Original.Property(nameof(PipelineRequest.Headers)).Invoke(nameof(PipelineRequestHeaders.Set), arguments).Terminate(); public override MethodBodyStatement AddCollectionHeaders(ValueExpression prefix, ValueExpression headers) - => Original.Property(nameof(PipelineRequest.Headers)).Invoke(nameof(PipelineRequestHeaders.Add), [prefix, headers]).Terminate(); + => Original.Property(nameof(PipelineRequest.Headers)) + .Invoke(nameof(PipelineRequestHeaders.Add), [prefix, headers], typeArguments: null, callAsAsync: false, extensionType: new PipelineRequestHeadersExtensionsDefinition().Type) + .Terminate(); public override HttpRequestApi ToExpression() => this; } 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 f00d78571d0..19b3474420c 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,61 @@ protected override FieldProvider[] BuildFields() return [.. pipelineMessage20xClassifiersFields]; } + protected override IReadOnlyList BuildHelperDependencyTypes() + { + var uriBuilderType = ScmCodeModelGenerator.Instance.TypeFactory.HttpRequestApi.ToExpression().UriBuilderType; + var dependencies = new List(); + var dependencyNames = new HashSet(StringComparer.Ordinal); + if (uriBuilderType == typeof(ClientUriBuilderDefinition)) + { + TryAddDependency(dependencies, dependencyNames, new ClientUriBuilderDefinition().Type); + } + + foreach (var serviceMethod in _inputClient.Methods) + { + foreach (var parameter in serviceMethod.Operation.Parameters) + { + if (IsContentTypeParameter(parameter) || + parameter is not InputHeaderParameter and not InputQueryParameter) + { + continue; + } + + var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type); + if (type?.IsDictionary == true) + { + TryAddDependency(dependencies, dependencyNames, ScmCodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType); + if (parameter is InputHeaderParameter { CollectionHeaderPrefix: not null and not "" }) + { + TryAddDependency(dependencies, dependencyNames, new PipelineRequestHeadersExtensionsDefinition().Type); + } + } + else if (type?.IsCollection == true) + { + TryAddDependency(dependencies, dependencyNames, ScmCodeModelGenerator.Instance.TypeFactory.ListInitializationType); + } + } + } + + return dependencies; + } + + protected override IReadOnlyList BuildBodyDependencyTypes() + { + var dependencies = new List(); + var dependencyNames = new HashSet(StringComparer.Ordinal); + TryAddDependency(dependencies, dependencyNames, new TypeFormattersDefinition().Type); + return dependencies; + } + + private static void TryAddDependency(List dependencies, HashSet dependencyNames, CSharpType dependency) + { + if (dependencyNames.Add(dependency.FullyQualifiedName)) + { + dependencies.Add(dependency); + } + } + protected override ScmMethodProvider[] BuildMethods() { List methods = new List(); @@ -275,7 +330,7 @@ private MethodBodyStatements BuildMessage( } else { - var contentParam = signature.Parameters.FirstOrDefault(p => p.Name == "content" && p.Location == ParameterLocation.Body); + var contentParam = GetBodyContentParameter(signature.Parameters); statements.AddRange(AppendHeaderParameters(request, operation, paramMap, contentParam: contentParam)); statements.AddRange(GetSetContent(request, signature.Parameters)); } @@ -342,11 +397,14 @@ private Dictionary GetReinjectedParametersMap( private IReadOnlyList GetSetContent(HttpRequestApi request, IReadOnlyList parameters) { - var contentParam = parameters.FirstOrDefault( - p => p.Location == ParameterLocation.Body); + var contentParam = GetBodyContentParameter(parameters); return contentParam is null ? [] : [request.Content().Assign(contentParam).Terminate()]; } + private static ParameterProvider? GetBodyContentParameter(IReadOnlyList parameters) + => parameters.FirstOrDefault(static p => p.InputParameter is InputBodyParameter) ?? + parameters.FirstOrDefault(static p => p.Location == ParameterLocation.Body); + private Dictionary, PropertyProvider> BuildPipelineMessage20xClassifiers() { // Contains a mapping of classifier status codes to their corresponding pipeline message classifier property @@ -919,7 +977,9 @@ private static void AppendLiteralSegment(ScopedApi uri, string literal, List paramMap, InputOperation operation, InputParameter inputParam, out CSharpType? type, out SerializationFormat? serializationFormat, out ValueExpression? valueExpression) { - type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputParam.Type); + type = IsContentTypeParameter(inputParam, includeInputHeaderParameter: false) + ? null + : ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputParam.Type); serializationFormat = null; if (inputParam.IsApiVersion && ClientProvider.IsMultiServiceClient) @@ -1208,7 +1268,13 @@ internal static List GetMethodParameters( // when one was already published. UpdateParameterNameWithBackCompat(inputParam, inputParam.Name, client.BackCompatProvider, serviceMethod); - ParameterProvider? parameter = ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParam)?.ToPublicInputParameter(); + // Protocol and CreateRequest methods send Content-Type as a plain header value even + // when the input is modeled as a literal/constant, so avoid the general factory's + // constant-to-enum parameter shaping for this generated header. + ParameterProvider? parameter = IsContentTypeParameter(inputParam) && + methodType is ScmMethodKind.Protocol or ScmMethodKind.CreateRequest + ? CreateContentTypeParameter(inputParam) + : ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParam)?.ToPublicInputParameter(); if (parameter is null) { continue; @@ -1249,8 +1315,8 @@ internal static List GetMethodParameters( break; case ParameterLocation.Query: case ParameterLocation.Header: - if (inputParam is InputHeaderParameter { IsContentType: true } - && !HasContentTypeBeforeBodyInLastContract(serviceMethod.Name, client.BackCompatProvider)) + if (IsContentTypeParameter(inputParam) + && !ShouldPreserveContentTypeBeforeBody(methodType, serviceMethod, client.BackCompatProvider)) { sortedParams.Add(contentType++, parameter); } @@ -1292,12 +1358,56 @@ internal static List GetMethodParameters( return [.. sortedParams.Values]; } + private static bool ShouldPreserveContentTypeBeforeBody( + ScmMethodKind methodType, + InputServiceMethod serviceMethod, + TypeProvider backCompatProvider) + { + if (HasContentTypeBeforeBodyInLastContract(serviceMethod, backCompatProvider)) + { + return true; + } + + // The baseline contract used for back-compat may come from a released package and can + // lag the generated sources in the repo. For generated convenience methods that expose a + // domain-named body parameter, keep the historic contentType-before-body ordering to avoid + // repo regen churn while protocol methods continue using the normalized "content" body. + return methodType is ScmMethodKind.Convenience && HasNamedBodyParameter(serviceMethod); + } + + private static bool HasNamedBodyParameter(InputServiceMethod serviceMethod) + { + foreach (var parameter in serviceMethod.Parameters) + { + if (parameter.Location == InputRequestLocation.Body && + !string.Equals(parameter.Name, "content", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private static ParameterProvider CreateContentTypeParameter(InputParameter inputParam) + { + var type = new CSharpType(typeof(string), isNullable: !inputParam.IsRequired); + return new ParameterProvider( + inputParam.Name, + DocHelpers.GetFormattableDescription(inputParam.Summary, inputParam.Doc) ?? FormattableStringHelpers.Empty, + type, + defaultValue: inputParam.IsRequired ? null : Default, + location: ParameterLocation.Header, + wireInfo: new WireInformation(SerializationFormat.Default, inputParam.SerializedName), + validation: inputParam.IsRequired ? ParameterValidationType.AssertNotNullOrEmpty : ParameterValidationType.None, + inputParameter: inputParam); + } + private static bool HasLiteralContentTypeHeader(InputOperation operation) { foreach (var p in operation.Parameters) { - if (p is InputHeaderParameter { IsContentType: true } header - && header.Type is InputLiteralType) + if (p is InputHeaderParameter { IsContentType: true } && p.Type is InputLiteralType) { return true; } @@ -1305,15 +1415,19 @@ private static bool HasLiteralContentTypeHeader(InputOperation operation) return false; } + private static bool IsContentTypeParameter(InputParameter parameter, bool includeInputHeaderParameter = true) => + includeInputHeaderParameter && parameter is InputHeaderParameter { IsContentType: true } || + parameter is InputMethodParameter { Location: InputRequestLocation.Header } && + string.Equals(parameter.SerializedName, "Content-Type", StringComparison.OrdinalIgnoreCase); + /// /// Checks if the last contract view contains a method matching the given name where - /// a "contentType" parameter appears before the body ("content") parameter. + /// a "contentType" parameter appears before the body parameter. /// If so, we should preserve that ordering for backward compatibility. /// - private static bool HasContentTypeBeforeBodyInLastContract(string methodName, TypeProvider backCompatProvider) + private static bool HasContentTypeBeforeBodyInLastContract(InputServiceMethod serviceMethod, TypeProvider backCompatProvider) { const string contentTypeParamName = "contentType"; - const string contentParamName = "content"; var lastContractMethods = backCompatProvider.LastContractView?.Methods; if (lastContractMethods == null || lastContractMethods.Count == 0) @@ -1321,8 +1435,9 @@ private static bool HasContentTypeBeforeBodyInLastContract(string methodName, Ty return false; } - var syncMethodName = methodName; - var asyncMethodName = methodName + "Async"; + var syncMethodName = serviceMethod.Name; + var asyncMethodName = serviceMethod.Name + "Async"; + var bodyParameterNames = GetBodyParameterNames(serviceMethod); foreach (var method in lastContractMethods) { @@ -1341,7 +1456,7 @@ private static bool HasContentTypeBeforeBodyInLastContract(string methodName, Ty { contentTypeIndex = i; } - else if (string.Equals(param.Name, contentParamName, StringComparison.OrdinalIgnoreCase)) + else if (IsLastContractBodyParameter(param, bodyParameterNames)) { bodyIndex = i; } @@ -1361,6 +1476,45 @@ private static bool HasContentTypeBeforeBodyInLastContract(string methodName, Ty return false; } + private static HashSet GetBodyParameterNames(InputServiceMethod serviceMethod) + { + var names = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var parameter in serviceMethod.Parameters) + { + if (parameter.Location == InputRequestLocation.Body) + { + names.Add(parameter.Name); + } + } + + foreach (var parameter in serviceMethod.Operation.Parameters) + { + if (parameter is InputBodyParameter) + { + names.Add(parameter.Name); + } + } + + return names; + } + + private static bool IsLastContractBodyParameter(ParameterProvider parameter, HashSet bodyParameterNames) + { + if (parameter.InputParameter is InputBodyParameter || + parameter.Location == ParameterLocation.Body || + bodyParameterNames.Contains(parameter.Name)) + { + return true; + } + + if (string.Equals(parameter.Name, "content", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + internal static InputModelType GetSpreadParameterModel(InputParameter inputParam) { if (inputParam.Type is InputModelType model) 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/Providers/ClientOptionsProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs index 3cdcd91a129..8bd327a1230 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs @@ -502,6 +502,28 @@ public void MultipleClientsWithRequiredCustomParametersShareSingletonOptions() Assert.AreEqual("SampleClientOptions", options1!.Name); } + [Test] + public void SharedSingletonOptionsArePublicWhenAnyClientIsPublic() + { + var client1 = InputFactory.Client("InternalClient", clientNamespace: "TestNamespace"); + var client2 = InputFactory.Client("PublicClient", clientNamespace: "TestNamespace"); + + MockHelpers.LoadMockGenerator(clients: () => [client1, client2]); + + var clientProvider1 = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client1); + var clientProvider2 = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client2); + + Assert.IsNotNull(clientProvider1); + Assert.IsNotNull(clientProvider2); + + clientProvider1!.Update(modifiers: TypeSignatureModifiers.Internal); + clientProvider2!.Update(modifiers: TypeSignatureModifiers.Public); + + var options = clientProvider1.ClientOptions; + Assert.AreSame(options, clientProvider2.ClientOptions); + Assert.IsTrue(options!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + [Test] public void NamespaceLastSegmentIsUsedForSingletonName() { 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 327e46a7261..bdf17516e28 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/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 79b77b510c9..66d06965efa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -5,6 +5,7 @@ using System.ClientModel.Primitives; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -115,6 +116,90 @@ public void ValidateModelReaderWriterBuildableAttributesAreGeneratedForNonModels "The ModelReaderWriterBuildableAttribute should be generated for TestMrwSerialization"); } + [Test] + public void RemovedProvidersDoNotContributeBuildableAttributes() + { + var keptProvider = new TestMrwSerialization(implementsPersistableModel: true, includeDepModelProperty: false); + var removedProvider = new RemovedProviderWithFrameworkDependency(); + var outputLibrary = new TestOutputLibrary([keptProvider, removedProvider]); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => outputLibrary); + + try + { + CodeModelGenerator.Instance.AddTypeToKeep(keptProvider); + ProviderReferenceMapAnalyzer.Analyze(ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = contextDefinition.Attributes + .Where(a => a.Type.IsFrameworkType && a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute)) + .Select(a => a.Arguments.First().ToDisplayString()) + .ToList(); + + Assert.AreEqual(1, buildableAttributes.Count); + Assert.AreEqual("typeof(global::Sample.TestMrwSerialization)", buildableAttributes[0]); + Assert.IsFalse( + buildableAttributes.Contains("typeof(global::Sample.RemovedProviderWithFrameworkDependency)"), + "Removed providers should not get standalone context entries."); + } + finally + { + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); + } + } + + [Test] + public void RemovedFrameworkTypeIsNotMatchedToKeptProviderWithSameSimpleName() + { + var keptProvider = new ShadowedBuildableProvider("Sample"); + var removedProvider = new RemovedShadowedBuildableProvider(); + var outputLibrary = new TestOutputLibrary([keptProvider, removedProvider]); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => outputLibrary); + + try + { + CodeModelGenerator.Instance.AddTypeToKeep(keptProvider); + ProviderReferenceMapAnalyzer.Analyze(ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = contextDefinition.Attributes + .Where(a => a.Type.IsFrameworkType && a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute)) + .Select(a => a.Arguments.First().ToDisplayString()) + .ToList(); + + Assert.AreEqual(1, buildableAttributes.Count); + Assert.AreEqual("typeof(global::Sample.ShadowedModel)", buildableAttributes[0]); + Assert.IsFalse( + buildableAttributes.Contains("typeof(global::Sample.Agents.ShadowedModel)"), + "Removed source types should not be resolved through a kept provider with the same simple name."); + } + finally + { + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); + } + } + + [Test] + public void ExternalModelProvidersDoNotContributeStandaloneBuildableAttributes() + { + MockHelpers.LoadMockGenerator( + inputModels: () => + [ + InputFactory.Model( + "File", + @namespace: "External.Library", + usage: InputModelTypeUsage.Json, + external: new InputExternalTypeMetadata("External.Library.File", package: null, minVersion: null)) + ]); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = contextDefinition.Attributes + .Where(a => a.Type.IsFrameworkType && a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute)) + .Select(a => a.Arguments.First().ToDisplayString()) + .ToList(); + + Assert.IsEmpty(buildableAttributes); + } + [Test] public void ValidateModelReaderWriterBuildableAttributesIncludeNestedModels() { @@ -1045,7 +1130,63 @@ protected internal override PropertyProvider[] BuildProperties() protected override string BuildRelativeFilePath() { - throw new NotImplementedException(); + return Path.Combine("src", "Generated", $"{Name}.cs"); + } + } + + private class RemovedProviderWithFrameworkDependency : TypeProvider + { + protected override string BuildName() => "RemovedProviderWithFrameworkDependency"; + + protected internal override CSharpType[] BuildImplements() + { + return [new CSharpType(typeof(IPersistableModel))]; + } + + protected internal override PropertyProvider[] BuildProperties() + { + return [new PropertyProvider(null, MethodSignatureModifiers.Public, new CSharpType(typeof(DependencyModel)), "p1", new AutoPropertyBody(false), this)]; + } + + protected override string BuildRelativeFilePath() + { + return Path.Combine("src", "Generated", $"{Name}.cs"); + } + } + + private class ShadowedBuildableProvider : TypeProvider + { + private readonly string _namespace; + + public ShadowedBuildableProvider(string ns) + { + _namespace = ns; + } + + protected override string BuildName() => "ShadowedModel"; + + protected override string BuildNamespace() => _namespace; + + protected internal override CSharpType[] BuildImplements() + { + return [new CSharpType(typeof(IPersistableModel))]; + } + + protected override string BuildRelativeFilePath() + { + return Path.Combine("src", "Generated", $"{Name}.cs"); + } + } + + private class RemovedShadowedBuildableProvider : ShadowedBuildableProvider + { + public RemovedShadowedBuildableProvider() : base("Sample.Agents") + { + } + + protected internal override PropertyProvider[] BuildProperties() + { + return [new PropertyProvider(null, MethodSignatureModifiers.Public, new CSharpType(typeof(Sample.Agents.ShadowedModel)), "p1", new AutoPropertyBody(false), this)]; } } @@ -1554,6 +1695,63 @@ public void ValidateFrameworkReturnTypesAreDiscoveredFromGenericResponseWrappers "FrameworkModelWithMRW should be discovered from generic response wrapper return type"); } + [Test] + public void ValidateFrameworkBodyDependencyTypesAreNotDiscovered() + { + var clientProvider = new TestClientProviderWithFrameworkBodyDependency(); + var outputLibrary = new TestOutputLibrary([clientProvider]); + var mockGenerator = MockHelpers.LoadMockGenerator( + createOutputLibrary: () => outputLibrary); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var attributes = contextDefinition.Attributes; + + Assert.IsNotNull(attributes); + var buildableAttributes = attributes.Where(a => a.Type.IsFrameworkType && + a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute)).ToList(); + + Assert.IsFalse(buildableAttributes.Any(a => a.Arguments.First().ToDisplayString().Contains("ResponseError")), + "Framework types referenced only from provider bodies should not be added to the MRW context"); + } + + [Test] + public async Task CustomizedBuildableAttributesAreNotRegenerated() + { + var clientProvider = new TestClientProviderWithResponseErrorReturnType(); + var outputLibrary = new TestOutputLibrary([clientProvider]); + var mockGenerator = MockHelpers.LoadMockGenerator(createOutputLibrary: () => outputLibrary); + var compilation = await Helpers.GetCompilationFromDirectoryAsync(); + mockGenerator.SetupProperty(p => p.SourceInputModel, new SourceInputModel(compilation, null)); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = contextDefinition.Attributes + .Where(a => a.Type.IsFrameworkType && + a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute)); + + Assert.IsFalse(buildableAttributes.Any(a => a.Arguments.First().ToDisplayString().Contains("ResponseError")), + "Buildable attributes supplied by a customized context should not be regenerated"); + } + + [Test] + public async Task CustomProjectionPropertiesDoNotAddBuildableTypes() + { + var model = InputFactory.Model("ModelWithProjectedProperty", properties: + [ + InputFactory.Property("Error", InputPrimitiveType.String) + ]); + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [model], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var buildableAttributes = contextDefinition.Attributes + .Where(a => a.Type.IsFrameworkType && + a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute)); + + Assert.IsFalse(buildableAttributes.Any(a => a.Arguments.First().ToDisplayString().Contains("ResponseError")), + "Public projections over CodeGenMember backing properties are not part of wire serialization"); + } + [Test] public async Task ValidateCustomPropertiesOnModelsAreDiscovered() { @@ -1745,5 +1943,100 @@ protected internal override MethodProvider[] BuildMethods() ]; } } + + private class TestClientProviderWithFrameworkBodyDependency : TypeProvider + { + protected override string BuildName() => "TestClient"; + + protected override string BuildRelativeFilePath() => "TestClient.cs"; + + protected internal override IReadOnlyList BuildBodyDependencyTypes() + { + return [new CSharpType(typeof(Azure.ResponseError))]; + } + } + + private class TestClientProviderWithResponseErrorReturnType : TypeProvider + { + protected override string BuildName() => "TestClient"; + + protected override string BuildRelativeFilePath() => "TestClient.cs"; + + protected internal override MethodProvider[] BuildMethods() + { + var signature = new MethodSignature( + Name: "GetError", + Description: null, + Modifiers: MethodSignatureModifiers.Public, + ReturnType: new CSharpType(typeof(Azure.ResponseError)), + ReturnDescription: null, + Parameters: []); + + return [new MethodProvider(signature, Statements.MethodBodyStatement.Empty, this)]; + } + } + } + +} + +namespace Azure +{ + public class ResponseError : IJsonModel + { + ResponseError? IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + ResponseError? IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + } +} + +namespace Sample.Agents +{ + public class ShadowedModel : IJsonModel + { + ShadowedModel? IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + ShadowedModel? IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + throw new NotImplementedException(); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/CustomProjectionPropertiesDoNotAddBuildableTypes/ModelWithProjectedProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/CustomProjectionPropertiesDoNotAddBuildableTypes/ModelWithProjectedProperty.cs new file mode 100644 index 00000000000..9041098996b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/CustomProjectionPropertiesDoNotAddBuildableTypes/ModelWithProjectedProperty.cs @@ -0,0 +1,12 @@ +using Microsoft.TypeSpec.Generator.Customizations; + +namespace Sample.Models +{ + public partial class ModelWithProjectedProperty + { + [CodeGenMember("Error")] + internal string ErrorInternal { get; } + + public Azure.ResponseError Error => new(); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/CustomizedBuildableAttributesAreNotRegenerated/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/CustomizedBuildableAttributesAreNotRegenerated/SampleContext.cs new file mode 100644 index 00000000000..dd85bacb583 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/CustomizedBuildableAttributesAreNotRegenerated/SampleContext.cs @@ -0,0 +1,9 @@ +using System.ClientModel.Primitives; + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Azure.ResponseError))] + public partial class SampleContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SystemObjectModelSerializationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SystemObjectModelSerializationTests.cs index fcc90582416..503ed1ad68f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SystemObjectModelSerializationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SystemObjectModelSerializationTests.cs @@ -121,31 +121,6 @@ public void JsonModelWriteCore_IsOverride_WhenBaseIsRegularModel() "JsonModelWriteCore should be 'override' with regular base too"); } - [Test] - public void JsonModelWriteCore_IsOverride_WhenBaseProviderIsResolvedAfterSerialization() - { - var baseInputModel = InputFactory.Model("Resource"); - var derivedInputModel = InputFactory.Model("TrackedResource", properties: [InputFactory.Property("Location", InputPrimitiveType.String)]); - MockHelpers.LoadMockGenerator(inputModels: () => [baseInputModel, derivedInputModel]); - - var derived = new DelayedBaseModelProvider(derivedInputModel); - var serialization = new MrwSerializationTypeDefinition(derivedInputModel, derived); - - // The serialization provider can be constructed before later visitors/customization - // resolution make the base model provider available. - derived.BaseModel = new SystemObjectModelProvider(new CSharpType(typeof(object)), baseInputModel); - - var method = serialization.BuildJsonModelWriteCoreMethod(); - - Assert.AreEqual(derived.BaseModel.Type, derived.Type.BaseType, - "The generated model type should inherit the base resolved after serialization construction."); - Assert.AreEqual(derived.BaseModel.Type, serialization.Type.BaseType, - "The serialization type should inherit the same resolved base."); - Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), - "JsonModelWriteCore should evaluate BaseModelProvider when the method is built, not when serialization is constructed"); - Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); - } - // ------------------------------------------------------------------- // PersistableModelWriteCore: 'virtual' with system base, 'override' with regular // (the framework base already implements this; derived model re-introduces it) @@ -353,14 +328,5 @@ FakeMrwBase IPersistableModel.Create(BinaryData data, ModelReaderWr string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; } - - private class DelayedBaseModelProvider(InputModelType inputModel) : ModelProvider(inputModel) - { - public ModelProvider? BaseModel { get; set; } - - protected override ModelProvider? BuildBaseModelProvider() => BaseModel; - - protected override CSharpType? BuildBaseType() => BaseModel?.Type; - } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index e8bcb2fa0fb..e1b63d53935 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.ClientModel.Providers; @@ -103,6 +104,21 @@ public void ValidateProperties() Assert.IsFalse(propertyHash.ContainsKey("PipelineMessageClassifier204")); } + [Test] + public void BodyDependencyTypesDoNotRootRequestBodyModels() + { + var requestBody = InputFactory.Model("CreateWidgetRequest"); + var operation = InputFactory.Operation( + "CreateWidget", + parameters: [InputFactory.BodyParameter("body", requestBody, isRequired: true)]); + var inputClient = InputFactory.Client( + "TestClient", + methods: [InputFactory.BasicServiceMethod("CreateWidget", operation)]); + var restClient = new ClientProvider(inputClient).RestClient; + + Assert.IsFalse(restClient.BodyDependencyTypes.Any(t => t.Name == requestBody.Name)); + } + [Test] public void Validate3xxRedirectStatusCode() { @@ -362,6 +378,135 @@ public async Task ContentTypeAfterBodyInLastContractView() Assert.AreEqual("contentType", methodParameters[2].Name); // contentType after body } + [Test] + public async Task ContentTypeOrderPreservedFromLastContractViewWithNamedBody() + { + var contentTypeEnum = InputFactory.StringEnum("ContentTypeEnum", + [("application/json", "application/json"), ("application/xml", "application/xml")], + isExtensible: true); + var contentTypeHeader = InputFactory.HeaderParameter( + "contentType", + contentTypeEnum, + isRequired: true, + isContentType: true, + serializedName: "Content-Type"); + var bodyParam = InputFactory.BodyParameter("schemaContent", InputPrimitiveType.String, isRequired: true); + var groupNameParam = InputFactory.PathParameter("groupName", InputPrimitiveType.String, isRequired: true); + var pathParam = InputFactory.PathParameter("schemaName", InputPrimitiveType.String, isRequired: true); + var methodGroupNameParam = InputFactory.MethodParameter( + "groupName", + InputPrimitiveType.String, + isRequired: true, + location: InputRequestLocation.Path); + var methodSchemaNameParam = InputFactory.MethodParameter( + "schemaName", + InputPrimitiveType.String, + isRequired: true, + location: InputRequestLocation.Path); + var methodContentTypeParam = InputFactory.MethodParameter( + "contentType", + contentTypeEnum, + isRequired: true, + serializedName: "Content-Type", + location: InputRequestLocation.Header); + var methodBodyParam = InputFactory.MethodParameter( + "schemaContent", + InputPrimitiveType.String, + isRequired: true, + location: InputRequestLocation.Body); + + var operation = InputFactory.Operation( + "RegisterSchema", + parameters: [groupNameParam, pathParam, contentTypeHeader, bodyParam]); + + var serviceMethod = InputFactory.BasicServiceMethod( + "RegisterSchema", + operation, + parameters: [methodGroupNameParam, methodSchemaNameParam, methodContentTypeParam, methodBodyParam]); + + var client = InputFactory.Client("TestClient", methods: [serviceMethod]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var methodParameters = RestClientProvider.GetMethodParameters(serviceMethod, ScmMethodKind.Protocol, clientProvider!); + + Assert.AreEqual(4, methodParameters.Count); + Assert.AreEqual("groupName", methodParameters[0].Name); + Assert.AreEqual("schemaName", methodParameters[1].Name); + Assert.AreEqual("contentType", methodParameters[2].Name); + Assert.AreEqual("content", methodParameters[3].Name); + + var convenienceMethodParameters = RestClientProvider.GetMethodParameters(serviceMethod, ScmMethodKind.Convenience, clientProvider!); + + Assert.AreEqual(4, convenienceMethodParameters.Count); + Assert.AreEqual("groupName", convenienceMethodParameters[0].Name); + Assert.AreEqual("schemaName", convenienceMethodParameters[1].Name); + Assert.AreEqual("contentType", convenienceMethodParameters[2].Name); + Assert.AreEqual("schemaContent", convenienceMethodParameters[3].Name); + } + + [Test] + public void ContentTypeOrderUsesBodyMetadataWhenLastContractHasMultipleBinaryDataParameters() + { + var checksumParam = InputFactory.MethodParameter( + "checksum", + InputPrimitiveType.Base64, + isRequired: true, + location: InputRequestLocation.Query); + var contentTypeParam = InputFactory.MethodParameter( + "contentType", + InputPrimitiveType.String, + isRequired: true, + serializedName: "Content-Type", + location: InputRequestLocation.Header); + var payloadParam = InputFactory.MethodParameter( + "payload", + InputPrimitiveType.Base64, + isRequired: true, + location: InputRequestLocation.Body); + var operation = InputFactory.Operation( + "Upload", + parameters: [ + InputFactory.QueryParameter("checksum", InputPrimitiveType.Base64, isRequired: true), + InputFactory.HeaderParameter("Content-Type", InputPrimitiveType.String, isRequired: true, isContentType: true), + InputFactory.BodyParameter("payload", InputPrimitiveType.Base64, isRequired: true) + ]); + var serviceMethod = InputFactory.BasicServiceMethod( + "Upload", + operation, + parameters: [checksumParam, contentTypeParam, payloadParam]); + var lastContractView = new LastContractTestTypeProvider(); + lastContractView.Update(methods: + [ + new MethodProvider( + new MethodSignature( + "Upload", + null, + MethodSignatureModifiers.Public, + null, + null, + [ + new ParameterProvider("checksum", $"", typeof(BinaryData)), + new ParameterProvider("contentType", $"", typeof(string)), + new ParameterProvider("payload", $"", typeof(BinaryData)) + ]), + MethodBodyStatement.Empty, + lastContractView) + ]); + var provider = new LastContractTestTypeProvider(lastContractView); + var method = typeof(RestClientProvider).GetMethod("HasContentTypeBeforeBodyInLastContract", BindingFlags.NonPublic | BindingFlags.Static)!; + + var shouldPreserveOrder = (bool)method.Invoke(null, [serviceMethod, provider])!; + + Assert.IsTrue(shouldPreserveOrder); + } + [Test] public async Task ParameterNamePreservedFromLastContractView() { @@ -738,38 +883,6 @@ public void ValidateGetResponseClassifiersThrowsWhenNoSuccess() Assert.Fail("Expected Exception to be thrown."); } - [Test] - public void TestBuildCreateRequestMethodWithExplodedModelQueryParameter() - { - var filterModel = InputFactory.Model( - "filterOptions", - properties: - [ - InputFactory.Property("field", InputPrimitiveType.String, isRequired: true), - InputFactory.Property("value", InputPrimitiveType.String, isRequired: true), - ]); - var operation = InputFactory.Operation( - "sampleOp", - parameters: [InputFactory.QueryParameter("filter", filterModel, isRequired: true, explode: true)]); - var client = InputFactory.Client( - "TestClient", - methods: [InputFactory.BasicServiceMethod("Test", operation)]); - var clientProvider = new ClientProvider(client); - var restClientProvider = new MockClientProvider(client, clientProvider); - - var method = restClientProvider.Methods.FirstOrDefault(m => m.Signature.Name == "CreateSampleOpRequest"); - Assert.IsNotNull(method); - var body = method!.BodyStatements!.ToDisplayString(); - - // A model-typed query parameter with `explode` is expanded into one query entry per - // property (RFC 6570 form explode) using each property's wire name, instead of serializing - // the whole object via ConvertToString (which produced the type name). - Assert.IsTrue(body.Contains("uri.AppendQuery(\"field\", filter.Field, true);"), body); - Assert.IsTrue(body.Contains("uri.AppendQuery(\"value\", filter.Value, true);"), body); - Assert.IsFalse(body.Contains("AppendQuery(\"filter\""), body); - Assert.IsFalse(body.Contains("ConvertToString(filter)"), body); - } - [Test] public void TestBuildCreateRequestMethodWithQueryParameters() { @@ -1741,6 +1854,29 @@ protected override ScmMethodProvider[] BuildMethods() protected override TypeProvider[] BuildNestedTypes() => []; } + private class LastContractTestTypeProvider : TypeProvider + { + private readonly MethodProvider[] _methods; + private readonly TypeProvider? _lastContractView; + + public LastContractTestTypeProvider(params MethodProvider[] methods) + { + _methods = methods; + } + + public LastContractTestTypeProvider(TypeProvider lastContractView) + { + _methods = []; + _lastContractView = lastContractView; + } + + protected override string BuildName() => "TestClient"; + protected override string BuildNamespace() => "Sample"; + protected override string BuildRelativeFilePath() => "TestClient.cs"; + protected internal override MethodProvider[] BuildMethods() => _methods; + private protected override TypeProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => _lastContractView; + } + private static IEnumerable ValidateApiVersionPathParameterTestCases() { InputMethodParameter endpointParameter = InputFactory.MethodParameter( diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeOrderPreservedFromLastContractViewWithNamedBody/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeOrderPreservedFromLastContractViewWithNamedBody/TestClient.cs new file mode 100644 index 00000000000..e7005ee3202 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/TestData/RestClientProviderTests/ContentTypeOrderPreservedFromLastContractViewWithNamedBody/TestClient.cs @@ -0,0 +1,20 @@ +#nullable disable + +using System; +using System.ClientModel; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + // This represents a previous contract where contentType appears before a named body parameter. + internal virtual Task RegisterSchemaAsync(string groupName, string schemaName, SchemaContentTypeValues contentType, BinaryData schemaContent, CancellationToken cancellationToken = default) { return null; } + internal virtual ClientResult RegisterSchema(string groupName, string schemaName, SchemaContentTypeValues contentType, BinaryData schemaContent, CancellationToken cancellationToken = default) { return null; } + } + + internal readonly partial struct SchemaContentTypeValues + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ReferenceMap/ClientBodyDependencyReferenceMapTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ReferenceMap/ClientBodyDependencyReferenceMapTests.cs new file mode 100644 index 00000000000..a8a2ca477f8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ReferenceMap/ClientBodyDependencyReferenceMapTests.cs @@ -0,0 +1,969 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.ReferenceMap +{ + public class ClientBodyDependencyReferenceMapTests + { + [Test] + public async Task OperationBodyParameterModelIsRemovedWhenNotOtherwiseReferenced() + { + 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: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "RequestBody.cs"), + Path.Combine("src", "Generated", "Models", "RequestBody.Serialization.cs") + ]); + } + + [Test] + public async Task ProtocolOnlyOperationBodyParameterModelIsRemovedWhenNotOtherwiseReferenced() + { + var requestModel = InputFactory.Model("RequestBody"); + var parameter = InputFactory.BodyParameter("body", requestModel, isRequired: true); + var operation = InputFactory.Operation("Create", parameters: [parameter], httpMethod: "POST", generateConvenienceMethod: false); + var method = InputFactory.BasicServiceMethod("Create", operation); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [requestModel], + clients: [client], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "RequestBody.cs"), + Path.Combine("src", "Generated", "Models", "RequestBody.Serialization.cs") + ]); + } + + [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 InternalModelReferencedByPublicModelPropertyIsPublicized() + { + var dependencyModel = InputFactory.Model("DependencyModel", access: "internal"); + var responseModel = InputFactory.Model( + "ResponseBody", + properties: [InputFactory.Property("Dependency", dependencyModel)]); + 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, dependencyModel], [client], ["ResponseBody", "DependencyModel"]); + } + + [Test] + public async Task InternalModelReferencedByPublicNonRootCollectionPropertyIsPublicized() + { + var dependencyModel = InputFactory.Model("DependencyModel", access: "internal"); + var responseModel = InputFactory.Model( + "ResponseBody", + properties: [InputFactory.Property("Dependencies", InputFactory.Array(dependencyModel))]); + 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 GenerateAndAssertFiles( + enums: [], + models: [responseModel, dependencyModel], + clients: [client], + customFiles: [ + (Path.Combine("src", "Generated", "SampleModelFactory.cs"), """ + using System.Collections.Generic; + using Sample.Models; + + namespace Sample; + + public static partial class SampleModelFactory + { + public static ResponseBody ResponseBody(IEnumerable dependencies = default) => null; + } + """) + ], + expectedFiles: [], + publicModelNames: ["ResponseBody", "DependencyModel"], + configureGenerator: () => + { + var responseProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Single(provider => provider.Name == "ResponseBody"); + var dependencyProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Single(provider => provider.Name == "DependencyModel"); + CodeModelGenerator.Instance.AddTypeToKeep(responseProvider, isRoot: false); + CodeModelGenerator.Instance.AddTypeToKeep(dependencyProvider, isRoot: false); + }); + } + + [Test] + public async Task CustomInternalBoundaryInternalizesPublicNonRootModel() + { + var customInternalModel = InputFactory.Model("CustomInternalModel"); + var publicWrapper = InputFactory.Model( + "PublicWrapper", + properties: [InputFactory.Property("CustomInternal", customInternalModel)]); + + var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(outputPath); + try + { + var customPath = Path.Combine(outputPath, "src", "Custom", "CustomInternalModel.cs"); + Directory.CreateDirectory(Path.GetDirectoryName(customPath)!); + File.WriteAllText(customPath, """ + using Microsoft.TypeSpec.Generator.Customizations; + + namespace Sample.Models; + + [CodeGenType("CustomInternalModel")] + internal partial class CustomInternalModel + { + } + """); + var modelFactoryPath = Path.Combine(outputPath, "src", "Generated", "SampleModelFactory.cs"); + Directory.CreateDirectory(Path.GetDirectoryName(modelFactoryPath)!); + File.WriteAllText(modelFactoryPath, """ + using Sample.Models; + + namespace Sample; + + public static partial class SampleModelFactory + { + public static PublicWrapper PublicWrapper(CustomInternalModel customInternal = default) => null; + } + """); + + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [publicWrapper, customInternalModel], + configuration: """{ "package-name": "Sample", "disable-xml-docs": true }""", + outputPath: outputPath); + + var publicWrapperProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Single(provider => provider.Name == "PublicWrapper"); + var customInternalProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Single(provider => provider.Name == "CustomInternalModel"); + CodeModelGenerator.Instance.AddTypeToKeep(publicWrapperProvider, isRoot: false); + CodeModelGenerator.Instance.AddTypeToKeep(customInternalProvider, isRoot: false); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(CodeModelGenerator.Instance.OutputLibrary.TypeProviders); + + Assert.IsTrue(publicWrapperProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal), "PublicWrapper should be internalized when its public surface exposes a custom/internal type."); + Assert.IsTrue(customInternalProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal), "CustomInternalModel should remain internal."); + } + finally + { + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); + if (Directory.Exists(outputPath)) + { + Directory.Delete(outputPath, recursive: true); + } + } + } + + [Test] + public async Task OperationResponseBodyModelIsRemovedWhenNotOtherwiseReferenced() + { + var metadataOnlyModel = InputFactory.Model("MetadataOnlyResponse"); + var operation = InputFactory.Operation( + "Get", + responses: [ + InputFactory.OperationResponse(bodytype: InputPrimitiveType.String), + new InputOperationResponse([202], metadataOnlyModel, [], isErrorResponse: false, ["application/json"]) + ]); + var method = InputFactory.BasicServiceMethod( + "Get", + operation, + response: InputFactory.ServiceMethodResponse(InputPrimitiveType.String, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [metadataOnlyModel], + clients: [client], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "MetadataOnlyResponse.cs"), + Path.Combine("src", "Generated", "Models", "MetadataOnlyResponse.Serialization.cs") + ]); + } + + [Test] + public async Task EmptyCustomPartialModelIsKept() + { + var customizedModel = InputFactory.Model("CustomizedModel"); + + await GenerateAndAssertFiles( + enums: [], + models: [customizedModel], + clients: [], + customFiles: [( + Path.Combine("src", "CustomizedModel.cs"), + """ + namespace Sample.Models + { + public partial class CustomizedModel + { + } + } + """)], + expectedFiles: [ + Path.Combine("src", "Generated", "Models", "CustomizedModel.cs"), + Path.Combine("src", "Generated", "Models", "CustomizedModel.Serialization.cs") + ]); + } + + [Test] + public async Task InternalAdditionalRootModelIsRemovedWhenNotOtherwiseReferenced() + { + var metadataOnlyModel = InputFactory.Model("MetadataOnlyResponse", access: "internal"); + var operation = InputFactory.Operation( + "Get", + responses: [ + InputFactory.OperationResponse(bodytype: InputPrimitiveType.String), + new InputOperationResponse([202], metadataOnlyModel, [], isErrorResponse: false, ["application/json"]) + ]); + var method = InputFactory.BasicServiceMethod( + "Get", + operation, + response: InputFactory.ServiceMethodResponse(InputPrimitiveType.String, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [metadataOnlyModel], + clients: [client], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "MetadataOnlyResponse.cs"), + Path.Combine("src", "Generated", "Models", "MetadataOnlyResponse.Serialization.cs") + ], + configureGenerator: () => + { + var provider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Single(provider => provider.Name == "MetadataOnlyResponse"); + CodeModelGenerator.Instance.AddTypeToKeep(provider); + }); + } + + [Test] + public async Task AdditionalRootEnumIsRemovedWhenNotOtherwiseReferenced() + { + var metadataOnlyEnum = InputFactory.StringEnum( + "MetadataOnlyResponseKind", + [("Accepted", "accepted")]); + var operation = InputFactory.Operation( + "Get", + responses: [ + InputFactory.OperationResponse(bodytype: InputPrimitiveType.String), + new InputOperationResponse([202], metadataOnlyEnum, [], isErrorResponse: false, ["application/json"]) + ]); + var method = InputFactory.BasicServiceMethod( + "Get", + operation, + response: InputFactory.ServiceMethodResponse(InputPrimitiveType.String, [])); + var client = InputFactory.Client("TestClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [metadataOnlyEnum], + models: [], + clients: [client], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "MetadataOnlyResponseKind.cs"), + Path.Combine("src", "Generated", "Models", "MetadataOnlyResponseKind.Serialization.cs") + ]); + } + + [Test] + public async Task ContentTypeHeaderEnumIsRemovedWhenNotOtherwiseReferenced() + { + var contentTypeEnum = InputFactory.StringEnum( + "UpdateSnapshotRequestContentType", + [ + ("ApplicationMergePatchJson", "application/merge-patch+json"), + ("ApplicationJson", "application/json") + ]); + var contentTypeParameter = InputFactory.MethodParameter( + "contentType", + InputFactory.Union([contentTypeEnum], "contentType"), + isRequired: true, + location: InputRequestLocation.Header, + serializedName: "Content-Type"); + var operation = InputFactory.Operation( + "UpdateSnapshot", + parameters: [contentTypeParameter], + httpMethod: "PATCH", + generateConvenienceMethod: false); + var method = InputFactory.BasicServiceMethod( + "UpdateSnapshot", + operation, + parameters: [ + InputFactory.MethodParameter("name", InputPrimitiveType.String, isRequired: true, location: InputRequestLocation.Path), + contentTypeParameter, + InputFactory.MethodParameter("content", InputPrimitiveType.Base64, isRequired: true) + ]); + var client = InputFactory.Client("ConfigurationClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [contentTypeEnum], + models: [], + clients: [client], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "UpdateSnapshotRequestContentType.cs"), + Path.Combine("src", "Generated", "Models", "UpdateSnapshotRequestContentType.Serialization.cs") + ], + configureGenerator: () => + CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(InputFactory.Union([contentTypeEnum], "contentType"))); + } + + [Test] + public async Task PublicEnumIsRemovedWhenNotOtherwiseReferenced() + { + var metadataOnlyEnum = InputFactory.StringEnum( + "MetadataOnlyKind", + [("One", "one")]); + + await GenerateAndAssertFiles( + enums: [metadataOnlyEnum], + models: [], + clients: [], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "MetadataOnlyKind.cs"), + Path.Combine("src", "Generated", "Models", "MetadataOnlyKind.Serialization.cs") + ]); + } + + [Test] + public async Task ContentTypeHeaderEnumReferencedByCustomSuppressionIsKept() + { + var contentTypeEnum = InputFactory.StringEnum( + "PutKeyValueRequestContentType", + [("ApplicationJson", "application/json")], + isExtensible: true); + var contentTypeParameter = InputFactory.HeaderParameter( + "contentType", + InputFactory.Union([contentTypeEnum], "contentType"), + isRequired: true, + isContentType: true, + serializedName: "Content-Type"); + var operation = InputFactory.Operation( + "SetConfigurationSettingInternal", + parameters: [contentTypeParameter], + httpMethod: "PUT"); + var method = InputFactory.BasicServiceMethod("SetConfigurationSettingInternal", operation); + var client = InputFactory.Client("ConfigurationClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [contentTypeEnum], + models: [], + clients: [client], + customFiles: [ + (Path.Combine("src", "PutKeyValueRequestContentType.cs"), """ + namespace Sample.Models; + + internal readonly partial struct PutKeyValueRequestContentType + { + public static PutKeyValueRequestContentType ApplicationJson { get; } = new PutKeyValueRequestContentType("application/json"); + } + """) + ], + expectedFiles: [ + Path.Combine("src", "Generated", "Models", "PutKeyValueRequestContentType.cs") + ]); + } + + [Test] + public async Task ContentTypeHeaderEnumReferencedOnlyByCustomSuppressionAttributeIsKept() + { + var contentTypeEnum = InputFactory.StringEnum( + "CreateSnapshotRequestContentType", + [("ApplicationJson", "application/json")], + isExtensible: true); + var contentTypeParameter = InputFactory.MethodParameter( + "contentType", + InputFactory.Union([contentTypeEnum], "contentType"), + isRequired: true, + location: InputRequestLocation.Header, + serializedName: "Content-Type"); + var operation = InputFactory.Operation( + "CreateSnapshot", + parameters: [contentTypeParameter], + httpMethod: "PUT", + generateConvenienceMethod: false); + var method = InputFactory.BasicServiceMethod( + "CreateSnapshot", + operation, + parameters: [ + InputFactory.MethodParameter("name", InputPrimitiveType.String, isRequired: true, location: InputRequestLocation.Path), + contentTypeParameter, + InputFactory.MethodParameter("content", InputPrimitiveType.Base64, isRequired: true) + ]); + var client = InputFactory.Client("ConfigurationClient", methods: [method]); + + await GenerateAndAssertFiles( + enums: [contentTypeEnum], + models: [], + clients: [client], + customFiles: [ + (Path.Combine("src", "ConfigurationClient.cs"), """ + namespace Sample; + + [Microsoft.TypeSpec.Generator.Customizations.CodeGenType("ConfigurationClient")] + [Microsoft.TypeSpec.Generator.Customizations.CodeGenSuppress("CreateSnapshot", typeof(string), typeof(CreateSnapshotRequestContentType))] + public partial class ConfigurationClient + { + } + """) + ], + expectedFiles: [ + Path.Combine("src", "Generated", "Models", "CreateSnapshotRequestContentType.cs") + ]); + } + + [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 GenerateAndAssertFiles( + enums: [], + models: [toolModel, nestedModel], + clients: [client], + customFiles: [], + expectedFiles: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "ToolConfig.cs"), + Path.Combine("src", "Generated", "Models", "ToolConfig.Serialization.cs"), + Path.Combine("src", "Generated", "Models", "NestedToolParameter.cs"), + Path.Combine("src", "Generated", "Models", "NestedToolParameter.Serialization.cs") + ]); + } + + [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 PublicCustomCodeTypeConstraintsStayPublic() + { + var generatedModel = InputFactory.Model("GeneratedModel", access: "internal"); + + await GenerateAndAssertFiles( + enums: [], + models: [generatedModel], + clients: [], + customFiles: [ + (Path.Combine("src", "PublicCustomApi.cs"), """ + using Sample.Models; + + namespace Sample; + + public partial class PublicCustomApi where T : GeneratedModel + { + } + """) + ], + 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 GeneratedPrefixedDictionaryHeaderKeepsExtensions() + { + var header = InputFactory.HeaderParameter( + "x-ms-meta", + InputFactory.Dictionary(InputPrimitiveType.String), + isRequired: true, + collectionHeaderPrefix: "x-ms-meta-"); + 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: [], + unexpectedFiles: [ + Path.Combine("src", "Generated", "Models", "RequestBody.cs"), + Path.Combine("src", "Generated", "Models", "RequestBody.Serialization.cs") + ]); + } + + [Test] + public async Task InternalCustomClientPartialOverridesLastContractPublicClient() + { + var responseModel = InputFactory.Model("CompactResource"); + var operation = InputFactory.Operation("Compact", responses: [InputFactory.OperationResponse(bodytype: responseModel)]); + var method = InputFactory.BasicServiceMethod("Compact", operation, response: InputFactory.ServiceMethodResponse(responseModel, [])); + var client = InputFactory.Client("Responses", methods: [method]); + + await GenerateAndAssertFiles( + enums: [], + models: [responseModel], + clients: [client], + customFiles: [ + (Path.Combine("src", "Generated", "Responses.cs"), """ + namespace Sample; + + public partial class Responses + { + } + """), + (Path.Combine("src", "Custom", "Internal", "Responses.cs"), """ + namespace Sample; + + internal partial class Responses + { + } + """) + ], + expectedFiles: [], + internalModelNames: ["CompactResource"], + internalClientNames: ["Responses"]); + } + + 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[] internalClientNames = null!, + string packageName = "Sample", + Action? configureGenerator = null) + { + publicModelNames ??= []; + internalModelNames ??= []; + internalClientNames ??= []; + 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); + configureGenerator?.Invoke(); + + 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."); + } + + foreach (var clientName in internalClientNames) + { + var clientPath = Path.Combine(outputPath, "src", "Generated", $"{clientName}.cs"); + Assert.IsTrue(File.Exists(clientPath), $"Expected generated client file '{clientPath}'."); + var text = File.ReadAllText(clientPath); + StringAssert.Contains($"internal partial class {clientName}", text, $"{clientName} should be internal."); + StringAssert.DoesNotContain($"public partial class {clientName}", text, $"{clientName} 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/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/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index c013817a72e..21e31761cb0 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 @@ -31,8 +31,8 @@ public async Task ExecuteAsync() 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. + // Resolve PackageReference items from the .csproj so custom code referencing external + // NuGet types compiles correctly. await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); // Pre-walk the input library and resolve any external types that point at NuGet packages. @@ -90,36 +90,80 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), { // 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())); + try + { + using var referenceMapSession = ProviderReferenceMapAnalyzer.PrepareForGeneration(output.TypeProviders); + foreach (var outputType in output.TypeProviders) + { + if (!outputType.ShouldAnalyzeAttributesInReferenceMap) + { + // Attributes for providers excluded from reference-map attribute analysis can still depend + // on the final reference-map result. Clear any attributes cached during pre-write setup so + // the writer rebuilds them after PrepareForGeneration has finalized provider removals. + outputType.ClearCachedAttributes(); + } + } - foreach (var serialization in outputType.SerializationProviders) + foreach (var outputType in output.TypeProviders) { - writer = CodeModelGenerator.Instance.GetWriter(serialization); + if (!referenceMapSession.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 (!referenceMapSession.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); + // Add all the generated files to the workspace + await Task.WhenAll(generateFilesTasks); - LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); + referenceMapSession.RestorePreWriteModelFactoryMethods(); - // Delete any old generated files - DeleteDirectory(generatedSourceOutputPath, _filesToKeep); + LoggingHelpers.LogElapsedTime("All generated types have been written into memory"); - LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); + // Delete any old generated files + DeleteDirectory(generatedSourceOutputPath, _filesToKeep); - await generatedCodeWorkspace.PostProcessAsync(); + LoggingHelpers.LogElapsedTime("All old generated files have been deleted"); + } + finally + { + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); + } - // Write the generated files to the output directory + var generatedFiles = new List<(string Name, string Text)>(); await foreach (var file in generatedCodeWorkspace.GetGeneratedFilesAsync()) { if (string.IsNullOrEmpty(file.Text)) { continue; } + + generatedFiles.Add((file.Name, file.Text)); + } + + // Write the generated files to the output directory + 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)!); @@ -140,15 +184,30 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), internal static void FilterAllCustomizedMembers(OutputLibrary output) { + var visited = new HashSet(); foreach (var typeProvider in output.TypeProviders) { - // Update the type with the potentially modified members, filtering out customized members - // after the visitors have been applied so that the filtering is done against the final version. - FilterCustomizedMembers(typeProvider); - foreach (var serializationProvider in typeProvider.SerializationProviders) - { - FilterCustomizedMembers(serializationProvider); - } + FilterAllCustomizedMembers(typeProvider, visited); + } + } + + private static void FilterAllCustomizedMembers(TypeProvider typeProvider, HashSet visited) + { + if (!visited.Add(typeProvider)) + { + return; + } + + // Update the type with the potentially modified members, filtering out customized members + // after the visitors have been applied so that the filtering is done against the final version. + FilterCustomizedMembers(typeProvider); + foreach (var serializationProvider in typeProvider.SerializationProviders) + { + FilterAllCustomizedMembers(serializationProvider, visited); + } + foreach (var nestedType in typeProvider.NestedTypes) + { + FilterAllCustomizedMembers(nestedType, visited); } } @@ -177,9 +236,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 +263,8 @@ private static void DeleteDirectory(string path, string[] filesToKeep) directoryInfo.Delete(); } } + + private static bool ShouldKeepGeneratedFile(FileInfo file, HashSet fileNamesToKeep) + => fileNamesToKeep.Contains(file.Name); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs index e8732f15ffd..02e25540478 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs @@ -242,7 +242,7 @@ public void AddTypeToKeep(string typeName, bool isRoot = true) /// /// /// The provider's fully qualified name is resolved lazily, when the keep list is consumed during - /// post-processing. This makes it safe to call this method from a + /// reference-map analysis. This makes it safe to call this method from a /// constructor (including base constructors that run before the derived constructor body), since /// it does not force evaluation of — which would dispatch virtual /// Build* methods on a not-yet-fully-constructed instance. 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..48a6039d4dd 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 @@ -116,7 +116,7 @@ internal static SyntaxTree GetTree(TypeProvider provider) } var docs = await Task.WhenAll(documents); - LoggingHelpers.LogElapsedTime("Roslyn post processing complete"); + LoggingHelpers.LogElapsedTime("Roslyn document processing complete"); foreach (var doc in docs) { @@ -260,33 +260,6 @@ internal static Project AddDirectory(Project project, string directory, Func - /// This method invokes the postProcessor to do some post processing work - /// Depending on the configuration, it will either remove + internalize, just internalize or do nothing - /// - public async Task PostProcessAsync() - { - var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; - var nonRootTypes = CodeModelGenerator.Instance.NonRootTypes; - var postProcessor = new PostProcessor( - [.. CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep, .. CodeModelGenerator.Instance.AdditionalRootTypes], - modelFactoryFullName: modelFactory.Type.FullyQualifiedName, - additionalNonRootTypeNames: nonRootTypes); - - switch (Configuration.UnreferencedTypesHandling) - { - case Configuration.UnreferencedTypesHandlingOption.KeepAll: - break; - case Configuration.UnreferencedTypesHandlingOption.Internalize: - _project = await postProcessor.InternalizeAsync(_project); - break; - case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: - _project = await postProcessor.InternalizeAsync(_project); - _project = await postProcessor.RemoveAsync(_project); - break; - } - } - /// /// Resolves PackageReference items from the project's .csproj file and adds their assemblies /// as metadata references so that custom code referencing external NuGet types compiles correctly. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs deleted file mode 100644 index dc42f801732..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ /dev/null @@ -1,669 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Simplification; - -namespace Microsoft.TypeSpec.Generator -{ - internal class PostProcessor - { - private readonly string? _modelFactoryFullName; - private readonly HashSet _additionalNonRootTypeNames; - private readonly HashSet _typesToKeep; - private INamedTypeSymbol? _modelFactorySymbol; - - public PostProcessor( - HashSet typesToKeep, - string? modelFactoryFullName = null, - IEnumerable? additionalNonRootTypeNames = null) - { - _typesToKeep = typesToKeep; - _modelFactoryFullName = modelFactoryFullName; - _additionalNonRootTypeNames = new HashSet(additionalNonRootTypeNames ?? []); - } - - private record TypeSymbols( - HashSet DeclaredSymbols, - INamedTypeSymbol? ModelFactorySymbol, - IReadOnlyDictionary> DeclaredNodesCache, - IReadOnlyDictionary> DocumentsCache); - - /// - /// This method reads the project, returns the types defined in it and build symbol caches to accelerate the calculation - /// By default, the types defined in shared documents are not included. Please override to tweak this behavior. - /// - /// The of the - /// The project to extract type symbols from - /// If is true, only public types will be included. If is false, all types will be included - /// A instance of which includes the information of the declared symbols of the given accessibility, along with some useful cache that is useful in this class. - private async Task GetTypeSymbolsAsync(Compilation compilation, - Project project, - bool publicOnly = true) - { - var result = new HashSet(SymbolEqualityComparer.Default); - var declarationCache = - new Dictionary>(SymbolEqualityComparer.Default); - var documentCache = new Dictionary>(); - - if (_modelFactoryFullName != null) - { - _modelFactorySymbol = compilation.GetTypeByMetadataName(_modelFactoryFullName); - } - - foreach (var document in project.Documents) - { - if (ShouldIncludeDocument(document)) - { - var root = await document.GetSyntaxRootAsync(); - if (root == null) - { - continue; - } - - var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); - - foreach (var typeDeclaration in root.DescendantNodes().OfType()) - { - var symbol = semanticModel.GetDeclaredSymbol(typeDeclaration); - if (symbol == null) - { - continue; - } - - if (publicOnly && symbol.DeclaredAccessibility != Accessibility.Public && - !document.Name.StartsWith("Internal/", StringComparison.Ordinal)) - { - continue; - } - - AddInList(declarationCache, symbol, typeDeclaration); - AddInList(documentCache, document, symbol, - () => new HashSet(SymbolEqualityComparer.Default)); - - // we do not add the model factory and additionalNonRootTypeSymbols to the declared symbol list - // so that it will never be included in any process of internalization or removal - if (SymbolEqualityComparer.Default.Equals(symbol, _modelFactorySymbol) - || _additionalNonRootTypeNames.Contains(symbol.Name) - || _additionalNonRootTypeNames.Contains(symbol.GetFullyQualifiedName())) - { - continue; - } - - result.Add(symbol); - } - } - } - - return new TypeSymbols(result, - _modelFactorySymbol, - declarationCache.ToDictionary(kv => kv.Key, kv => kv.Value.ToHashSet(), - (IEqualityComparer)SymbolEqualityComparer.Default), - documentCache.ToDictionary(kv => kv.Key, - kv => kv.Value.ToHashSet(SymbolEqualityComparer.Default))); - } - - protected virtual bool ShouldIncludeDocument(Document document) => - !GeneratedCodeWorkspace.IsGeneratedTestDocument(document); - - /// - /// This method marks the "not publicly" referenced types as internal if they are previously defined as public. It will do this job in the following steps: - /// 1. This method will read all the public types defined in the given , and build a cache for those symbols - /// 2. Build a public reference map for those symbols - /// 3. Finds all the root symbols, please override the to control which document you would like to include - /// 4. Visit all the symbols starting from the root symbols following the reference map to get all unvisited symbols - /// 5. Change the accessibility of the unvisited symbols in step 4 to internal - /// - /// The project to process - /// The processed . is immutable, therefore this should usually be a new instance - public async Task InternalizeAsync(Project project) - { - var compilation = await project.GetCompilationAsync(); - if (compilation == null) - { - return project; - } - - // 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]) - { - nodesToInternalize[node] = project.GetDocumentId(node.SyntaxTree)!; - } - } - - foreach (var (model, documentId) in nodesToInternalize) - { - project = MarkInternal(project, model, documentId); - } - - var modelNamesToRemove = - nodesToInternalize.Keys.Select(item => item.Identifier.Text); - project = await RemoveMethodsFromModelFactoryAsync(project, definitions, modelNamesToRemove.ToHashSet()); - - return project; - } - - private async Task RemoveMethodsFromModelFactoryAsync(Project project, - TypeSymbols definitions, - HashSet namesToRemove) - { - var modelFactorySymbol = definitions.ModelFactorySymbol; - if (modelFactorySymbol == null) - { - return project; - } - - var nodesToRemove = new List(); - - foreach (var method in modelFactorySymbol.GetMembers().OfType()) - { - if (namesToRemove.Contains(method.Name)) - { - foreach (var reference in method.DeclaringSyntaxReferences) - { - var node = await reference.GetSyntaxAsync(); - nodesToRemove.Add(node); - } - } - } - - // find the GENERATED document of model factory (we may have the customized document of this for overloads) - Document? modelFactoryGeneratedDocument = null; - // the nodes corresponding to the model factory symbol has never been changed therefore the nodes inside the cache are still usable - if (definitions.DeclaredNodesCache.TryGetValue(modelFactorySymbol, out var nodes)) - { - foreach (var declarationNode in nodes) - { - var document = project.GetDocument(declarationNode.SyntaxTree); - if (document != null && GeneratedCodeWorkspace.IsGeneratedDocument(document)) - { - modelFactoryGeneratedDocument = document; - break; - } - } - } - - // maybe this is possible, for instance, we could be adding the customization all entries previously inside the generated model factory so that the generated model factory is empty and removed. - if (modelFactoryGeneratedDocument == null) - { - return project; - } - - var root = await modelFactoryGeneratedDocument.GetSyntaxRootAsync(); - Debug.Assert(root is not null); - root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia)!; - modelFactoryGeneratedDocument = modelFactoryGeneratedDocument.WithSyntaxRoot(root); - - // see if this class still has any method, if it contains nothing, we should remove this document - var methods = root.DescendantNodes().OfType(); - if (!methods.Any()) - { - return project.RemoveDocument(modelFactoryGeneratedDocument.Id); - } - - return modelFactoryGeneratedDocument.Project; - } - - /// - /// This method removes the no-referenced types from the . It will do this job in the following steps: - /// 1. This method will read all the defined types in the given , and build a cache for those symbols - /// 2. Build a reference map for those symbols (including non-public usage) - /// 3. Finds all the root symbols, please override the to control which document you would like to include - /// 4. Visit all the symbols starting from the root symbols following the reference map to get all unvisited symbols - /// 5. Remove the definition of the unvisited symbols in step 4 - /// - /// The project to process - /// The processed . is immutable, therefore this should usually be a new instance - public async Task RemoveAsync(Project project) - { - var compilation = await 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) - { - rootSymbols.Add(_modelFactorySymbol); - } - // traverse the map to determine the declarations that we are about to remove, starting from root nodes - var referencedSymbols = VisitSymbolsFromRootAsync(rootSymbols, referenceMap); - - referencedSymbols = AddSampleSymbols(referencedSymbols, definitions.DeclaredSymbols); - var referencedSet = new HashSet(referencedSymbols, SymbolEqualityComparer.Default); - - var symbolsToRemove = definitions.DeclaredSymbols.Except(referencedSet); - - var nodesToRemove = new List(); - foreach (var symbol in symbolsToRemove) - { - if (referencedSet.Contains(GetBase(symbol))) - { - continue; - } - nodesToRemove.AddRange(definitions.DeclaredNodesCache[symbol]); - } - - // remove them one by one - project = await RemoveModelsAsync(project, nodesToRemove); - - return project; - } - - private INamedTypeSymbol GetBase(INamedTypeSymbol symbol) - { - var baseType = symbol.BaseType; - if (baseType == null || baseType.SpecialType == SpecialType.System_Object) - { - return symbol; - } - - return GetBase(baseType); - } - - private IEnumerable AddSampleSymbols( - IEnumerable referencedSymbols, - HashSet declaredSymbols) - { - List symbolsToAdd = new List(); - foreach (var symbol in declaredSymbols) - { - if (symbol.ContainingNamespace.Name == "Samples" && symbol.Name.StartsWith("Samples_") && - !referencedSymbols.Any(s => s.Name == symbol.Name)) - { - symbolsToAdd.Add(symbol); - } - } - - return referencedSymbols.Concat(symbolsToAdd); - } - - /// - /// Do a BFS starting from the by following the - /// - /// - /// - /// - private static IEnumerable VisitSymbolsFromRootAsync( - IEnumerable rootSymbols, - ReferenceMap referenceMap) - { - var queue = new Queue(rootSymbols.Concat(referenceMap.GlobalReferencedSymbols)); - var visited = new HashSet(SymbolEqualityComparer.Default); - while (queue.Count > 0) - { - var definition = queue.Dequeue(); - if (visited.Contains(definition)) - { - continue; - } - - visited.Add(definition); - // add this definition to the result - yield return definition; - // add every type referenced by this node to the queue - foreach (var child in GetReferencedTypes(definition, referenceMap)) - { - queue.Enqueue(child); - } - } - } - - private static IEnumerable GetReferencedTypes(T definition, - IReadOnlyDictionary> referenceMap) where T : notnull - { - if (referenceMap.TryGetValue(definition, out var references)) - { - return references; - } - - return Enumerable.Empty(); - } - - private Project MarkInternal(Project project, BaseTypeDeclarationSyntax declarationNode, DocumentId documentId) - { - 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; - } - - private async Task RemoveModelsAsync(Project project, - IEnumerable unusedModels) - { - // accumulate the definitions from the same document together - var documents = new Dictionary>(); - - foreach (var model in unusedModels) - { - var document = project.GetDocument(model.SyntaxTree); - Debug.Assert(document != null); - if (!documents.ContainsKey(document)) - { - documents.Add(document, new HashSet()); - } - - documents[document].Add(model); - } - - foreach (var models in documents.Values) - { - project = await RemoveModelsFromDocumentAsync(project, models); - } - - // remove what are now invalid references due to the models being removed - project = await RemoveInvalidRefs(project); - - return project; - } - - private static BaseTypeDeclarationSyntax ChangeModifier(BaseTypeDeclarationSyntax memberDeclaration, - SyntaxKind from, - SyntaxKind to) - { - var originalTokenInList = memberDeclaration.Modifiers.FirstOrDefault(token => token.IsKind(from)); - - // skip this if there is nothing to replace - if (originalTokenInList == default) - { - return memberDeclaration; - } - - var newToken = - SyntaxFactory.Token(originalTokenInList.LeadingTrivia, to, originalTokenInList.TrailingTrivia); - var newModifiers = memberDeclaration.Modifiers.Replace(originalTokenInList, newToken); - return memberDeclaration.WithModifiers(newModifiers); - } - - private async Task RemoveModelsFromDocumentAsync(Project project, - IEnumerable models) - { - var tree = models.First().SyntaxTree; - var document = project.GetDocument(tree); - if (document == null) - { - return project; - } - - var root = await tree.GetRootAsync(); - root = root.RemoveNodes(models, SyntaxRemoveOptions.KeepNoTrivia); - - var emptyNamespaces = root! - .DescendantNodes() - .OfType() - .Where(ns => !ns.Members.OfType().Any()) - .ToList(); - - if (emptyNamespaces.Any()) - { - root = root.RemoveNodes(emptyNamespaces, SyntaxRemoveOptions.KeepNoTrivia); - } - - document = document.WithSyntaxRoot(root!); - return document.Project; - } - - private async Task RemoveInvalidRefs(Project project) - { - var solution = project.Solution; - - // Process each document for invalid usings - foreach (var documentId in project.DocumentIds) - { - solution = await RemoveInvalidUsings(solution, documentId); - } - - // Process each document for invalid attributes (with fresh semantic models) - foreach (var documentId in project.DocumentIds) - { - solution = await RemoveInvalidAttributes(solution, documentId); - } - - return solution.GetProject(project.Id)!; - } - - private async Task RemoveInvalidUsings(Solution solution, DocumentId documentId) - { - var document = solution.GetDocument(documentId)!; - var root = await document.GetSyntaxRootAsync(); - var model = await document.GetSemanticModelAsync(); - - if (root is not CompilationUnitSyntax cu || model == null) - { - return solution; - } - - var invalidUsings = cu.Usings - .Where(u => - { - var info = model.GetSymbolInfo(u.Name!); - var sym = info.Symbol; - return sym is null || sym.Kind != SymbolKind.Namespace; - }) - .ToList(); - - if (invalidUsings.Count > 0) - { - cu = cu.RemoveNodes(invalidUsings, SyntaxRemoveOptions.KeepNoTrivia)!; - solution = solution.WithDocumentSyntaxRoot(documentId, cu); - } - - return solution; - } - - private async Task RemoveInvalidAttributes(Solution solution, DocumentId documentId) - { - var document = solution.GetDocument(documentId)!; - var root = await document.GetSyntaxRootAsync(); - var model = await document.GetSemanticModelAsync(); - - if (root is not CompilationUnitSyntax cu || model == null) - { - return solution; - } - - var attributes = cu.DescendantNodes().OfType(); - var firstAttribute = attributes.FirstOrDefault(); - - var invalidAttributes = attributes - .Where(attr => attr.Attributes.Any(attribute => - attribute.ArgumentList?.Arguments.Any(arg => - arg.Expression is TypeOfExpressionSyntax typeOfExpr && - model.GetTypeInfo(typeOfExpr.Type).Type?.TypeKind == TypeKind.Error) == true)) - .ToHashSet(); - - if (invalidAttributes.Count > 0) - { - cu = cu.RemoveNodes(invalidAttributes, SyntaxRemoveOptions.KeepNoTrivia)!; - - if (invalidAttributes.Contains(firstAttribute!)) - { - var leadingTrivia = firstAttribute!.GetLeadingTrivia(); - // Find where XML docs end and indentation begins - var xmlDocTrivia = new List(); - var lastXmlIndex = -1; - - for (int i = 0; i < leadingTrivia.Count; i++) - { - var trivia = leadingTrivia[i]; - if (trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)) - { - lastXmlIndex = i; - } - } - - // Collect trivia up to and including the last XML doc line's newline - if (lastXmlIndex >= 0) - { - for (int i = 0; i <= lastXmlIndex; i++) - { - xmlDocTrivia.Add(leadingTrivia[i]); - } - - // Include the newline after the last XML doc if present - if (lastXmlIndex + 1 < leadingTrivia.Count && - leadingTrivia[lastXmlIndex + 1].IsKind(SyntaxKind.EndOfLineTrivia)) - { - xmlDocTrivia.Add(leadingTrivia[lastXmlIndex + 1]); - } - } - - // Find the updated type and add the XML docs to it - var updatedType = cu.DescendantNodes() - .OfType() - .FirstOrDefault(); - - if (updatedType != null && xmlDocTrivia.Any()) - { - var existingTrivia = updatedType.GetLeadingTrivia(); - cu = cu.ReplaceNode(updatedType, - updatedType.WithLeadingTrivia(xmlDocTrivia.Concat(existingTrivia))); - } - } - - solution = solution.WithDocumentSyntaxRoot(documentId, cu); - } - - return solution; - } - - private async Task> GetRootSymbolsAsync(Project project, TypeSymbols modelSymbols) - { - var result = new HashSet(SymbolEqualityComparer.Default); - foreach (var symbol in modelSymbols.DeclaredSymbols) - { - foreach (var declarationNode in modelSymbols.DeclaredNodesCache[symbol]) - { - var document = project.GetDocument(declarationNode.SyntaxTree); - if (document == null) - { - continue; - } - - if (await IsRootDocument(document)) - { - result.Add(symbol); - break; - // if any of the declaring document of this symbol is considered as a root document, we add the symbol to the root list, skipping the processing of any other documents of this symbol - } - } - } - - return result; - } - - protected virtual async Task IsRootDocument(Document document) - { - var root = await document.GetSyntaxRootAsync(); - // a document is a root document, when - // 1. it is a custom document (not generated or shared) - // 2. it is a client - // 3. user exceptions - return GeneratedCodeWorkspace.IsCustomDocument(document) || IsClientDocument(document) || - ShouldKeepType(root, _typesToKeep); - } - - private static bool ShouldKeepType(SyntaxNode? root, HashSet typesToKeep) - { - if (root is null) - { - return false; - } - - // use `BaseTypeDeclarationSyntax` to also include enums because `EnumDeclarationSyntax` extends `BaseTypeDeclarationSyntax` - // `ClassDeclarationSyntax` and `StructDeclarationSyntax` both inherit `TypeDeclarationSyntax` - var typeNodes = root.DescendantNodes().OfType(); - // there is possibility that we have multiple types defined in the same document (for instance, custom code) - return typeNodes.Any(t => - { - // Get simple name - var simpleName = t.Identifier.Text; - if (typesToKeep.Contains(simpleName)) - { - return true; - } - - // Get fully qualified name - var fullName = GetFullyQualifiedName(t); - return typesToKeep.Contains(fullName); - }); - } - - private static string GetFullyQualifiedName(BaseTypeDeclarationSyntax typeDeclaration) - { - var namespaceDeclaration = typeDeclaration.Ancestors() - .OfType() - .FirstOrDefault(); - - return namespaceDeclaration != null - ? $"{namespaceDeclaration.Name}.{typeDeclaration.Identifier.Text}" - : typeDeclaration.Identifier.Text; - } - - private static bool IsClientDocument(Document document) - { - return document.Name.EndsWith("Client.cs", StringComparison.Ordinal); - } - - private static void AddInList(Dictionary dictionary, - TKey key, - TValue value, - Func? collectionConstructor = null) where TKey : notnull where TList : ICollection, new() - { - if (dictionary.TryGetValue(key, out var list)) - { - list.Add(value); - } - else - { - TList newList; - if (collectionConstructor == null) - { - newList = new TList(); - } - else - { - newList = collectionConstructor(); - } - - newList.Add(value); - dictionary.Add(key, newList); - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ReferenceMap.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ReferenceMap.cs deleted file mode 100644 index 7fe83d20f2e..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ReferenceMap.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; - -namespace Microsoft.TypeSpec.Generator -{ - internal class ReferenceMap : IReadOnlyDictionary> - { - private Dictionary> _referenceMap; - - private List _globalReference; - - public ReferenceMap() - { - _referenceMap = new(SymbolEqualityComparer.Default); - _globalReference = new(); - } - - /// - /// Adds the value into the list corresponding to key - /// - /// - /// - /// true if the value is added into the list, false if the value already exists in the list - internal bool AddInList(INamedTypeSymbol key, INamedTypeSymbol value) - { - if (_referenceMap.TryGetValue(key, out var list)) - { - // the list is guaranteed to be a HashSet - var set = (HashSet)list; - return set.Add(value); - } - else - { - var newList = new HashSet(SymbolEqualityComparer.Default) { value }; - _referenceMap.Add(key, newList); - return true; - } - } - - internal void AddGlobal(INamedTypeSymbol typeSymbol) - { - _globalReference.Add(typeSymbol); - } - - public IEnumerable GlobalReferencedSymbols => _globalReference; - - public IEnumerable this[INamedTypeSymbol key] => _referenceMap[key]; - - public IEnumerable Keys => _referenceMap.Keys; - - public IEnumerable> Values => _referenceMap.Values; - - public int Count => _referenceMap.Count; - - public bool ContainsKey(INamedTypeSymbol key) => _referenceMap.ContainsKey(key); - - public IEnumerator>> GetEnumerator() => - _referenceMap.GetEnumerator(); - - public bool TryGetValue(INamedTypeSymbol key, [MaybeNullWhen(false)] out IEnumerable value) => - _referenceMap.TryGetValue(key, out value); - - IEnumerator IEnumerable.GetEnumerator() => _referenceMap.GetEnumerator(); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ReferenceMapBuilder.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ReferenceMapBuilder.cs deleted file mode 100644 index a4b1e31f90b..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/ReferenceMapBuilder.cs +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.FindSymbols; - -namespace Microsoft.TypeSpec.Generator -{ - internal class ReferenceMapBuilder - { - private readonly Compilation _compilation; - private readonly Project _project; - - public ReferenceMapBuilder(Compilation compilation, Project project) - { - _compilation = compilation; - _project = project; - } - - public async Task BuildPublicReferenceMapAsync(IEnumerable definitions, IReadOnlyDictionary> nodeCache) - { - var referenceMap = new ReferenceMap(); - foreach (var definition in definitions) - { - await ProcessPublicSymbolAsync(definition, referenceMap, nodeCache); - } - - return referenceMap; - } - - public async Task BuildAllReferenceMapAsync(IEnumerable definitions, IReadOnlyDictionary> documentCache) - { - var referenceMap = new ReferenceMap(); - foreach (var definition in definitions) - { - await ProcessSymbolAsync(definition, referenceMap, documentCache); - } - - return referenceMap; - } - - private async Task ProcessPublicSymbolAsync(INamedTypeSymbol symbol, ReferenceMap referenceMap, IReadOnlyDictionary> cache) - { - // only add to reference when myself is public - if (symbol.DeclaredAccessibility != Accessibility.Public) - { - return; - } - - // process myself, adding base and generic arguments - AddTypeSymbol(symbol, symbol, referenceMap); - - // add my sibling classes - foreach (var declaration in cache[symbol]) - { - // first find all the derived types from this type - foreach (var derivedTypeSymbol in await SymbolFinder.FindDerivedClassesAsync(symbol, _project.Solution)) - { - AddTypeSymbol(symbol, derivedTypeSymbol, referenceMap); - } - } - - // go over all the members - foreach (var member in symbol.GetMembers()) - { - // only go through the public members - if (member.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - switch (member) - { - case IMethodSymbol methodSymbol: - ProcessMethodSymbol(symbol, methodSymbol, referenceMap); - break; - case IPropertySymbol propertySymbol: - ProcessPropertySymbol(symbol, propertySymbol, referenceMap); - break; - case IFieldSymbol fieldSymbol: - ProcessFieldSymbol(symbol, fieldSymbol, referenceMap); - break; - case IEventSymbol eventSymbol: - ProcessEventSymbol(symbol, eventSymbol, referenceMap); - break; - case INamedTypeSymbol innerTypeSymbol: - break; // do nothing for the inner types - default: - throw new InvalidOperationException($"This case has not been covered {member.GetType()}"); - } - } - } - - private async Task ProcessSymbolAsync(INamedTypeSymbol symbol, ReferenceMap referenceMap, IReadOnlyDictionary> documentCache) - { - foreach (var reference in await SymbolFinder.FindReferencesAsync(symbol, _project.Solution)) - { - await AddReferenceToReferenceMapAsync(symbol, reference, referenceMap, documentCache); - } - - // static class can have direct references, like ClassName.Method, but the extension methods might not have direct reference to the class itself - // therefore here we find the references of all its members and add them to the reference map - await ProcessExtensionSymbol(symbol, referenceMap, documentCache); - } - - private async Task ProcessExtensionSymbol(INamedTypeSymbol extensionClassSymbol, ReferenceMap referenceMap, IReadOnlyDictionary> documentCache) - { - if (!extensionClassSymbol.IsStatic) - { - return; - } - - foreach (var member in extensionClassSymbol.GetMembers()) - { - if (member is not IMethodSymbol methodSymbol) - { - continue; - } - - if (!methodSymbol.IsExtensionMethod) - { - continue; - } - - foreach (var reference in await SymbolFinder.FindReferencesAsync(member, _project.Solution)) - { - await AddReferenceToReferenceMapAsync(extensionClassSymbol, reference, referenceMap, documentCache); - } - - // this is to hook the extension class like this: - // internal static class FooExtensions - // { - // public static string ToSerialString(this Foo foo) => foo.ToString(); - // public static Foo ToFoo(this string foo) => // omit body - // } - - // if this is an extension method, we add it to the reference map of the type it is extending to pretend that this class is a part of that type - // handle the first method above - if (methodSymbol.Parameters.FirstOrDefault()?.Type is INamedTypeSymbol typeSymbol) - { - referenceMap.AddInList(typeSymbol, extensionClassSymbol); - } - - // we also add the return type into the reference map of the extension class to cover both cases - // handle the second method above - if (methodSymbol.ReturnType is INamedTypeSymbol returnTypeSymbol) - { - referenceMap.AddInList(returnTypeSymbol, extensionClassSymbol); - } - } - } - - private async Task AddReferenceToReferenceMapAsync(INamedTypeSymbol symbol, ReferencedSymbol reference, ReferenceMap referenceMap, IReadOnlyDictionary> documentCache) - { - foreach (var location in reference.Locations) - { - var document = location.Document; - - // skip this reference if it comes from a document that does not define any symbol - if (!documentCache.TryGetValue(document, out var candidateReferenceSymbols)) - { - continue; - } - - if (candidateReferenceSymbols.Count == 1) - { - referenceMap.AddInList(candidateReferenceSymbols.Single(), symbol); - } - else - { - // fallback to calculate the symbol when the document contains multiple type symbols - // this should never happen in the generated code - // customized code might have this issue - var root = await document.GetSyntaxRootAsync(); - if (root == null) - { - continue; - } - // get the node of this reference - var node = root.FindNode(location.Location.SourceSpan); - var owner = GetOwnerTypeOfReference(node); - if (owner == null) - { - referenceMap.AddGlobal(symbol); - } - else - { - var semanticModel = _compilation.GetSemanticModel(owner.SyntaxTree); - var ownerSymbol = semanticModel.GetDeclaredSymbol(owner); - - if (ownerSymbol == null) - { - continue; - } - // add it to the map - referenceMap.AddInList(ownerSymbol, symbol); - } - } - } - } - - /// - /// This method recursively adds all related types in to the reference map as the value of key - /// - /// - /// - /// - private void AddTypeSymbol(ITypeSymbol keySymbol, ITypeSymbol? valueSymbol, ReferenceMap referenceMap) - { - if (keySymbol is not INamedTypeSymbol keyTypeSymbol) - { - return; - } - - if (valueSymbol is not INamedTypeSymbol valueTypeSymbol) - { - return; - } - // add the class and all its partial classes to the map - // this will make all the partial classes are referencing each other in the reference map - // when we make the travesal over the reference map, we will not only remove one of the partial class, instead we will either keep all the partial classes (if at least one of them has references), or remove all of them (if none of them has references) - if (!referenceMap.AddInList(keyTypeSymbol, valueTypeSymbol)) - { - return; // we short cut if the valueTypeSymbol has already existed in the list to avoid infinite loops - } - // add the base type - AddTypeSymbol(keyTypeSymbol, valueTypeSymbol.BaseType, referenceMap); - // add the interfaces if there is any - foreach (var interfaceSymbol in valueTypeSymbol.Interfaces) - { - AddTypeSymbol(keyTypeSymbol, interfaceSymbol, referenceMap); - } - // add the generic type arguments - foreach (var typeArgument in valueTypeSymbol.TypeArguments) - { - AddTypeSymbol(keyTypeSymbol, typeArgument, referenceMap); - } - } - - private void ProcessMethodSymbol(INamedTypeSymbol keySymbol, IMethodSymbol methodSymbol, ReferenceMap referenceMap) - { - // add the return type - AddTypeSymbol(keySymbol, methodSymbol.ReturnType, referenceMap); - // add the parameters - foreach (var parameter in methodSymbol.Parameters) - { - AddTypeSymbol(keySymbol, parameter.Type, referenceMap); - } - } - - private void ProcessPropertySymbol(INamedTypeSymbol keySymbol, IPropertySymbol propertySymbol, ReferenceMap referenceMap) - { - AddTypeSymbol(keySymbol, propertySymbol.Type, referenceMap); - - // find the node that defines myself - var xml = propertySymbol.GetDocumentationCommentXml(); - if (string.IsNullOrEmpty(xml)) - { - return; - } - - var xDocument = XDocument.Parse(xml); - var cRefs = xDocument.Descendants().Attributes("cref").Select(a => a.Value).Where(a => a[0] == 'T' && a[1] == ':').Select(a => a.Substring(2)); - - foreach (var cref in cRefs) - { - var symbol = _compilation.GetTypeByMetadataName(cref); - AddTypeSymbol(keySymbol, symbol, referenceMap); - } - } - - private void ProcessFieldSymbol(INamedTypeSymbol keySymbol, IFieldSymbol fieldSymbol, ReferenceMap referenceMap) => AddTypeSymbol(keySymbol, fieldSymbol.Type, referenceMap); - - private void ProcessEventSymbol(INamedTypeSymbol keySymbol, IEventSymbol eventSymbol, ReferenceMap referenceMap) => AddTypeSymbol(keySymbol, eventSymbol.Type, referenceMap); - - /// - /// Returns the node that defines inside the document, which should be , or - /// The here should come from the result of , therefore a result is guaranteed - /// - /// - /// - private static BaseTypeDeclarationSyntax? GetOwnerTypeOfReference(SyntaxNode node) - { - SyntaxNode? current = node; - while (current != null) - { - if (current is BaseTypeDeclarationSyntax declarationNode) - { - return declarationNode; - } - - current = current.Parent; - } - - // this means owner of the reference is outside a type definition. For instance, we could have an assembly attribute that is referencing a class using `nameof` - return null; - } - } -} 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..bf7bb056b58 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,8 +1,13 @@ // 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.Expressions; using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Statements; namespace Microsoft.TypeSpec.Generator.Primitives @@ -45,11 +50,11 @@ private bool IsPublicContext(TypeProvider provider) private void WriteType(CodeWriter writer) { - if (IsPublicContext(_provider)) + if (_provider.PreserveTypeXmlDocs || _provider.ShouldWriteTypeXmlDocs || IsPublicContext(_provider)) { writer.WriteXmlDocsNoScope(_provider.XmlDocs); } - foreach (var attribute in _provider.GetAttributes()) + foreach (var attribute in GetTypeAttributes()) { attribute.Write(writer); if (attribute is AttributeStatement) @@ -97,6 +102,42 @@ private void WriteType(CodeWriter writer) } } + private IReadOnlyList GetTypeAttributes() + { + var attributes = _provider.GetAttributes(); + if (_provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + return attributes; + } + + var seen = attributes.Select(static attribute => attribute.ToDisplayString()).ToHashSet(); + var lastContractAttributes = _provider.LastContractView?.Attributes + .Where(attribute => + ShouldPreserveLastContractAttribute(attribute) && + IsResolvableLastContractAttribute(attribute) && + seen.Add(attribute.ToDisplayString())) ?? []; + return [.. attributes, .. lastContractAttributes]; + } + + private static bool ShouldPreserveLastContractAttribute(AttributeStatement attribute) + { + var attributeName = attribute.Data?.AttributeClass?.Name; + return attributeName is not + (CodeGenAttributes.CodeGenSuppressAttributeName or + CodeGenAttributes.CodeGenMemberAttributeName or + CodeGenAttributes.CodeGenTypeAttributeName or + CodeGenAttributes.CodeGenSerializationAttributeName); + } + + private static bool IsResolvableLastContractAttribute(AttributeStatement attribute) => + ProviderReferenceMapAnalyzer.IsResolvableBuildableType(attribute.Type) && + attribute.Arguments.All(IsResolvableLastContractAttributeArgument) && + attribute.PositionalArguments.All(static argument => IsResolvableLastContractAttributeArgument(argument.Value)); + + private static bool IsResolvableLastContractAttributeArgument(ValueExpression argument) => + argument is not TypeOfExpression typeOf || + ProviderReferenceMapAnalyzer.IsResolvableBuildableType(typeOf.Type); + private void WriteClassOrStructContent(CodeWriter writer) { using (writer.Scope()) @@ -207,6 +248,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/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs index b2c2915f915..77835278816 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs @@ -48,7 +48,39 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu protected override IReadOnlyList BuildAttributes() { - return [.. _generatedTypeProvider.Attributes, .. _generatedTypeProvider.CustomCodeView?.Attributes ?? []]; + var lastContractAttributes = _generatedTypeProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) + ? null + : _generatedTypeProvider.LastContractView?.Attributes.Where(ShouldPreserveLastContractAttribute); + // TODO https://github.com/microsoft/typespec/issues/11232: Move this generated/last-contract/custom attribute merge into a shared TypeProvider back-compat API. + return DeduplicateAttributes( + _generatedTypeProvider.Attributes, + lastContractAttributes, + _generatedTypeProvider.CustomCodeView?.Attributes); + } + + private static IReadOnlyList DeduplicateAttributes(params IEnumerable?[] attributeSets) + { + var seen = new HashSet(); + var attributes = new List(); + foreach (var attribute in attributeSets.SelectMany(static attributeSet => attributeSet ?? [])) + { + if (seen.Add(attribute.ToDisplayString())) + { + attributes.Add(attribute); + } + } + + return attributes; + } + + private static bool ShouldPreserveLastContractAttribute(AttributeStatement attribute) + { + var attributeName = attribute.Data?.AttributeClass?.Name; + return attributeName is not + (CodeGenAttributes.CodeGenSuppressAttributeName or + CodeGenAttributes.CodeGenMemberAttributeName or + CodeGenAttributes.CodeGenTypeAttributeName or + CodeGenAttributes.CodeGenSerializationAttributeName); } private protected override CanonicalTypeProvider BuildCanonicalView() => this; 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..e236d6b67a2 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,8 @@ internal sealed class NamedTypeSymbolProvider : TypeProvider { private INamedTypeSymbol _namedTypeSymbol; private readonly Compilation _compilation; + private string? _metadataName; + private string? _metadataSimpleName; private TypeProvider? _baseTypeProvider; public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation) @@ -30,9 +32,37 @@ public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation com _compilation = compilation; } + internal string MetadataName + { + get + { + if (_metadataName != null) + { + return _metadataName; + } + + var ns = _namedTypeSymbol.ContainingNamespace.GetFullyQualifiedNameFromDisplayString(); + var typeName = GetMetadataName(_namedTypeSymbol); + _metadataName = string.IsNullOrEmpty(ns) ? typeName : $"{ns}.{typeName}"; + return _metadataName; + } + } + + internal string MetadataSimpleName => _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; + private static string GetMetadataName(INamedTypeSymbol symbol) + { + if (symbol.ContainingType is null) + { + return symbol.MetadataName; + } + + return $"{GetMetadataName(symbol.ContainingType)}+{symbol.MetadataName}"; + } + protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writing in generation"); protected override string BuildName() => _namedTypeSymbol.Name; @@ -321,6 +351,378 @@ [.. 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); + AddPublicTypeSignatureDependencyTypes(typeDeclaration, dependencies, semanticModel, namespaceCandidates); + } + + 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 TypeDeclarationSyntax type: + AddPublicTypeSignatureDependencyTypes(type, dependencies, semanticModel, namespaceCandidates); + break; + case BaseTypeDeclarationSyntax type: + AddSyntaxTypeReferences(type.BaseList, dependencies, semanticModel, namespaceCandidates); + break; + } + } + + private void AddPublicTypeSignatureDependencyTypes( + TypeDeclarationSyntax typeDeclaration, + HashSet dependencies, + SemanticModel semanticModel, + IReadOnlyList namespaceCandidates) + { + AddSyntaxTypeReferences(typeDeclaration.BaseList, dependencies, semanticModel, namespaceCandidates); + AddSyntaxTypeReferences(typeDeclaration.ConstraintClauses, dependencies, semanticModel, namespaceCandidates); + foreach (var member in typeDeclaration.Members) + { + if (IsPublicApiMember(member)) + { + AddPublicSignatureDependencyTypes(member, dependencies); + } + } + } + + 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) || IsImplicitPublicInterfaceMember(@event), + EventFieldDeclarationSyntax @event => IsPublic(@event.Modifiers) || IsImplicitPublicInterfaceMember(@event), + BaseFieldDeclarationSyntax field => IsPublic(field.Modifiers) || IsImplicitPublicInterfaceMember(field), + BaseMethodDeclarationSyntax method => IsPublic(method.Modifiers) || IsImplicitPublicInterfaceMember(method), + BasePropertyDeclarationSyntax property => IsPublic(property.Modifiers) || IsImplicitPublicInterfaceMember(property), + DelegateDeclarationSyntax @delegate => IsPublic(@delegate.Modifiers) || IsImplicitPublicInterfaceMember(@delegate), + BaseTypeDeclarationSyntax type => IsPublic(type.Modifiers) || IsImplicitPublicInterfaceMember(type), + _ => false + }; + + private static bool IsPublic(SyntaxTokenList modifiers) + => modifiers.Any(static modifier => + modifier.IsKind(SyntaxKind.PublicKeyword) || + modifier.IsKind(SyntaxKind.ProtectedKeyword)); + + private static bool IsImplicitPublicInterfaceMember(MemberDeclarationSyntax member) + => member.Parent is InterfaceDeclarationSyntax && + !member.Modifiers.Any(static modifier => + modifier.IsKind(SyntaxKind.PrivateKeyword) || + modifier.IsKind(SyntaxKind.InternalKeyword)); + + 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 3d71670d5a9..2b6099e0bf0 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 @@ -44,14 +44,26 @@ protected TypeProvider() : this(null) => CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( generatedTypeNamespace ?? BuildNamespace(), generatedTypeName ?? BuildName(), - // Use the Type.Name so that any customizations to the declaring type are applied for the lookup. - DeclaringTypeProvider?.Type.Name); + GetDeclaringTypeName(DeclaringTypeProvider)); private protected virtual TypeProvider? BuildLastContractView(string? generatedTypeName = null, string? generatedTypeNamespace = null) => CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContract( generatedTypeNamespace ?? CustomCodeView?.Type.Namespace ?? BuildNamespace(), generatedTypeName ?? CustomCodeView?.Name ?? BuildName(), - DeclaringTypeProvider?.Type.Name); + GetDeclaringTypeName(DeclaringTypeProvider)); + + private static string? GetDeclaringTypeName(TypeProvider? declaringTypeProvider) + { + if (declaringTypeProvider is null) + { + return null; + } + + var parentName = GetDeclaringTypeName(declaringTypeProvider.DeclaringTypeProvider); + return parentName is null + ? declaringTypeProvider.Type.Name + : $"{parentName}+{declaringTypeProvider.Type.Name}"; + } private protected virtual TypeProvider BuildSpecView() => new SpecTypeProvider(this); @@ -143,6 +155,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; @@ -290,7 +311,21 @@ private IReadOnlyList ApplyCustomizationFilter(IEnumerable? _serializationProviders; - public IReadOnlyList SerializationProviders => _serializationProviders ??= BuildSerializationProviders(); + public IReadOnlyList SerializationProviders => _serializationProviders ??= BuildSerializationProvidersInternal(); + + internal TypeProvider? SerializationProviderOwner { get; private set; } + + private IReadOnlyList? _helperDependencyTypes; + internal IReadOnlyList HelperDependencyTypes => _helperDependencyTypes ??= BuildHelperDependencyTypes(); + protected internal virtual IReadOnlyList BuildHelperDependencyTypes() => []; + + private IReadOnlyList? _bodyDependencyTypes; + public IReadOnlyList BodyDependencyTypes => _bodyDependencyTypes ??= BuildBodyDependencyTypes(); + protected internal virtual IReadOnlyList BuildBodyDependencyTypes() => []; + + private IReadOnlyList? _signatureDependencyTypes; + public IReadOnlyList SignatureDependencyTypes => _signatureDependencyTypes ??= BuildSignatureDependencyTypes(); + protected internal virtual IReadOnlyList BuildSignatureDependencyTypes() => []; private IReadOnlyList? _attributes; @@ -313,6 +348,26 @@ public IReadOnlyList Attributes internal IReadOnlyList GetAttributes() => _attributes ??= BuildAttributes(); + internal void ClearCachedAttributes() => _attributes = null; + + /// + /// Indicates whether this provider's attributes should contribute to the reference map. Providers whose + /// attributes depend on the final reference-map result can opt out so cached attributes are rebuilt after analysis. + /// + protected internal virtual bool ShouldAnalyzeAttributesInReferenceMap => true; + + /// + /// Determines whether a provider remains in the generated output after reference-map analysis. + /// + protected static bool ShouldWriteProvider(TypeProvider provider) => + ProviderReferenceMapAnalyzer.ShouldWriteProvider(provider); + + /// + /// Determines whether a type remains resolvable after reference-map analysis. + /// + protected static bool IsResolvableBuildableType(CSharpType type) => + ProviderReferenceMapAnalyzer.IsResolvableBuildableType(type); + protected virtual CSharpType[] GetTypeArguments() => []; internal PropertyProvider[] FilterCustomizedProperties(IEnumerable specProperties) @@ -495,6 +550,20 @@ private TypeProvider[] BuildNestedTypesInternal() protected virtual TypeProvider[] BuildSerializationProviders() => []; + private IReadOnlyList BuildSerializationProvidersInternal() + => AssignSerializationProviderOwners(BuildSerializationProviders()); + + private IReadOnlyList AssignSerializationProviderOwners(IEnumerable serializationProviders) + { + var providers = (serializationProviders as IReadOnlyList) ?? [.. serializationProviders]; + foreach (var serializationProvider in providers) + { + serializationProvider.SerializationProviderOwner = this; + } + + return providers; + } + protected virtual CSharpType BuildEnumUnderlyingType() => throw new InvalidOperationException("Not an EnumProvider type"); protected virtual IReadOnlyList BuildAttributes() => []; @@ -538,6 +607,7 @@ public virtual void Reset() _serializationProviders = null; _nestedTypes = null; _xmlDocs = null; + PreserveTypeXmlDocs = false; _declarationModifiers = null; _relativeFilePath = null; _customCodeView = new(() => BuildCustomCodeView()); @@ -612,7 +682,7 @@ public void Update( } if (serializations != null) { - _serializationProviders = (serializations as IReadOnlyList) ?? serializations.ToList(); + _serializationProviders = AssignSerializationProviderOwners(serializations); } if (nestedTypes != null) { @@ -632,7 +702,7 @@ public void Update( } if (attributes != null) { - _attributes = (attributes as IReadOnlyList) ?? [.. attributes]; + _attributes = [.. attributes]; } if (name != null) 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..7b8174ff204 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.BodyReferences.cs @@ -0,0 +1,406 @@ +// 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.Snippets; +using Microsoft.TypeSpec.Generator.Statements; + +namespace Microsoft.TypeSpec.Generator +{ + internal static partial class ProviderReferenceMapAnalyzer + { + private static void AddGeneratedBodyReferences(IReadOnlyList providers, ProviderReferenceGraph graph) + { + var generatedProviders = GetGeneratedProviders(providers); + var serializationProviderNamesByType = GetSerializationProviderNamesByType(generatedProviders); + 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; + } + + var bodyReferences = CollectStructuredBodyReferences(provider); + AddStructuredBodyReferenceTypes( + graph.References[providerName], + provider, + bodyReferences.Types, + graph.Nodes, + serializationProviderNamesByType); + AddExtensionMethodProviderReferences( + graph.References[providerName], + bodyReferences.ExtensionMethods, + generatedProviders, + graph.Nodes); + AddProviderBodyDependencyTypes(graph.References[providerName], provider.BodyDependencyTypes, graph.Nodes); + AddGeneratedProviderNamespaceBodyDependencyTypes(graph.References[providerName], provider, graph.Nodes); + } + } + + private static void AddGeneratedProviderNamespaceBodyDependencyTypes(HashSet references, TypeProvider provider, HashSet nodes) + { + var providerNamespace = provider.Type.Namespace; + if (string.IsNullOrEmpty(providerNamespace)) + { + return; + } + + foreach (var dependency in provider.BodyDependencyTypes) + { + AddGeneratedProviderNamespaceBodyDependencyType(references, dependency, providerNamespace, nodes); + } + } + + private static void AddGeneratedProviderNamespaceBodyDependencyType(HashSet references, CSharpType? dependency, string providerNamespace, HashSet nodes) + { + if (dependency == null) + { + return; + } + + AddNamespaceBodyDependencyName(references, providerNamespace, dependency, nodes); + foreach (var argument in dependency.Arguments) + { + AddGeneratedProviderNamespaceBodyDependencyType(references, argument, providerNamespace, nodes); + } + } + + private static void AddNamespaceBodyDependencyName(HashSet references, string providerNamespace, CSharpType dependency, HashSet nodes) + { + var dependencyName = GetSimpleName(GetProviderTypeName(dependency)); + AddExactMetadataNameMatch(references, $"{providerNamespace}.{dependencyName}", nodes); + var aritylessDependencyName = StripGenericArity(dependencyName); + if (!string.Equals(aritylessDependencyName, dependencyName, StringComparison.Ordinal)) + { + AddExactMetadataNameMatch(references, $"{providerNamespace}.{aritylessDependencyName}", nodes); + } + } + + private static void AddStructuredBodyReferenceTypes( + HashSet references, + TypeProvider provider, + IReadOnlyList dependencies, + HashSet nodes, + IReadOnlyDictionary serializationProviderNamesByType) + { + foreach (var dependency in dependencies) + { + if (!IsEnumProviderDependency(dependency, nodes)) + { + AddTypeReference( + references, + dependency, + nodes, + serializationProviderNamesByType, + provider.Type.Namespace); + } + } + } + + private static StructuredBodyReferences CollectStructuredBodyReferences(TypeProvider provider) + { + var references = new HashSet(); + var extensionMethods = new HashSet(); + var visited = new HashSet(ReferenceEqualityComparer.Instance); + + foreach (var field in provider.Fields) + { + CollectStructuredBodyReferences(field.InitializationValue, references, extensionMethods, visited); + } + + foreach (var property in provider.Properties) + { + CollectStructuredBodyReferences(property.Body, references, extensionMethods, visited); + } + + foreach (var constructor in provider.Constructors) + { + CollectStructuredBodyReferences(constructor.BodyExpression, references, extensionMethods, visited); + CollectStructuredBodyReferences(constructor.BodyStatements, references, extensionMethods, visited); + } + + foreach (var method in provider.Methods) + { + if (method.IsMethodSuppressed()) + { + continue; + } + + CollectStructuredBodyReferences(method.BodyExpression, references, extensionMethods, visited); + CollectStructuredBodyReferences(method.BodyStatements, references, extensionMethods, visited); + } + + return new([.. references], [.. extensionMethods]); + } + + private static void CollectStructuredBodyReferences( + object? value, + HashSet references, + HashSet extensionMethods, + HashSet visited) + { + switch (value) + { + case null: + case string: + case FormattableString: + return; + } + + if (!value.GetType().IsValueType && !visited.Add(value)) + { + return; + } + + if (value is InvokeMethodExpression { ExtensionType: null, MethodName: { } methodName } invocation) + { + var receiverType = GetExpressionType(invocation.InstanceReference); + if (receiverType != null) + { + extensionMethods.Add(new(methodName, receiverType)); + } + } + + switch (value) + { + case CSharpType type: + references.Add(type); + return; + case Type type: + references.Add(type); + return; + case ParameterProvider parameter: + references.Add(parameter.Type); + CollectStructuredBodyReferences(parameter.DefaultValue, references, extensionMethods, visited); + CollectStructuredBodyReferences(parameter.InitializationValue, references, extensionMethods, visited); + return; + case MethodSignatureBase signature: + CollectStructuredBodyReferences(signature.ReturnType, references, extensionMethods, visited); + CollectStructuredBodyReferences(signature.Parameters, references, extensionMethods, visited); + return; + case KeyValuePair positionalArgument: + CollectStructuredBodyReferences(positionalArgument.Value, references, extensionMethods, visited); + return; + case FieldProvider field: + references.Add(field.Type); + CollectStructuredBodyReferences(field.InitializationValue, references, extensionMethods, visited); + return; + } + + if (IsStructuredBodyReferenceObject(value)) + { + foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.GetIndexParameters().Length > 0) + { + continue; + } + + CollectStructuredBodyReferences(property.GetValue(value), references, extensionMethods, visited); + } + + return; + } + + if (value is not IEnumerable values) + { + return; + } + + foreach (var item in values) + { + CollectStructuredBodyReferences(item, references, extensionMethods, visited); + } + } + + private static CSharpType? GetExpressionType(ValueExpression? expression) => + expression switch + { + VariableExpression variable => variable.Type, + ScopedApi scopedApi => scopedApi.Type, + _ => null + }; + + private static void AddExtensionMethodProviderReferences( + HashSet references, + IReadOnlyList extensionMethods, + IReadOnlyList generatedProviders, + HashSet nodes) + { + foreach (var extensionMethod in extensionMethods) + { + var provider = FindUniqueExtensionMethodProvider(extensionMethod, generatedProviders); + if (provider != null) + { + AddTypeReference(references, provider.Type, nodes); + } + } + } + + private static TypeProvider? FindUniqueExtensionMethodProvider( + ExtensionMethodReference extensionMethod, + IReadOnlyList generatedProviders) + { + TypeProvider? match = null; + foreach (var provider in generatedProviders) + { + if (!provider.Methods.Any(method => IsMatchingExtensionMethod(method.Signature, extensionMethod))) + { + continue; + } + + if (match != null) + { + return null; + } + + match = provider; + } + + return match; + } + + private static bool IsMatchingExtensionMethod(MethodSignature signature, ExtensionMethodReference extensionMethod) + { + if (!signature.Modifiers.HasFlag(MethodSignatureModifiers.Extension) || + !string.Equals(signature.Name, extensionMethod.Name, StringComparison.Ordinal)) + { + return false; + } + + return extensionMethod.ReceiverType == null || + signature.Parameters.Count > 0 && + string.Equals( + GetProviderTypeName(signature.Parameters[0].Type), + GetProviderTypeName(extensionMethod.ReceiverType), + StringComparison.Ordinal); + } + + private readonly record struct StructuredBodyReferences( + IReadOnlyList Types, + IReadOnlyList ExtensionMethods); + + private readonly record struct ExtensionMethodReference(string Name, CSharpType? ReceiverType); + + 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) + { + foreach (var dependency in dependencies) + { + AddProviderBodyDependencyType(references, dependency, nodes, includeSimpleNameReferences, includeUnqualifiedSimpleNameReferences); + } + } + + private static void AddProviderBodyDependencyType( + HashSet references, + CSharpType? dependency, + HashSet nodes, + bool includeSimpleNameReferences, + bool includeUnqualifiedSimpleNameReferences) + { + 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); + } + foreach (var argument in dependency.Arguments) + { + AddProviderBodyDependencyType(references, argument, nodes, includeSimpleNameReferences, includeUnqualifiedSimpleNameReferences); + } + } + + 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) + { + return IsGeneratedImplementationBodyReferenceCandidate(provider, isSerializationProvider) || + provider.HelperDependencyTypes.Count > 0 || + provider.BodyDependencyTypes.Count > 0; + } + + private static bool IsGeneratedImplementationBodyReferenceCandidate(TypeProvider provider, bool isSerializationProvider) + { + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static) || + IsClientProvider(provider) || + isSerializationProvider) + { + return true; + } + + return provider is not ModelProvider && + provider is not EnumProvider && + !IsModelFactoryProvider(provider) && + provider.DeclaringTypeProvider == null && + provider.SerializationProviders.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..603a185e347 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs @@ -0,0 +1,396 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal 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 GetPublicDeclaredNodes( + IReadOnlyList generatedProviders, + HashSet nodes) + { + return GetGeneratedDeclaredNodes(generatedProviders, nodes, publicOnly: false); + } + + private static HashSet GetPublicApiTraversalNodes( + HashSet internalizeDeclaredNodes, + HashSet publicDeclaredNodes, + 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 publicDeclaredNodes) + { + if (!generatedInternalDeclarations.Contains(node) && + !generatedImplementationInternalDeclarations.Contains(node)) + { + traversalNodes.Add(node); + } + } + + return traversalNodes; + } + + private static HashSet GetInternalizeCandidates( + HashSet internalizeDeclaredNodes, + HashSet publicReachable, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet customInternalBoundaryNodes, + HashSet customPublicRoots, + HashSet publicRoots, + HashSet nodes, + IReadOnlyDictionary> references) + { + var candidates = new HashSet(StringComparer.Ordinal); + foreach (var node in internalizeDeclaredNodes) + { + var isNonRootKept = IsKeptName(node, CodeModelGenerator.Instance.NonRootTypes, nodes); + var isPublicReachableNonRootKept = isNonRootKept && publicReachable.Contains(node); + if (!publicReachable.Contains(node) || + customInternalDeclarations.Contains(node) || + generatedInternalDeclarations.Contains(node) && !customPublicRoots.Contains(node) && !isPublicReachableNonRootKept || + customInternalBoundaryNodes.Contains(node) && (!publicRoots.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) || + publicRoots.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 RemovePublicApiExposedCandidates( + HashSet internalizeDeclaredNodes, + HashSet candidates, + HashSet customInternalDeclarations, + IReadOnlyDictionary> references) + { + var removedCandidate = true; + while (removedCandidate) + { + removedCandidate = false; + foreach (var candidate in candidates.ToArray()) + { + if (customInternalDeclarations.Contains(candidate)) + { + continue; + } + + foreach (var node in internalizeDeclaredNodes) + { + if (candidates.Contains(node) || + !references.TryGetValue(node, out var nodeReferences) || + !nodeReferences.Contains(candidate)) + { + continue; + } + + candidates.Remove(candidate); + removedCandidate = true; + break; + } + } + } + } + + 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; + } + } + } + } + + /// + /// Internalizes dependencies whose generated predecessors are all internal, unless the dependency + /// is an explicit public root. Repeats until transitive internal-only dependencies are included. + /// + 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; + } + } + } + } + + /// + /// Finds generated declarations that must be promoted to public because they are exposed by a + /// reachable public API, while respecting explicit internal boundaries and helper-only roots. + /// + private static HashSet GetPublicCandidates( + HashSet publicDeclaredNodes, + HashSet publicReachable, + HashSet customInternalDeclarations, + HashSet customInternalBoundaryNodes, + HashSet internalizeHelperRoots, + HashSet publicRootExclusions, + HashSet generatedInternalDeclarations, + HashSet publicRoots, + Dictionary> publicApiReferences, + Dictionary> internalizeReferences, + HashSet generatedImplementationInternalDeclarations) + { + var candidates = new HashSet(StringComparer.Ordinal); + foreach (var node in publicDeclaredNodes) + { + if (customInternalDeclarations.Contains(node) || + customInternalBoundaryNodes.Contains(node) || + internalizeHelperRoots.Contains(node) || + publicRootExclusions.Contains(node) || + !publicReachable.Contains(node)) + { + continue; + } + + if (generatedInternalDeclarations.Contains(node) && + !publicRoots.Contains(node) && + !HasPublicApiPredecessor(node, publicApiReferences, publicReachable, generatedImplementationInternalDeclarations)) + { + continue; + } + + if (!publicRoots.Contains(node) && + !HasPublicApiPredecessor(node, internalizeReferences, publicReachable, generatedInternalDeclarations, 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); + AddKeptNonRootNames(removeRoots, graph.Nodes); + + 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, includeModelSerializationProviders: true); + removeRoots.UnionWith(removeHelperRoots); + + var removeReachable = GetReachableTypes(removeRoots, graph.References); + AddBasePreservedReferences(generatedProviders, graph.Nodes, graph.References, removeReachable); + + var removeDeclaredNodes = GetGeneratedDeclaredNodes(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..41c6d6f181e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs @@ -0,0 +1,382 @@ +// 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.CodeAnalysis; +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, providers, 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, providers, 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 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 AddCustomCodeViewRoots( + HashSet roots, + TypeProvider customCodeView, + HashSet generatedTypeNames, + IReadOnlyList generatedProviders, + bool publicOnly) + { + AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); + AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames, includeUnqualifiedSimpleNameReferences: true); + if (!publicOnly) + { + AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); + AddUniqueExtensionMethodProviderRoots(roots, customCodeView.BodyDependencyTypes, generatedProviders, generatedTypeNames); + AddCustomCodeViewNamespaceBodyDependencyTypes(roots, customCodeView, generatedTypeNames); + AddAttributes(roots, customCodeView.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); + } + + 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 AddUniqueExtensionMethodProviderRoots( + HashSet roots, + IReadOnlyList dependencies, + IReadOnlyList generatedProviders, + HashSet generatedTypeNames) + { + foreach (var dependency in dependencies) + { + if (string.IsNullOrEmpty(dependency.Namespace)) + { + var match = FindUniqueExtensionMethodProvider(new(dependency.Name, ReceiverType: null), generatedProviders); + if (match != null) + { + AddTypeReference(roots, match.Type, generatedTypeNames); + } + } + + AddUniqueExtensionMethodProviderRoots(roots, dependency.Arguments, generatedProviders, generatedTypeNames); + } + } + + 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; + } + + var dependencyName = GetProviderTypeName(dependency); + var simpleDependencyName = StripGenericArity(GetSimpleName(dependencyName)); + if (string.IsNullOrEmpty(dependency.Namespace) && + !customMemberNames.Contains(simpleDependencyName)) + { + for (var namespaceCandidate = customNamespace; namespaceCandidate != null; namespaceCandidate = GetNamespaceName(namespaceCandidate)) + { + AddExactMetadataNameMatch(roots, $"{namespaceCandidate}.{dependencyName}", generatedTypeNames); + if (!string.Equals(simpleDependencyName, dependencyName, StringComparison.Ordinal)) + { + AddExactMetadataNameMatch(roots, $"{namespaceCandidate}.{simpleDependencyName}", 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; + } + + /// + /// Matches internal custom declarations to the generated provider identities whose accessibility + /// they constrain. Metadata names preserve nested-type identity when available. + /// + private static HashSet 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.ShouldAnalyzeAttributesInReferenceMap) + { + continue; + } + + if (provider.Attributes.Any(static attribute => IsAttributeNamed(attribute, "PersistableModelProxy"))) + { + AddTypeReference(proxyTypes, provider.Type, generatedTypeNames); + } + } + + return proxyTypes; + } + + private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Internal); + + private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) + => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Public); + + private static HashSet GetGeneratedPublicTypeDeclarationsFromLastContract(IReadOnlyList providers, HashSet generatedTypeNames) + => 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, + GetSimpleName(GetProviderTypeName(provider.Type)), + provider.DeclaringTypeProvider is { } declaringTypeProvider + ? GetSimpleName(GetProviderTypeName(declaringTypeProvider.Type)) + : null); + if (lastContractView?.DeclarationModifiers.HasFlag(accessibility) != true) + { + continue; + } + + AddTypeReference(declarations, provider.Type, generatedTypeNames); + } + + return declarations; + } + + 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) && + !IsGeneratedInternalImplementation(provider)) + { + continue; + } + + AddTypeReference(implementationDeclarations, provider.Type, generatedInternalDeclarations); + } + + 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(StripGenericArity(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..2f011f89af7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs @@ -0,0 +1,183 @@ +// 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); + var providerNamespace = provider.Type.Namespace; + AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType, providerNamespace); + + if (publicOnly && !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + if (!publicOnly && IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) + { + continue; + } + + // Model factory signatures mention many models, but methods for unreachable models + // are removed with those models. Only helper dependencies contribute reachability. + if (IsModelFactoryProvider(provider)) + { + continue; + } + + foreach (var implementedType in provider.Implements) + { + AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType, providerNamespace); + } + + if (!publicOnly) + { + foreach (var nestedType in provider.NestedTypes) + { + AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType, providerNamespace); + } + } + + if (!publicOnly) + { + foreach (var serializationProvider in provider.SerializationProviders) + { + AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType, providerNamespace); + } + } + + foreach (var signatureDependency in provider.SignatureDependencyTypes) + { + AddTypeReference(references[current], signatureDependency, nodes, serializationReferenceNamesByType, providerNamespace); + } + + foreach (var property in provider.Properties) + { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + + AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType, providerNamespace); + 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, providerNamespace); + 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, providerNamespace: providerNamespace); + } + + 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, providerNamespace: providerNamespace); + } + } + + return new ProviderReferenceGraph(nodes, references); + } + + private static Dictionary GetSerializationProviderNamesByType(IReadOnlyList generatedProviders) + { + var namesByType = new Dictionary>(StringComparer.Ordinal); + foreach (var provider in generatedProviders) + { + if (provider.SerializationProviderOwner is { } owner) + { + AddSerializationProviderName(owner.Type, provider.Type); + } + + if (provider.SerializationProviders.Count == 0) + { + continue; + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddSerializationProviderName(provider.Type, serializationProvider.Type); + } + } + + var result = new Dictionary(StringComparer.Ordinal); + foreach (var (providerName, serializationProviderNames) in namesByType) + { + result.Add(providerName, [.. serializationProviderNames]); + } + + return result; + + void AddSerializationProviderName(CSharpType ownerType, CSharpType serializationProviderType) + { + var ownerName = GetProviderTypeName(ownerType); + if (!namesByType.TryGetValue(ownerName, out var serializationProviderNames)) + { + serializationProviderNames = new HashSet(StringComparer.Ordinal); + namesByType.Add(ownerName, serializationProviderNames); + } + + serializationProviderNames.Add(GetProviderTypeName(serializationProviderType)); + } + } + } +} 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..dc30e88210a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs @@ -0,0 +1,450 @@ +// 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 || + CodeModelGenerator.Instance.SourceInputModel.LastContract == null && + 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 IsModelFactoryProvider(TypeProvider provider) + => provider is ModelFactoryProvider; + + private static bool IsGeneratedInternalHelperDeclaration(TypeProvider provider) + { + if (provider is ModelProvider || + provider is EnumProvider || + IsModelFactoryProvider(provider) || + provider.DeclaringTypeProvider != null || + provider.SerializationProviders.Count > 0 || + IsModelSerializationProviderDeclaration(provider)) + { + return false; + } + + return !string.Equals(provider.RelativeFilePath, Path.Combine("src", "Generated", "Models", $"{provider.Name}.cs"), StringComparison.Ordinal); + } + + private static bool IsModelSerializationProviderDeclaration(TypeProvider provider) + { + var modelsDirectory = Path.Combine("src", "Generated", "Models"); + var relativeFilePath = provider.RelativeFilePath; + return relativeFilePath.StartsWith(modelsDirectory + Path.DirectorySeparatorChar, StringComparison.Ordinal) && + Path.GetFileNameWithoutExtension(relativeFilePath).Contains(".Serialization", StringComparison.Ordinal); + } + + private static HashSet GetHelperRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet reachableTypes, + IReadOnlyDictionary>? references = null, + bool includeModelSerializationProviders = false) + { + var roots = new HashSet(StringComparer.Ordinal); + var generatedHelperNames = references == null ? null : GetGeneratedHelperNames(providers, nodes, includeModelSerializationProviders); + 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], provider.Type.Namespace); + if (references != null) + { + AddReferencedHelperRoots(roots, references[providerName], generatedHelperNames!); + } + + 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); + } + } + } + } + + return roots; + } + + private static HashSet GetGeneratedHelperNames(IReadOnlyList providers, HashSet nodes, bool includeModelSerializationProviders) + { + var helpers = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (!IsGeneratedInternalHelperDeclaration(provider) && + (!includeModelSerializationProviders || !IsModelSerializationProviderDeclaration(provider))) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (nodes.Contains(providerName)) + { + helpers.Add(providerName); + } + } + + return helpers; + } + + private static void AddReferencedHelperRoots(HashSet roots, HashSet referencedNames, HashSet generatedHelperNames) + { + foreach (var referencedName in referencedNames) + { + if (generatedHelperNames.Contains(referencedName)) + { + roots.Add(referencedName); + } + } + } + + private static void AddParameterValidationHelperRoot(HashSet roots, ParameterProvider parameter, HashSet nodes) + { + if (parameter.Validation != ParameterValidationType.None) + { + AddTypeReference(roots, new ArgumentDefinition().Type, nodes); + } + } + + private static void AddHelperDependencies( + HashSet roots, + IReadOnlyList dependencies, + HashSet nodes, + HashSet? referencedNames, + string? providerNamespace) + { + foreach (var dependency in dependencies) + { + var matches = new HashSet(StringComparer.Ordinal); + AddTypeReference(matches, dependency, nodes); + AddProviderNamespaceDependencyMatches(matches, dependency, providerNamespace, nodes); + foreach (var match in matches) + { + roots.Add(match); + } + } + } + + private static void AddProviderNamespaceDependencyMatches(HashSet matches, CSharpType? dependency, string? providerNamespace, HashSet nodes) + { + if (dependency == null || string.IsNullOrEmpty(providerNamespace)) + { + return; + } + + AddNamespaceBodyDependencyName(matches, providerNamespace, dependency, nodes); + foreach (var argument in dependency.Arguments) + { + AddProviderNamespaceDependencyMatches(matches, argument, providerNamespace, nodes); + } + } + + 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 publicReachable, + HashSet generatedImplementationInternalDeclarations) + => HasPublicApiPredecessor(name, references, publicReachable, excludedPredecessors: null, generatedImplementationInternalDeclarations); + + private static bool HasPublicApiPredecessor( + string name, + IReadOnlyDictionary> references, + HashSet publicReachable, + HashSet? excludedPredecessors, + HashSet generatedImplementationInternalDeclarations) + { + foreach (var (owner, children) in references) + { + if (!publicReachable.Contains(owner) || + string.Equals(owner, name, StringComparison.Ordinal) || + excludedPredecessors?.Contains(owner) == true || + 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..77784249906 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs @@ -0,0 +1,194 @@ +// 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)) + { + continue; + } + + if (!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(); + var visited = new HashSet(); + foreach (var provider in providers) + { + AddGeneratedProvider(generatedProviders, visited, provider); + } + + return generatedProviders; + } + + private static void AddGeneratedProvider(List generatedProviders, HashSet visited, TypeProvider provider) + { + if (!visited.Add(provider)) + { + return; + } + + generatedProviders.Add(provider); + foreach (var nestedType in provider.NestedTypes) + { + AddGeneratedProvider(generatedProviders, visited, nestedType); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddGeneratedProvider(generatedProviders, visited, 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..976ed735cf5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs @@ -0,0 +1,184 @@ +// 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) || + method.Signature.ReturnType == null) + { + 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.FullyQualifiedName)) + { + continue; + } + + AddMatchingName(roots, GetProviderTypeName(provider.Type), nodes); + } + } + + private static bool ShouldUseUnionVariantFallbackRoots() => + CodeModelGenerator.Instance.SourceInputModel.LastContract == null; + + private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) + { + if (namesToRemove.Count == 0) + { + return; + } + + var fullNamesToRemove = namesToRemove; + var simpleNamesToRemove = GetSimpleNames(namesToRemove); + 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 (!ShouldRemoveModelFactoryMethod(method, fullNamesToRemove, simpleNamesToRemove)) + { + methodsToKeep.Add(method); + continue; + } + + if (i == 0) + { + removedLeadingMethod = true; + } + } + + if (removedLeadingMethod && methodsToKeep.Count > 0) + { + modelFactory.PreserveLeadingMethodSeparatorForBackCompat(); + } + + modelFactory.Update(methods: methodsToKeep); + } + + private static bool ShouldRemoveModelFactoryMethod( + MethodProvider method, + HashSet fullNamesToRemove, + HashSet simpleNamesToRemove) + { + if (simpleNamesToRemove.Contains(method.Signature.Name)) + { + return true; + } + + if (method.Signature.ReturnType == null) + { + return false; + } + + var returnTypeName = GetProviderTypeName(method.Signature.ReturnType); + return fullNamesToRemove.Contains(returnTypeName) || + simpleNamesToRemove.Contains(StripGenericArity(GetSimpleName(returnTypeName))); + } + + private static HashSet GetGeneratedDeclaredNodes(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..8186266c383 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs @@ -0,0 +1,193 @@ +// 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, + string? providerNamespace = null) + { + AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType, providerNamespace); + if (includeAttributes) + { + AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); + } + + foreach (var parameter in signature.Parameters) + { + AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType, providerNamespace); + if (includeAttributes) + { + AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); + } + } + + if (signature is MethodSignature methodSignature) + { + AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType, providerNamespace); + if (methodSignature.GenericArguments != null) + { + foreach (var genericArgument in methodSignature.GenericArguments) + { + AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType, providerNamespace); + } + } + + if (methodSignature.GenericParameterConstraints != null) + { + foreach (var constraint in methodSignature.GenericParameterConstraints) + { + AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType, providerNamespace); + } + } + } + + if (signature is ConstructorSignature constructorSignature) + { + AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType, providerNamespace); + } + } + + 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, + string? providerNamespace = null) + { + if (type == null) + { + return; + } + + if (type.IsArray) + { + AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType, providerNamespace); + 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); + } + } + else if (!nodes.Contains(providerTypeName) && + string.IsNullOrEmpty(type.Namespace) && + !string.IsNullOrEmpty(providerNamespace)) + { + AddExactMetadataNameMatch(references, $"{providerNamespace}.{providerTypeName}", nodes); + } + + AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType, providerNamespace); + AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType, providerNamespace); + foreach (var argument in type.Arguments) + { + AddTypeReference(references, argument, nodes, serializationProviderNamesByType, providerNamespace); + } + } + + 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('`'); + var typeArgumentStart = name.IndexOf('<'); + var genericSuffixStart = tick < 0 + ? typeArgumentStart + : typeArgumentStart < 0 + ? tick + : Math.Min(tick, typeArgumentStart); + return genericSuffixStart < 0 ? name : name.Substring(0, genericSuffixStart); + } + } +} 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..edf62a5ffb3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.cs @@ -0,0 +1,378 @@ +// 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 ProviderReferenceMapSession PrepareForGeneration(IReadOnlyList providers) + { + ResetPreWriteAccessibility(); + ApplyPreWriteAccessibility(providers); + Analyze(providers); + return new ProviderReferenceMapSession(); + } + + public static bool ShouldWriteProvider(TypeProvider provider) => + _latestResult?.RemoveCandidates.Contains(GetProviderTypeName(provider.Type)) != true; + + public static bool IsResolvableBuildableType(CSharpType type) + { + if (TryGetProvider(type, exact: true, out var provider)) + { + return IsResolvableBuildableProvider(provider); + } + + if (MatchesRemovedProvider(type)) + { + return false; + } + + return !TryGetProvider(type, exact: false, out provider) || + IsResolvableBuildableProvider(provider); + } + + private static bool IsResolvableBuildableProvider(TypeProvider provider) => + provider is not SystemObjectModelProvider && + provider is not ModelProvider { IsExternal: true } && + ShouldWriteProvider(provider); + + private static bool TryGetProvider(CSharpType type, bool exact, out TypeProvider provider) + { + var outputProvider = FindOutputProviderByName(CodeModelGenerator.Instance.OutputLibrary.TypeProviders, type, exact); + if (outputProvider != null) + { + provider = outputProvider; + return true; + } + + if (!exact && + string.IsNullOrEmpty(type.Namespace) && + CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue(type.Name, out var mappedProvider)) + { + provider = mappedProvider; + return true; + } + + provider = null!; + return false; + } + + private static TypeProvider? FindOutputProviderByName(IEnumerable providers, CSharpType type, bool exact) + { + var name = GetProviderTypeName(type); + foreach (var provider in providers) + { + if (string.Equals(GetProviderTypeName(provider.Type), name, StringComparison.Ordinal) || + !exact && + string.IsNullOrEmpty(type.Namespace) && + string.Equals(provider.Type.Name, type.Name, StringComparison.Ordinal)) + { + return provider; + } + + var nestedProvider = FindOutputProviderByName(provider.NestedTypes, type, exact); + if (nestedProvider != null) + { + return nestedProvider; + } + + var serializationProvider = FindOutputProviderByName(provider.SerializationProviders, type, exact); + if (serializationProvider != null) + { + return serializationProvider; + } + } + + return null; + } + + private static bool MatchesRemovedProvider(CSharpType type) + { + if (_latestResult == null || _latestResult.RemoveCandidates.Count == 0) + { + return false; + } + + var providerName = GetProviderTypeName(type); + if (_latestResult.RemoveCandidates.Contains(providerName)) + { + return true; + } + + var simpleNameLookup = _simpleNameLookupCache.GetValue(_latestResult.RemoveCandidates, BuildSimpleNameLookup); + return string.IsNullOrEmpty(type.Namespace) && + simpleNameLookup.TryGetValue(type.Name, out var matches) && + matches.Length == 1; + } + + public static void ResetPreWriteAccessibility() + { + RestorePreWriteModelFactoryMethods(); + _latestResult = null; + } + + public static void ApplyPreWriteAccessibility(IReadOnlyList providers) + { + 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, publicCandidates) = 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 (publicCandidates.Contains(providerName) && !IsGeneratedInternalImplementation(provider)) + { + provider.Update(modifiers: MakePublic(provider.DeclarationModifiers)); + } + } + + RemoveMethodsFromModelFactory(GetSimpleNames(internalizeCandidates)); + } + + 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 generatedPublicDeclarations = GetGeneratedPublicTypeDeclarationsFromLastContract(generatedProviders, graph.Nodes); + customPublicRoots.UnionWith(generatedPublicDeclarations); + var customCodeRemovalRoots = GetCustomCodeGeneratedTypeRoots(generatedProviders, graph.Nodes); + var customRemovalRoots = new HashSet(customCodeRemovalRoots, StringComparer.Ordinal); + customRemovalRoots.UnionWith(generatedPublicDeclarations); + var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); + + // 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, publicCandidates, _) = 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, + publicCandidates, + removeCandidates); + if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize) + { + RemoveMethodsFromModelFactory(GetSimpleNames(removeCandidates)); + } + } + + private static (HashSet InternalizeCandidates, HashSet PublicCandidates) GetPreWriteAccessibilityCandidates(IReadOnlyList providers) + { + var generatedProviders = GetGeneratedProviders(providers); + var graph = BuildGraph(generatedProviders); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true); + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); + 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, publicCandidates, _) = GetAccessibilityCandidates( + providers, + generatedProviders, + graph, + publicGraph, + customPublicRoots, + customInternalDeclarations, + generatedInternalDeclarations, + generatedDiscriminatorBaseNames); + + return (internalizeCandidates, publicCandidates); + } + + /// + /// Computes accessibility changes by traversing public signatures from explicit API roots, + /// then propagating internal boundaries and determining which reachable declarations must be public. + /// + private static (HashSet InternalizeCandidates, HashSet PublicCandidates, HashSet InternalizeHelperRoots) GetAccessibilityCandidates( + IReadOnlyList providers, + IReadOnlyList generatedProviders, + ProviderReferenceGraph graph, + ProviderReferenceGraph publicGraph, + HashSet customPublicRoots, + HashSet customInternalDeclarations, + HashSet generatedInternalDeclarations, + HashSet generatedDiscriminatorBaseNames) + { + var internalizeReferences = CloneReferences(publicGraph.References); + + // Build the public API roots and add derived-model edges that are known only after traversal. + 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 publicRoots = new HashSet(internalizeRoots, StringComparer.Ordinal); + var publicApiReferences = CloneReferences(publicGraph.References); + var internalizeHelperRoots = GetHelperRootNames(generatedProviders, graph.Nodes, internalizeReachableWithoutHelpers, graph.References); + internalizeRoots.UnionWith(internalizeHelperRoots); + var internalizeDeclaredNodes = GetGeneratedDeclaredNodes(generatedProviders, graph.Nodes, publicOnly: true); + var customInternalBoundaryNodes = GetCustomInternalBoundaryNodes(publicGraph, customInternalDeclarations); + var publicDeclaredNodes = GetPublicDeclaredNodes(generatedProviders, graph.Nodes); + var generatedImplementationInternalDeclarations = GetGeneratedImplementationInternalTypeDeclarations(generatedProviders, generatedInternalDeclarations); + var publicApiTraversalNodes = GetPublicApiTraversalNodes( + internalizeDeclaredNodes, + publicDeclaredNodes, + generatedInternalDeclarations, + generatedImplementationInternalDeclarations); + + // First determine everything that cannot remain public, including transitive internal-only + // dependencies and nested types whose declaring type becomes internal. + var publicReachable = GetReachableTypes(publicRoots, internalizeReferences, publicApiTraversalNodes); + var internalizeCandidates = GetInternalizeCandidates( + internalizeDeclaredNodes, + publicReachable, + customInternalDeclarations, + generatedInternalDeclarations, + customInternalBoundaryNodes, + customPublicRoots, + publicRoots, + graph.Nodes, + internalizeReferences); + AddNestedInternalizeCandidates(generatedProviders, internalizeCandidates, graph.Nodes); + AddInternalOnlyDependencyCandidates( + internalizeDeclaredNodes, + internalizeCandidates, + customInternalDeclarations, + generatedInternalDeclarations, + customPublicRoots, + internalizeReferences, + generatedImplementationInternalDeclarations); + RemovePublicApiExposedCandidates( + internalizeDeclaredNodes, + internalizeCandidates, + customInternalDeclarations, + internalizeReferences); + AddNestedInternalizeCandidates(generatedProviders, internalizeCandidates, graph.Nodes); + + // Recompute reachability without internalized roots, then promote only declarations that + // are still exposed by a public predecessor. + publicRoots.ExceptWith(internalizeCandidates); + publicReachable = GetReachableTypes(publicRoots, internalizeReferences, publicApiTraversalNodes); + var publicRootExclusions = GetRootNames( + providers, + graph.Nodes, + helperRoots: [], + includeModelFactory: true, + includeAdditionalRoots: true, + includeUnionVariantRoots: true, + includeModelFactorySignatureRoots: false, + publicClientRootsOnly: true); + var publicCandidates = GetPublicCandidates( + publicDeclaredNodes, + publicReachable, + customInternalDeclarations, + customInternalBoundaryNodes, + internalizeHelperRoots, + publicRootExclusions, + generatedInternalDeclarations, + publicRoots, + publicApiReferences, + internalizeReferences, + generatedImplementationInternalDeclarations); + return (internalizeCandidates, publicCandidates, 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..8ba130f45df --- /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 PublicCandidates, + HashSet RemoveCandidates) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapSession.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapSession.cs new file mode 100644 index 00000000000..54be2cd1d43 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapSession.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.TypeSpec.Generator.Providers; + +namespace Microsoft.TypeSpec.Generator +{ + internal sealed class ProviderReferenceMapSession : IDisposable + { + private bool _disposed; + + public bool ShouldWriteProvider(TypeProvider provider) => ProviderReferenceMapAnalyzer.ShouldWriteProvider(provider); + + public void RestorePreWriteModelFactoryMethods() => ProviderReferenceMapAnalyzer.RestorePreWriteModelFactoryMethods(); + + public void Dispose() + { + if (_disposed) + { + return; + } + + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); + _disposed = true; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputHelper.cs index 05359e3abc8..74f26fa321d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputHelper.cs @@ -24,7 +24,23 @@ internal static IEnumerable GetSymbols(INamespaceSymbol namespaceSy foreach (INamedTypeSymbol symbol in namespaceSymbol.GetTypeMembers()) { - yield return symbol; + foreach (var type in GetSymbols(symbol)) + { + yield return type; + } + } + } + + private static IEnumerable GetSymbols(INamedTypeSymbol typeSymbol) + { + yield return typeSymbol; + + foreach (var nestedTypeSymbol in typeSymbol.GetTypeMembers()) + { + foreach (var type in GetSymbols(nestedTypeSymbol)) + { + yield return type; + } } } 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..eba9f3036d7 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() @@ -67,32 +69,38 @@ private IReadOnlyDictionary PopulateNameMap() public TypeProvider? FindForTypeInLastContract(string ns, string name, string? declaringTypeName = null) { - return FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); + return FindTypeInCustomization(LastContract, ns, name, true, declaringTypeName, includeInternal: false); } - private TypeProvider? FindTypeInCompilation( - Compilation? compilation, - string ns, - string name, - bool includeReferencedAssemblies, - string? declaringTypeName, - bool includeInternal = true) + internal TypeProvider? FindForTypeInLastContractIncludingInternal(string ns, string name, string? declaringTypeName = null) { - if (compilation == null) + return FindTypeInCustomization(LastContract, ns, name, true, declaringTypeName); + } + + private IReadOnlyList PopulateCustomizationTypeProviders() + { + var providers = new List(); + if (Customization == null) { - return null; + return providers; } - string fullyQualifiedMetadataName = GetFullyQualifiedMetadataName(ns, name, declaringTypeName); - var type = FindNamedTypeSymbol(compilation, includeReferencedAssemblies, fullyQualifiedMetadataName); - if (!includeInternal && type != null && type.DeclaredAccessibility != Accessibility.Public) + foreach (IModuleSymbol module in Customization.Assembly.Modules) { - type = null; + foreach (var type in SourceInputHelper.GetSymbols(module.GlobalNamespace)) + { + if (type is INamedTypeSymbol namedTypeSymbol) + { + providers.Add(new NamedTypeSymbolProvider(namedTypeSymbol, Customization)); + } + } } - return type != null ? new NamedTypeSymbolProvider(type, compilation) : null; + return providers; } + internal IReadOnlyList GetCustomizationTypeProviders() => _customizationTypeProviders.Value; + private static INamedTypeSymbol? FindNamedTypeSymbol(Compilation compilation, bool includeReferencedAssemblies, string fullyQualifiedMetadataName) => includeReferencedAssemblies ? compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) @@ -108,7 +116,8 @@ private static string GetFullyQualifiedMetadataName(string ns, string name, stri string ns, string name, bool includeReferencedAssemblies, - string? declaringTypeName = null) + string? declaringTypeName = null, + bool includeInternal = true) { if (compilation == null) { @@ -117,10 +126,24 @@ private static string GetFullyQualifiedMetadataName(string ns, string name, stri var fullyQualifiedMetadataName = GetFullyQualifiedMetadataName(ns, name, declaringTypeName); - // Either find by the CodeGenType attribute or by the actual type name. - if (!_nameMap.Value.TryGetValue(name, out var type)) + // Either find by the CodeGenType attribute in customization or by the actual type name. + INamedTypeSymbol? type = null; + if (ReferenceEquals(compilation, Customization) && + _nameMap.Value.TryGetValue(name, out var mappedType) && + IsContainingTypeMatch(mappedType, ns, declaringTypeName)) + { + type = mappedType; + } + + if (type == null) { type = FindNamedTypeSymbol(compilation, includeReferencedAssemblies, fullyQualifiedMetadataName); + type ??= FindNestedNamedTypeSymbol(compilation, ns, name, declaringTypeName); + } + + if (!includeInternal && type != null && type.DeclaredAccessibility != Accessibility.Public) + { + type = null; } return type != null ? new NamedTypeSymbolProvider(type, compilation) : null; @@ -150,5 +173,47 @@ private bool TryGetName(ISymbol symbol, [NotNullWhen(true)] out string? name) return name != null; } + + private static INamedTypeSymbol? FindNestedNamedTypeSymbol(Compilation compilation, string ns, string name, string? declaringTypeName) + { + if (declaringTypeName == null) + { + return null; + } + + foreach (var module in compilation.Assembly.Modules) + { + foreach (var type in SourceInputHelper.GetSymbols(module.GlobalNamespace)) + { + if (type is not INamedTypeSymbol namedTypeSymbol || + !string.Equals(namedTypeSymbol.Name, name, StringComparison.Ordinal) || + !string.Equals(GetContainingTypeName(namedTypeSymbol), declaringTypeName, StringComparison.Ordinal) || + !string.Equals(namedTypeSymbol.ContainingNamespace.ToDisplayString(), ns, StringComparison.Ordinal)) + { + continue; + } + + return namedTypeSymbol; + } + } + + return null; + } + + private static string? GetContainingTypeName(INamedTypeSymbol symbol) + { + if (symbol.ContainingType is null) + { + return null; + } + + var parentName = GetContainingTypeName(symbol.ContainingType); + return parentName is null + ? symbol.ContainingType.MetadataName + : $"{parentName}+{symbol.ContainingType.MetadataName}"; + } + + private static bool IsContainingTypeMatch(INamedTypeSymbol symbol, string ns, string? declaringTypeName) + => string.Equals(GetContainingTypeName(symbol), declaringTypeName, StringComparison.Ordinal); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index 93891c4e500..03f643021c1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Reflection; using System.Text.Json; +using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Utilities; @@ -308,9 +309,12 @@ internal static string GetPackageDirectory(string rootDirectory, string package) // Read both streams to avoid deadlocks. 'dotnet build' writes build/compiler // errors to standard output rather than standard error, so we include both when // reporting a failure. - var stdout = process.StandardOutput.ReadToEnd(); - var stderr = process.StandardError.ReadToEnd(); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); process.WaitForExit(); + Task.WaitAll(stdoutTask, stderrTask); + var stdout = stdoutTask.Result; + var stderr = stderrTask.Result; if (process.ExitCode != 0) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Statements/XmlDocStatement.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Statements/XmlDocStatement.cs index cfca3c2db7b..110f92af48e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Statements/XmlDocStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Statements/XmlDocStatement.cs @@ -217,6 +217,8 @@ public static string EscapeLine(string s) "", "", "", + "", + "", "", "", " _optionalProvider ??= new(); + private Dictionary InputTypeToModelProvider { get; } = []; public IDictionary CSharpTypeMap { get; } = new Dictionary(CSharpType.IgnoreNullableComparer); @@ -97,11 +100,7 @@ protected internal TypeFactory() if (unionInput != null) { unionInputs.Add(unionInput); - // we only keep the type if it is not framework type and not literal - if (!unionInput.IsFrameworkType && !unionInput.IsLiteral) - { - UnionVariantTypesToKeep.Add(unionInput.Name); - } + AddUnionVariantTypesToKeep(unionInput); } } type = CSharpType.FromUnion(unionInputs); @@ -131,6 +130,24 @@ protected internal TypeFactory() return type; } + private void AddUnionVariantTypesToKeep(CSharpType type) + { + if (!type.IsFrameworkType && !type.IsLiteral) + { + UnionVariantTypesToKeep.Add(type.FullyQualifiedName); + } + + if (type.IsArray && type.ElementType is { } elementType) + { + AddUnionVariantTypesToKeep(elementType); + } + + foreach (var argument in type.Arguments) + { + AddUnionVariantTypesToKeep(argument); + } + } + /// /// Factory method for creating a based on an input type . /// @@ -200,11 +217,8 @@ protected internal TypeFactory() if (modelProvider != null) { - if (model.Access == "public") - { - CodeModelGenerator.Instance.AddTypeToKeep(modelProvider); - } - + // Input accessibility alone does not make a model part of the emitted public API. + // The provider reference map roots models from actual generated/custom signatures. CSharpTypeMap[modelProvider.Type] = modelProvider; TypeProvidersByName[modelProvider.Type.Name] = modelProvider; } @@ -299,11 +313,6 @@ protected virtual ModelFactoryProvider CreateModelFactoryCore(IEnumerable enumProvider, }; - if (enumType.Access == "public") - { - CodeModelGenerator.Instance.AddTypeToKeep(enumProvider); - } - EnumCache.Add(enumCacheKey, enumProvider); if (enumProvider != null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs index 8af371a063c..65924a3c308 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs @@ -43,6 +43,9 @@ public static bool IsSameType(this INamedTypeSymbol symbol, CSharpType type) } public static CSharpType GetCSharpType(this ITypeSymbol typeSymbol) + => GetCSharpType(typeSymbol, new HashSet(SymbolEqualityComparer.Default)); + + private static CSharpType GetCSharpType(this ITypeSymbol typeSymbol, HashSet visited) { var fullyQualifiedName = GetFullyQualifiedName(typeSymbol); var namedTypeSymbol = typeSymbol as INamedTypeSymbol; @@ -55,20 +58,20 @@ public static CSharpType GetCSharpType(this ITypeSymbol typeSymbol) if (namedTypeSymbol?.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T && namedTypeSymbol.TypeArguments.Length == 1) { - var underlying = GetCSharpType(namedTypeSymbol.TypeArguments[0]); + var underlying = GetCSharpType(namedTypeSymbol.TypeArguments[0], visited); if (underlying.IsFrameworkType) { return underlying.WithNullable(true); } } - return ConstructCSharpTypeFromSymbol(typeSymbol, fullyQualifiedName, namedTypeSymbol); + return ConstructCSharpTypeFromSymbol(typeSymbol, fullyQualifiedName, namedTypeSymbol, visited); } CSharpType result = new CSharpType(type); if (namedTypeSymbol is not null && namedTypeSymbol.IsGenericType && !result.IsNullable) { - return result.MakeGenericType([.. namedTypeSymbol.TypeArguments.Select(GetCSharpType)]); + return result.MakeGenericType([.. namedTypeSymbol.TypeArguments.Select(t => GetCSharpType(t, visited))]); } return result; @@ -154,11 +157,16 @@ public static string GetFullyQualifiedName(this ITypeSymbol typeSymbol) return GetNonNullableGenericTypeName(namedTypeSymbol); } - var typeNameSpan = namedTypeSymbol.ConstructedFrom.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).AsSpan(); - var start = typeNameSpan.IndexOf(':') + 2; - var end = typeNameSpan.IndexOf('<'); - typeNameSpan = typeNameSpan.Slice(start, end - start); - return $"{typeNameSpan}`{namedTypeSymbol.TypeArguments.Length}"; + var constructedFrom = namedTypeSymbol.ConstructedFrom; + var ns = constructedFrom.ContainingNamespace.GetFullyQualifiedNameFromDisplayString(); + var name = constructedFrom.MetadataName; + var tickIndex = name.IndexOf('`', StringComparison.Ordinal); + if (tickIndex >= 0) + { + name = name.Substring(0, tickIndex); + } + + return $"{(string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}")}`{namedTypeSymbol.TypeArguments.Length}"; } } @@ -175,8 +183,14 @@ public static string GetFullyQualifiedNameFromDisplayString(this ISymbol typeSym private static CSharpType ConstructCSharpTypeFromSymbol( ITypeSymbol typeSymbol, string fullyQualifiedName, - INamedTypeSymbol? namedTypeSymbol) + INamedTypeSymbol? namedTypeSymbol, + HashSet visited) { + if (!visited.Add(typeSymbol)) + { + return ConstructShallowCSharpTypeFromSymbol(typeSymbol, fullyQualifiedName); + } + var typeArg = namedTypeSymbol?.TypeArguments.FirstOrDefault(); bool isValueType = typeSymbol.IsValueType; bool isNullable = fullyQualifiedName.StartsWith(NullableTypeName); @@ -193,7 +207,7 @@ private static CSharpType ConstructCSharpTypeFromSymbol( if (namedTypeSymbol?.IsGenericType == true && (!isNullable || (namedTypeArg?.IsGenericType == true))) { - arguments.AddRange(namedTypeSymbol.TypeArguments.Select(GetCSharpType)); + arguments.AddRange(namedTypeSymbol.TypeArguments.Select(t => GetCSharpType(t, visited))); } // handle nullables @@ -207,9 +221,9 @@ private static CSharpType ConstructCSharpTypeFromSymbol( string ns = string.Join('.', pieces.Take(pieces.Length - 1)); CSharpType? containingType = null; - if (typeSymbol.ContainingType != null) + if (typeSymbol.ContainingType != null && typeSymbol.TypeKind != TypeKind.TypeParameter) { - containingType = GetCSharpType(typeSymbol.ContainingType); + containingType = GetCSharpType(typeSymbol.ContainingType, visited); ns = string.Join('.', pieces.Take(pieces.Length - 2)); } @@ -219,10 +233,10 @@ private static CSharpType ConstructCSharpTypeFromSymbol( !isNullableUnknownType && !ContainsTypeAsArgument(typeSymbol.BaseType, typeSymbol)) { - baseType = GetCSharpType(typeSymbol.BaseType); + baseType = GetCSharpType(typeSymbol.BaseType, visited); } - return new CSharpType( + var result = new CSharpType( name, ns, isValueType, @@ -233,8 +247,25 @@ private static CSharpType ConstructCSharpTypeFromSymbol( isValueType && !isEnum, baseType: baseType, underlyingEnumType: enumUnderlyingType != null - ? GetCSharpType(enumUnderlyingType).FrameworkType + ? GetCSharpType(enumUnderlyingType, visited).FrameworkType : null); + visited.Remove(typeSymbol); + return result; + } + + private static CSharpType ConstructShallowCSharpTypeFromSymbol(ITypeSymbol typeSymbol, string fullyQualifiedName) + { + // Break recursive symbol graphs without recursively materializing arguments, bases, or containing types. + string[] pieces = fullyQualifiedName.Split('`')[0].Split('.'); + return new CSharpType( + typeSymbol.Name, + string.Join('.', pieces.Take(pieces.Length - 1)), + typeSymbol.IsValueType, + fullyQualifiedName.StartsWith(NullableTypeName), + null, + [], + typeSymbol.DeclaredAccessibility == Accessibility.Public, + typeSymbol.IsValueType && typeSymbol.TypeKind != TypeKind.Enum); } internal static bool ContainsTypeAsArgument(ITypeSymbol potentialGenericType, ITypeSymbol targetType) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/CSharpGenTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/CSharpGenTests.cs index 688010522ba..cbaeb82c1ac 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/CSharpGenTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/CSharpGenTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.IO; +using System.Reflection; using Moq; using NUnit.Framework; @@ -31,5 +33,27 @@ public void VisitorsAreVisited() Assert.DoesNotThrowAsync(async () => await csharpGen.ExecuteAsync()); mockVisitor.Verify(m => m.VisitLibrary(mockGenerator.Object.OutputLibrary), Times.Once); } + + [Test] + public void DeleteDirectoryDeletesGeneratedExtensionFilesUnlessExplicitlyKept() + { + var outputPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, "CSharpGen", nameof(DeleteDirectoryDeletesGeneratedExtensionFilesUnlessExplicitlyKept)); + if (Directory.Exists(outputPath)) + { + Directory.Delete(outputPath, recursive: true); + } + + Directory.CreateDirectory(outputPath); + var staleExtensionPath = Path.Combine(outputPath, "StaleExtensions.cs"); + var configurationPath = Path.Combine(outputPath, "Configuration.json"); + File.WriteAllText(staleExtensionPath, "// stale generated helper"); + File.WriteAllText(configurationPath, "{}"); + + var deleteDirectory = typeof(CSharpGen).GetMethod("DeleteDirectory", BindingFlags.Static | BindingFlags.NonPublic)!; + deleteDirectory.Invoke(null, [outputPath, new[] { "Configuration.json" }]); + + Assert.IsFalse(File.Exists(staleExtensionPath)); + Assert.IsTrue(File.Exists(configurationPath)); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs index f716aad5ab3..49b40624443 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs @@ -331,6 +331,40 @@ public async Task SerializationProviderConstructorIsFilteredWhenMatchingCustomCo Assert.AreEqual(0, serializationProvider.Constructors.Count, "Serialization provider constructor should be filtered when matching custom code"); } + [Test] + public async Task NestedProviderConstructorIsFilteredWhenMatchingCustomCode() + { + var typeProvider = new TestTypeProvider(ns: "Sample"); + var customNestedProvider = new TestConstructorTypeProvider("NestedType", "Sample"); + customNestedProvider.ConstructorProviders = + [ + new ConstructorProvider( + new ConstructorSignature(customNestedProvider.Type, $"", MethodSignatureModifiers.Public, [new ParameterProvider("param1", $"", typeof(string))]), + Snippet.Throw(Snippet.Null), customNestedProvider) + ]; + var nestedProvider = new TestConstructorTypeProvider("NestedType", "Sample", typeProvider, customNestedProvider); + var constructor = new ConstructorProvider( + new ConstructorSignature(nestedProvider.Type, $"", MethodSignatureModifiers.Public, [new ParameterProvider("param1", $"", typeof(string))]), + Snippet.Throw(Snippet.Null), nestedProvider); + nestedProvider.ConstructorProviders = [constructor]; + typeProvider.NestedTypesInternal = [nestedProvider]; + + var generator = await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(typeProvider)); + + foreach (var type in generator.Object.OutputLibrary.TypeProviders) + { + type.EnsureBuilt(); + } + + Assert.AreEqual(1, nestedProvider.Constructors.Count, "Nested provider should have 1 constructor before filtering"); + Assert.IsNotNull(nestedProvider.CustomCodeView, "Nested provider custom code view should be found"); + + CSharpGen.FilterAllCustomizedMembers(generator.Object.OutputLibrary); + + Assert.AreEqual(0, nestedProvider.Constructors.Count, "Nested provider constructor should be filtered when matching custom code"); + } + [Test] public async Task MatchingPropertyIsFilteredAfterVisitorMutation() { @@ -520,5 +554,34 @@ private class TestSerializationTypeProvider : TypeProvider protected override string BuildNamespace() => "Test"; protected internal override ConstructorProvider[] BuildConstructors() => ConstructorProviders; } + + private class TestConstructorTypeProvider : TypeProvider + { + private readonly string _name; + private readonly string _namespace; + private readonly TypeProvider? _declaringTypeProvider; + + public TestConstructorTypeProvider(string name, string @namespace, TypeProvider? declaringTypeProvider = null) + : this(name, @namespace, declaringTypeProvider, customCodeView: null) + { + } + + public TestConstructorTypeProvider(string name, string @namespace, TypeProvider? declaringTypeProvider, TypeProvider? customCodeView) + { + _name = name; + _namespace = @namespace; + _declaringTypeProvider = declaringTypeProvider; + _customCodeView = customCodeView; + } + + private readonly TypeProvider? _customCodeView; + public ConstructorProvider[] ConstructorProviders { get; set; } = []; + protected override string BuildRelativeFilePath() => $"{Name}.cs"; + protected override string BuildName() => _name; + protected override string BuildNamespace() => _namespace; + protected override TypeProvider? BuildDeclaringTypeProvider() => _declaringTypeProvider; + private protected override TypeProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => _customCodeView; + protected internal override ConstructorProvider[] BuildConstructors() => ConstructorProviders; + } } } 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 deleted file mode 100644 index 28981148a4d..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/PostProcessorTests.cs +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.TypeSpec.Generator.Tests.Common; -using NUnit.Framework; - -namespace Microsoft.TypeSpec.Generator.Tests.PostProcessing -{ - public class PostProcessorTests - { - [Test] - public async Task RemovesInvalidUsings() - { - 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 = project.AddDocument( - "RemovesInvalidUsings.cs", - File.ReadAllText(Path.Join(folder, "RemovesInvalidUsings.cs"))).Project; - project = project.AddDocument( - "Model.cs", - File.ReadAllText(Path.Join(folder, "Model.cs"))).Project; - var postProcessor = new TestPostProcessor("RemovesInvalidUsings.cs"); - - var resultProject = await postProcessor.RemoveAsync(project); - var doc= resultProject.Documents - .Single(d => d.Name == "RemovesInvalidUsings.cs"); - var root = await doc.GetSyntaxRootAsync(); - var compilation = (CompilationUnitSyntax)root!; - - var typeNames = compilation - .DescendantNodes() - .OfType() - .Select(t => t.Identifier.Text) - .ToList(); - CollectionAssert.Contains(typeNames, "KeepMe"); - - var usings = compilation.Usings.Select(u => u.Name!.ToString()).ToList(); - // The invalid using should be removed - CollectionAssert.DoesNotContain(usings, "Sample.Models"); - CollectionAssert.Contains(usings, "System"); - } - - - [Test] - public async Task DoesNotRemoveValidUsings() - { - 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 = project.AddDocument( - "DoesNotRemoveValidUsings.cs", - File.ReadAllText(Path.Join(folder, "DoesNotRemoveValidUsings.cs"))).Project; - project = project.AddDocument( - "Model.cs", - File.ReadAllText(Path.Join(folder, "Model.cs"))).Project; - var postProcessor = new TestPostProcessor("DoesNotRemoveValidUsings.cs"); - - var resultProject = await postProcessor.RemoveAsync(project); - var doc= resultProject.Documents - .Single(d => d.Name == "DoesNotRemoveValidUsings.cs"); - var root = await doc.GetSyntaxRootAsync(); - var compilation = (CompilationUnitSyntax)root!; - - var typeNames = compilation - .DescendantNodes() - .OfType() - .Select(t => t.Identifier.Text) - .ToList(); - CollectionAssert.Contains(typeNames, "KeepMe"); - - var usings = compilation.Usings.Select(u => u.Name!.ToString()).ToList(); - CollectionAssert.Contains(usings, "Sample.Models"); - CollectionAssert.Contains(usings, "System"); - } - - [Test] - public async Task RemovesInvalidAttributes() - { - 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), - MetadataReference.CreateFromFile(typeof(ModelReaderWriterBuildableAttribute).Assembly.Location) - }); - - var project = workspace.AddProject(projectInfo); - var folder = Helpers.GetAssetFileOrDirectoryPath(false); - const string removesInvalidAttributesFileName = "RemovesInvalidAttributes.cs"; - project = project.AddDocument( - removesInvalidAttributesFileName, - File.ReadAllText(Path.Join(folder, removesInvalidAttributesFileName))).Project; - project = project.AddDocument( - "Model.cs", - File.ReadAllText(Path.Join(folder, "Model.cs"))).Project; - project = project.AddDocument( - "RootClass.cs", - File.ReadAllText(Path.Join(folder, "RootClass.cs"))).Project; - var postProcessor = new TestPostProcessor("RootClass.cs", nonRootTypes: ["Sample.KeepMe"]); - - var resultProject = await postProcessor.RemoveAsync(project); - var doc= resultProject.Documents - .Single(d => d.Name == removesInvalidAttributesFileName); - var root = await doc.GetSyntaxRootAsync(); - var compilation = (CompilationUnitSyntax)root!; - - var namespaceDeclaration = compilation - .DescendantNodes() - .OfType() - .SingleOrDefault(t => t.Name.ToString() == "Sample"); - var output = namespaceDeclaration!.ToString(); - - Assert.AreEqual(Helpers.GetExpectedFromFile().TrimEnd(), output, "The output should match the expected content."); - } - - [Test] - public async Task RemovesInvalidAttributesAndKeepsValidAttributes() - { - 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), - MetadataReference.CreateFromFile(typeof(ModelReaderWriterBuildableAttribute).Assembly.Location) - }); - - var project = workspace.AddProject(projectInfo); - var folder = Helpers.GetAssetFileOrDirectoryPath(false); - const string removesInvalidAttributesFileName = "RemovesInvalidAttributesAndKeepsValidAttributes.cs"; - project = project.AddDocument( - removesInvalidAttributesFileName, - File.ReadAllText(Path.Join(folder, removesInvalidAttributesFileName))).Project; - project = project.AddDocument( - "Model.cs", - File.ReadAllText(Path.Join(folder, "Model.cs"))).Project; - project = project.AddDocument( - "RootClass.cs", - File.ReadAllText(Path.Join(folder, "RootClass.cs"))).Project; - var postProcessor = new TestPostProcessor("RootClass.cs", nonRootTypes: ["Sample.KeepMe"]); - - var resultProject = await postProcessor.RemoveAsync(project); - var doc= resultProject.Documents - .Single(d => d.Name == removesInvalidAttributesFileName); - var root = await doc.GetSyntaxRootAsync(); - var compilation = (CompilationUnitSyntax)root!; - - var namespaceDeclaration = compilation - .DescendantNodes() - .OfType() - .SingleOrDefault(t => t.Name.ToString() == "Sample"); - var output = namespaceDeclaration!.ToString(); - - Assert.AreEqual(Helpers.GetExpectedFromFile().TrimEnd(), output, "The output should match the expected content."); - } - - [Test] - public async Task RemovesInvalidAttributesAndKeepsValidAttributesNoDocs() - { - 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), - MetadataReference.CreateFromFile(typeof(ModelReaderWriterBuildableAttribute).Assembly.Location) - }); - - var project = workspace.AddProject(projectInfo); - var folder = Helpers.GetAssetFileOrDirectoryPath(false); - const string removesInvalidAttributesFileName = "RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs"; - project = project.AddDocument( - removesInvalidAttributesFileName, - File.ReadAllText(Path.Join(folder, removesInvalidAttributesFileName))).Project; - project = project.AddDocument( - "Model.cs", - File.ReadAllText(Path.Join(folder, "Model.cs"))).Project; - project = project.AddDocument( - "RootClass.cs", - File.ReadAllText(Path.Join(folder, "RootClass.cs"))).Project; - var postProcessor = new TestPostProcessor("RootClass.cs", nonRootTypes: ["Sample.KeepMe"]); - - var resultProject = await postProcessor.RemoveAsync(project); - var doc= resultProject.Documents - .Single(d => d.Name == removesInvalidAttributesFileName); - var root = await doc.GetSyntaxRootAsync(); - var compilation = (CompilationUnitSyntax)root!; - - var namespaceDeclaration = compilation - .DescendantNodes() - .OfType() - .SingleOrDefault(t => t.Name.ToString() == "Sample"); - var output = namespaceDeclaration!.ToString(); - - Assert.AreEqual(Helpers.GetExpectedFromFile().TrimEnd(), output, "The output should match the expected content."); - } - - [Test] - public async Task DoesNotRemoveValidAttributes() - { - 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), - MetadataReference.CreateFromFile(typeof(ModelReaderWriterBuildableAttribute).Assembly.Location) - }); - - var project = workspace.AddProject(projectInfo); - var folder = Helpers.GetAssetFileOrDirectoryPath(false); - const string doesNotRemoveValidAttributesFileName = "DoesNotRemoveValidAttributes.cs"; - project = project.AddDocument( - doesNotRemoveValidAttributesFileName, - File.ReadAllText(Path.Join(folder, doesNotRemoveValidAttributesFileName))).Project; - project = project.AddDocument( - "Model.cs", - File.ReadAllText(Path.Join(folder, "Model.cs"))).Project; - project = project.AddDocument( - "RootClass.cs", - File.ReadAllText(Path.Join(folder, "RootClass.cs"))).Project; - var postProcessor = new TestPostProcessor("RootClass.cs"); - - var resultProject = await postProcessor.RemoveAsync(project); - var doc= resultProject.Documents - .Single(d => d.Name == doesNotRemoveValidAttributesFileName); - var root = await doc.GetSyntaxRootAsync(); - var compilation = (CompilationUnitSyntax)root!; - - var namespaceDeclaration = compilation - .DescendantNodes() - .OfType() - .SingleOrDefault(t => t.Name.ToString() == "Sample"); - var output = namespaceDeclaration!.ToString(); - - Assert.AreEqual(Helpers.GetExpectedFromFile().TrimEnd(), output, "The output should match the expected content."); - } - - private class TestPostProcessor : PostProcessor - { - private readonly string _rootFile; - - public TestPostProcessor(string rootFile, IEnumerable? nonRootTypes = null) : base([], additionalNonRootTypeNames: nonRootTypes) - { - _rootFile = rootFile; - } - - protected override Task IsRootDocument(Document document) - { - return document.Name == _rootFile ? Task.FromResult(true) : Task.FromResult(false); - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes.cs deleted file mode 100644 index 0ee90baa775..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Sample -{ - /// - /// Class docs that should be kept. - /// - [ModelReaderWriterBuildable(typeof(Model))] - public class KeepMe - { - public void Foo() => Console.WriteLine(""hello""); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/DoesNotRemoveValidAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/DoesNotRemoveValidAttributes.cs deleted file mode 100644 index e44051470ab..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/DoesNotRemoveValidAttributes.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.ClientModel.Primitives; -using Sample.Models; - -namespace Sample -{ - /// - /// Class docs that should be kept. - /// - [ModelReaderWriterBuildable(typeof(Model))] - public class KeepMe - { - public void Foo() => Console.WriteLine(""hello""); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/Model.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/Model.cs deleted file mode 100644 index 20f76bb882a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/Model.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Sample.Models -{ - public class Model - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/RootClass.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/RootClass.cs deleted file mode 100644 index b799f680532..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidAttributes/RootClass.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Sample -{ - public class RootClass - { - public void Foo() => (new KeepMe()).Foo(); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidUsings/DoesNotRemoveValidUsings.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidUsings/DoesNotRemoveValidUsings.cs deleted file mode 100644 index ba2ed5a94b2..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidUsings/DoesNotRemoveValidUsings.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Sample.Models; - -namespace Sample -{ - public class KeepMe - { - public Model Foo() => new Model(); - } -} -"; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidUsings/Model.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidUsings/Model.cs deleted file mode 100644 index 20f76bb882a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/DoesNotRemoveValidUsings/Model.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Sample.Models -{ - public class Model - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes.cs deleted file mode 100644 index 11498989c01..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Sample -{ - /// - /// Class docs that should be kept. - /// - public class KeepMe - { - public void Foo() => Console.WriteLine(""hello""); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/Model.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/Model.cs deleted file mode 100644 index 20f76bb882a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/Model.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Sample.Models -{ - public class Model - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/RemovesInvalidAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/RemovesInvalidAttributes.cs deleted file mode 100644 index e44051470ab..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/RemovesInvalidAttributes.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.ClientModel.Primitives; -using Sample.Models; - -namespace Sample -{ - /// - /// Class docs that should be kept. - /// - [ModelReaderWriterBuildable(typeof(Model))] - public class KeepMe - { - public void Foo() => Console.WriteLine(""hello""); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/RootClass.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/RootClass.cs deleted file mode 100644 index b799f680532..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributes/RootClass.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Sample -{ - public class RootClass - { - public void Foo() => (new KeepMe()).Foo(); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes.cs deleted file mode 100644 index a0eda596240..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Sample -{ - /// - /// Class docs that should be kept. - /// - [ModelReaderWriterBuildable(typeof(object))] - public class KeepMe - { - public void Foo() => Console.WriteLine("hello"); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/Model.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/Model.cs deleted file mode 100644 index 20f76bb882a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/Model.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Sample.Models -{ - public class Model - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/RemovesInvalidAttributesAndKeepsValidAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/RemovesInvalidAttributesAndKeepsValidAttributes.cs deleted file mode 100644 index afa5fb8d462..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/RemovesInvalidAttributesAndKeepsValidAttributes.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.ClientModel.Primitives; -using Sample.Models; - -namespace Sample -{ - /// - /// Class docs that should be kept. - /// - [ModelReaderWriterBuildable(typeof(Model))] - [ModelReaderWriterBuildable(typeof(object))] - public class KeepMe - { - public void Foo() => Console.WriteLine("hello"); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/RootClass.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/RootClass.cs deleted file mode 100644 index b799f680532..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributes/RootClass.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Sample -{ - public class RootClass - { - public void Foo() => (new KeepMe()).Foo(); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs deleted file mode 100644 index 4ffad5d191e..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Sample -{ - [ModelReaderWriterBuildable(typeof(object))] - public class KeepMe - { - public void Foo() => Console.WriteLine("hello"); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/Model.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/Model.cs deleted file mode 100644 index 20f76bb882a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/Model.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Sample.Models -{ - public class Model - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs deleted file mode 100644 index 1b99245b052..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.ClientModel.Primitives; -using Sample.Models; - -namespace Sample -{ - [ModelReaderWriterBuildable(typeof(Model))] - [ModelReaderWriterBuildable(typeof(object))] - public class KeepMe - { - public void Foo() => Console.WriteLine("hello"); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/RootClass.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/RootClass.cs deleted file mode 100644 index b799f680532..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidAttributesAndKeepsValidAttributesNoDocs/RootClass.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Sample -{ - public class RootClass - { - public void Foo() => (new KeepMe()).Foo(); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsings/Model.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsings/Model.cs deleted file mode 100644 index 20f76bb882a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsings/Model.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Sample.Models -{ - public class Model - { - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsings/RemovesInvalidUsings.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsings/RemovesInvalidUsings.cs deleted file mode 100644 index c8e295f4ef6..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/PostProcessing/TestData/PostProcessorTests/RemovesInvalidUsings/RemovesInvalidUsings.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Sample.Models; // initially valid - -namespace Sample -{ - public class KeepMe - { - public void Foo() => Console.WriteLine(""hello""); - } -} -"; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs index 3bbc8cb463e..e112cec28b0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs @@ -331,7 +331,7 @@ public void ExtensibleFloatEnum_HasOnlyNonNullableImplicitOperator() } [Test] - public void PublicModelsAreIncludedInAdditionalRootTypes() + public void PublicEnumsAreNotIncludedInAdditionalRootTypes() { var inputEnum = InputFactory.StringEnum( "StringEnum", @@ -345,7 +345,7 @@ public void PublicModelsAreIncludedInAdditionalRootTypes() Assert.IsNotNull(enumProvider); var rootTypes = CodeModelGenerator.Instance.AdditionalRootTypes; - Assert.IsTrue(rootTypes.Contains("Sample.Models.StringEnum")); + Assert.IsFalse(rootTypes.Contains("Sample.Models.StringEnum")); } [Test] 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..acb83e3d6f9 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] @@ -1577,12 +1577,11 @@ public void KeepSetsReflectTypeProvidersAddedAfterFirstAccess() // Regression test for two complementary fixes: // - // 1. ModelProvider no longer registers itself with AddTypeToKeep from its constructor; - // registration is performed by TypeFactory.CreateModel after construction completes. - // This mirrors the EnumProvider lifecycle and prevents a virtual call chain - // (AddTypeToKeep -> TypeProvider.Type -> BaseType -> virtual BuildBaseType()) from - // being dispatched on a partially-constructed derived ModelProvider whose override - // reads derived-class fields that are still uninitialized. + // 1. ModelProvider no longer registers itself with AddTypeToKeep from its constructor, + // so construction does not dispatch a virtual call chain + // (AddTypeToKeep -> TypeProvider.Type -> BaseType -> virtual BuildBaseType()) on a + // partially-constructed derived ModelProvider whose override reads derived-class fields + // that are still uninitialized. // // 2. AddTypeToKeep(TypeProvider) defers FQN resolution until the keep set is consumed, // so even ctor-time callers cannot force premature TypeProvider.Type evaluation. 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..9b18cdb2b6a 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 @@ -9,6 +9,7 @@ using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; @@ -370,6 +371,68 @@ public async Task ValidatePartialMethodWithBodyIsNotDetectedAsPartialDeclaration Assert.IsFalse(doIt.IsPartialMethod, "Partial methods with bodies should not be treated as customization signals."); } + [Test] + public async Task BodyDependenciesIncludeUsingNamespaceCandidatesForUnresolvedTypeSyntax() + { + var compilation = await Helpers.GetCompilationFromDirectoryAsync(); + 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 async Task PublicInterfaceMemberSignatureDependenciesAreIncluded() + { + var compilation = await Helpers.GetCompilationFromDirectoryAsync(); + var symbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "ICustomApi")!; + var provider = new NamedTypeSymbolProvider(symbol, compilation); + + Assert.IsTrue(provider.SignatureDependencyTypes.Any(type => type.FullyQualifiedName == "Sample.Models.GeneratedModel")); + } + + [Test] + public async Task PublicNestedMemberSignatureDependenciesAreIncluded() + { + var compilation = await Helpers.GetCompilationFromDirectoryAsync(); + var symbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "CustomApi")!; + var provider = new NamedTypeSymbolProvider(symbol, compilation); + + Assert.IsTrue(provider.SignatureDependencyTypes.Any(type => type.FullyQualifiedName == "Sample.Models.GeneratedModel")); + } + + [Test] + public async Task SourceInputHelperYieldsNestedSymbols() + { + var compilation = await Helpers.GetCompilationFromDirectoryAsync(); + + var symbols = Microsoft.TypeSpec.Generator.SourceInput.SourceInputHelper.GetSymbols(compilation.Assembly.Modules.First().GlobalNamespace); + + Assert.IsTrue(symbols.Any(symbol => symbol.MetadataName == "Nested")); + } + + [Test] + public async Task SourceInputLookupUsesFullNestedDeclaringTypeName() + { + var compilation = await Helpers.GetCompilationFromDirectoryAsync(); + var sourceInputModel = new SourceInputModel(compilation, lastContract: null); + + var nestedType = sourceInputModel.FindForTypeInCustomization("Sample", "Target", "Outer+Middle"); + + Assert.IsNotNull(nestedType); + Assert.AreEqual("Sample.Outer+Middle+Target", ((NamedTypeSymbolProvider)nestedType!).MetadataName); + } + + [Test] + public async Task MetadataNamePreservesGenericArity() + { + var compilation = await Helpers.GetCompilationFromDirectoryAsync(); + 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/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/BodyDependenciesIncludeUsingNamespaceCandidatesForUnresolvedTypeSyntax/CustomClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/BodyDependenciesIncludeUsingNamespaceCandidatesForUnresolvedTypeSyntax/CustomClient.cs new file mode 100644 index 00000000000..01ad16aa4ac --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/BodyDependenciesIncludeUsingNamespaceCandidatesForUnresolvedTypeSyntax/CustomClient.cs @@ -0,0 +1,12 @@ +using Sample.Models; + +namespace Sample +{ + public partial class CustomClient + { + public void Test(object response) + { + ReferencedModel result = (ReferencedModel)response; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/MetadataNamePreservesGenericArity/CustomModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/MetadataNamePreservesGenericArity/CustomModel.cs new file mode 100644 index 00000000000..0499f0d3f56 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/MetadataNamePreservesGenericArity/CustomModel.cs @@ -0,0 +1,6 @@ +namespace Sample.Models +{ + public partial class CustomModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/PublicInterfaceMemberSignatureDependenciesAreIncluded/CustomApi.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/PublicInterfaceMemberSignatureDependenciesAreIncluded/CustomApi.cs new file mode 100644 index 00000000000..61c46fefacf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/PublicInterfaceMemberSignatureDependenciesAreIncluded/CustomApi.cs @@ -0,0 +1,9 @@ +using Sample.Models; + +namespace Sample +{ + public partial interface ICustomApi + { + GeneratedModel Item { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/PublicNestedMemberSignatureDependenciesAreIncluded/CustomApi.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/PublicNestedMemberSignatureDependenciesAreIncluded/CustomApi.cs new file mode 100644 index 00000000000..bee92bb5c88 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/PublicNestedMemberSignatureDependenciesAreIncluded/CustomApi.cs @@ -0,0 +1,12 @@ +using Sample.Models; + +namespace Sample +{ + public partial class CustomApi + { + public class Nested + { + public GeneratedModel Item { get; } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/SourceInputHelperYieldsNestedSymbols/CustomApi.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/SourceInputHelperYieldsNestedSymbols/CustomApi.cs new file mode 100644 index 00000000000..7116cc92bfe --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/SourceInputHelperYieldsNestedSymbols/CustomApi.cs @@ -0,0 +1,9 @@ +namespace Sample +{ + public partial class CustomApi + { + public class Nested + { + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/SourceInputLookupUsesFullNestedDeclaringTypeName/NestedTypes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/SourceInputLookupUsesFullNestedDeclaringTypeName/NestedTypes.cs new file mode 100644 index 00000000000..8e4353bc359 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/SourceInputLookupUsesFullNestedDeclaringTypeName/NestedTypes.cs @@ -0,0 +1,19 @@ +namespace Sample +{ + public partial class Outer + { + public partial class Middle + { + public class Target + { + } + } + } + + public partial class Other + { + public class Target + { + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index 9ec0b21c32d..03ca7cd9f0c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -229,7 +229,7 @@ public void TestCanUpdateAttributes() new MethodSignature("TestMethod", $"", MethodSignatureModifiers.Public, null, $"", []), Snippet.Throw(Snippet.Null), new TestTypeProvider())]); typeProvider.Update(attributes: [ - new(typeof(ObsoleteAttribute)) + new AttributeStatement(typeof(ObsoleteAttribute)) ]); Assert.IsNotNull(typeProvider.Attributes); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs new file mode 100644 index 00000000000..b94a22b332a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs @@ -0,0 +1,1246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +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; +using Microsoft.TypeSpec.Generator.Tests.Common; +using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.TypeSpec.Generator.Tests.Providers.NamedTypeSymbolProviders; +using Microsoft.TypeSpec.Generator.Tests.TestHelpers; +using NUnit.Framework; +using static Microsoft.TypeSpec.Generator.Snippets.Snippet; + +namespace Microsoft.TypeSpec.Generator.Tests.ReferenceMap +{ + public class ProviderReferenceMapAnalyzerTests + { + [SetUp] + public void SetUp() + { + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); + } + + [TearDown] + public void TearDown() + { + ProviderReferenceMapAnalyzer.ResetPreWriteAccessibility(); + } + + [Test] + public void NonRootKeptTypesKeepTheirAccessibility() + { + var context = new TestTypeProvider("SampleContext", TypeSignatureModifiers.Public); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(context)); + CodeModelGenerator.Instance.AddTypeToKeep(context.Type.FullyQualifiedName, isRoot: false); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([context]); + + Assert.IsTrue(context.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(context.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public void NonRootKeptTypesAreWrittenWithoutRootingOtherTypes() + { + var context = new TestTypeProvider("SampleContext", TypeSignatureModifiers.Public); + var unusedModel = new TestTypeProvider("UnusedModel", TypeSignatureModifiers.Public); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(context, unusedModel)); + CodeModelGenerator.Instance.AddTypeToKeep(context.Type.FullyQualifiedName, isRoot: false); + + ProviderReferenceMapAnalyzer.Analyze([context, unusedModel]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(context)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(unusedModel)); + } + + [Test] + public void AmbiguousNamespaceLessCustomCodeBodyDependencyDoesNotRootGeneratedTypeBySimpleName() + { + var customCodeView = new BodyDependencyTestTypeProvider("CustomType", CreateNamedType("Error", string.Empty)); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var generatedError = new TestTypeProvider("Error", TypeSignatureModifiers.Public, ns: "Sample.Models"); + var otherGeneratedError = new TestTypeProvider("Error", TypeSignatureModifiers.Public, ns: "Other.Models"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, generatedError, otherGeneratedError)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, generatedError, otherGeneratedError]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(customType)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(generatedError)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(otherGeneratedError)); + } + + [Test] + public void NamespaceLessCustomCodeBodyDependencyRootsGeneratedTypeInCustomCodeNamespace() + { + var customCodeView = new BodyDependencyTestTypeProvider("CustomType", "Sample", CreateNamedType("ReferencedModel", string.Empty)); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var referencedModel = new TestTypeProvider("ReferencedModel", TypeSignatureModifiers.Public, ns: "Sample"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, referencedModel)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, referencedModel]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(customType)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(referencedModel)); + } + + [Test] + public void NamespaceLessCustomCodeBodyDependencyRootsGeneratedTypeInParentNamespace() + { + var customCodeView = new BodyDependencyTestTypeProvider("CustomType", "Sample.Child", CreateNamedType("ReferencedModel", string.Empty)); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample.Child"); + var referencedModel = new TestTypeProvider("ReferencedModel", TypeSignatureModifiers.Public, ns: "Sample"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, referencedModel)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, referencedModel]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(customType)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(referencedModel)); + } + + [Test] + public void NamespaceLessCustomCodeGenericBodyDependencyRootsGeneratedTypeInCustomCodeNamespace() + { + var genericArgument = CreateNamedType("T", string.Empty); + var customCodeView = new BodyDependencyTestTypeProvider("CustomType", "Sample", CreateNamedType("ErrorResult", string.Empty, genericArgument)); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var errorResult = new GenericTestTypeProvider("ErrorResult", TypeSignatureModifiers.Internal, "Sample", genericArgument); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, errorResult)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, errorResult]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(customType)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(errorResult)); + } + + [Test] + public void NamespaceLessGeneratedGenericBodyDependencyRootsGeneratedTypeInProviderNamespace() + { + var genericArgument = CreateNamedType("T", string.Empty); + var provider = new BodyDependencyTestTypeProvider("ClientPipelineExtensions", "Sample", CreateNamedType("ErrorResult", "Microsoft.TypeSpec.Generator.ClientModel.Providers", genericArgument)); + var errorResult = new GenericTestTypeProvider("ErrorResult", TypeSignatureModifiers.Internal, "Sample", genericArgument); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(provider, errorResult)); + CodeModelGenerator.Instance.AddTypeToKeep(provider.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([provider, errorResult]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(provider)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(errorResult)); + } + + [Test] + public void UnionVariantFallbackRootsOnlyMatchFullyQualifiedModelName() + { + var keptVariant = new ModelProvider(InputFactory.Model("Variant", "Sample")); + var collidingVariant = new ModelProvider(InputFactory.Model("Variant", "Other")); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(keptVariant, collidingVariant)); + CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep.Add(keptVariant.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([keptVariant, collidingVariant]); + + Assert.IsTrue(keptVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(collidingVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public void CollectionUnionVariantFallbackRootsOnlyMatchFullyQualifiedModelName() + { + var keptInput = InputFactory.Model("Variant", "Sample"); + var keptVariant = new ModelProvider(keptInput); + var collidingVariant = new ModelProvider(InputFactory.Model("Variant", "Other")); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(keptVariant, collidingVariant)); + CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(InputFactory.Union([InputFactory.Array(keptInput)])); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([keptVariant, collidingVariant]); + + Assert.IsTrue(keptVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(collidingVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public void QualifiedExternalBuildableTypeDoesNotMatchRemovedGeneratedTypeBySimpleName() + { + var generatedFoo = new GeneratedModelTestTypeProvider("Foo", TypeSignatureModifiers.Public, ns: "Sample"); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => new TestOutputLibrary(generatedFoo), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + + ProviderReferenceMapAnalyzer.Analyze([generatedFoo]); + + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(generatedFoo)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.IsResolvableBuildableType(CreateNamedType("Foo", "Contoso"))); + Assert.IsFalse(ProviderReferenceMapAnalyzer.IsResolvableBuildableType(CreateNamedType("Foo", string.Empty))); + } + + [Test] + public void HelperRootBodyDependencyRootsGeneratedGenericDependency() + { + var genericArgument = CreateNamedType("T", string.Empty); + var client = new HelperDependencyTestTypeProvider("SampleClient", "Sample", CreateNamedType("ClientPipelineExtensions", "Microsoft.TypeSpec.Generator.ClientModel.Providers")); + var pipelineExtensions = new BodyDependencyTestTypeProvider("ClientPipelineExtensions", "Sample", CreateNamedType("ErrorResult", "Microsoft.TypeSpec.Generator.ClientModel.Providers", genericArgument)); + var errorResult = new GenericTestTypeProvider("ErrorResult", TypeSignatureModifiers.Internal, "Sample", genericArgument); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(client, pipelineExtensions, errorResult)); + CodeModelGenerator.Instance.AddTypeToKeep(client.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([client, pipelineExtensions, errorResult]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(client)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(pipelineExtensions)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(errorResult)); + } + + [Test] + public void UnreferencedHelperDependencyDoesNotKeepUnreferencedSiblingHelper() + { + var client = new HelperDependencyTestTypeProvider("SampleClient", "Sample", CreateNamedType("ClientPipelineExtensions", "Sample")); + var pipelineExtensions = new TestTypeProvider("ClientPipelineExtensions", TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static, ns: "Sample"); + var cancellationTokenExtensions = new TestTypeProvider("CancellationTokenExtensions", TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static, ns: "Sample"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(client, pipelineExtensions, cancellationTokenExtensions)); + CodeModelGenerator.Instance.AddTypeToKeep(client.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([client, pipelineExtensions, cancellationTokenExtensions]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(client)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(pipelineExtensions)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(cancellationTokenExtensions)); + } + + [Test] + public async Task LastContractInternalHelperDoesNotRootAbsentGeneratedHelper() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample + { + internal static class ClientPipelineExtensions + { + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var client = new TestTypeProvider("SampleClient", TypeSignatureModifiers.Public, ns: "Sample"); + var pipelineExtensions = new TestTypeProvider("ClientPipelineExtensions", TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static, ns: "Sample"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(client, pipelineExtensions), + lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + CodeModelGenerator.Instance.AddTypeToKeep(client.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([client, pipelineExtensions]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(client)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(pipelineExtensions)); + } + + [Test] + public void NamespaceLessCustomCodeBodyDependencyDoesNotRootGeneratedTypeInDifferentNamespace() + { + var customCodeView = new BodyDependencyTestTypeProvider("CustomType", "Sample", CreateNamedType("ReferencedModel", string.Empty)); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var referencedModel = new TestTypeProvider("ReferencedModel", TypeSignatureModifiers.Public, ns: "Other.Models"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, referencedModel)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, referencedModel]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(customType)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(referencedModel)); + } + + [Test] + public void NamespaceLessCustomCodeBodyDependencyDoesNotRootInternalModelWithSameNamespaceMemberName() + { + var customCodeView = new BodyDependencyTestTypeProvider( + "CustomSetting", + "Sample", + [CreateNamedType("Key", string.Empty)], + [new PropertyProvider($"", MethodSignatureModifiers.Public, typeof(string), "Key", new AutoPropertyBody(true), TestTypeProvider.Empty)]); + var customType = new CustomizableTestTypeProvider("CustomSetting", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var generatedKey = new TestTypeProvider("Key", TypeSignatureModifiers.Internal, ns: "Sample"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, generatedKey)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, generatedKey]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(customType)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(generatedKey)); + } + + [Test] + public void CustomCodeBodyDependencyOnExternalTypeDoesNotRootGeneratedTypeBySimpleName() + { + var customCodeView = new BodyDependencyTestTypeProvider("CustomType", "Sample", CreateNamedType("OperationState", "Azure")); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var generatedOperationState = new TestTypeProvider("OperationState", TypeSignatureModifiers.Internal, ns: "Sample"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, generatedOperationState)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, generatedOperationState]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(customType)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(generatedOperationState)); + } + + [Test] + public void CustomCodeBodyDependencyRootsUniqueGeneratedExtensionMethodProvider() + { + var customCodeView = new BodyDependencyTestTypeProvider("CustomType", "Sample", CreateNamedType("SetValue", string.Empty)); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var extensionProvider = new ExtensionMethodTestTypeProvider("GeneratedExtensions", "SetValue", ns: "Sample"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, extensionProvider)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, extensionProvider]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(extensionProvider)); + } + + [Test] + public void AmbiguousCustomCodeExtensionMethodDependencyDoesNotRootGeneratedProvider() + { + var customCodeView = new BodyDependencyTestTypeProvider("CustomType", "Sample", CreateNamedType("SetValue", string.Empty)); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var firstExtensionProvider = new ExtensionMethodTestTypeProvider("FirstExtensions", "SetValue", ns: "Sample"); + var secondExtensionProvider = new ExtensionMethodTestTypeProvider("SecondExtensions", "SetValue", ns: "Sample"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, firstExtensionProvider, secondExtensionProvider)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([customType, firstExtensionProvider, secondExtensionProvider]); + + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(firstExtensionProvider)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(secondExtensionProvider)); + } + + [Test] + public void GeneratedBodyExtensionCallRootsProviderByMethodAndReceiverType() + { + var requestContextType = CreateNamedType("RequestContext", "Azure"); + var otherContextType = CreateNamedType("OtherContext", "Azure"); + var client = new ClientProvider("SampleClient", "Sample"); + client.Update(methods: + [ + new MethodProvider( + new MethodSignature("Process", $"", MethodSignatureModifiers.Public, typeof(void), $"", []), + new InvokeMethodExpression(new VariableExpression(requestContextType, "context"), "Parse", []), + client) + ]); + var requestContextExtensions = new ExtensionMethodTestTypeProvider("RequestContextExtensions", "Parse", "Sample", requestContextType); + var otherContextExtensions = new ExtensionMethodTestTypeProvider("OtherContextExtensions", "Parse", "Sample", otherContextType); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(client, requestContextExtensions, otherContextExtensions)); + CodeModelGenerator.Instance.AddTypeToKeep(client.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([client, requestContextExtensions, otherContextExtensions]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(requestContextExtensions)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(otherContextExtensions)); + } + + [Test] + public async Task CustomCodeTypeDoesNotRootGeneratedExtensionsTypeByNamingConvention() + { + var customCompilation = CSharpCompilation.Create( + "Customization", + [CSharpSyntaxTree.ParseText(""" + namespace Sample + { + public partial class Widget + { + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var client = new ClientProvider("SampleClient", "Sample"); + var widgetExtensions = new TestTypeProvider("WidgetExtensions", TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static, ns: "Sample"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(client, widgetExtensions), + compilation: () => Task.FromResult(customCompilation)); + CodeModelGenerator.Instance.AddTypeToKeep(client.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([client, widgetExtensions]); + + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(widgetExtensions)); + } + + [Test] + public void PublicCustomSignatureExternalTypeDoesNotPublicizeGeneratedTypeBySimpleName() + { + var customCodeView = new SignatureDependencyTestTypeProvider( + "CustomType", + TypeSignatureModifiers.Public, + CreateNamedType("Action", "System")); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); + var generatedAction = new TestTypeProvider("Action", TypeSignatureModifiers.Internal, ns: "Sample.Models"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, generatedAction)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + CodeModelGenerator.Instance.AddTypeToKeep(generatedAction.Type.FullyQualifiedName, isRoot: false); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([customType, generatedAction]); + + Assert.IsTrue(generatedAction.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(generatedAction.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public void InternalizeModeDoesNotRemoveUnreferencedProviders() + { + var context = new TestTypeProvider("SampleContext", TypeSignatureModifiers.Public); + var unusedModel = new TestTypeProvider("UnusedModel", TypeSignatureModifiers.Public); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => new TestOutputLibrary(context, unusedModel), + configuration: "{\"unreferenced-types-handling\":\"internalize\"}"); + CodeModelGenerator.Instance.AddTypeToKeep(context.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([context, unusedModel]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(context)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(unusedModel)); + Assert.IsEmpty(ProviderReferenceMapAnalyzer.LatestResult!.RemoveCandidates); + } + + [Test] + public void SerializationProviderInfrastructureRootsUseProviderBodyDependencies() + { + var optional = new TestTypeProvider("Optional", TypeSignatureModifiers.Public); + var modelSerializationExtensions = new TestTypeProvider("ModelSerializationExtensions", TypeSignatureModifiers.Public); + var serializationProvider = new BodyDependencyTestTypeProvider( + "SampleModelSerializer", + ns: null, + optional.Type, + modelSerializationExtensions.Type); + var model = new ClientProvider("SampleModel", serializationProvider); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary( + model, + serializationProvider, + optional, + modelSerializationExtensions)); + + ProviderReferenceMapAnalyzer.Analyze([model, serializationProvider, optional, modelSerializationExtensions]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(serializationProvider)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(optional)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(modelSerializationExtensions)); + } + + [Test] + public async Task InternalCustomizationTypeDoesNotInternalizeGeneratedTypeWithSameSimpleName() + { + var customCompilation = CompilationHelper.LoadCompilation( + [new TestTypeProvider("Error", TypeSignatureModifiers.Internal, ns: "Custom.Models")]); + var context = new TestTypeProvider("SampleContext", TypeSignatureModifiers.Public); + var generatedError = new TestTypeProvider("Error", TypeSignatureModifiers.Public, ns: "Generated.Models"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(context, generatedError), + compilation: () => Task.FromResult(customCompilation)); + CodeModelGenerator.Instance.AddTypeToKeep(context.Type.FullyQualifiedName); + CodeModelGenerator.Instance.AddTypeToKeep(generatedError.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([context, generatedError]); + + Assert.IsTrue(generatedError.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(generatedError.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public async Task PublicGenericCustomizationDoesNotPublicizeGeneratedNonGenericTypeWithSameSimpleName() + { + var customCompilation = CSharpCompilation.Create( + "TestAssembly", + [CSharpSyntaxTree.ParseText(""" + namespace Generated.Models + { + public partial class SearchResult + { + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var context = new TestTypeProvider("SampleContext", TypeSignatureModifiers.Public); + var generatedSearchResult = new TestTypeProvider("SearchResult", TypeSignatureModifiers.Internal, ns: "Generated.Models"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(context, generatedSearchResult), + compilation: () => Task.FromResult(customCompilation)); + CodeModelGenerator.Instance.AddTypeToKeep(context.Type.FullyQualifiedName); + CodeModelGenerator.Instance.AddTypeToKeep(generatedSearchResult.Type.FullyQualifiedName, isRoot: false); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([context, generatedSearchResult]); + + Assert.IsTrue(generatedSearchResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(generatedSearchResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public void PublicGenericCustomSignatureDoesNotRootGeneratedNonGenericTypeBySimpleName() + { + var genericArgument = CreateNamedType("T", string.Empty); + var customCodeView = new SignatureDependencyTestTypeProvider( + "CustomType", + TypeSignatureModifiers.Public, + CreateNamedType("SearchResult", "Generated.Models", genericArgument)); + var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Generated.Models"); + var generatedSearchResult = new TestTypeProvider("SearchResult", TypeSignatureModifiers.Internal, ns: "Generated.Models"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(customType, generatedSearchResult)); + CodeModelGenerator.Instance.AddTypeToKeep(customType.Type.FullyQualifiedName); + CodeModelGenerator.Instance.AddTypeToKeep(generatedSearchResult.Type.FullyQualifiedName, isRoot: false); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([customType, generatedSearchResult]); + + Assert.IsTrue(generatedSearchResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(generatedSearchResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public void PublicProviderSignatureDependencyKeepsNonRootTypePublic() + { + var client = new ClientProvider( + "SampleClient", + "Sample", + signatureDependencyTypes: CreateNamedType("GeneratedModel", "Sample.Models")); + var generatedModel = new GeneratedModelTestTypeProvider("GeneratedModel", TypeSignatureModifiers.Public, ns: "Sample.Models"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(client, generatedModel)); + CodeModelGenerator.Instance.AddTypeToKeep(generatedModel.Type.FullyQualifiedName, isRoot: false); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, generatedModel]); + + Assert.IsTrue(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public void PublicProviderMethodWithNamespaceLessSignatureKeepsNonRootTypePublic() + { + var client = new ClientProvider("SampleClient", "Sample"); + var generatedModel = new GeneratedModelTestTypeProvider("GeneratedModel", TypeSignatureModifiers.Public, ns: "Sample"); + client.Update(methods: + [ + new MethodProvider( + new MethodSignature( + "GetModel", + $"", + MethodSignatureModifiers.Public, + CreateNamedType("GeneratedModel", string.Empty), + $"", + []), + MethodBodyStatement.Empty, + client) + ]); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(client, generatedModel)); + CodeModelGenerator.Instance.AddTypeToKeep(generatedModel.Type.FullyQualifiedName, isRoot: false); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, generatedModel]); + + Assert.IsTrue(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public async Task LastContractGenericDeclarationKeepsOnlyMatchingGeneratedGenericTypePublic() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + public partial class PageResult + { + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + + var genericArgument = CreateNamedType("T", string.Empty); + var genericPageResult = new GenericTestTypeProvider("PageResult", TypeSignatureModifiers.Public, "Sample.Models", genericArgument); + var nonGenericPageResult = new GeneratedModelTestTypeProvider("PageResult", TypeSignatureModifiers.Public, ns: "Sample.Models"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(genericPageResult, nonGenericPageResult), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}", + lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([genericPageResult, nonGenericPageResult]); + + Assert.IsTrue(genericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(genericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(nonGenericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(nonGenericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public async Task LastContractNestedPublicDeclarationKeepsGeneratedNestedTypePublic() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + public partial class OuterModel + { + public partial class InnerModel + { + } + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + + var outerModel = new TestTypeProvider("OuterModel", TypeSignatureModifiers.Public, ns: "Sample.Models"); + var innerModel = new NestedTestTypeProvider("InnerModel", TypeSignatureModifiers.Public, outerModel, ns: "Sample.Models"); + outerModel.NestedTypesInternal = [innerModel]; + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(outerModel), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}", + lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([outerModel]); + + Assert.IsTrue(outerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(outerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(innerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(innerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public void DependenciesOfInternalizedClientAreNotPublicizedFromClientRootTraversal() + { + var clientOptions = new TestTypeProvider("SampleClientOptions", TypeSignatureModifiers.Public, ns: "Sample"); + var client = new ClientProvider( + "SampleClient", + "Sample", + new SignatureDependencyTestTypeProvider("SampleClient", TypeSignatureModifiers.Internal, ns: "Sample"), + signatureDependencyTypes: clientOptions.Type); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(client, clientOptions)); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, clientOptions]); + + Assert.IsTrue(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public void PublicConstructorOnInternalClientDoesNotPublicizeGeneratedOptions() + { + var clientOptions = new TestTypeProvider("SampleClientOptions", TypeSignatureModifiers.Public, ns: "Sample"); + var customCodeView = new SignatureDependencyTestTypeProvider("SampleClient", TypeSignatureModifiers.Internal, ns: "Sample"); + var client = new ConstructorDependencyTestTypeProvider( + "SampleClient", + TypeSignatureModifiers.Public, + customCodeView, + ns: "Sample", + constructorParameterTypes: [CreateNamedType("SampleClientOptions", "Sample")]); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(client, clientOptions)); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, clientOptions]); + + Assert.IsTrue(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public async Task GeneratedTypeInternalInLastContractIsInternalized() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample + { + internal partial class SampleClient + { + } + + internal partial class SampleClientOptions + { + internal enum ServiceVersion + { + V1 = 1 + } + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var clientOptions = new TestTypeProvider("SampleClientOptions", TypeSignatureModifiers.Public, ns: "Sample"); + clientOptions.NestedTypesInternal = + [ + new NestedTestTypeProvider("ServiceVersion", TypeSignatureModifiers.Public | TypeSignatureModifiers.Enum, clientOptions, ns: "Sample") + ]; + var client = new ClientProvider("SampleClient", "Sample", signatureDependencyTypes: CreateNamedType("SampleClientOptions", "Sample")); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(client, clientOptions), + lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, clientOptions]); + + Assert.IsTrue(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(clientOptions.NestedTypes[0].DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(clientOptions.NestedTypes[0].DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public async Task PublicCustomPartialKeepsGeneratedTypePublicWhenLastContractGeneratedDeclarationIsInternal() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample + { + internal abstract partial class AbstractModel + { + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var customCodeView = new TestTypeProvider("AbstractModel", TypeSignatureModifiers.Public | TypeSignatureModifiers.Abstract, ns: "Sample"); + var model = new CustomizableTestTypeProvider("AbstractModel", TypeSignatureModifiers.Public | TypeSignatureModifiers.Abstract, customCodeView, ns: "Sample"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(model), + lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([model]); + + Assert.IsTrue(model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public async Task TopLevelTypeWithSameNameAsNestedTypeDoesNotPublicizeDeclaringType() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample + { + public enum ServiceVersion + { + V1 = 1 + } + + internal partial class SampleClientOptions + { + internal enum ServiceVersion + { + V1 = 1 + } + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var publicServiceVersion = new TestTypeProvider("ServiceVersion", TypeSignatureModifiers.Public | TypeSignatureModifiers.Enum, ns: "Sample"); + var clientOptions = new TestTypeProvider("SampleClientOptions", TypeSignatureModifiers.Public, ns: "Sample"); + clientOptions.NestedTypesInternal = + [ + new NestedTestTypeProvider("ServiceVersion", TypeSignatureModifiers.Public | TypeSignatureModifiers.Enum, clientOptions, ns: "Sample") + ]; + var customPublicApi = new SignatureDependencyTestTypeProvider( + "CustomPublicApi", + TypeSignatureModifiers.Public, + ns: "Sample", + CreateNamedType("ServiceVersion", "Sample")); + var generatedPublicApi = new CustomizableTestTypeProvider( + "CustomPublicApi", + TypeSignatureModifiers.Public, + customPublicApi, + ns: "Sample"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(publicServiceVersion, clientOptions, generatedPublicApi), + lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([publicServiceVersion, clientOptions, generatedPublicApi]); + + Assert.IsTrue(publicServiceVersion.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(publicServiceVersion.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(generatedPublicApi.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(generatedPublicApi.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(clientOptions.NestedTypes[0].DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(clientOptions.NestedTypes[0].DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public async Task ExistingGeneratedSourceDoesNotChangeAccessibilityWithoutLastContract() + { + var outputPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, "ReferenceMap", nameof(ExistingGeneratedSourceDoesNotChangeAccessibilityWithoutLastContract)); + var projectPath = Path.Combine(outputPath, "src"); + Directory.CreateDirectory(projectPath); + await File.WriteAllTextAsync(Path.Combine(projectPath, "SampleClient.cs"), """ + namespace Sample + { + internal partial class SampleClient + { + } + } + """); + await File.WriteAllTextAsync(Path.Combine(projectPath, "SampleClientOptions.cs"), """ + namespace Sample + { + internal partial class SampleClientOptions + { + internal enum ServiceVersion + { + V1 = 1 + } + } + } + """); + var clientOptions = new TestTypeProvider("SampleClientOptions", TypeSignatureModifiers.Public, ns: "Sample"); + clientOptions.NestedTypesInternal = + [ + new NestedTestTypeProvider("ServiceVersion", TypeSignatureModifiers.Public | TypeSignatureModifiers.Enum, clientOptions, ns: "Sample") + ]; + var client = new ClientProvider("SampleClient", "Sample", signatureDependencyTypes: CreateNamedType("SampleClientOptions", "Sample")); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(client, clientOptions), + configuration: "{}", + outputPath: outputPath); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, clientOptions]); + + Assert.IsTrue(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(clientOptions.NestedTypes[0].DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public async Task ModelFactorySignatureRootsDoNotDependOnModelName() + { + MockHelpers.LoadMockGenerator(); + var lastContract = await Helpers.GetCompilationFromDirectoryAsync(); + var pagedModel = new TestTypeProvider("PagedWidget", TypeSignatureModifiers.Public, ns: "Sample"); + var requestModel = new TestTypeProvider("WidgetRequest", TypeSignatureModifiers.Public, ns: "Sample"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(pagedModel, requestModel), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}", + lastContractCompilation: () => Task.FromResult(lastContract)); + var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + Assert.IsNotNull(modelFactory.LastContractView, modelFactory.Name); + + ProviderReferenceMapAnalyzer.Analyze([pagedModel, requestModel, modelFactory]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(pagedModel)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(requestModel)); + } + + [Test] + public void ApiDirectoryDoesNotChangePublicClientRootsWithoutLastContract() + { + var outputPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, "ReferenceMap", nameof(ApiDirectoryDoesNotChangePublicClientRootsWithoutLastContract)); + Directory.CreateDirectory(Path.Combine(outputPath, "api")); + MockHelpers.LoadMockGenerator( + configuration: "{}", + outputPath: Path.Combine(outputPath, "src")); + var options = new TestTypeProvider("SampleClientOptions", TypeSignatureModifiers.Public, ns: "Sample"); + var client = new ClientProvider("SampleClient", "Sample", signatureDependencyTypes: options.Type); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, options]); + + Assert.IsTrue(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(options.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public void PublicCustomCodeArraySignatureKeepsGeneratedTypePublic() + { + var customCodeView = new SignatureDependencyTestTypeProvider("PublicCustomApi", TypeSignatureModifiers.Public, CreateNamedType("GeneratedModel", string.Empty)); + var generatedModel = new CustomizableTestTypeProvider("GeneratedModel", TypeSignatureModifiers.Public, customCodeView, ns: "Generated.Models"); + MockHelpers.LoadMockGenerator(createOutputLibrary: () => new TestOutputLibrary(generatedModel)); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([generatedModel]); + + Assert.IsTrue(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public async Task InternalizedGeneratedPredecessorDoesNotPublicizeItsInternalDependency() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + internal partial class InternalModel + { + } + + internal partial class InternalDependency + { + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + + var dependency = new GeneratedModelTestTypeProvider("InternalDependency", TypeSignatureModifiers.Public, ns: "Sample.Models"); + var internalModel = new GeneratedModelTestTypeProvider( + "InternalModel", + TypeSignatureModifiers.Public, + ns: "Sample.Models"); + var publicRoot = new GeneratedModelTestTypeProvider( + "PublicRoot", + TypeSignatureModifiers.Public, + ns: "Sample.Models"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(publicRoot, internalModel, dependency), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}", + lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + CodeModelGenerator.Instance.AddTypeToKeep(publicRoot.Type.FullyQualifiedName); + CodeModelGenerator.Instance.AddTypeToKeep(internalModel.Type.FullyQualifiedName); + CodeModelGenerator.Instance.AddTypeToKeep(dependency.Type.FullyQualifiedName); + + publicRoot.Update(properties: [ + new PropertyProvider($"", MethodSignatureModifiers.Public, internalModel.Type, "InternalModel", new AutoPropertyBody(false), publicRoot) + ]); + internalModel.Update(properties: [ + new PropertyProvider($"", MethodSignatureModifiers.Public, dependency.Type, "Dependency", new AutoPropertyBody(false), internalModel) + ]); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([internalModel, dependency]); + + Assert.IsTrue(internalModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(internalModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(dependency.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(dependency.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + + [Test] + public void InternalizedGenericModelRemovesAritylessModelFactoryMethod() + { + var genericArgument = CreateNamedType("T", string.Empty); + var genericModel = new GenericTestTypeProvider( + "GenericModel`1", + TypeSignatureModifiers.Public, + "Sample.Models", + genericArgument); + var outputLibrary = new TestOutputLibrary(genericModel); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => outputLibrary, + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + modelFactory.Update(methods: + [ + new MethodProvider( + new MethodSignature( + "GenericModel", + $"", + MethodSignatureModifiers.Static | MethodSignatureModifiers.Public, + genericModel.Type, + $"", + []), + MethodBodyStatement.Empty, + modelFactory) + ]); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([genericModel]); + + Assert.IsTrue(genericModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(genericModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsEmpty(modelFactory.Methods.Select(m => m.Signature.Name)); + } + + [Test] + public void GeneratedTypePreservesLastContractTypeAttributes() + { + var lastContractView = new AttributedTestTypeProvider( + "GeneratedModel", + TypeSignatureModifiers.Internal, + ns: "Sample", + new AttributeStatement(typeof(ExperimentalAttribute), Literal("TEST001"))); + var generatedModel = new ContractedTestTypeProvider( + "GeneratedModel", + TypeSignatureModifiers.Internal, + lastContractView, + ns: "Sample"); + var outputLibrary = new TestOutputLibrary(generatedModel); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => outputLibrary, + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([generatedModel]); + + Assert.IsTrue(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + var codeFile = new TypeProviderWriter(generatedModel).Write(); + StringAssert.Contains("[global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute(\"TEST001\")]", codeFile.Content); + StringAssert.Contains("internal partial class GeneratedModel", codeFile.Content); + } + + [Test] + public void GeneratedTypeDoesNotPreserveLastContractAttributeReferencingRemovedGeneratedType() + { + var removedModel = new GeneratedModelTestTypeProvider("RemovedModel", TypeSignatureModifiers.Internal, ns: "Sample"); + var lastContractView = new AttributedTestTypeProvider( + "GeneratedModel", + TypeSignatureModifiers.Internal, + ns: "Sample", + new AttributeStatement(typeof(ExperimentalAttribute), TypeOf(CreateNamedType("RemovedModel", "Sample")))); + var generatedModel = new ContractedTestTypeProvider( + "GeneratedModel", + TypeSignatureModifiers.Internal, + lastContractView, + ns: "Sample"); + var outputLibrary = new TestOutputLibrary(generatedModel, removedModel); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => outputLibrary, + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + + ProviderReferenceMapAnalyzer.Analyze([generatedModel, removedModel]); + + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(removedModel)); + var codeFile = new TypeProviderWriter(generatedModel).Write(); + StringAssert.DoesNotContain("RemovedModel", codeFile.Content); + StringAssert.Contains("internal partial class GeneratedModel", codeFile.Content); + } + + private sealed class BodyDependencyTestTypeProvider : TestTypeProvider + { + private readonly CSharpType[] _bodyDependencyTypes; + + public BodyDependencyTestTypeProvider(string name, params CSharpType[] bodyDependencyTypes) + : this(name, bodyDependencyTypes, ns: null) + { + } + + public BodyDependencyTestTypeProvider(string name, string? ns, params CSharpType[] bodyDependencyTypes) + : this(name, bodyDependencyTypes, ns) + { + } + + public BodyDependencyTestTypeProvider(string name, string? ns, CSharpType[] bodyDependencyTypes, PropertyProvider[] properties) + : base(name, TypeSignatureModifiers.Public, properties: properties, ns: ns) + { + _bodyDependencyTypes = bodyDependencyTypes; + } + + public BodyDependencyTestTypeProvider(string name, CSharpType[] bodyDependencyTypes, string? ns) + : base(name, TypeSignatureModifiers.Public, ns: ns) + { + _bodyDependencyTypes = bodyDependencyTypes; + } + + protected internal override IReadOnlyList BuildBodyDependencyTypes() => _bodyDependencyTypes; + } + + private sealed class HelperDependencyTestTypeProvider : TestTypeProvider + { + private readonly CSharpType[] _helperDependencyTypes; + + public HelperDependencyTestTypeProvider(string name, string? ns, params CSharpType[] helperDependencyTypes) + : base(name, TypeSignatureModifiers.Public, ns: ns) + { + _helperDependencyTypes = helperDependencyTypes; + } + + protected internal override IReadOnlyList BuildHelperDependencyTypes() => _helperDependencyTypes; + } + + private sealed class ExtensionMethodTestTypeProvider : TestTypeProvider + { + private readonly string _methodName; + private readonly CSharpType? _receiverType; + + public ExtensionMethodTestTypeProvider(string name, string methodName, string ns, CSharpType? receiverType = null) + : base(name, TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static, ns: ns) + { + _methodName = methodName; + _receiverType = receiverType; + } + + protected internal override MethodProvider[] BuildMethods() => + [ + new MethodProvider( + new MethodSignature( + _methodName, + $"", + MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension, + typeof(void), + $"", + _receiverType == null ? [] : [new ParameterProvider("value", $"", _receiverType)]), + MethodBodyStatement.Empty, + this) + ]; + } + + private sealed class SignatureDependencyTestTypeProvider : TestTypeProvider + { + private readonly CSharpType[] _signatureDependencyTypes; + + public SignatureDependencyTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, params CSharpType[] signatureDependencyTypes) + : this(name, declarationModifiers, ns: null, signatureDependencyTypes) + { + } + + public SignatureDependencyTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, string? ns, params CSharpType[] signatureDependencyTypes) + : base(name, declarationModifiers, ns: ns) + { + _signatureDependencyTypes = signatureDependencyTypes; + } + + protected internal override IReadOnlyList BuildSignatureDependencyTypes() => _signatureDependencyTypes; + } + + private sealed class CustomizableTestTypeProvider : TestTypeProvider + { + private readonly TypeProvider _customCodeView; + + public CustomizableTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, TypeProvider customCodeView, string ns) + : base(name, declarationModifiers, ns: ns) + { + _customCodeView = customCodeView; + } + + private protected override TypeProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => _customCodeView; + } + + private sealed class AttributedTestTypeProvider : TestTypeProvider + { + private readonly AttributeStatement[] _attributes; + + public AttributedTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, string ns, params AttributeStatement[] attributes) + : base(name, declarationModifiers, ns: ns) + { + _attributes = attributes; + } + + protected override IReadOnlyList BuildAttributes() => _attributes; + } + + private sealed class ContractedTestTypeProvider : TestTypeProvider + { + private readonly TypeProvider _lastContractView; + + public ContractedTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, TypeProvider lastContractView, string ns) + : base(name, declarationModifiers, ns: ns) + { + _lastContractView = lastContractView; + } + + private protected override TypeProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => _lastContractView; + } + + private sealed class ConstructorDependencyTestTypeProvider : TestTypeProvider + { + private readonly TypeProvider _customCodeView; + private readonly CSharpType[] _constructorParameterTypes; + + public ConstructorDependencyTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, TypeProvider customCodeView, string ns, params CSharpType[] constructorParameterTypes) + : base(name, declarationModifiers, ns: ns) + { + _customCodeView = customCodeView; + _constructorParameterTypes = constructorParameterTypes; + } + + protected internal override ConstructorProvider[] BuildConstructors() + { + var parameters = _constructorParameterTypes + .Select((type, index) => new ParameterProvider($"p{index}", $"", type)) + .ToArray(); + return + [ + new ConstructorProvider( + new ConstructorSignature(Type, $"", MethodSignatureModifiers.Public, parameters), + MethodBodyStatement.Empty, + this) + ]; + } + + private protected override TypeProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => _customCodeView; + } + + private sealed class NestedTestTypeProvider : TestTypeProvider + { + private readonly TypeProvider _declaringTypeProvider; + + public NestedTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, TypeProvider declaringTypeProvider, string ns) + : base(name, declarationModifiers, ns: ns) + { + _declaringTypeProvider = declaringTypeProvider; + } + + protected override TypeProvider? BuildDeclaringTypeProvider() => _declaringTypeProvider; + } + + private sealed class GeneratedModelTestTypeProvider : TypeProvider + { + private readonly string _name; + private readonly string _namespace; + private readonly TypeSignatureModifiers _declarationModifiers; + + public GeneratedModelTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, string ns) + { + _name = name; + _namespace = ns; + _declarationModifiers = declarationModifiers; + } + + protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Models", $"{Name}.cs"); + protected override string BuildName() => _name; + protected override string BuildNamespace() => _namespace; + protected override TypeSignatureModifiers BuildDeclarationModifiers() => _declarationModifiers; + } + + private sealed class GenericTestTypeProvider : TestTypeProvider + { + private readonly CSharpType[] _typeArguments; + + public GenericTestTypeProvider(string name, TypeSignatureModifiers declarationModifiers, string ns, params CSharpType[] typeArguments) + : base(name, declarationModifiers, ns: ns) + { + _typeArguments = typeArguments; + } + + protected override CSharpType[] GetTypeArguments() => _typeArguments; + } + + private sealed class ClientProvider : TestTypeProvider + { + private readonly TypeProvider[] _serializationProviders; + private readonly TypeProvider? _customCodeView; + private readonly CSharpType[] _signatureDependencyTypes; + + public ClientProvider(string name, params TypeProvider[] serializationProviders) + : this(name, ns: null, customCodeView: null, serializationProviders: serializationProviders) + { + } + + public ClientProvider(string name, string? ns, TypeProvider? customCodeView = null, TypeProvider[]? serializationProviders = null, params CSharpType[] signatureDependencyTypes) + : base(name, TypeSignatureModifiers.Public, ns: ns) + { + _serializationProviders = serializationProviders ?? []; + _customCodeView = customCodeView; + _signatureDependencyTypes = signatureDependencyTypes; + } + + protected override TypeProvider[] BuildSerializationProviders() => _serializationProviders; + private protected override TypeProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => _customCodeView; + protected internal override IReadOnlyList BuildSignatureDependencyTypes() => _signatureDependencyTypes; + } + + private static CSharpType CreateNamedType(string name, string ns, params CSharpType[] arguments) + { + var constructor = typeof(CSharpType).GetConstructor( + BindingFlags.Instance | BindingFlags.NonPublic, + binder: null, + [typeof(string), typeof(string), typeof(bool), typeof(bool), typeof(CSharpType), typeof(IReadOnlyList), typeof(bool), typeof(bool), typeof(CSharpType), typeof(Type)], + modifiers: null)!; + + return (CSharpType)constructor.Invoke([name, ns, false, false, null, arguments, true, false, null, null]); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/ModelFactorySignatureRootsDoNotDependOnModelName/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/ModelFactorySignatureRootsDoNotDependOnModelName/LastContract.cs new file mode 100644 index 00000000000..b91419eb56e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/ModelFactorySignatureRootsDoNotDependOnModelName/LastContract.cs @@ -0,0 +1,17 @@ +namespace Sample +{ + internal class PagedWidget + { + } + + internal class WidgetRequest + { + } + + public static class SampleModelFactory + { + public static PagedWidget PagedWidget() => throw null; + + public static WidgetRequest WidgetRequest() => throw null; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs index 13ce5b2ef7f..3315fab866b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs @@ -791,12 +791,12 @@ public void BuildPlugin_FindsOutputForMultiTargetedProject() { Directory.CreateDirectory(testDir); - // Multi-targeting (multiple frameworks) produces a separate output folder - // per framework. The previous path-computation logic could not reliably - // pick a framework; the scan should still locate a loadable assembly. + // Multi-targeting uses and can produce a separate output + // folder per framework. Keep this test on installed target packs so it stays + // hermetic in CI; the scan should still locate a loadable assembly. File.WriteAllText(Path.Combine(testDir, "MultiTarget.csproj"), @" - net10.0;netstandard2.0 + net10.0;net10.0 "); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestOutputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestOutputLibrary.cs index 00119db05dd..7490c2f41e5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestOutputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/TestOutputLibrary.cs @@ -14,6 +14,11 @@ public TestOutputLibrary(TypeProvider typeProvider) _types = [typeProvider]; } + public TestOutputLibrary(params TypeProvider[] typeProviders) + { + _types = typeProviders; + } + protected override TypeProvider[] BuildTypeProviders() => _types; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TypeFactoryTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TypeFactoryTests.cs index 1a57a7029a3..22ad024f8fc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TypeFactoryTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TypeFactoryTests.cs @@ -39,6 +39,17 @@ public void ExtensibleStringEnumType() Assert.AreEqual(expected, actual); } + [Test] + public void CreateUnion_TracksNestedCollectionModelVariantsToKeep() + { + var variant = InputFactory.Model("InputContent", "Sample.Models"); + var union = InputFactory.Union([InputPrimitiveType.String, InputFactory.Array(variant)]); + + CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(union); + + Assert.That(CodeModelGenerator.Instance.TypeFactory.UnionVariantTypesToKeep, Does.Contain("Sample.Models.InputContent")); + } + [Test] public void ExtensibleStringNullableEnumType() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TestData/TypeSymbolExtensionsTests/TypeParameterDoesNotResolveContainingGenericType/GenericContainer.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TestData/TypeSymbolExtensionsTests/TypeParameterDoesNotResolveContainingGenericType/GenericContainer.cs new file mode 100644 index 00000000000..27bd4cf11cd --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TestData/TypeSymbolExtensionsTests/TypeParameterDoesNotResolveContainingGenericType/GenericContainer.cs @@ -0,0 +1,6 @@ +namespace Sample +{ + public class GenericContainer + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TypeSymbolExtensionsTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TypeSymbolExtensionsTests.cs index 3884f8d584e..e8fcf0bb0fb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TypeSymbolExtensionsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TypeSymbolExtensionsTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Tests.Common; using Moq; @@ -83,6 +84,46 @@ public async Task NonNullableKnownFrameworkTypeResolvesUnchanged() Assert.IsFalse(csharpType.IsNullable); } + [Test] + public async Task TypeParameterDoesNotResolveContainingGenericType() + { + var compilation = await Helpers.GetCompilationFromDirectoryAsync(); + var typeSymbol = compilation.GetTypeByMetadataName("Sample.GenericContainer`1"); + Assert.IsNotNull(typeSymbol, "Failed to resolve generic type symbol from compiled source."); + + var csharpType = typeSymbol!.TypeParameters[0].GetCSharpType(); + + Assert.AreEqual("T", csharpType.Name); + Assert.IsNull(csharpType.DeclaringType); + } + + [Test] + public void CollectionGenericSymbolWithoutAngleBracketDisplayNameGetsFullyQualifiedMetadataName() + { + var compilation = CSharpCompilation.Create( + "TestAssembly", + [CSharpSyntaxTree.ParseText(""" + using System.Collections.Generic; + + namespace Sample + { + public class Container + { + public IReadOnlyList GetResult() => null; + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var method = compilation.GetTypeByMetadataName("Sample.Container")! + .GetMembers("GetResult") + .OfType() + .Single(); + + var name = method.ReturnType.GetFullyQualifiedName(); + + Assert.AreEqual("System.Collections.Generic.IReadOnlyList`1", name); + } + private static IPropertySymbol GetPropertySymbol(Compilation compilation, string containerName, string propertyName) { var typeSymbol = compilation.GetTypeByMetadataName($"Sample.{containerName}"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Writers/TypeProviderWriterTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Writers/TypeProviderWriterTests.cs index 1258ccb409d..69d03d31859 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Writers/TypeProviderWriterTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Writers/TypeProviderWriterTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; @@ -21,6 +22,12 @@ public TypeProviderWriterTests() MockHelpers.LoadMockGenerator(); } + [TearDown] + public void TearDown() + { + MockHelpers.LoadMockGenerator(); + } + // Tests that the Write method is successfully overridden. [Test] public void Write_Override() @@ -117,5 +124,34 @@ protected internal override FieldProvider[] BuildFields() ]; } } + + [Test] + public void TypeProviderWriter_DoesNotPreserveStaleModelFactoryUsings() + { + var outputPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Writers", nameof(TypeProviderWriter_DoesNotPreserveStaleModelFactoryUsings)); + if (Directory.Exists(outputPath)) + { + Directory.Delete(outputPath, recursive: true); + } + + var generatedPath = Path.Combine(outputPath, "src", "Generated"); + Directory.CreateDirectory(generatedPath); + File.WriteAllText(Path.Combine(generatedPath, "SampleModelFactory.cs"), """ + using Removed.Namespace; + + namespace Sample + { + public static partial class SampleModelFactory + { + } + } + """); + + MockHelpers.LoadMockGenerator(configuration: "{}", outputPath: outputPath, inputNamespaceName: "Sample"); + + var codeFile = new TypeProviderWriter(new ModelFactoryProvider([])).Write(); + + Assert.IsFalse(codeFile.Content.Contains("using Removed.Namespace;", StringComparison.Ordinal)); + } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/Models/ParametersBasicContext.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/Models/ParametersBasicContext.cs index d21f56d38b7..b594d3e6f15 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/Models/ParametersBasicContext.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/basic/src/Generated/Models/ParametersBasicContext.cs @@ -4,7 +4,6 @@ using System.ClientModel.Primitives; using Parameters.Basic._ExplicitBody; -using Parameters.Basic._ImplicitBody; namespace Parameters.Basic { 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/Models/ParametersSpreadContext.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/Models/ParametersSpreadContext.cs index fca7b7b5ee9..334e0eb3cb1 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/Models/ParametersSpreadContext.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/parameters/spread/src/Generated/Models/ParametersSpreadContext.cs @@ -3,7 +3,6 @@ #nullable disable using System.ClientModel.Primitives; -using Parameters.Spread._Alias; using Parameters.Spread._Model; namespace Parameters.Spread 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/Models/PayloadMultiPartContext.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/Models/PayloadMultiPartContext.cs index 48490ff12b9..2e915db6195 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/Models/PayloadMultiPartContext.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/multipart/src/Generated/Models/PayloadMultiPartContext.cs @@ -3,7 +3,6 @@ #nullable disable using System.ClientModel.Primitives; -using Payload.MultiPart._FormData.File; namespace Payload.MultiPart { 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/Models/PayloadPageableContext.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/Models/PayloadPageableContext.cs index 347e4c01fa3..9a53d9a8f3d 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/Models/PayloadPageableContext.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/payload/pageable/src/Generated/Models/PayloadPageableContext.cs @@ -3,10 +3,7 @@ #nullable disable using System.ClientModel.Primitives; -using Payload.Pageable._PageSize; -using Payload.Pageable._ServerDrivenPagination; using Payload.Pageable._ServerDrivenPagination.AlternateInitialVerb; -using Payload.Pageable._ServerDrivenPagination.ContinuationToken; namespace Payload.Pageable { 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..9f8c3ce6d6d 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,12 +2,7 @@ #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 { diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/Models/SpecialWordsContext.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/Models/SpecialWordsContext.cs index 42db04881c2..9469e8aa953 100644 --- a/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/Models/SpecialWordsContext.cs +++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/special-words/src/Generated/Models/SpecialWordsContext.cs @@ -5,7 +5,6 @@ using System.ClientModel.Primitives; using SpecialWords._ModelProperties; using SpecialWords._Models; -using SpecialWords._ReservedOperationBodyParams; namespace SpecialWords { 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 { From 82989563b41513ee0156b588f4a32b0c8dd59892 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 14 Jul 2026 02:45:54 +0000 Subject: [PATCH 2/8] refactor(csharp): address reference map review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../src/Providers/ClientOptionsProvider.cs | 3 +-- .../Providers/ModelSerializationExtensionsDefinition.cs | 2 +- .../src/Providers/MrwSerializationTypeDefinition.cs | 2 +- .../Providers/MultipartFormDataSerializationDefinition.cs | 2 +- .../src/Providers/RestClientProvider.cs | 5 +++-- .../src/ScmCodeModelGenerator.cs | 8 ++++++++ .../src/ScmOutputLibrary.cs | 5 ++--- .../Microsoft.TypeSpec.Generator/src/CSharpGen.cs | 2 +- .../src/Providers/ModelFactoryProvider.cs | 4 +--- .../src/Providers/TypeProvider.cs | 2 +- .../ProviderReferenceMapAnalyzer.RootSelection.cs | 2 +- 11 files changed, 21 insertions(+), 16 deletions(-) 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 82c5820812c..20258bebed5 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 @@ -29,6 +29,7 @@ public class ClientOptionsProvider : TypeProvider private readonly ClientProvider _clientProvider; private readonly Dictionary? _serviceVersionsEnums; private static ClientOptionsProvider? _singletonInstance; + // All clients sharing the singleton are tracked so its accessibility reflects every owner. private static readonly List _singletonClientProviders = []; internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientProvider) @@ -78,11 +79,9 @@ public static ClientOptionsProvider CreateClientOptionsProvider(InputClient inpu { if (UseSingletonInstance(inputClient)) { - // Use singleton instance if (_singletonInstance == null) { _singletonClientProviders.Clear(); - // Create singleton with namespace-based naming _singletonInstance = new ClientOptionsProvider(inputClient, clientProvider); } if (!_singletonClientProviders.Contains(clientProvider)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.cs index 5d1b2102001..ddd5ffff87b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.cs @@ -109,7 +109,7 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override string BuildName() => "ModelSerializationExtensions"; protected override IReadOnlyList BuildBodyDependencyTypes() => - [new TypeFormattersDefinition().Type]; + [ScmCodeModelGenerator.Instance.TypeFormattersDefinition.Type]; protected override FieldProvider[] BuildFields() { 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 c93e46c6173..5a9b9240003 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 @@ -122,7 +122,7 @@ protected override IReadOnlyList BuildHelperDependencyTypes() => _ra : []; protected override IReadOnlyList BuildBodyDependencyTypes() => - [new OptionalDefinition().Type, ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.Type]; + [ScmCodeModelGenerator.Instance.SystemOptionalDefinition.Type, ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.Type]; protected override SuppressionStatement[] BuildDisabledFileWarnings() { 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 0581bf32615..a6b849bbb2e 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 @@ -55,7 +55,7 @@ protected override SuppressionStatement[] BuildDisabledFileWarnings() => [new SuppressionStatement(null, Literal(ScmModelProvider.FileBinaryContentDiagnosticId), ScmModelProvider.ScmEvaluationTypeSuppressionJustification)]; protected override IReadOnlyList BuildBodyDependencyTypes() => - [new OptionalDefinition().Type, ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.Type]; + [ScmCodeModelGenerator.Instance.SystemOptionalDefinition.Type, ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.Type]; protected override ConstructorProvider[] BuildConstructors() { 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 19b3474420c..fbb1636082a 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 @@ -102,7 +102,8 @@ protected override IReadOnlyList BuildHelperDependencyTypes() if (type?.IsDictionary == true) { TryAddDependency(dependencies, dependencyNames, ScmCodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType); - if (parameter is InputHeaderParameter { CollectionHeaderPrefix: not null and not "" }) + if (parameter is InputHeaderParameter headerParameter && + !string.IsNullOrEmpty(headerParameter.CollectionHeaderPrefix)) { TryAddDependency(dependencies, dependencyNames, new PipelineRequestHeadersExtensionsDefinition().Type); } @@ -121,7 +122,7 @@ protected override IReadOnlyList BuildBodyDependencyTypes() { var dependencies = new List(); var dependencyNames = new HashSet(StringComparer.Ordinal); - TryAddDependency(dependencies, dependencyNames, new TypeFormattersDefinition().Type); + TryAddDependency(dependencies, dependencyNames, ScmCodeModelGenerator.Instance.TypeFormattersDefinition.Type); return dependencies; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs index ba4c5ffe6a1..046505d695c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs @@ -30,6 +30,14 @@ public class ScmCodeModelGenerator : CodeModelGenerator internal SerializationFormatDefinition SerializationFormatDefinition { get; } = new SerializationFormatDefinition(); + private SystemOptionalDefinition? _systemOptionalDefinition; + internal SystemOptionalDefinition SystemOptionalDefinition => + _systemOptionalDefinition ??= new SystemOptionalDefinition(); + + private TypeFormattersDefinition? _typeFormattersDefinition; + internal TypeFormattersDefinition TypeFormattersDefinition => + _typeFormattersDefinition ??= new TypeFormattersDefinition(); + /// /// Gets the options that control ConfigurationSchema.json generation. /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs index dadaad84144..8b601265f76 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs @@ -65,13 +65,12 @@ private static void BuildClient(InputClient inputClient, HashSet t protected override TypeProvider[] BuildTypeProviders() { var baseTypes = base.BuildTypeProviders(); - var systemOptionalProvider = new SystemOptionalDefinition(); for (var i = 0; i < baseTypes.Length; i++) { if (baseTypes[i] is OptionalDefinition) { - baseTypes[i] = systemOptionalProvider; + baseTypes[i] = ScmCodeModelGenerator.Instance.SystemOptionalDefinition; } } @@ -80,7 +79,7 @@ protected override TypeProvider[] BuildTypeProviders() ..BuildClientTypes(), ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition, ScmCodeModelGenerator.Instance.SerializationFormatDefinition, - new TypeFormattersDefinition(), + ScmCodeModelGenerator.Instance.TypeFormattersDefinition, new ErrorResultDefinition(), new ClientUriBuilderDefinition(), new Utf8JsonBinaryContentDefinition(), 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 21e31761cb0..e321fa59009 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 @@ -102,7 +102,7 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), // Attributes for providers excluded from reference-map attribute analysis can still depend // on the final reference-map result. Clear any attributes cached during pre-write setup so // the writer rebuilds them after PrepareForGeneration has finalized provider removals. - outputType.ClearCachedAttributes(); + outputType.ResetAttributes(); } } 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 fef9e4b4c95..c38267b18fc 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,9 +29,7 @@ protected internal ModelFactoryProvider(IEnumerable models) _models = models; } - internal bool PreserveLeadingMethodSeparator { get; private set; } - - internal void PreserveLeadingMethodSeparatorForBackCompat() => PreserveLeadingMethodSeparator = true; + internal bool PreserveLeadingMethodSeparator { get; set; } protected override string BuildName() => string.Concat(CodeModelGenerator.Instance.TypeFactory.ServiceName, ModelFactorySuffix); 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 2b6099e0bf0..253e4f08723 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 @@ -348,7 +348,7 @@ public IReadOnlyList Attributes internal IReadOnlyList GetAttributes() => _attributes ??= BuildAttributes(); - internal void ClearCachedAttributes() => _attributes = null; + internal void ResetAttributes() => _attributes = null; /// /// Indicates whether this provider's attributes should contribute to the reference map. Providers whose 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 index 976ed735cf5..369e6d073b7 100644 --- 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 @@ -127,7 +127,7 @@ private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) if (removedLeadingMethod && methodsToKeep.Count > 0) { - modelFactory.PreserveLeadingMethodSeparatorForBackCompat(); + modelFactory.PreserveLeadingMethodSeparator = true; } modelFactory.Update(methods: methodsToKeep); From 22dd2fed4836c7c94f360f8e46e588efe0e54c04 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 14 Jul 2026 03:28:59 +0000 Subject: [PATCH 3/8] refactor(csharp): use parameter factory for content type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../src/Providers/RestClientProvider.cs | 34 +++++-------- .../RestClientProviderTests.cs | 51 +++++++++++++++++++ 2 files changed, 63 insertions(+), 22 deletions(-) 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 fbb1636082a..fa68198ebec 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 @@ -1269,13 +1269,7 @@ internal static List GetMethodParameters( // when one was already published. UpdateParameterNameWithBackCompat(inputParam, inputParam.Name, client.BackCompatProvider, serviceMethod); - // Protocol and CreateRequest methods send Content-Type as a plain header value even - // when the input is modeled as a literal/constant, so avoid the general factory's - // constant-to-enum parameter shaping for this generated header. - ParameterProvider? parameter = IsContentTypeParameter(inputParam) && - methodType is ScmMethodKind.Protocol or ScmMethodKind.CreateRequest - ? CreateContentTypeParameter(inputParam) - : ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParam)?.ToPublicInputParameter(); + ParameterProvider? parameter = ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParam)?.ToPublicInputParameter(); if (parameter is null) { continue; @@ -1297,7 +1291,17 @@ methodType is ScmMethodKind.Protocol or ScmMethodKind.CreateRequest } else { - parameter.Type = parameter.Type.IsEnum ? parameter.Type.UnderlyingEnumType : parameter.Type; + if (IsContentTypeParameter(inputParam)) + { + parameter.Type = new CSharpType(typeof(string), isNullable: !inputParam.IsRequired); + parameter.Validation = inputParam.IsRequired + ? ParameterValidationType.AssertNotNullOrEmpty + : ParameterValidationType.None; + } + else + { + parameter.Type = parameter.Type.IsEnum ? parameter.Type.UnderlyingEnumType : parameter.Type; + } } } else if (methodType is ScmMethodKind.Convenience && @@ -1390,20 +1394,6 @@ private static bool HasNamedBodyParameter(InputServiceMethod serviceMethod) return false; } - private static ParameterProvider CreateContentTypeParameter(InputParameter inputParam) - { - var type = new CSharpType(typeof(string), isNullable: !inputParam.IsRequired); - return new ParameterProvider( - inputParam.Name, - DocHelpers.GetFormattableDescription(inputParam.Summary, inputParam.Doc) ?? FormattableStringHelpers.Empty, - type, - defaultValue: inputParam.IsRequired ? null : Default, - location: ParameterLocation.Header, - wireInfo: new WireInformation(SerializationFormat.Default, inputParam.SerializedName), - validation: inputParam.IsRequired ? ParameterValidationType.AssertNotNullOrEmpty : ParameterValidationType.None, - inputParameter: inputParam); - } - private static bool HasLiteralContentTypeHeader(InputOperation operation) { foreach (var p in operation.Parameters) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index e1b63d53935..97472c67514 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -288,6 +288,57 @@ public void TestGetMethodParameters_ContentTypeAfterBody(bool isRequired, bool i Assert.AreEqual("skillId", methodParameters[0].Name); Assert.AreEqual("content", methodParameters[1].Name); Assert.AreEqual("contentType", methodParameters[2].Name); + Assert.AreEqual(typeof(string), methodParameters[2].Type.FrameworkType); + Assert.AreEqual(!isRequired, methodParameters[2].Type.IsNullable); + Assert.AreEqual( + isRequired ? ParameterValidationType.AssertNotNullOrEmpty : ParameterValidationType.None, + methodParameters[2].Validation); + } + + [Test] + public void ProtocolContentTypeParameterUsesTypeFactory() + { + var contentTypeEnum = InputFactory.StringEnum( + "ContentTypeEnum", + [("application/json", "application/json")], + isExtensible: true); + var contentTypeParameter = InputFactory.MethodParameter( + "contentType", + InputFactory.Union([contentTypeEnum], "contentType"), + isRequired: true, + location: InputRequestLocation.Header, + serializedName: "Content-Type"); + var operation = InputFactory.Operation( + "Upload", + parameters: [contentTypeParameter]); + var serviceMethod = InputFactory.BasicServiceMethod( + "Upload", + operation, + parameters: [contentTypeParameter]); + var client = InputFactory.Client("TestClient", methods: [serviceMethod]); + var factoryCalled = false; + + MockHelpers.LoadMockGenerator( + clients: () => [client], + createParameterCore: parameter => + { + factoryCalled = true; + return new ParameterProvider(parameter); + }); + + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + Assert.IsNotNull(clientProvider); + factoryCalled = false; + + var methodParameters = RestClientProvider.GetMethodParameters( + serviceMethod, + ScmMethodKind.Protocol, + clientProvider!); + + Assert.IsTrue(factoryCalled); + Assert.AreEqual(1, methodParameters.Count); + Assert.AreEqual(typeof(string), methodParameters[0].Type.FrameworkType); + Assert.AreEqual(ParameterValidationType.AssertNotNullOrEmpty, methodParameters[0].Validation); } [Test] From 3f9a6d5f4eb633edbc50cd0f11473fcf3f176238 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 14 Jul 2026 04:08:06 +0000 Subject: [PATCH 4/8] fix(csharp): avoid reference map name collisions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../ProviderReferenceMapAnalyzer.Helpers.cs | 39 ++++++++-- ...viderReferenceMapAnalyzer.RootSelection.cs | 28 ++++---- .../ProviderReferenceMapAnalyzer.cs | 70 ++++++++++++------ .../ProviderReferenceMapResult.cs | 3 +- .../ProviderReferenceMapAnalyzerTests.cs | 71 +++++++++++++++++++ 5 files changed, 166 insertions(+), 45 deletions(-) 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 index dc30e88210a..f36dec85b04 100644 --- 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 @@ -24,20 +24,33 @@ private static bool IsKeptName(string providerName, HashSet roots, HashS private static bool IsKeptName(string providerName, string simpleName, HashSet roots, HashSet nodes) { - if (roots.Contains(providerName) && nodes.Contains(providerName)) + return MatchesGeneratedNode(providerName, simpleName, roots, nodes); + } + + private static bool MatchesGeneratedNode( + string providerName, + string simpleName, + HashSet candidateNames, + HashSet nodes, + bool ignoreGenericArity = true) + { + if (candidateNames.Contains(providerName) && nodes.Contains(providerName)) { return true; } - if (!roots.Contains(simpleName)) + var simpleNameLookup = ignoreGenericArity + ? _simpleNameLookupCache.GetValue(nodes, BuildSimpleNameLookup) + : _metadataSimpleNameLookupCache.GetValue(nodes, BuildMetadataSimpleNameLookup); + if (!simpleNameLookup.TryGetValue(simpleName, out var matches) || matches.Length != 1) { 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); + var matchingNode = matches[0]; + return (string.Equals(providerName, simpleName, StringComparison.Ordinal) || + string.Equals(providerName, matchingNode, StringComparison.Ordinal)) && + (candidateNames.Contains(matchingNode) || candidateNames.Contains(simpleName)); } private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => @@ -333,6 +346,14 @@ private static void AddMatchingNamesWithSimpleNameSuffix(HashSet target, } private static Dictionary BuildSimpleNameLookup(HashSet nodes) + => BuildSimpleNameLookup(nodes, ignoreGenericArity: true); + + private static Dictionary BuildMetadataSimpleNameLookup(HashSet nodes) + => BuildSimpleNameLookup(nodes, ignoreGenericArity: false); + + private static Dictionary BuildSimpleNameLookup( + HashSet nodes, + bool ignoreGenericArity) { var lookup = new Dictionary>(StringComparer.Ordinal); foreach (var node in nodes) @@ -342,7 +363,11 @@ private static Dictionary BuildSimpleNameLookup(HashSet roots, IReadOnlyList CodeModelGenerator.Instance.SourceInputModel.LastContract == null; - private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) + private static void RemoveMethodsFromModelFactory(HashSet namesToRemove, HashSet nodes) { if (namesToRemove.Count == 0) { return; } - var fullNamesToRemove = namesToRemove; - var simpleNamesToRemove = GetSimpleNames(namesToRemove); var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; _preWriteModelFactory = modelFactory; _preWriteModelFactoryMethods ??= [.. modelFactory.Methods]; @@ -113,7 +111,7 @@ private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) for (int i = 0; i < modelFactory.Methods.Count; i++) { var method = modelFactory.Methods[i]; - if (!ShouldRemoveModelFactoryMethod(method, fullNamesToRemove, simpleNamesToRemove)) + if (!ShouldRemoveModelFactoryMethod(method, namesToRemove, nodes)) { methodsToKeep.Add(method); continue; @@ -135,22 +133,24 @@ private static void RemoveMethodsFromModelFactory(HashSet namesToRemove) private static bool ShouldRemoveModelFactoryMethod( MethodProvider method, - HashSet fullNamesToRemove, - HashSet simpleNamesToRemove) + HashSet namesToRemove, + HashSet nodes) { - if (simpleNamesToRemove.Contains(method.Signature.Name)) - { - return true; - } - if (method.Signature.ReturnType == null) { - return false; + return MatchesGeneratedNode( + method.Signature.Name, + method.Signature.Name, + namesToRemove, + nodes); } var returnTypeName = GetProviderTypeName(method.Signature.ReturnType); - return fullNamesToRemove.Contains(returnTypeName) || - simpleNamesToRemove.Contains(StripGenericArity(GetSimpleName(returnTypeName))); + return MatchesGeneratedNode( + returnTypeName, + StripGenericArity(GetSimpleName(returnTypeName)), + namesToRemove, + nodes); } private static HashSet GetGeneratedDeclaredNodes(IReadOnlyList providers, HashSet nodes, bool publicOnly) 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 index edf62a5ffb3..61dcc8142c5 100644 --- 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 @@ -20,6 +20,7 @@ internal static partial class ProviderReferenceMapAnalyzer { private static ProviderReferenceMapResult? _latestResult; private static readonly ConditionalWeakTable, Dictionary> _simpleNameLookupCache = new(); + private static readonly ConditionalWeakTable, Dictionary> _metadataSimpleNameLookupCache = new(); private static TypeProvider? _preWriteModelFactory; private static MethodProvider[]? _preWriteModelFactoryMethods; @@ -59,45 +60,64 @@ provider is not SystemObjectModelProvider && private static bool TryGetProvider(CSharpType type, bool exact, out TypeProvider provider) { - var outputProvider = FindOutputProviderByName(CodeModelGenerator.Instance.OutputLibrary.TypeProviders, type, exact); + var outputProvider = FindOutputProviderByName( + CodeModelGenerator.Instance.OutputLibrary.TypeProviders, + GetProviderTypeName(type)); if (outputProvider != null) { provider = outputProvider; return true; } - if (!exact && - string.IsNullOrEmpty(type.Namespace) && - CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue(type.Name, out var mappedProvider)) + if (!exact && string.IsNullOrEmpty(type.Namespace)) { - provider = mappedProvider; - return true; + if (_latestResult != null) + { + var simpleNameLookup = _metadataSimpleNameLookupCache.GetValue(_latestResult.Nodes, BuildMetadataSimpleNameLookup); + var simpleName = GetSimpleName(GetProviderTypeName(type)); + if (!simpleNameLookup.TryGetValue(simpleName, out var matches) || + matches.Length != 1) + { + provider = null!; + return false; + } + + outputProvider = FindOutputProviderByName( + CodeModelGenerator.Instance.OutputLibrary.TypeProviders, + matches[0]); + if (outputProvider != null) + { + provider = outputProvider; + return true; + } + } + else if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue(type.Name, out var mappedProvider)) + { + provider = mappedProvider; + return true; + } } provider = null!; return false; } - private static TypeProvider? FindOutputProviderByName(IEnumerable providers, CSharpType type, bool exact) + private static TypeProvider? FindOutputProviderByName(IEnumerable providers, string name) { - var name = GetProviderTypeName(type); foreach (var provider in providers) { - if (string.Equals(GetProviderTypeName(provider.Type), name, StringComparison.Ordinal) || - !exact && - string.IsNullOrEmpty(type.Namespace) && - string.Equals(provider.Type.Name, type.Name, StringComparison.Ordinal)) + if (string.Equals(GetProviderTypeName(provider.Type), name, StringComparison.Ordinal)) { return provider; } - var nestedProvider = FindOutputProviderByName(provider.NestedTypes, type, exact); + var nestedProvider = FindOutputProviderByName(provider.NestedTypes, name); if (nestedProvider != null) { return nestedProvider; } - var serializationProvider = FindOutputProviderByName(provider.SerializationProviders, type, exact); + var serializationProvider = FindOutputProviderByName(provider.SerializationProviders, name); if (serializationProvider != null) { return serializationProvider; @@ -120,10 +140,13 @@ private static bool MatchesRemovedProvider(CSharpType type) return true; } - var simpleNameLookup = _simpleNameLookupCache.GetValue(_latestResult.RemoveCandidates, BuildSimpleNameLookup); return string.IsNullOrEmpty(type.Namespace) && - simpleNameLookup.TryGetValue(type.Name, out var matches) && - matches.Length == 1; + MatchesGeneratedNode( + providerName, + GetSimpleName(providerName), + _latestResult.RemoveCandidates, + _latestResult.Nodes, + ignoreGenericArity: false); } public static void ResetPreWriteAccessibility() @@ -141,7 +164,7 @@ public static void ApplyPreWriteAccessibility(IReadOnlyList provid // 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, publicCandidates) = GetPreWriteAccessibilityCandidates(providers); + var (internalizeCandidates, publicCandidates, nodes) = GetPreWriteAccessibilityCandidates(providers); foreach (var provider in GetGeneratedProviders(providers)) { var providerName = GetProviderTypeName(provider.Type); @@ -159,7 +182,7 @@ public static void ApplyPreWriteAccessibility(IReadOnlyList provid } } - RemoveMethodsFromModelFactory(GetSimpleNames(internalizeCandidates)); + RemoveMethodsFromModelFactory(internalizeCandidates, nodes); } public static void RestorePreWriteModelFactoryMethods() @@ -222,14 +245,15 @@ public static void Analyze(IReadOnlyList providers) _latestResult = new ProviderReferenceMapResult( internalizeCandidates, publicCandidates, - removeCandidates); + removeCandidates, + graph.Nodes); if (Configuration.UnreferencedTypesHandling == Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize) { - RemoveMethodsFromModelFactory(GetSimpleNames(removeCandidates)); + RemoveMethodsFromModelFactory(removeCandidates, graph.Nodes); } } - private static (HashSet InternalizeCandidates, HashSet PublicCandidates) GetPreWriteAccessibilityCandidates(IReadOnlyList providers) + private static (HashSet InternalizeCandidates, HashSet PublicCandidates, HashSet Nodes) GetPreWriteAccessibilityCandidates(IReadOnlyList providers) { var generatedProviders = GetGeneratedProviders(providers); var graph = BuildGraph(generatedProviders); @@ -251,7 +275,7 @@ private static (HashSet InternalizeCandidates, HashSet PublicCan generatedInternalDeclarations, generatedDiscriminatorBaseNames); - return (internalizeCandidates, publicCandidates); + return (internalizeCandidates, publicCandidates, graph.Nodes); } /// 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 index 8ba130f45df..30519d304e2 100644 --- 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 @@ -8,7 +8,8 @@ namespace Microsoft.TypeSpec.Generator internal sealed record ProviderReferenceMapResult( HashSet InternalizeCandidates, HashSet PublicCandidates, - HashSet RemoveCandidates) + HashSet RemoveCandidates, + HashSet Nodes) { } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs index b94a22b332a..a43ab7523de 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs @@ -185,6 +185,45 @@ public void QualifiedExternalBuildableTypeDoesNotMatchRemovedGeneratedTypeBySimp Assert.IsFalse(ProviderReferenceMapAnalyzer.IsResolvableBuildableType(CreateNamedType("Foo", string.Empty))); } + [Test] + public void AmbiguousUnqualifiedBuildableTypeDoesNotMatchRemovedProviderBySimpleName() + { + var keptFoo = new GeneratedModelTestTypeProvider("Foo", TypeSignatureModifiers.Public, ns: "Kept"); + var removedFoo = new GeneratedModelTestTypeProvider("Foo", TypeSignatureModifiers.Public, ns: "Removed"); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => new TestOutputLibrary(removedFoo, keptFoo), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + CodeModelGenerator.Instance.AddTypeToKeep(keptFoo.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([keptFoo, removedFoo]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(keptFoo)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(removedFoo)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.IsResolvableBuildableType(CreateNamedType("Foo", string.Empty))); + } + + [Test] + public void UnqualifiedGenericBuildableTypeUsesArityWhenMatchingRemovedProvider() + { + var genericArgument = CreateNamedType("T", string.Empty); + var keptFoo = new GeneratedModelTestTypeProvider("Foo", TypeSignatureModifiers.Public, ns: "Kept"); + var removedGenericFoo = new GenericTestTypeProvider( + "Foo`1", + TypeSignatureModifiers.Public, + "Removed", + genericArgument); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => new TestOutputLibrary(keptFoo, removedGenericFoo), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + CodeModelGenerator.Instance.AddTypeToKeep(keptFoo.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.Analyze([keptFoo, removedGenericFoo]); + + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(removedGenericFoo)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.IsResolvableBuildableType( + CreateNamedType("Foo", string.Empty, genericArgument))); + } + [Test] public void HelperRootBodyDependencyRootsGeneratedGenericDependency() { @@ -955,6 +994,38 @@ public void InternalizedGenericModelRemovesAritylessModelFactoryMethod() Assert.IsEmpty(modelFactory.Methods.Select(m => m.Signature.Name)); } + [Test] + public void RemovedModelDoesNotRemoveFactoryMethodForSameNamedModel() + { + var keptModel = new GeneratedModelTestTypeProvider("Widget", TypeSignatureModifiers.Public, ns: "Kept"); + var removedModel = new GeneratedModelTestTypeProvider("Widget", TypeSignatureModifiers.Public, ns: "Removed"); + var outputLibrary = new TestOutputLibrary(keptModel, removedModel); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => outputLibrary, + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + CodeModelGenerator.Instance.AddTypeToKeep(keptModel.Type.FullyQualifiedName); + var modelFactory = CodeModelGenerator.Instance.OutputLibrary.ModelFactory.Value; + modelFactory.Update(methods: + [ + new MethodProvider( + new MethodSignature( + "Widget", + $"", + MethodSignatureModifiers.Static | MethodSignatureModifiers.Public, + keptModel.Type, + $"", + []), + MethodBodyStatement.Empty, + modelFactory) + ]); + + ProviderReferenceMapAnalyzer.Analyze([keptModel, removedModel, modelFactory]); + + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(keptModel)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(removedModel)); + Assert.AreEqual(new[] { "Widget" }, modelFactory.Methods.Select(m => m.Signature.Name)); + } + [Test] public void GeneratedTypePreservesLastContractTypeAttributes() { From ea694fccbd9fa815b20ceb691714cb4b76a7c09c Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 14 Jul 2026 04:34:30 +0000 Subject: [PATCH 5/8] refactor(csharp): remove unused name helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../ProviderReferenceMapAnalyzer.CustomCodeRoots.cs | 11 ----------- 1 file changed, 11 deletions(-) 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 index 41c6d6f181e..85b47a35b65 100644 --- 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 @@ -367,16 +367,5 @@ 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(StripGenericArity(GetSimpleName(name))); - } - - return simpleNames; - } } } From 381e182dcb44ece652e298cd5ee39561adc3fa0b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 15 Jul 2026 01:25:48 +0000 Subject: [PATCH 6/8] refactor(csharp): remove last contract reference map fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../src/Primitives/TypeProviderWriter.cs | 43 +----------- .../src/Providers/CanonicalTypeProvider.cs | 16 +---- ...ProviderReferenceMapAnalyzer.Candidates.cs | 1 - ...derReferenceMapAnalyzer.CustomCodeRoots.cs | 22 +----- .../ProviderReferenceMapAnalyzer.Helpers.cs | 4 +- ...viderReferenceMapAnalyzer.RootSelection.cs | 35 ---------- .../ProviderReferenceMapAnalyzer.cs | 13 +--- .../src/SourceInput/SourceInputModel.cs | 5 -- .../ProviderReferenceMapAnalyzerTests.cs | 67 +++++++------------ .../LastContract.cs | 0 10 files changed, 32 insertions(+), 174 deletions(-) rename packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/{ModelFactorySignatureRootsDoNotDependOnModelName => LastContractModelFactorySignaturesDoNotRootGeneratedModels}/LastContract.cs (100%) 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 bf7bb056b58..4d349707c84 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,13 +1,8 @@ // 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.Expressions; using Microsoft.TypeSpec.Generator.Providers; -using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Statements; namespace Microsoft.TypeSpec.Generator.Primitives @@ -54,7 +49,7 @@ private void WriteType(CodeWriter writer) { writer.WriteXmlDocsNoScope(_provider.XmlDocs); } - foreach (var attribute in GetTypeAttributes()) + foreach (var attribute in _provider.GetAttributes()) { attribute.Write(writer); if (attribute is AttributeStatement) @@ -102,42 +97,6 @@ private void WriteType(CodeWriter writer) } } - private IReadOnlyList GetTypeAttributes() - { - var attributes = _provider.GetAttributes(); - if (_provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - return attributes; - } - - var seen = attributes.Select(static attribute => attribute.ToDisplayString()).ToHashSet(); - var lastContractAttributes = _provider.LastContractView?.Attributes - .Where(attribute => - ShouldPreserveLastContractAttribute(attribute) && - IsResolvableLastContractAttribute(attribute) && - seen.Add(attribute.ToDisplayString())) ?? []; - return [.. attributes, .. lastContractAttributes]; - } - - private static bool ShouldPreserveLastContractAttribute(AttributeStatement attribute) - { - var attributeName = attribute.Data?.AttributeClass?.Name; - return attributeName is not - (CodeGenAttributes.CodeGenSuppressAttributeName or - CodeGenAttributes.CodeGenMemberAttributeName or - CodeGenAttributes.CodeGenTypeAttributeName or - CodeGenAttributes.CodeGenSerializationAttributeName); - } - - private static bool IsResolvableLastContractAttribute(AttributeStatement attribute) => - ProviderReferenceMapAnalyzer.IsResolvableBuildableType(attribute.Type) && - attribute.Arguments.All(IsResolvableLastContractAttributeArgument) && - attribute.PositionalArguments.All(static argument => IsResolvableLastContractAttributeArgument(argument.Value)); - - private static bool IsResolvableLastContractAttributeArgument(ValueExpression argument) => - argument is not TypeOfExpression typeOf || - ProviderReferenceMapAnalyzer.IsResolvableBuildableType(typeOf.Type); - private void WriteClassOrStructContent(CodeWriter writer) { using (writer.Scope()) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs index 77835278816..90e71ace797 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs @@ -48,13 +48,9 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu protected override IReadOnlyList BuildAttributes() { - var lastContractAttributes = _generatedTypeProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) - ? null - : _generatedTypeProvider.LastContractView?.Attributes.Where(ShouldPreserveLastContractAttribute); - // TODO https://github.com/microsoft/typespec/issues/11232: Move this generated/last-contract/custom attribute merge into a shared TypeProvider back-compat API. + // TODO https://github.com/microsoft/typespec/issues/11232: Move this generated/custom attribute merge into a shared TypeProvider API. return DeduplicateAttributes( _generatedTypeProvider.Attributes, - lastContractAttributes, _generatedTypeProvider.CustomCodeView?.Attributes); } @@ -73,16 +69,6 @@ private static IReadOnlyList DeduplicateAttributes(params IE return attributes; } - private static bool ShouldPreserveLastContractAttribute(AttributeStatement attribute) - { - var attributeName = attribute.Data?.AttributeClass?.Name; - return attributeName is not - (CodeGenAttributes.CodeGenSuppressAttributeName or - CodeGenAttributes.CodeGenMemberAttributeName or - CodeGenAttributes.CodeGenTypeAttributeName or - CodeGenAttributes.CodeGenSerializationAttributeName); - } - private protected override CanonicalTypeProvider BuildCanonicalView() => this; protected internal override ConstructorProvider[] BuildConstructors() 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 index 603a185e347..34e5b4c3a96 100644 --- 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 @@ -303,7 +303,6 @@ private static HashSet GetRemovalCandidates( includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, - includeModelFactorySignatureRoots: true, publicClientRootsOnly: false); removeRoots.UnionWith(customRemovalRoots); 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 index 85b47a35b65..ffa6c72d5f4 100644 --- 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 @@ -310,30 +310,14 @@ private static HashSet GetGeneratedPersistableModelProxyTypeNames(IReadO return proxyTypes; } - private static HashSet GetGeneratedInternalTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) - => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Internal); - - private static HashSet GetGeneratedPublicTypeDeclarations(IReadOnlyList providers, HashSet generatedTypeNames) - => GetGeneratedTypeDeclarationsByLastContractAccessibility(providers, generatedTypeNames, TypeSignatureModifiers.Public); - - private static HashSet GetGeneratedPublicTypeDeclarationsFromLastContract(IReadOnlyList providers, HashSet generatedTypeNames) - => GetGeneratedPublicTypeDeclarations(providers, generatedTypeNames); - - private static HashSet GetGeneratedTypeDeclarationsByLastContractAccessibility( + private static HashSet GetGeneratedInternalTypeDeclarations( IReadOnlyList providers, - HashSet generatedTypeNames, - TypeSignatureModifiers accessibility) + HashSet generatedTypeNames) { var declarations = new HashSet(StringComparer.Ordinal); foreach (var provider in GetGeneratedProviders(providers)) { - var lastContractView = CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContractIncludingInternal( - provider.Type.Namespace, - GetSimpleName(GetProviderTypeName(provider.Type)), - provider.DeclaringTypeProvider is { } declaringTypeProvider - ? GetSimpleName(GetProviderTypeName(declaringTypeProvider.Type)) - : null); - if (lastContractView?.DeclarationModifiers.HasFlag(accessibility) != true) + if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) { continue; } 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 index f36dec85b04..dadf50a7fe6 100644 --- 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 @@ -55,9 +55,7 @@ private static bool MatchesGeneratedNode( private static bool IsClientProviderRoot(TypeProvider provider, bool publicOnly) => IsClientProvider(provider) && - (!publicOnly || - CodeModelGenerator.Instance.SourceInputModel.LastContract == null && - provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + (!publicOnly || provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); private static bool IsClientProvider(TypeProvider provider) { 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 index 2e016a8e5ac..1d16b60028b 100644 --- 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 @@ -18,7 +18,6 @@ private static HashSet GetRootNames( bool includeModelFactory, bool includeAdditionalRoots, bool includeUnionVariantRoots, - bool includeModelFactorySignatureRoots, bool publicClientRootsOnly) { var generator = CodeModelGenerator.Instance; @@ -37,11 +36,6 @@ private static HashSet GetRootNames( } } - if (includeModelFactorySignatureRoots) - { - AddLastContractModelFactorySignatureRoots(providers, roots, nodes); - } - if (!includeUnionVariantRoots) { return roots; @@ -52,32 +46,6 @@ private static HashSet GetRootNames( 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) || - method.Signature.ReturnType == null) - { - 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; @@ -93,9 +61,6 @@ private static void AddUnionVariantRoots(HashSet roots, IReadOnlyList - CodeModelGenerator.Instance.SourceInputModel.LastContract == null; - private static void RemoveMethodsFromModelFactory(HashSet namesToRemove, HashSet nodes) { if (namesToRemove.Count == 0) 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 index 61dcc8142c5..2deea26ab20 100644 --- 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 @@ -207,11 +207,8 @@ public static void Analyze(IReadOnlyList providers) var publicGraph = BuildGraph(generatedProviders, publicOnly: true); var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); - var generatedPublicDeclarations = GetGeneratedPublicTypeDeclarationsFromLastContract(generatedProviders, graph.Nodes); - customPublicRoots.UnionWith(generatedPublicDeclarations); var customCodeRemovalRoots = GetCustomCodeGeneratedTypeRoots(generatedProviders, graph.Nodes); var customRemovalRoots = new HashSet(customCodeRemovalRoots, StringComparer.Ordinal); - customRemovalRoots.UnionWith(generatedPublicDeclarations); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); @@ -259,8 +256,6 @@ private static (HashSet InternalizeCandidates, HashSet PublicCan var graph = BuildGraph(generatedProviders); var publicGraph = BuildGraph(generatedProviders, publicOnly: true); var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); - 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); @@ -301,13 +296,8 @@ private static (HashSet InternalizeCandidates, HashSet PublicCan helperRoots: [], includeModelFactory: false, includeAdditionalRoots: true, - includeUnionVariantRoots: false, - includeModelFactorySignatureRoots: true, + includeUnionVariantRoots: true, publicClientRootsOnly: true); - if (ShouldUseUnionVariantFallbackRoots()) - { - AddUnionVariantRoots(internalizeRoots, providers, graph.Nodes); - } var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); @@ -369,7 +359,6 @@ private static (HashSet InternalizeCandidates, HashSet PublicCan includeModelFactory: true, includeAdditionalRoots: true, includeUnionVariantRoots: true, - includeModelFactorySignatureRoots: false, publicClientRootsOnly: true); var publicCandidates = GetPublicCandidates( publicDeclaredNodes, 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 eba9f3036d7..99127506a70 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 @@ -72,11 +72,6 @@ private IReadOnlyDictionary PopulateNameMap() return FindTypeInCustomization(LastContract, ns, name, true, declaringTypeName, includeInternal: false); } - internal TypeProvider? FindForTypeInLastContractIncludingInternal(string ns, string name, string? declaringTypeName = null) - { - return FindTypeInCustomization(LastContract, ns, name, true, declaringTypeName); - } - private IReadOnlyList PopulateCustomizationTypeProviders() { var providers = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs index a43ab7523de..239b7f0598c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs @@ -585,7 +585,7 @@ public void PublicProviderMethodWithNamespaceLessSignatureKeepsNonRootTypePublic } [Test] - public async Task LastContractGenericDeclarationKeepsOnlyMatchingGeneratedGenericTypePublic() + public async Task LastContractGenericDeclarationDoesNotRootGeneratedType() { var lastContractCompilation = CSharpCompilation.Create( "LastContract", @@ -609,14 +609,14 @@ await MockHelpers.LoadMockGeneratorAsync( ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([genericPageResult, nonGenericPageResult]); - Assert.IsTrue(genericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); - Assert.IsFalse(genericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(genericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(genericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); Assert.IsTrue(nonGenericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); Assert.IsFalse(nonGenericPageResult.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); } [Test] - public async Task LastContractNestedPublicDeclarationKeepsGeneratedNestedTypePublic() + public async Task LastContractNestedPublicDeclarationDoesNotRootGeneratedType() { var lastContractCompilation = CSharpCompilation.Create( "LastContract", @@ -643,10 +643,10 @@ await MockHelpers.LoadMockGeneratorAsync( ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([outerModel]); - Assert.IsTrue(outerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); - Assert.IsFalse(outerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); - Assert.IsTrue(innerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); - Assert.IsFalse(innerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(outerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(outerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(innerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(innerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); } [Test] @@ -690,7 +690,7 @@ public void PublicConstructorOnInternalClientDoesNotPublicizeGeneratedOptions() } [Test] - public async Task GeneratedTypeInternalInLastContractIsInternalized() + public async Task LastContractInternalDeclarationsDoNotChangeProviderAccessibility() { var lastContractCompilation = CSharpCompilation.Create( "LastContract", @@ -723,16 +723,16 @@ await MockHelpers.LoadMockGeneratorAsync( ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, clientOptions]); - Assert.IsTrue(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); - Assert.IsFalse(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); - Assert.IsTrue(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); - Assert.IsFalse(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(clientOptions.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); Assert.IsTrue(clientOptions.NestedTypes[0].DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); Assert.IsFalse(clientOptions.NestedTypes[0].DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); } [Test] - public async Task PublicCustomPartialKeepsGeneratedTypePublicWhenLastContractGeneratedDeclarationIsInternal() + public async Task LastContractInternalDeclarationDoesNotOverridePublicCustomPartial() { var lastContractCompilation = CSharpCompilation.Create( "LastContract", @@ -859,7 +859,7 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public async Task ModelFactorySignatureRootsDoNotDependOnModelName() + public async Task LastContractModelFactorySignaturesDoNotRootGeneratedModels() { MockHelpers.LoadMockGenerator(); var lastContract = await Helpers.GetCompilationFromDirectoryAsync(); @@ -874,8 +874,8 @@ await MockHelpers.LoadMockGeneratorAsync( ProviderReferenceMapAnalyzer.Analyze([pagedModel, requestModel, modelFactory]); - Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(pagedModel)); - Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(requestModel)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(pagedModel)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(requestModel)); } [Test] @@ -909,37 +909,20 @@ public void PublicCustomCodeArraySignatureKeepsGeneratedTypePublic() } [Test] - public async Task InternalizedGeneratedPredecessorDoesNotPublicizeItsInternalDependency() + public void InternalGeneratedPredecessorDoesNotPublicizeItsInternalDependency() { - var lastContractCompilation = CSharpCompilation.Create( - "LastContract", - [CSharpSyntaxTree.ParseText(""" - namespace Sample.Models - { - internal partial class InternalModel - { - } - - internal partial class InternalDependency - { - } - } - """)], - [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); - - var dependency = new GeneratedModelTestTypeProvider("InternalDependency", TypeSignatureModifiers.Public, ns: "Sample.Models"); + var dependency = new GeneratedModelTestTypeProvider("InternalDependency", TypeSignatureModifiers.Internal, ns: "Sample.Models"); var internalModel = new GeneratedModelTestTypeProvider( "InternalModel", - TypeSignatureModifiers.Public, + TypeSignatureModifiers.Internal, ns: "Sample.Models"); var publicRoot = new GeneratedModelTestTypeProvider( "PublicRoot", TypeSignatureModifiers.Public, ns: "Sample.Models"); - await MockHelpers.LoadMockGeneratorAsync( + MockHelpers.LoadMockGenerator( createOutputLibrary: () => new TestOutputLibrary(publicRoot, internalModel, dependency), - configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}", - lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); CodeModelGenerator.Instance.AddTypeToKeep(publicRoot.Type.FullyQualifiedName); CodeModelGenerator.Instance.AddTypeToKeep(internalModel.Type.FullyQualifiedName); CodeModelGenerator.Instance.AddTypeToKeep(dependency.Type.FullyQualifiedName); @@ -951,7 +934,7 @@ await MockHelpers.LoadMockGeneratorAsync( new PropertyProvider($"", MethodSignatureModifiers.Public, dependency.Type, "Dependency", new AutoPropertyBody(false), internalModel) ]); - ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([internalModel, dependency]); + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([publicRoot, internalModel, dependency]); Assert.IsTrue(internalModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); Assert.IsFalse(internalModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); @@ -1027,7 +1010,7 @@ public void RemovedModelDoesNotRemoveFactoryMethodForSameNamedModel() } [Test] - public void GeneratedTypePreservesLastContractTypeAttributes() + public void GeneratedTypeDoesNotPreserveLastContractTypeAttributes() { var lastContractView = new AttributedTestTypeProvider( "GeneratedModel", @@ -1048,7 +1031,7 @@ public void GeneratedTypePreservesLastContractTypeAttributes() Assert.IsTrue(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); var codeFile = new TypeProviderWriter(generatedModel).Write(); - StringAssert.Contains("[global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute(\"TEST001\")]", codeFile.Content); + StringAssert.DoesNotContain("[global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute(\"TEST001\")]", codeFile.Content); StringAssert.Contains("internal partial class GeneratedModel", codeFile.Content); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/ModelFactorySignatureRootsDoNotDependOnModelName/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/LastContractModelFactorySignaturesDoNotRootGeneratedModels/LastContract.cs similarity index 100% rename from packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/ModelFactorySignatureRootsDoNotDependOnModelName/LastContract.cs rename to packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/LastContractModelFactorySignaturesDoNotRootGeneratedModels/LastContract.cs From 0e5d4c421843f65e52ee1d8b91a876a04010c959 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 15 Jul 2026 02:19:09 +0000 Subject: [PATCH 7/8] fix(csharp): preserve visitor-updated type attributes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../src/Providers/ClientProvider.cs | 2 +- .../ModelReaderWriterContextDefinition.cs | 18 + .../src/Providers/RestClientProvider.cs | 5 +- .../Providers/ScmMethodProviderCollection.cs | 3 +- .../src/ScmCodeModelGenerator.cs | 9 + .../src/ScmOutputLibrary.cs | 4 +- ...ModelReaderWriterContextDefinitionTests.cs | 36 ++ .../src/CSharpGen.cs | 11 - .../src/CodeModelGenerator.cs | 20 + .../src/Primitives/TypeProviderWriter.cs | 2 +- .../src/Providers/NamedTypeSymbolProvider.cs | 10 +- .../src/Providers/TypeProvider.cs | 11 +- ...iderReferenceMapAnalyzer.BodyReferences.cs | 178 +++++++ ...ProviderReferenceMapAnalyzer.Candidates.cs | 181 +++++++ ...derReferenceMapAnalyzer.CustomCodeRoots.cs | 355 -------------- ...oviderReferenceMapAnalyzer.GraphBuilder.cs | 183 ------- .../ProviderReferenceMapAnalyzer.Helpers.cs | 453 ++++++++++++++++++ ...ReferenceMapAnalyzer.ReferenceTraversal.cs | 194 -------- ...viderReferenceMapAnalyzer.RootSelection.cs | 149 ------ ...renceMapAnalyzer.TypeReferenceCollector.cs | 193 -------- .../ProviderReferenceMapAnalyzer.cs | 170 +++++++ .../src/SourceInput/SourceInputModel.cs | 2 +- .../ProviderReferenceMapAnalyzerTests.cs | 4 +- 23 files changed, 1092 insertions(+), 1101 deletions(-) delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs 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 46b67fe6146..69bd5648f0f 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 @@ -432,7 +432,7 @@ protected override IReadOnlyList BuildHelperDependencyTypes() { if (method.BodyStatements != null) { - return [new ClientPipelineExtensionsDefinition().Type]; + return [ScmCodeModelGenerator.Instance.ClientPipelineExtensionsDefinition.Type]; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index fe07fe05573..c1b52a4c443 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -35,6 +35,12 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override bool ShouldAnalyzeAttributesInReferenceMap => false; + protected override IReadOnlyList BuildAttributesForWrite() + { + var visitorAttributes = base.BuildAttributesForWrite().Where(static attribute => !IsBuildableAttribute(attribute)); + return [.. BuildAttributes(), .. visitorAttributes]; + } + protected override IReadOnlyList BuildAttributes() { var attributes = new Dictionary(); @@ -90,6 +96,18 @@ protected override IReadOnlyList BuildAttributes() return attributes.OrderBy(a => GetSimpleTypeName(a.Key)).Select(kvp => kvp.Value).ToList(); } + private static bool IsBuildableAttribute(MethodBodyStatement statement) + { + var attribute = statement switch + { + AttributeStatement directAttribute => directAttribute, + SuppressionStatement suppression => suppression.AsStatement(), + _ => null + }; + + return attribute?.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)) == true; + } + private HashSet GetCustomizedBuildableTypes() { var customizedTypes = new HashSet(StringComparer.Ordinal); 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 fa68198ebec..cc2087b82e3 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 @@ -28,6 +28,7 @@ public class RestClientProvider : TypeProvider private const string TopParameterName = "top"; private const string MaxCountParameterName = "maxCount"; private const string MaxPageSizeParameterName = "maxPageSize"; + private const string ContentParameterName = "content"; private static readonly Dictionary _knownSpecialHeaderParams = new(StringComparer.OrdinalIgnoreCase) { @@ -1385,7 +1386,7 @@ private static bool HasNamedBodyParameter(InputServiceMethod serviceMethod) foreach (var parameter in serviceMethod.Parameters) { if (parameter.Location == InputRequestLocation.Body && - !string.Equals(parameter.Name, "content", StringComparison.OrdinalIgnoreCase)) + !string.Equals(parameter.Name, ContentParameterName, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -1498,7 +1499,7 @@ private static bool IsLastContractBodyParameter(ParameterProvider parameter, Has return true; } - if (string.Equals(parameter.Name, "content", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(parameter.Name, ContentParameterName, StringComparison.OrdinalIgnoreCase)) { return true; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs index b18c378a1bd..ebf9a548fb7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs @@ -29,7 +29,6 @@ namespace Microsoft.TypeSpec.Generator.ClientModel.Providers public class ScmMethodProviderCollection : IReadOnlyList { private readonly MethodProvider _createRequestMethod; - private static readonly ClientPipelineExtensionsDefinition _clientPipelineExtensionsDefinition = new(); private static readonly CancellationTokenExtensionsDefinition _cancellationTokenExtensionsDefinition = new(); private const string JsonMediaType = "application/json"; private const string XmlMediaType = "application/xml"; @@ -1152,7 +1151,7 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod This.Invoke(createRequestMethod.Signature, BuildCreateRequestArguments(createRequestMethod.Signature, bodyParameters)), out var message), Return(ScmCodeModelGenerator.Instance.TypeFactory.ClientResponseApi.ToExpression().FromResponse(client - .PipelineProperty.Invoke(processMessageName, [message, requestOptionsParameter], isAsync, true, extensionType: _clientPipelineExtensionsDefinition.Type))) + .PipelineProperty.Invoke(processMessageName, [message, requestOptionsParameter], isAsync, true, extensionType: ScmCodeModelGenerator.Instance.ClientPipelineExtensionsDefinition.Type))) ]; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs index 046505d695c..98bc7385b1c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs @@ -38,6 +38,14 @@ public class ScmCodeModelGenerator : CodeModelGenerator internal TypeFormattersDefinition TypeFormattersDefinition => _typeFormattersDefinition ??= new TypeFormattersDefinition(); + private ClientPipelineExtensionsDefinition? _clientPipelineExtensionsDefinition; + internal ClientPipelineExtensionsDefinition ClientPipelineExtensionsDefinition => + _clientPipelineExtensionsDefinition ??= new ClientPipelineExtensionsDefinition(); + + private PipelineRequestHeadersExtensionsDefinition? _pipelineRequestHeadersExtensionsDefinition; + internal PipelineRequestHeadersExtensionsDefinition PipelineRequestHeadersExtensionsDefinition => + _pipelineRequestHeadersExtensionsDefinition ??= new PipelineRequestHeadersExtensionsDefinition(); + /// /// Gets the options that control ConfigurationSchema.json generation. /// @@ -57,6 +65,7 @@ protected override void Configure() AddMetadataReference(MetadataReference.CreateFromFile(typeof(ClientResult).Assembly.Location)); AddMetadataReference(MetadataReference.CreateFromFile(typeof(BinaryData).Assembly.Location)); AddMetadataReference(MetadataReference.CreateFromFile(typeof(JsonSerializer).Assembly.Location)); + AddCustomCodeMethodDependency("SetDelimited", PipelineRequestHeadersExtensionsDefinition); AddTypeToKeep(ModelReaderWriterContextDefinition.s_name, isRoot: false); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs index 8b601265f76..ed527e4c116 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs @@ -84,9 +84,9 @@ protected override TypeProvider[] BuildTypeProviders() new ClientUriBuilderDefinition(), new Utf8JsonBinaryContentDefinition(), new BinaryContentHelperDefinition(), - new ClientPipelineExtensionsDefinition(), + ScmCodeModelGenerator.Instance.ClientPipelineExtensionsDefinition, new CancellationTokenExtensionsDefinition(), - new PipelineRequestHeadersExtensionsDefinition(), + ScmCodeModelGenerator.Instance.PipelineRequestHeadersExtensionsDefinition, .. GetMultipartFormDataTypes(), new ModelReaderWriterContextDefinition() ]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 66d06965efa..d55537cc0d0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -147,6 +147,29 @@ public void RemovedProvidersDoNotContributeBuildableAttributes() } } + [Test] + public async Task VisitorAttributesArePreservedAfterReferenceMapAnalysis() + { + var outputPath = Path.Combine( + TestContext.CurrentContext.WorkDirectory, + nameof(ModelReaderWriterContextDefinitionTests), + nameof(VisitorAttributesArePreservedAfterReferenceMapAnalysis)); + var outputLibrary = new TestOutputLibrary([new RemovedProviderWithFrameworkDependency()]); + var mockGenerator = MockHelpers.LoadMockGenerator( + createOutputLibrary: () => outputLibrary, + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}", + outputPath: outputPath); + mockGenerator.Object.AddVisitor(new ContextAttributeVisitor()); + + await new CSharpGen().ExecuteAsync(); + + var context = outputLibrary.TypeProviders.OfType().Single(); + Assert.IsTrue(context.Attributes.Any(attribute => attribute.Type.Equals(typeof(ObsoleteAttribute)))); + var content = await File.ReadAllTextAsync(Path.Combine(outputPath, context.RelativeFilePath)); + StringAssert.Contains("[Obsolete]", content); + StringAssert.DoesNotContain(nameof(RemovedProviderWithFrameworkDependency), content); + } + [Test] public void RemovedFrameworkTypeIsNotMatchedToKeptProviderWithSameSimpleName() { @@ -1476,6 +1499,19 @@ .. _typeProviders } } + private sealed class ContextAttributeVisitor : LibraryVisitor + { + protected override TypeProvider? VisitType(TypeProvider type) + { + if (type is ModelReaderWriterContextDefinition) + { + type.Update(attributes: [.. type.Attributes, new AttributeStatement(typeof(ObsoleteAttribute))]); + } + + return type; + } + } + // Test class for a framework type marked with [Obsolete] [Obsolete("This type is obsolete. Use NewFrameworkType instead.")] public class ObsoleteFrameworkType : IJsonModel, IPersistableModel 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 e321fa59009..1dc1058c43a 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 @@ -95,17 +95,6 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), try { using var referenceMapSession = ProviderReferenceMapAnalyzer.PrepareForGeneration(output.TypeProviders); - foreach (var outputType in output.TypeProviders) - { - if (!outputType.ShouldAnalyzeAttributesInReferenceMap) - { - // Attributes for providers excluded from reference-map attribute analysis can still depend - // on the final reference-map result. Clear any attributes cached during pre-write setup so - // the writer rebuilds them after PrepareForGeneration has finalized provider removals. - outputType.ResetAttributes(); - } - } - foreach (var outputType in output.TypeProviders) { if (!referenceMapSession.ShouldWriteProvider(outputType)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs index 02e25540478..413cf395a7f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CodeModelGenerator.cs @@ -26,6 +26,7 @@ public abstract class CodeModelGenerator { private List _visitors = []; private List _additionalMetadataReferences = []; + private readonly Dictionary> _customCodeMethodDependencies = new(StringComparer.Ordinal); private static CodeModelGenerator? _instance; private List _sharedSourceDirectories = []; public const string GeneratorMetadataName = "GeneratorName"; @@ -161,6 +162,25 @@ public virtual void AddMetadataReference(MetadataReference reference) _additionalMetadataReferences.Add(reference); } + /// + /// Registers a generated provider required by an unresolved custom-code method invocation. + /// + /// The invoked method name. + /// The generated provider that defines the method. + protected void AddCustomCodeMethodDependency(string methodName, TypeProvider provider) + { + if (!_customCodeMethodDependencies.TryGetValue(methodName, out var providers)) + { + providers = []; + _customCodeMethodDependencies.Add(methodName, providers); + } + + providers.Add(provider); + } + + internal IReadOnlyList GetCustomCodeMethodDependencies(string methodName) => + _customCodeMethodDependencies.TryGetValue(methodName, out var providers) ? providers : []; + public virtual void AddSharedSourceDirectory(string sharedSourceDirectory) { _sharedSourceDirectories.Add(sharedSourceDirectory); 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 4d349707c84..4797be0d90b 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 @@ -49,7 +49,7 @@ private void WriteType(CodeWriter writer) { writer.WriteXmlDocsNoScope(_provider.XmlDocs); } - foreach (var attribute in _provider.GetAttributes()) + foreach (var attribute in _provider.GetAttributesForWrite()) { attribute.Write(writer); if (attribute is AttributeStatement) 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 e236d6b67a2..39c6ef9d896 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 @@ -396,9 +396,15 @@ private void AddBodyDependencyTypes(SyntaxNode syntax, HashSet depen foreach (var invocation in syntax.DescendantNodes().OfType()) { - if (GetInvocationName(invocation) == "SetDelimited") + var invocationName = GetInvocationName(invocation); + if (invocationName == null) { - dependencies.Add(CreateUnresolvedDependencyType("SetDelimited")); + continue; + } + + foreach (var provider in CodeModelGenerator.Instance.GetCustomCodeMethodDependencies(invocationName)) + { + dependencies.Add(provider.Type); } } } 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 253e4f08723..f2f8982d4b7 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 @@ -348,11 +348,16 @@ public IReadOnlyList Attributes internal IReadOnlyList GetAttributes() => _attributes ??= BuildAttributes(); - internal void ResetAttributes() => _attributes = null; + internal IReadOnlyList GetAttributesForWrite() => BuildAttributesForWrite(); /// - /// Indicates whether this provider's attributes should contribute to the reference map. Providers whose - /// attributes depend on the final reference-map result can opt out so cached attributes are rebuilt after analysis. + /// Builds the attributes emitted by the writer. Providers whose generated attributes depend on final + /// generation decisions can override this without replacing attributes updated by visitors. + /// + protected internal virtual IReadOnlyList BuildAttributesForWrite() => GetAttributes(); + + /// + /// Indicates whether this provider's attributes should contribute to reference-map analysis. /// protected internal virtual bool ShouldAnalyzeAttributesInReferenceMap => true; 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 index 7b8174ff204..99c2960efe1 100644 --- 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 @@ -402,5 +402,183 @@ provider is not EnumProvider && provider.DeclaringTypeProvider == null && provider.SerializationProviders.Count == 0; } + + private static void AddSignatureReferences( + HashSet references, + MethodSignatureBase signature, + HashSet nodes, + IReadOnlyDictionary? serializationProviderNamesByType, + bool includeAttributes = true, + bool includeAttributeArguments = true, + string? providerNamespace = null) + { + AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType, providerNamespace); + if (includeAttributes) + { + AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); + } + + foreach (var parameter in signature.Parameters) + { + AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType, providerNamespace); + if (includeAttributes) + { + AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); + } + } + + if (signature is MethodSignature methodSignature) + { + AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType, providerNamespace); + if (methodSignature.GenericArguments != null) + { + foreach (var genericArgument in methodSignature.GenericArguments) + { + AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType, providerNamespace); + } + } + + if (methodSignature.GenericParameterConstraints != null) + { + foreach (var constraint in methodSignature.GenericParameterConstraints) + { + AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType, providerNamespace); + } + } + } + + if (signature is ConstructorSignature constructorSignature) + { + AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType, providerNamespace); + } + } + + 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, + string? providerNamespace = null) + { + if (type == null) + { + return; + } + + if (type.IsArray) + { + AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType, providerNamespace); + 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); + } + } + else if (!nodes.Contains(providerTypeName) && + string.IsNullOrEmpty(type.Namespace) && + !string.IsNullOrEmpty(providerNamespace)) + { + AddExactMetadataNameMatch(references, $"{providerNamespace}.{providerTypeName}", nodes); + } + + AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType, providerNamespace); + AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType, providerNamespace); + foreach (var argument in type.Arguments) + { + AddTypeReference(references, argument, nodes, serializationProviderNamesByType, providerNamespace); + } + } + + 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('`'); + var typeArgumentStart = name.IndexOf('<'); + var genericSuffixStart = tick < 0 + ? typeArgumentStart + : typeArgumentStart < 0 + ? tick + : Math.Min(tick, typeArgumentStart); + return genericSuffixStart < 0 ? name : name.Substring(0, genericSuffixStart); + } } } 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 index 34e5b4c3a96..b5f9ef6fb22 100644 --- 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 @@ -391,5 +391,186 @@ private static bool HasCandidateReference( return false; } + + 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)) + { + continue; + } + + if (!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(); + var visited = new HashSet(); + foreach (var provider in providers) + { + AddGeneratedProvider(generatedProviders, visited, provider); + } + + return generatedProviders; + } + + private static void AddGeneratedProvider(List generatedProviders, HashSet visited, TypeProvider provider) + { + if (!visited.Add(provider)) + { + return; + } + + generatedProviders.Add(provider); + foreach (var nestedType in provider.NestedTypes) + { + AddGeneratedProvider(generatedProviders, visited, nestedType); + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddGeneratedProvider(generatedProviders, visited, serializationProvider); + } + } } } 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 deleted file mode 100644 index ffa6c72d5f4..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.CustomCodeRoots.cs +++ /dev/null @@ -1,355 +0,0 @@ -// 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.CodeAnalysis; -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, providers, 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, providers, 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 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 AddCustomCodeViewRoots( - HashSet roots, - TypeProvider customCodeView, - HashSet generatedTypeNames, - IReadOnlyList generatedProviders, - bool publicOnly) - { - AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); - AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames, includeUnqualifiedSimpleNameReferences: true); - if (!publicOnly) - { - AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); - AddUniqueExtensionMethodProviderRoots(roots, customCodeView.BodyDependencyTypes, generatedProviders, generatedTypeNames); - AddCustomCodeViewNamespaceBodyDependencyTypes(roots, customCodeView, generatedTypeNames); - AddAttributes(roots, customCodeView.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); - } - - 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 AddUniqueExtensionMethodProviderRoots( - HashSet roots, - IReadOnlyList dependencies, - IReadOnlyList generatedProviders, - HashSet generatedTypeNames) - { - foreach (var dependency in dependencies) - { - if (string.IsNullOrEmpty(dependency.Namespace)) - { - var match = FindUniqueExtensionMethodProvider(new(dependency.Name, ReceiverType: null), generatedProviders); - if (match != null) - { - AddTypeReference(roots, match.Type, generatedTypeNames); - } - } - - AddUniqueExtensionMethodProviderRoots(roots, dependency.Arguments, generatedProviders, generatedTypeNames); - } - } - - 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; - } - - var dependencyName = GetProviderTypeName(dependency); - var simpleDependencyName = StripGenericArity(GetSimpleName(dependencyName)); - if (string.IsNullOrEmpty(dependency.Namespace) && - !customMemberNames.Contains(simpleDependencyName)) - { - for (var namespaceCandidate = customNamespace; namespaceCandidate != null; namespaceCandidate = GetNamespaceName(namespaceCandidate)) - { - AddExactMetadataNameMatch(roots, $"{namespaceCandidate}.{dependencyName}", generatedTypeNames); - if (!string.Equals(simpleDependencyName, dependencyName, StringComparison.Ordinal)) - { - AddExactMetadataNameMatch(roots, $"{namespaceCandidate}.{simpleDependencyName}", 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; - } - - /// - /// Matches internal custom declarations to the generated provider identities whose accessibility - /// they constrain. Metadata names preserve nested-type identity when available. - /// - private static HashSet 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.ShouldAnalyzeAttributesInReferenceMap) - { - continue; - } - - 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 = new HashSet(StringComparer.Ordinal); - foreach (var provider in GetGeneratedProviders(providers)) - { - if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) - { - continue; - } - - AddTypeReference(declarations, provider.Type, generatedTypeNames); - } - - return declarations; - } - - 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) && - !IsGeneratedInternalImplementation(provider)) - { - continue; - } - - AddTypeReference(implementationDeclarations, provider.Type, generatedInternalDeclarations); - } - - return implementationDeclarations; - } - - private static bool IsGeneratedInternalImplementation(TypeProvider provider) - => provider.RelativeFilePath.Contains( - $"{Path.DirectorySeparatorChar}Generated{Path.DirectorySeparatorChar}Internal{Path.DirectorySeparatorChar}", - StringComparison.Ordinal); - } -} 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 deleted file mode 100644 index 2f011f89af7..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.GraphBuilder.cs +++ /dev/null @@ -1,183 +0,0 @@ -// 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); - var providerNamespace = provider.Type.Namespace; - AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType, providerNamespace); - AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType, providerNamespace); - AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType, providerNamespace); - - if (publicOnly && !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) - { - continue; - } - - if (!publicOnly && IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) - { - continue; - } - - // Model factory signatures mention many models, but methods for unreachable models - // are removed with those models. Only helper dependencies contribute reachability. - if (IsModelFactoryProvider(provider)) - { - continue; - } - - foreach (var implementedType in provider.Implements) - { - AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType, providerNamespace); - } - - if (!publicOnly) - { - foreach (var nestedType in provider.NestedTypes) - { - AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType, providerNamespace); - } - } - - if (!publicOnly) - { - foreach (var serializationProvider in provider.SerializationProviders) - { - AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType, providerNamespace); - } - } - - foreach (var signatureDependency in provider.SignatureDependencyTypes) - { - AddTypeReference(references[current], signatureDependency, nodes, serializationReferenceNamesByType, providerNamespace); - } - - foreach (var property in provider.Properties) - { - if (publicOnly && !IsPublic(property.Modifiers)) - { - continue; - } - - AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType, providerNamespace); - AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType, providerNamespace); - 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, providerNamespace); - 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, providerNamespace: providerNamespace); - } - - 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, providerNamespace: providerNamespace); - } - } - - return new ProviderReferenceGraph(nodes, references); - } - - private static Dictionary GetSerializationProviderNamesByType(IReadOnlyList generatedProviders) - { - var namesByType = new Dictionary>(StringComparer.Ordinal); - foreach (var provider in generatedProviders) - { - if (provider.SerializationProviderOwner is { } owner) - { - AddSerializationProviderName(owner.Type, provider.Type); - } - - if (provider.SerializationProviders.Count == 0) - { - continue; - } - - foreach (var serializationProvider in provider.SerializationProviders) - { - AddSerializationProviderName(provider.Type, serializationProvider.Type); - } - } - - var result = new Dictionary(StringComparer.Ordinal); - foreach (var (providerName, serializationProviderNames) in namesByType) - { - result.Add(providerName, [.. serializationProviderNames]); - } - - return result; - - void AddSerializationProviderName(CSharpType ownerType, CSharpType serializationProviderType) - { - var ownerName = GetProviderTypeName(ownerType); - if (!namesByType.TryGetValue(ownerName, out var serializationProviderNames)) - { - serializationProviderNames = new HashSet(StringComparer.Ordinal); - namesByType.Add(ownerName, serializationProviderNames); - } - - serializationProviderNames.Add(GetProviderTypeName(serializationProviderType)); - } - } - } -} 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 index dadf50a7fe6..9fb6098fc88 100644 --- 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 @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; @@ -469,5 +471,456 @@ private static bool HasPublicApiPredecessor( return false; } + + private static HashSet GetRootNames( + IReadOnlyList providers, + HashSet nodes, + HashSet helperRoots, + bool includeModelFactory, + bool includeAdditionalRoots, + bool includeUnionVariantRoots, + 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 (!includeUnionVariantRoots) + { + return roots; + } + + AddUnionVariantRoots(roots, providers, nodes); + + return roots; + } + + 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.FullyQualifiedName)) + { + continue; + } + + AddMatchingName(roots, GetProviderTypeName(provider.Type), nodes); + } + } + + private static void RemoveMethodsFromModelFactory(HashSet namesToRemove, HashSet nodes) + { + 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 (!ShouldRemoveModelFactoryMethod(method, namesToRemove, nodes)) + { + methodsToKeep.Add(method); + continue; + } + + if (i == 0) + { + removedLeadingMethod = true; + } + } + + if (removedLeadingMethod && methodsToKeep.Count > 0) + { + modelFactory.PreserveLeadingMethodSeparator = true; + } + + modelFactory.Update(methods: methodsToKeep); + } + + private static bool ShouldRemoveModelFactoryMethod( + MethodProvider method, + HashSet namesToRemove, + HashSet nodes) + { + if (method.Signature.ReturnType == null) + { + return MatchesGeneratedNode( + method.Signature.Name, + method.Signature.Name, + namesToRemove, + nodes); + } + + var returnTypeName = GetProviderTypeName(method.Signature.ReturnType); + return MatchesGeneratedNode( + returnTypeName, + StripGenericArity(GetSimpleName(returnTypeName)), + namesToRemove, + nodes); + } + + private static HashSet GetGeneratedDeclaredNodes(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; + } + + 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.CustomizationTypeProviders) + { + 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 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 AddCustomCodeViewRoots( + HashSet roots, + TypeProvider customCodeView, + HashSet generatedTypeNames, + bool publicOnly) + { + AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); + AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames, includeUnqualifiedSimpleNameReferences: true); + if (!publicOnly) + { + AddProviderBodyDependencyTypes(roots, customCodeView.BodyDependencyTypes, generatedTypeNames); + AddCustomCodeViewNamespaceBodyDependencyTypes(roots, customCodeView, generatedTypeNames); + AddAttributes(roots, customCodeView.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); + } + + 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; + } + + var dependencyName = GetProviderTypeName(dependency); + var simpleDependencyName = StripGenericArity(GetSimpleName(dependencyName)); + if (string.IsNullOrEmpty(dependency.Namespace) && + !customMemberNames.Contains(simpleDependencyName)) + { + for (var namespaceCandidate = customNamespace; namespaceCandidate != null; namespaceCandidate = GetNamespaceName(namespaceCandidate)) + { + AddExactMetadataNameMatch(roots, $"{namespaceCandidate}.{dependencyName}", generatedTypeNames); + if (!string.Equals(simpleDependencyName, dependencyName, StringComparison.Ordinal)) + { + AddExactMetadataNameMatch(roots, $"{namespaceCandidate}.{simpleDependencyName}", 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; + } + + /// + /// Matches internal custom declarations to the generated provider identities whose accessibility + /// they constrain. Metadata names preserve nested-type identity when available. + /// + private static HashSet 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.ShouldAnalyzeAttributesInReferenceMap) + { + continue; + } + + 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 = new HashSet(StringComparer.Ordinal); + foreach (var provider in GetGeneratedProviders(providers)) + { + if (!provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) + { + continue; + } + + AddTypeReference(declarations, provider.Type, generatedTypeNames); + } + + return declarations; + } + + 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) && + !IsGeneratedInternalImplementation(provider)) + { + continue; + } + + AddTypeReference(implementationDeclarations, provider.Type, generatedInternalDeclarations); + } + + return implementationDeclarations; + } + + private static bool IsGeneratedInternalImplementation(TypeProvider provider) + => provider.RelativeFilePath.Contains( + $"{Path.DirectorySeparatorChar}Generated{Path.DirectorySeparatorChar}Internal{Path.DirectorySeparatorChar}", + StringComparison.Ordinal); } } 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 deleted file mode 100644 index 77784249906..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.ReferenceTraversal.cs +++ /dev/null @@ -1,194 +0,0 @@ -// 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)) - { - continue; - } - - if (!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(); - var visited = new HashSet(); - foreach (var provider in providers) - { - AddGeneratedProvider(generatedProviders, visited, provider); - } - - return generatedProviders; - } - - private static void AddGeneratedProvider(List generatedProviders, HashSet visited, TypeProvider provider) - { - if (!visited.Add(provider)) - { - return; - } - - generatedProviders.Add(provider); - foreach (var nestedType in provider.NestedTypes) - { - AddGeneratedProvider(generatedProviders, visited, nestedType); - } - - foreach (var serializationProvider in provider.SerializationProviders) - { - AddGeneratedProvider(generatedProviders, visited, 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 deleted file mode 100644 index 1d16b60028b..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.RootSelection.cs +++ /dev/null @@ -1,149 +0,0 @@ -// 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 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 (!includeUnionVariantRoots) - { - return roots; - } - - AddUnionVariantRoots(roots, providers, nodes); - - return roots; - } - - 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.FullyQualifiedName)) - { - continue; - } - - AddMatchingName(roots, GetProviderTypeName(provider.Type), nodes); - } - } - - private static void RemoveMethodsFromModelFactory(HashSet namesToRemove, HashSet nodes) - { - 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 (!ShouldRemoveModelFactoryMethod(method, namesToRemove, nodes)) - { - methodsToKeep.Add(method); - continue; - } - - if (i == 0) - { - removedLeadingMethod = true; - } - } - - if (removedLeadingMethod && methodsToKeep.Count > 0) - { - modelFactory.PreserveLeadingMethodSeparator = true; - } - - modelFactory.Update(methods: methodsToKeep); - } - - private static bool ShouldRemoveModelFactoryMethod( - MethodProvider method, - HashSet namesToRemove, - HashSet nodes) - { - if (method.Signature.ReturnType == null) - { - return MatchesGeneratedNode( - method.Signature.Name, - method.Signature.Name, - namesToRemove, - nodes); - } - - var returnTypeName = GetProviderTypeName(method.Signature.ReturnType); - return MatchesGeneratedNode( - returnTypeName, - StripGenericArity(GetSimpleName(returnTypeName)), - namesToRemove, - nodes); - } - - private static HashSet GetGeneratedDeclaredNodes(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 deleted file mode 100644 index 8186266c383..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.TypeReferenceCollector.cs +++ /dev/null @@ -1,193 +0,0 @@ -// 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, - string? providerNamespace = null) - { - AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType, providerNamespace); - if (includeAttributes) - { - AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); - } - - foreach (var parameter in signature.Parameters) - { - AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType, providerNamespace); - if (includeAttributes) - { - AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); - } - } - - if (signature is MethodSignature methodSignature) - { - AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType, providerNamespace); - if (methodSignature.GenericArguments != null) - { - foreach (var genericArgument in methodSignature.GenericArguments) - { - AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType, providerNamespace); - } - } - - if (methodSignature.GenericParameterConstraints != null) - { - foreach (var constraint in methodSignature.GenericParameterConstraints) - { - AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType, providerNamespace); - } - } - } - - if (signature is ConstructorSignature constructorSignature) - { - AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType, providerNamespace); - } - } - - 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, - string? providerNamespace = null) - { - if (type == null) - { - return; - } - - if (type.IsArray) - { - AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType, providerNamespace); - 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); - } - } - else if (!nodes.Contains(providerTypeName) && - string.IsNullOrEmpty(type.Namespace) && - !string.IsNullOrEmpty(providerNamespace)) - { - AddExactMetadataNameMatch(references, $"{providerNamespace}.{providerTypeName}", nodes); - } - - AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType, providerNamespace); - AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType, providerNamespace); - foreach (var argument in type.Arguments) - { - AddTypeReference(references, argument, nodes, serializationProviderNamesByType, providerNamespace); - } - } - - 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('`'); - var typeArgumentStart = name.IndexOf('<'); - var genericSuffixStart = tick < 0 - ? typeArgumentStart - : typeArgumentStart < 0 - ? tick - : Math.Min(tick, typeArgumentStart); - return genericSuffixStart < 0 ? name : name.Substring(0, genericSuffixStart); - } - } -} 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 index 2deea26ab20..f96c2fe17a4 100644 --- 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 @@ -375,6 +375,176 @@ private static (HashSet InternalizeCandidates, HashSet PublicCan return (internalizeCandidates, publicCandidates, internalizeHelperRoots); } + 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); + var providerNamespace = provider.Type.Namespace; + AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType, providerNamespace); + + if (publicOnly && !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + continue; + } + + if (!publicOnly && IsKept(provider.Type, CodeModelGenerator.Instance.NonRootTypes, nodes)) + { + continue; + } + + // Model factory signatures mention many models, but methods for unreachable models + // are removed with those models. Only helper dependencies contribute reachability. + if (IsModelFactoryProvider(provider)) + { + continue; + } + + foreach (var implementedType in provider.Implements) + { + AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType, providerNamespace); + } + + if (!publicOnly) + { + foreach (var nestedType in provider.NestedTypes) + { + AddTypeReference(references[current], nestedType.Type, nodes, serializationReferenceNamesByType, providerNamespace); + } + } + + if (!publicOnly) + { + foreach (var serializationProvider in provider.SerializationProviders) + { + AddTypeReference(references[current], serializationProvider.Type, nodes, serializationReferenceNamesByType, providerNamespace); + } + } + + foreach (var signatureDependency in provider.SignatureDependencyTypes) + { + AddTypeReference(references[current], signatureDependency, nodes, serializationReferenceNamesByType, providerNamespace); + } + + foreach (var property in provider.Properties) + { + if (publicOnly && !IsPublic(property.Modifiers)) + { + continue; + } + + AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType, providerNamespace); + 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, providerNamespace); + 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, providerNamespace: providerNamespace); + } + + 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, providerNamespace: providerNamespace); + } + } + + return new ProviderReferenceGraph(nodes, references); + } + + private static Dictionary GetSerializationProviderNamesByType(IReadOnlyList generatedProviders) + { + var namesByType = new Dictionary>(StringComparer.Ordinal); + foreach (var provider in generatedProviders) + { + if (provider.SerializationProviderOwner is { } owner) + { + AddSerializationProviderName(owner.Type, provider.Type); + } + + if (provider.SerializationProviders.Count == 0) + { + continue; + } + + foreach (var serializationProvider in provider.SerializationProviders) + { + AddSerializationProviderName(provider.Type, serializationProvider.Type); + } + } + + var result = new Dictionary(StringComparer.Ordinal); + foreach (var (providerName, serializationProviderNames) in namesByType) + { + result.Add(providerName, [.. serializationProviderNames]); + } + + return result; + + void AddSerializationProviderName(CSharpType ownerType, CSharpType serializationProviderType) + { + var ownerName = GetProviderTypeName(ownerType); + if (!namesByType.TryGetValue(ownerName, out var serializationProviderNames)) + { + serializationProviderNames = new HashSet(StringComparer.Ordinal); + namesByType.Add(ownerName, serializationProviderNames); + } + + serializationProviderNames.Add(GetProviderTypeName(serializationProviderType)); + } + } + private static bool IsPublic(MethodSignatureModifiers modifiers) => modifiers.HasFlag(MethodSignatureModifiers.Public); private static bool IsPublic(FieldModifiers modifiers) => modifiers.HasFlag(FieldModifiers.Public); 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 99127506a70..00133c450df 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 @@ -94,7 +94,7 @@ private IReadOnlyList PopulateCustomizationTypeProviders() return providers; } - internal IReadOnlyList GetCustomizationTypeProviders() => _customizationTypeProviders.Value; + internal IReadOnlyList CustomizationTypeProviders => _customizationTypeProviders.Value; private static INamedTypeSymbol? FindNamedTypeSymbol(Compilation compilation, bool includeReferencedAssemblies, string fullyQualifiedMetadataName) => includeReferencedAssemblies diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs index 239b7f0598c..5a572b8ebe3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/ProviderReferenceMapAnalyzerTests.cs @@ -334,7 +334,7 @@ public void CustomCodeBodyDependencyOnExternalTypeDoesNotRootGeneratedTypeBySimp } [Test] - public void CustomCodeBodyDependencyRootsUniqueGeneratedExtensionMethodProvider() + public void UnregisteredCustomCodeMethodDependencyDoesNotRootGeneratedExtensionProvider() { var customCodeView = new BodyDependencyTestTypeProvider("CustomType", "Sample", CreateNamedType("SetValue", string.Empty)); var customType = new CustomizableTestTypeProvider("CustomType", TypeSignatureModifiers.Public, customCodeView, ns: "Sample"); @@ -344,7 +344,7 @@ public void CustomCodeBodyDependencyRootsUniqueGeneratedExtensionMethodProvider( ProviderReferenceMapAnalyzer.Analyze([customType, extensionProvider]); - Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(extensionProvider)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(extensionProvider)); } [Test] From bfabf1f41a97262e13866ada301486906b1c2916 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 15 Jul 2026 03:57:47 +0000 Subject: [PATCH 8/8] fix(csharp): resolve shared options accessibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../src/Providers/ClientOptionsProvider.cs | 50 ++++++++----- .../src/Providers/ClientProvider.cs | 5 +- .../src/ScmOutputLibrary.cs | 15 ++++ .../Providers/ClientOptionsProviderTests.cs | 73 +++++++++++++++---- .../ClientA.cs | 6 ++ .../ClientB.cs | 6 ++ .../InternalClient.cs | 6 ++ 7 files changed, 128 insertions(+), 33 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsAreInternalWhenAllClientsAreInternal/ClientA.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsAreInternalWhenAllClientsAreInternal/ClientB.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsArePublicWhenAnyClientIsPublic/InternalClient.cs 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 20258bebed5..c62e74461fd 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 @@ -27,7 +27,7 @@ public class ClientOptionsProvider : TypeProvider private readonly InputClient _inputClient; private readonly ClientProvider _clientProvider; - private readonly Dictionary? _serviceVersionsEnums; + private readonly Lazy?> _serviceVersionsEnums; private static ClientOptionsProvider? _singletonInstance; // All clients sharing the singleton are tracked so its accessibility reflects every owner. private static readonly List _singletonClientProviders = []; @@ -36,6 +36,11 @@ internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientPro { _inputClient = inputClient; _clientProvider = clientProvider; + _serviceVersionsEnums = new(BuildServiceVersionsEnums); + } + + private Dictionary? BuildServiceVersionsEnums() + { List inputEnums = [.. _inputClient.Parameters .Where(p => p.IsApiVersion && p.Type is InputEnumType) .Select(p => (InputEnumType)p.Type)]; @@ -47,7 +52,7 @@ internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientPro if (inputEnums.Count > 0) { - _serviceVersionsEnums = []; + Dictionary serviceVersionsEnums = []; foreach (var inputEnum in inputEnums) { var enumProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateEnum(inputEnum, this); @@ -55,7 +60,7 @@ internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientPro { // Ensure the service version enum uses the same namespace as the options class since it is nested. enumProvider.Update(@namespace: Type.Namespace); - _serviceVersionsEnums.Add(inputEnum, enumProvider); + serviceVersionsEnums.Add(inputEnum, enumProvider); } // Only create one version property for single service clients @@ -64,7 +69,11 @@ internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientPro break; } } + + return serviceVersionsEnums; } + + return null; } /// @@ -165,13 +174,14 @@ private static bool UseSingletonInstance(InputClient inputClient) private Dictionary? BuildVersionProperties() { - if (_serviceVersionsEnums is null) + var serviceVersionsEnums = _serviceVersionsEnums.Value; + if (serviceVersionsEnums is null) { return null; } - var properties = new Dictionary(_serviceVersionsEnums.Count); - foreach (var (inputEnum, enumProvider) in _serviceVersionsEnums) + var properties = new Dictionary(serviceVersionsEnums.Count); + foreach (var (inputEnum, enumProvider) in serviceVersionsEnums) { string versionPropertyName; if (!_inputClient.IsMultiServiceClient) @@ -182,10 +192,10 @@ private static bool UseSingletonInstance(InputClient inputClient) { var serviceNamespace = inputEnum.Namespace; if (!string.IsNullOrEmpty(serviceNamespace) && - ClientHelper.HasLastSegmentCollision(serviceNamespace, inputEnum, _serviceVersionsEnums.Keys)) + ClientHelper.HasLastSegmentCollision(serviceNamespace, inputEnum, serviceVersionsEnums.Keys)) { // Last segment collides — find the shortest unique namespace suffix. - string uniquePrefix = ClientHelper.GetShortestUniqueNamespacePrefix(serviceNamespace, inputEnum, _serviceVersionsEnums.Keys); + string uniquePrefix = ClientHelper.GetShortestUniqueNamespacePrefix(serviceNamespace, inputEnum, serviceVersionsEnums.Keys); versionPropertyName = $"{uniquePrefix.ToIdentifierName()}{ApiVersionSuffix}"; } else @@ -210,13 +220,14 @@ private static bool UseSingletonInstance(InputClient inputClient) private Dictionary? BuildLatestVersionsFields() { - if (_serviceVersionsEnums is null) + var serviceVersionsEnums = _serviceVersionsEnums.Value; + if (serviceVersionsEnums is null) { return null; } - Dictionary latestVersionFields = new(_serviceVersionsEnums.Count); - foreach (var (inputEnum, enumProvider) in _serviceVersionsEnums) + Dictionary latestVersionFields = new(serviceVersionsEnums.Count); + foreach (var (inputEnum, enumProvider) in serviceVersionsEnums) { string fieldName; if (!_inputClient.IsMultiServiceClient) @@ -227,10 +238,10 @@ private static bool UseSingletonInstance(InputClient inputClient) { var serviceNamespace = inputEnum.Namespace; if (!string.IsNullOrEmpty(serviceNamespace) && - ClientHelper.HasLastSegmentCollision(serviceNamespace, inputEnum, _serviceVersionsEnums.Keys)) + ClientHelper.HasLastSegmentCollision(serviceNamespace, inputEnum, serviceVersionsEnums.Keys)) { // Last segment collides — find the shortest unique namespace suffix. - string uniquePrefix = ClientHelper.GetShortestUniqueNamespacePrefix(serviceNamespace, inputEnum, _serviceVersionsEnums.Keys); + string uniquePrefix = ClientHelper.GetShortestUniqueNamespacePrefix(serviceNamespace, inputEnum, serviceVersionsEnums.Keys); fieldName = $"{LatestPrefix}{uniquePrefix.ToIdentifierName()}{VersionSuffix}"; } else @@ -260,13 +271,15 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() { if (this == _singletonInstance) { - TypeSignatureModifiers singletonAccessibility = TypeSignatureModifiers.None; foreach (var clientProvider in _singletonClientProviders) { - singletonAccessibility |= GetAccessibilityModifiers(clientProvider.DeclarationModifiers); + if (clientProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + return TypeSignatureModifiers.Public; + } } - return singletonAccessibility; + return TypeSignatureModifiers.Internal; } return GetAccessibilityModifiers(_clientProvider.DeclarationModifiers); @@ -316,12 +329,13 @@ protected override FieldProvider[] BuildFields() protected override TypeProvider[] BuildNestedTypes() { - if (_serviceVersionsEnums is null) + var serviceVersionsEnums = _serviceVersionsEnums.Value; + if (serviceVersionsEnums is null) { return []; } - return [.. _serviceVersionsEnums.Values.OrderBy(e => e.Name)]; + return [.. serviceVersionsEnums.Values.OrderBy(e => e.Name)]; } protected override ConstructorProvider[] BuildConstructors() 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 69bd5648f0f..2b3ce1a18c6 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 @@ -72,7 +72,9 @@ private record ApiVersionFields(FieldProvider Field, PropertyProvider? Correspon /// internal TypeProvider BackCompatProvider => _backCompatProvider ?? this; - public ParameterProvider? ClientOptionsParameter { get; } + private ParameterProvider? _clientOptionsParameter; + public ParameterProvider? ClientOptionsParameter + => ClientOptions is null ? null : _clientOptionsParameter ??= ScmKnownParameters.ClientOptions(ClientOptions.Type); protected override FormattableString BuildDescription() { @@ -108,7 +110,6 @@ public ClientProvider(InputClient inputClient) _subClientEndpointParameter = BuildSubClientEndpointParameter(); _publicCtorDescription = $"Initializes a new instance of {Name}."; ClientOptions = _inputClient.Parent is null ? ClientOptionsProvider.CreateClientOptionsProvider(_inputClient, this) : null; - ClientOptionsParameter = ClientOptions != null ? ScmKnownParameters.ClientOptions(ClientOptions.Type) : null; bool isIndividuallyInitialized = (_inputClient.InitializedBy & InputClientInitializedBy.Individually) != 0; ClientSettings = isIndividuallyInitialized && DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs index ed527e4c116..e7d755c0239 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmOutputLibrary.cs @@ -16,6 +16,11 @@ private static TypeProvider[] BuildClientTypes() var inputClients = ScmCodeModelGenerator.Instance.InputLibrary.InputNamespace.RootClients; var types = new HashSet(); + foreach (var inputClient in inputClients) + { + CreateClientProviders(inputClient); + } + foreach (var inputClient in inputClients) { BuildClient(inputClient, types); @@ -24,6 +29,16 @@ private static TypeProvider[] BuildClientTypes() return [.. types]; } + private static void CreateClientProviders(InputClient inputClient) + { + foreach (var child in inputClient.Children) + { + CreateClientProviders(child); + } + + ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); + } + private static void BuildClient(InputClient inputClient, HashSet types) { foreach (var child in inputClient.Children) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs index 8bd327a1230..40f166fc741 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs @@ -503,25 +503,72 @@ public void MultipleClientsWithRequiredCustomParametersShareSingletonOptions() } [Test] - public void SharedSingletonOptionsArePublicWhenAnyClientIsPublic() + public async Task SharedSingletonOptionsArePublicWhenAnyClientIsPublic() { - var client1 = InputFactory.Client("InternalClient", clientNamespace: "TestNamespace"); - var client2 = InputFactory.Client("PublicClient", clientNamespace: "TestNamespace"); - - MockHelpers.LoadMockGenerator(clients: () => [client1, client2]); - - var clientProvider1 = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client1); - var clientProvider2 = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client2); - - Assert.IsNotNull(clientProvider1); - Assert.IsNotNull(clientProvider2); + List apiVersions = ["1.0"]; + var inputEnum = InputFactory.StringEnum( + "ServiceVersion", + apiVersions.Select(version => (version, version)), + usage: InputModelTypeUsage.ApiVersionEnum); + var apiVersionParameter = InputFactory.QueryParameter( + "apiVersion", + inputEnum, + isRequired: true, + scope: InputParameterScope.Client, + isApiVersion: true); + var operation = InputFactory.Operation("Get", parameters: [apiVersionParameter]); + var client1 = InputFactory.Client( + "InternalClient", + methods: [InputFactory.BasicServiceMethod("Get", operation)], + parameters: [apiVersionParameter]); + var client2 = InputFactory.Client("PublicClient"); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client1, client2], + apiVersions: () => apiVersions, + inputEnums: () => [inputEnum], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + configuration: "{\"unreferenced-types-handling\":\"keepAll\"}"); - clientProvider1!.Update(modifiers: TypeSignatureModifiers.Internal); - clientProvider2!.Update(modifiers: TypeSignatureModifiers.Public); + var clientProviders = mockGenerator.Object.OutputLibrary.TypeProviders.OfType().ToList(); + var clientProvider1 = clientProviders.Single(client => client.Name == "InternalClient"); + var clientProvider2 = clientProviders.Single(client => client.Name == "PublicClient"); + Assert.IsTrue(clientProvider1.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(clientProvider2.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); var options = clientProvider1.ClientOptions; Assert.AreSame(options, clientProvider2.ClientOptions); + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(mockGenerator.Object.OutputLibrary.TypeProviders); + Assert.IsTrue(options!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(options.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + + var declaration = new TypeProviderWriter(options).Write().Content; + StringAssert.Contains("public partial class SampleClientOptions", declaration); + StringAssert.DoesNotContain("public internal partial class", declaration); + } + + [Test] + public async Task SharedSingletonOptionsAreInternalWhenAllClientsAreInternal() + { + var client1 = InputFactory.Client("ClientA"); + var client2 = InputFactory.Client("ClientB"); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client1, client2], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + configuration: "{\"unreferenced-types-handling\":\"keepAll\"}"); + + var clientProviders = mockGenerator.Object.OutputLibrary.TypeProviders.OfType().ToList(); + var options = clientProviders.Single(client => client.Name == "ClientA").ClientOptions; + Assert.AreSame(options, clientProviders.Single(client => client.Name == "ClientB").ClientOptions); + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(mockGenerator.Object.OutputLibrary.TypeProviders); + + Assert.IsTrue(options!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(options.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + + var declaration = new TypeProviderWriter(options).Write().Content; + StringAssert.Contains("internal partial class SampleClientOptions", declaration); } [Test] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsAreInternalWhenAllClientsAreInternal/ClientA.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsAreInternalWhenAllClientsAreInternal/ClientA.cs new file mode 100644 index 00000000000..a34d00e3c6e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsAreInternalWhenAllClientsAreInternal/ClientA.cs @@ -0,0 +1,6 @@ +namespace Sample +{ + internal partial class ClientA + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsAreInternalWhenAllClientsAreInternal/ClientB.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsAreInternalWhenAllClientsAreInternal/ClientB.cs new file mode 100644 index 00000000000..2bfb3b16ae8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsAreInternalWhenAllClientsAreInternal/ClientB.cs @@ -0,0 +1,6 @@ +namespace Sample +{ + internal partial class ClientB + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsArePublicWhenAnyClientIsPublic/InternalClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsArePublicWhenAnyClientIsPublic/InternalClient.cs new file mode 100644 index 00000000000..3eb98f9a9e3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/SharedSingletonOptionsArePublicWhenAnyClientIsPublic/InternalClient.cs @@ -0,0 +1,6 @@ +namespace Sample +{ + internal partial class InternalClient + { + } +}