From 484b04b05545bf2d0c38db37a133982eb7205183 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 17 Jul 2026 04:11:43 +0000 Subject: [PATCH 1/6] fix(csharp): preserve hidden union accessibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- ...iderReferenceMapAnalyzer.BodyReferences.cs | 42 +++++++---- .../ProviderReferenceMapAnalyzer.Helpers.cs | 41 +++++++--- .../ProviderReferenceMapAnalyzer.cs | 43 ++++++----- .../ProviderReferenceMapAnalyzerTests.cs | 75 +++++++++++++++++++ 4 files changed, 160 insertions(+), 41 deletions(-) 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 8ece3a31fa3..1944cf44651 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 @@ -412,7 +412,8 @@ private static void AddSignatureReferences( bool includeAttributeArguments = true, string? providerNamespace = null, string? containingProviderTypeName = null, - IReadOnlySet? contextualTypeExclusions = null) + IReadOnlySet? contextualTypeExclusions = null, + HashSet? unionItemTypeExclusions = null) { HashSet? signatureTypeExclusions = contextualTypeExclusions == null ? null @@ -423,7 +424,7 @@ private static void AddSignatureReferences( signatureTypeExclusions.UnionWith(genericMethodSignature.GenericArguments.Select(argument => argument.Name)); } - AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions); + AddTypeReference(references, signature.ReturnType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions, unionItemTypeExclusions); if (includeAttributes) { AddAttributes(references, signature.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); @@ -431,7 +432,7 @@ private static void AddSignatureReferences( foreach (var parameter in signature.Parameters) { - AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions); + AddTypeReference(references, parameter.Type, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions, unionItemTypeExclusions); if (includeAttributes) { AddAttributes(references, parameter.Attributes, nodes, serializationProviderNamesByType, includeAttributeArguments); @@ -440,12 +441,12 @@ private static void AddSignatureReferences( if (signature is MethodSignature methodSignature) { - AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions); + AddTypeReference(references, methodSignature.ExplicitInterface, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions, unionItemTypeExclusions); if (methodSignature.GenericArguments != null) { foreach (var genericArgument in methodSignature.GenericArguments) { - AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions); + AddTypeReference(references, genericArgument, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions, unionItemTypeExclusions); } } @@ -453,14 +454,14 @@ private static void AddSignatureReferences( { foreach (var constraint in methodSignature.GenericParameterConstraints) { - AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions); + AddTypeReference(references, constraint.Type, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions, unionItemTypeExclusions); } } } if (signature is ConstructorSignature constructorSignature) { - AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions); + AddTypeReference(references, constructorSignature.Type, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, signatureTypeExclusions, unionItemTypeExclusions); } } @@ -515,20 +516,33 @@ private static void AddTypeReference( IReadOnlyDictionary? serializationProviderNamesByType = null, string? providerNamespace = null, string? containingProviderTypeName = null, - IReadOnlySet? contextualTypeExclusions = null) + IReadOnlySet? contextualTypeExclusions = null, + HashSet? unionItemTypeExclusions = null, + bool isUnionItemType = false) { if (type == null) { return; } + var providerTypeName = GetProviderTypeName(type); + if (isUnionItemType && + unionItemTypeExclusions != null && + MatchesGeneratedNode( + providerTypeName, + StripGenericArity(GetSimpleName(providerTypeName)), + unionItemTypeExclusions, + nodes)) + { + return; + } + if (type.IsArray) { - AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions); + AddTypeReference(references, type.ElementType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions, unionItemTypeExclusions, isUnionItemType); return; } - var providerTypeName = GetProviderTypeName(type); var matchedContainingType = string.IsNullOrEmpty(type.Namespace) && contextualTypeExclusions?.Contains(type.Name) != true && !string.IsNullOrEmpty(containingProviderTypeName) && @@ -554,18 +568,18 @@ private static void AddTypeReference( AddExactMetadataNameMatch(references, $"{providerNamespace}.{providerTypeName}", nodes); } - AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions); - AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions); + AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions, unionItemTypeExclusions, isUnionItemType); + AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions, unionItemTypeExclusions, isUnionItemType); if (type.IsUnion) { foreach (var unionItemType in type.UnionItemTypes) { - AddTypeReference(references, unionItemType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions); + AddTypeReference(references, unionItemType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions, unionItemTypeExclusions, isUnionItemType: true); } } foreach (var argument in type.Arguments) { - AddTypeReference(references, argument, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions); + AddTypeReference(references, argument, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions, unionItemTypeExclusions, isUnionItemType); } } 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 ac67a94f7c7..17de7367faf 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 @@ -573,7 +573,10 @@ private static HashSet GetCustomCodeGeneratedTypeRoots(IReadOnlyList GetCustomCodePublicGeneratedTypeRoots(IReadOnlyList providers, HashSet generatedTypeNames) + private static HashSet GetCustomCodePublicGeneratedTypeRoots( + IReadOnlyList providers, + HashSet generatedTypeNames, + HashSet unionItemTypeExclusions) { var roots = new HashSet(StringComparer.Ordinal); foreach (var customCodeView in GetCustomCodeViews(providers)) @@ -584,7 +587,7 @@ private static HashSet GetCustomCodePublicGeneratedTypeRoots(IReadOnlyLi } AddCustomCodeViewGeneratedTypeRoot(roots, customCodeView, generatedTypeNames); - AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: true); + AddCustomCodeViewRoots(roots, customCodeView, generatedTypeNames, publicOnly: true, unionItemTypeExclusions); } return roots; @@ -639,13 +642,14 @@ private static void AddCustomCodeViewRoots( HashSet roots, TypeProvider customCodeView, HashSet generatedTypeNames, - bool publicOnly) + bool publicOnly, + HashSet? unionItemTypeExclusions = null) { var containingProviderTypeName = NormalizeMetadataTypeName(GetCustomCodeViewIdentity(customCodeView)); var contextualTypeExclusions = customCodeView.Type.Arguments .Select(argument => argument.Name) .ToHashSet(StringComparer.Ordinal); - AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames); + AddTypeReference(roots, customCodeView.BaseType, generatedTypeNames, unionItemTypeExclusions: unionItemTypeExclusions); AddProviderBodyDependencyTypes(roots, customCodeView.SignatureDependencyTypes, generatedTypeNames, includeUnqualifiedSimpleNameReferences: true); if (!publicOnly) { @@ -656,7 +660,7 @@ private static void AddCustomCodeViewRoots( foreach (var implementedType in customCodeView.Implements) { - AddTypeReference(roots, implementedType, generatedTypeNames); + AddTypeReference(roots, implementedType, generatedTypeNames, unionItemTypeExclusions: unionItemTypeExclusions); } foreach (var constructor in customCodeView.Constructors) @@ -673,7 +677,8 @@ private static void AddCustomCodeViewRoots( serializationProviderNamesByType: null, includeAttributes: !publicOnly, containingProviderTypeName: containingProviderTypeName, - contextualTypeExclusions: contextualTypeExclusions); + contextualTypeExclusions: contextualTypeExclusions, + unionItemTypeExclusions: unionItemTypeExclusions); } foreach (var method in customCodeView.Methods) @@ -690,7 +695,8 @@ private static void AddCustomCodeViewRoots( serializationProviderNamesByType: null, includeAttributes: !publicOnly, containingProviderTypeName: containingProviderTypeName, - contextualTypeExclusions: contextualTypeExclusions); + contextualTypeExclusions: contextualTypeExclusions, + unionItemTypeExclusions: unionItemTypeExclusions); } foreach (var property in customCodeView.Properties) @@ -705,8 +711,9 @@ private static void AddCustomCodeViewRoots( property.Type, generatedTypeNames, containingProviderTypeName: containingProviderTypeName, - contextualTypeExclusions: contextualTypeExclusions); - AddTypeReference(roots, property.ExplicitInterface, generatedTypeNames); + contextualTypeExclusions: contextualTypeExclusions, + unionItemTypeExclusions: unionItemTypeExclusions); + AddTypeReference(roots, property.ExplicitInterface, generatedTypeNames, unionItemTypeExclusions: unionItemTypeExclusions); if (!publicOnly) { AddAttributes(roots, property.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); @@ -725,7 +732,8 @@ private static void AddCustomCodeViewRoots( field.Type, generatedTypeNames, containingProviderTypeName: containingProviderTypeName, - contextualTypeExclusions: contextualTypeExclusions); + contextualTypeExclusions: contextualTypeExclusions, + unionItemTypeExclusions: unionItemTypeExclusions); if (!publicOnly) { AddAttributes(roots, field.Attributes, generatedTypeNames, serializationProviderNamesByType: null, includeArguments: true); @@ -888,6 +896,19 @@ private static HashSet GetGeneratedInternalTypeDeclarations( return declarations; } + private static void AddAbstractModelDeclarations( + IReadOnlyList providers, + HashSet unionItemTypeExclusions, + HashSet generatedTypeNames) + { + foreach (var abstractModel in GetGeneratedProviders(providers) + .OfType() + .Where(provider => provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract))) + { + AddTypeReference(unionItemTypeExclusions, abstractModel.Type, generatedTypeNames); + } + } + private static HashSet GetGeneratedImplementationInternalTypeDeclarations( IReadOnlyList providers, HashSet generatedInternalDeclarations) 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 25382973239..34f7f9b17c1 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 @@ -204,13 +204,16 @@ public static void Analyze(IReadOnlyList 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 customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); + var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); + var unionItemTypeExclusions = new HashSet(customInternalDeclarations, StringComparer.Ordinal); + unionItemTypeExclusions.UnionWith(generatedInternalDeclarations); + AddAbstractModelDeclarations(generatedProviders, unionItemTypeExclusions, graph.Nodes); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true, unionItemTypeExclusions); - var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes, unionItemTypeExclusions); var customCodeRemovalRoots = GetCustomCodeGeneratedTypeRoots(generatedProviders, graph.Nodes); var customRemovalRoots = new HashSet(customCodeRemovalRoots, StringComparer.Ordinal); - 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. @@ -254,10 +257,13 @@ private static (HashSet InternalizeCandidates, HashSet PublicCan { var generatedProviders = GetGeneratedProviders(providers); var graph = BuildGraph(generatedProviders); - var publicGraph = BuildGraph(generatedProviders, publicOnly: true); - var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes); var customInternalDeclarations = GetCustomCodeInternalGeneratedTypeDeclarations(generatedProviders, graph.Nodes); var generatedInternalDeclarations = GetGeneratedInternalTypeDeclarations(generatedProviders, graph.Nodes); + var unionItemTypeExclusions = new HashSet(customInternalDeclarations, StringComparer.Ordinal); + unionItemTypeExclusions.UnionWith(generatedInternalDeclarations); + AddAbstractModelDeclarations(generatedProviders, unionItemTypeExclusions, graph.Nodes); + var publicGraph = BuildGraph(generatedProviders, publicOnly: true, unionItemTypeExclusions); + var customPublicRoots = GetCustomCodePublicGeneratedTypeRoots(generatedProviders, graph.Nodes, unionItemTypeExclusions); var generatedDiscriminatorBaseNames = new HashSet(StringComparer.Ordinal); var (internalizeCandidates, publicCandidates, _) = GetAccessibilityCandidates( @@ -373,7 +379,10 @@ private static (HashSet InternalizeCandidates, HashSet PublicCan return (internalizeCandidates, publicCandidates, internalizeHelperRoots); } - private static ProviderReferenceGraph BuildGraph(IReadOnlyList generatedProviders, bool publicOnly = false) + private static ProviderReferenceGraph BuildGraph( + IReadOnlyList generatedProviders, + bool publicOnly = false, + HashSet? unionItemTypeExclusions = null) { // Each generated provider becomes a node, and provider metadata supplies the edges: // inheritance, signatures, properties, fields, nested/serialization providers, attributes, @@ -396,9 +405,9 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen { 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); + AddTypeReference(references[current], provider.Type, nodes, serializationReferenceNamesByType, providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); + AddTypeReference(references[current], provider.BaseType, nodes, serializationReferenceNamesByType, providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); + AddTypeReference(references[current], provider.DeclaringTypeProvider?.Type, nodes, serializationReferenceNamesByType, providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); if (publicOnly && !provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) { @@ -419,7 +428,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen foreach (var implementedType in provider.Implements) { - AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], implementedType, nodes, serializationReferenceNamesByType, providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); } if (!publicOnly) @@ -440,7 +449,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen foreach (var signatureDependency in provider.SignatureDependencyTypes) { - AddTypeReference(references[current], signatureDependency, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], signatureDependency, nodes, serializationReferenceNamesByType, providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); } foreach (var property in provider.Properties) @@ -450,8 +459,8 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen continue; } - AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType, providerNamespace); - AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], property.Type, nodes, serializationReferenceNamesByType, providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); + AddTypeReference(references[current], property.ExplicitInterface, nodes, serializationReferenceNamesByType, providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); if (!publicOnly) { AddAttributes(references[current], property.Attributes, nodes, serializationReferenceNamesByType, includeArguments: false); @@ -465,7 +474,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen continue; } - AddTypeReference(references[current], field.Type, nodes, serializationReferenceNamesByType, providerNamespace); + AddTypeReference(references[current], field.Type, nodes, serializationReferenceNamesByType, providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); if (!publicOnly) { AddAttributes(references[current], field.Attributes, nodes, serializationReferenceNamesByType, includeArguments: false); @@ -479,7 +488,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen continue; } - AddSignatureReferences(references[current], constructor.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly, includeAttributeArguments: false, providerNamespace: providerNamespace); + AddSignatureReferences(references[current], constructor.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly, includeAttributeArguments: false, providerNamespace: providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); } foreach (var method in provider.Methods) @@ -494,7 +503,7 @@ private static ProviderReferenceGraph BuildGraph(IReadOnlyList gen continue; } - AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly, includeAttributeArguments: false, providerNamespace: providerNamespace); + AddSignatureReferences(references[current], method.Signature, nodes, serializationReferenceNamesByType, includeAttributes: !publicOnly, includeAttributeArguments: false, providerNamespace: providerNamespace, unionItemTypeExclusions: unionItemTypeExclusions); } } 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 945f24e2d9f..75f3b598c0a 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 @@ -193,6 +193,81 @@ public void UnionVariantPropertyReferenceOnlyMatchesFullyQualifiedModelName() Assert.IsFalse(collidingVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); } + [Test] + public void BinaryDataUnionPropertyDoesNotPublicizeInternalUnionMembers() + { + var internalVariant = new GeneratedModelTestTypeProvider( + "InternalVariant", + TypeSignatureModifiers.Internal, + "Sample"); + var client = new ClientTestTypeProvider("SampleClient", "Sample"); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => new TestOutputLibrary(client, internalVariant), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + client.Update(properties: + [ + new PropertyProvider( + $"", + MethodSignatureModifiers.Public, + CSharpType.FromUnion([internalVariant.Type, typeof(string)]), + "Value", + new AutoPropertyBody(false), + client) + ]); + + using var session = ProviderReferenceMapAnalyzer.PrepareForGeneration([client, internalVariant]); + + Assert.IsTrue(internalVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(internalVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(internalVariant)); + } + + [Test] + public void BinaryDataUnionPropertyDoesNotPublicizeAbstractBaseModel() + { + var discriminator = InputFactory.Property( + "kind", + InputPrimitiveType.String, + isRequired: true, + isDiscriminator: true); + var derivedInput = InputFactory.Model( + "InternalVariant", + "Sample", + access: "internal", + discriminatedKind: "internal"); + var baseInput = InputFactory.Model( + "Variant", + "Sample", + access: null!, + properties: [discriminator], + derivedModels: [derivedInput], + discriminatorProperty: discriminator); + MockHelpers.LoadMockGenerator(inputModelTypes: [baseInput, derivedInput]); + var providers = CodeModelGenerator.Instance.OutputLibrary.TypeProviders; + var baseVariant = providers.OfType().Single(provider => provider.Name == "Variant"); + var internalVariant = providers.OfType().Single(provider => provider.Name == "InternalVariant"); + var client = new ClientTestTypeProvider("SampleClient", "Sample"); + client.Update(properties: + [ + new PropertyProvider( + $"", + MethodSignatureModifiers.Public, + CSharpType.FromUnion( + [ + new CSharpType(typeof(IList<>), baseVariant.Type), + typeof(string) + ]), + "Value", + new AutoPropertyBody(false), + client) + ]); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([client, baseVariant, internalVariant]); + + Assert.IsTrue(baseVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(internalVariant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + [Test] public void UnionVariantEnumRemainsPublicWhenReferencedByPublicProperty() { From 99637b3d140cc3a27e51dbba2bfaae0f2dcc583e Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 17 Jul 2026 07:55:41 +0000 Subject: [PATCH 2/6] fix(csharp): preserve nested discriminator variants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- ...ProviderReferenceMapAnalyzer.Candidates.cs | 15 ++- .../ProviderReferenceMapAnalyzerTests.cs | 104 ++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) 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 622aa980a89..0852115ffe9 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 @@ -422,13 +422,17 @@ private static void AddDerivedModelReferences( modelProviders.Add(modelProvider); - if (modelProvider.DiscriminatorProperty != null) + var hasKnownDiscriminatorVariants = modelProvider.DerivedModels.Any( + derivedModel => !derivedModel.IsUnknownDiscriminatorModel && derivedModel.DiscriminatorValue != null); + if (modelProvider.DiscriminatorProperty != null || hasKnownDiscriminatorVariants) { discriminatorBaseNames.Add(GetProviderTypeName(modelProvider.Type)); } if (!modelProvider.IsUnknownDiscriminatorModel && - (modelProvider.DiscriminatorProperty != null || modelProvider.DiscriminatorValue != null)) + (modelProvider.DiscriminatorProperty != null || + modelProvider.DiscriminatorValue != null || + hasKnownDiscriminatorVariants)) { discriminatorProviders.Add(modelProvider); } @@ -500,6 +504,13 @@ private static void AddDerivedModelReferences( addedReference = true; } } + + var reachableCount = publicBaseModels.Count; + publicBaseModels.UnionWith(GetReachableTypes(publicBaseModels, references)); + if (publicBaseModels.Count != reachableCount) + { + addedReference = true; + } } } 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 75f3b598c0a..88a5d907a0b 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 @@ -345,6 +345,110 @@ public void KnownDiscriminatorVariantRemainsPublicWhenBaseIsRooted() Assert.IsTrue(derivedProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); } + [Test] + public void NestedKnownDiscriminatorVariantRemainsPublicWhenOuterBaseIsCustomRoot() + { + var innerDiscriminator = InputFactory.Property( + "innerKind", + InputPrimitiveType.String, + isRequired: true, + isDiscriminator: true); + var innerDerivedInput = InputFactory.Model( + "InnerKnownVariant", + "Sample", + access: null!, + discriminatedKind: "inner"); + var innerBaseInput = InputFactory.Model( + "InnerBaseModel", + "Sample", + access: null!, + properties: [innerDiscriminator], + derivedModels: [innerDerivedInput], + discriminatorProperty: innerDiscriminator); + var outerDiscriminator = InputFactory.Property( + "outerKind", + InputPrimitiveType.String, + isRequired: true, + isDiscriminator: true); + var outerDerivedInput = InputFactory.Model( + "OuterKnownVariant", + "Sample", + access: null!, + properties: [InputFactory.Property("nested", innerBaseInput, isRequired: true)], + discriminatedKind: "outer"); + var outerBaseInput = InputFactory.Model( + "OuterBaseModel", + "Sample", + access: null!, + properties: [outerDiscriminator], + derivedModels: [outerDerivedInput], + discriminatorProperty: outerDiscriminator); + MockHelpers.LoadMockGenerator(inputModelTypes: + [ + outerBaseInput, + outerDerivedInput, + innerBaseInput, + innerDerivedInput + ]); + var providers = CodeModelGenerator.Instance.OutputLibrary.TypeProviders; + var outerBaseProvider = providers.OfType().Single(provider => provider.Name == "OuterBaseModel"); + var innerDerivedProvider = providers.OfType().Single(provider => provider.Name == "InnerKnownVariant"); + var customCodeView = new SignatureDependencyTestTypeProvider( + "CustomApi", + TypeSignatureModifiers.Public, + outerBaseProvider.Type); + var customApi = new CustomizableTestTypeProvider( + "CustomApi", + TypeSignatureModifiers.Public, + customCodeView, + "Sample"); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([customApi, .. providers]); + + Assert.IsTrue(innerDerivedProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(innerDerivedProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + + [Test] + public void KnownDiscriminatorVariantRemainsPublicWhenIntermediateBaseHasNoDiscriminatorProperty() + { + var discriminator = InputFactory.Property( + "kind", + InputPrimitiveType.String, + isRequired: true, + isDiscriminator: true); + var leafInput = InputFactory.Model( + "KnownVariant", + "Sample", + access: null!, + discriminatedKind: "known"); + var intermediateInput = InputFactory.Model( + "IntermediateBase", + "Sample", + access: null!, + derivedModels: [leafInput]); + var rootInput = InputFactory.Model( + "RootBase", + "Sample", + access: null!, + properties: [discriminator], + derivedModels: [intermediateInput], + discriminatorProperty: discriminator); + MockHelpers.LoadMockGenerator(inputModelTypes: [rootInput, intermediateInput, leafInput]); + var providers = CodeModelGenerator.Instance.OutputLibrary.TypeProviders; + var rootProvider = providers.OfType().Single(provider => provider.Name == "RootBase"); + var intermediateProvider = providers.OfType().Single(provider => provider.Name == "IntermediateBase"); + var leafProvider = providers.OfType().Single(provider => provider.Name == "KnownVariant"); + CodeModelGenerator.Instance.AddTypeToKeep(rootProvider); + + Assert.IsNull(intermediateProvider.DiscriminatorProperty); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(providers); + + Assert.IsTrue(leafProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(leafProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + [Test] public void DisconnectedKnownDiscriminatorVariantIsNotPublicRoot() { From 2a214fd40183eb7a182bfc9fcf1c2dd2ee3b634c Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 17 Jul 2026 10:08:02 +0000 Subject: [PATCH 3/6] fix(csharp): ignore metadata-only union references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../src/Primitives/CSharpType.cs | 17 +++++++++ .../src/Providers/ModelProvider.cs | 2 +- ...iderReferenceMapAnalyzer.BodyReferences.cs | 5 ++- .../ModelProviders/ModelProviderTests.cs | 20 +++++++++++ .../ProviderReferenceMapAnalyzerTests.cs | 35 +++++++++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs index 961075449e8..f2bc01a85e0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs @@ -11,6 +11,12 @@ namespace Microsoft.TypeSpec.Generator.Primitives { + internal enum UnionItemTypeReferenceKind + { + PublicSurface, + MetadataOnly + } + /// /// CSharpType represents the C# type of an input type. /// It is constructed from a and its properties. @@ -21,6 +27,7 @@ public class CSharpType private object? _literal; private Type? _underlyingType; private IReadOnlyList? _unionItemTypes; + private UnionItemTypeReferenceKind _unionItemTypeReferenceKind; private bool? _isReadOnlyMemory; private bool? _isList; @@ -221,6 +228,7 @@ private init public CSharpType InputType => _inputType ??= GetInputType(); public CSharpType OutputType => _outputType ??= GetOutputType(); public IReadOnlyList UnionItemTypes => _unionItemTypes ?? throw new InvalidOperationException("Not a union type"); + internal UnionItemTypeReferenceKind UnionItemTypeReferenceKind => _unionItemTypeReferenceKind; private bool TypeIsReadOnlyMemory() => IsFrameworkType && _type == typeof(ReadOnlyMemory<>); @@ -559,6 +567,7 @@ public CSharpType WithNullable(bool isNullable) type._literal = _literal; type._unionItemTypes = _unionItemTypes; + type._unionItemTypeReferenceKind = _unionItemTypeReferenceKind; return type; } @@ -582,6 +591,7 @@ internal CSharpType WithUnderlyingEnumType(Type underlyingEnumType) type._underlyingType = underlyingEnumType; type._literal = _literal; type._unionItemTypes = _unionItemTypes; + type._unionItemTypeReferenceKind = _unionItemTypeReferenceKind; return type; } @@ -691,9 +701,16 @@ public static CSharpType FromLiteral(CSharpType type, object literalValue) /// Flag used to determine if a type is nullable. /// A instance representing those unioned types. public static CSharpType FromUnion(IReadOnlyList unionItemTypes, bool isNullable = false) + => FromUnion(unionItemTypes, isNullable, UnionItemTypeReferenceKind.PublicSurface); + + internal static CSharpType FromUnion( + IReadOnlyList unionItemTypes, + bool isNullable, + UnionItemTypeReferenceKind referenceKind) { var type = new CSharpType(typeof(BinaryData), isNullable); type._unionItemTypes = unionItemTypes; + type._unionItemTypeReferenceKind = referenceKind; return type; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 53142b6c74c..e0766da6f75 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -1355,7 +1355,7 @@ _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type, _ when type.IsUnion => type, _ when type.IsList => type.MakeGenericType([ReplaceUnverifiableType(type.Arguments[0])]), _ when type.IsDictionary => type.MakeGenericType([ReplaceUnverifiableType(type.Arguments[0]), ReplaceUnverifiableType(type.Arguments[1])]), - _ => CSharpType.FromUnion([type]) + _ => CSharpType.FromUnion([type], false, UnionItemTypeReferenceKind.MetadataOnly) }; } 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 1944cf44651..7e4e88f1ade 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 @@ -570,7 +570,10 @@ private static void AddTypeReference( AddTypeReference(references, type.BaseType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions, unionItemTypeExclusions, isUnionItemType); AddTypeReference(references, type.DeclaringType, nodes, serializationProviderNamesByType, providerNamespace, containingProviderTypeName, contextualTypeExclusions, unionItemTypeExclusions, isUnionItemType); - if (type.IsUnion) + if (type.IsUnion && + type.UnionItemTypeReferenceKind != UnionItemTypeReferenceKind.MetadataOnly && + (unionItemTypeExclusions == null || + type.UnionItemTypeReferenceKind == UnionItemTypeReferenceKind.PublicSurface)) { foreach (var unionItemType in type.UnionItemTypes) { 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 336318f812a..88ab6c206b8 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 @@ -670,6 +670,26 @@ public void TestBuildAdditionalProperties( } } + [Test] + public void UnverifiableAdditionalPropertyTypeIsMetadataOnly() + { + var valueModel = InputFactory.Model("ValueModel"); + var dictionaryModel = new ModelProvider( + InputFactory.Model( + "DictionaryModel", + properties: [], + additionalProperties: valueModel)); + + var valueType = dictionaryModel.Properties + .Single(property => property.IsAdditionalProperties) + .Type + .ElementType; + + Assert.IsTrue(valueType.IsUnion); + Assert.AreEqual(UnionItemTypeReferenceKind.MetadataOnly, valueType.UnionItemTypeReferenceKind); + Assert.AreEqual("ValueModel", valueType.UnionItemTypes.Single().Name); + } + [Test] public void TestAdditionalPropertiesPropertyNamesAndAccessors() { 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 88a5d907a0b..ba19c627e24 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 @@ -222,6 +222,41 @@ public void BinaryDataUnionPropertyDoesNotPublicizeInternalUnionMembers() Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(internalVariant)); } + [Test] + public void MetadataOnlyUnionPropertyDoesNotReferenceUnionMember() + { + var variant = new GeneratedModelTestTypeProvider( + "Variant", + TypeSignatureModifiers.Public, + "Sample"); + var client = new ClientTestTypeProvider("SampleClient", "Sample"); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => new TestOutputLibrary(client, variant), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + client.Update(properties: + [ + new PropertyProvider( + $"", + MethodSignatureModifiers.Public, + new CSharpType( + typeof(IReadOnlyDictionary<,>), + typeof(string), + CSharpType.FromUnion( + [variant.Type], + false, + UnionItemTypeReferenceKind.MetadataOnly)), + "Value", + new AutoPropertyBody(false), + client) + ]); + + using var session = ProviderReferenceMapAnalyzer.PrepareForGeneration([client, variant]); + + Assert.IsTrue(variant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(variant.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(ProviderReferenceMapAnalyzer.ShouldWriteProvider(variant)); + } + [Test] public void BinaryDataUnionPropertyDoesNotPublicizeAbstractBaseModel() { From fe963e2531c7a2229d81bb4ec3cace1417968cca Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 17 Jul 2026 13:41:27 +0000 Subject: [PATCH 4/6] fix(csharp): preserve GA types and scope request helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../Providers/Abstractions/HttpRequestApi.cs | 1 + .../src/Providers/PipelineRequestProvider.cs | 6 +- .../src/Providers/RestClientProvider.cs | 8 +- .../RestClientProviderTests.cs | 65 ++++++++++++++++ .../ClientBodyDependencyReferenceMapTests.cs | 17 +++++ .../test/TestHelpers/MockHelpers.cs | 6 ++ ...ProviderReferenceMapAnalyzer.Candidates.cs | 1 + .../ProviderReferenceMapAnalyzer.Helpers.cs | 36 +++++++++ .../ProviderReferenceMapAnalyzer.cs | 1 + .../ProviderReferenceMapAnalyzerTests.cs | 74 ++++++++++++++++++- 10 files changed, 209 insertions(+), 6 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Abstractions/HttpRequestApi.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Abstractions/HttpRequestApi.cs index 434d24f5c6c..8d1c52279a0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Abstractions/HttpRequestApi.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Abstractions/HttpRequestApi.cs @@ -17,6 +17,7 @@ protected HttpRequestApi(CSharpType type, ValueExpression original) : base(type, } public abstract Type UriBuilderType { get; } + public virtual CSharpType? GetCollectionHeaderHelperType() => null; public abstract MethodBodyStatement SetHeaders(IReadOnlyList arguments); public abstract MethodBodyStatement AddCollectionHeaders(ValueExpression prefix, ValueExpression headers); 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 9468f7c58cf..20f7c776f8b 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 @@ -5,6 +5,7 @@ using System.ClientModel.Primitives; using System.Collections.Generic; using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Statements; namespace Microsoft.TypeSpec.Generator.ClientModel.Providers @@ -20,6 +21,9 @@ public PipelineRequestProvider(ValueExpression original) : base(typeof(PipelineR public override Type UriBuilderType => typeof(ClientUriBuilderDefinition); + public override CSharpType GetCollectionHeaderHelperType() + => ScmCodeModelGenerator.Instance.PipelineRequestHeadersExtensionsDefinition.Type; + public override ValueExpression Content() => Original.Property(nameof(PipelineRequest.Content)); @@ -34,7 +38,7 @@ public override MethodBodyStatement SetHeaders(IReadOnlyList ar public override MethodBodyStatement AddCollectionHeaders(ValueExpression prefix, ValueExpression headers) => Original.Property(nameof(PipelineRequest.Headers)) - .Invoke(nameof(PipelineRequestHeaders.Add), [prefix, headers], typeArguments: null, callAsAsync: false, extensionType: ScmCodeModelGenerator.Instance.PipelineRequestHeadersExtensionsDefinition.Type) + .Invoke(nameof(PipelineRequestHeaders.Add), [prefix, headers], typeArguments: null, callAsAsync: false, extensionType: GetCollectionHeaderHelperType()) .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 9076d21ded5..aa917efa291 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 @@ -81,7 +81,8 @@ protected override FieldProvider[] BuildFields() protected override IReadOnlyList BuildHelperDependencyTypes() { - var uriBuilderType = ScmCodeModelGenerator.Instance.TypeFactory.HttpRequestApi.ToExpression().UriBuilderType; + var requestApi = ScmCodeModelGenerator.Instance.TypeFactory.HttpRequestApi.ToExpression(); + var uriBuilderType = requestApi.UriBuilderType; var dependencies = new List(); var dependencyNames = new HashSet(StringComparer.Ordinal); if (uriBuilderType == typeof(ClientUriBuilderDefinition)) @@ -104,9 +105,10 @@ protected override IReadOnlyList BuildHelperDependencyTypes() { TryAddDependency(dependencies, dependencyNames, ScmCodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType); if (parameter is InputHeaderParameter headerParameter && - !string.IsNullOrEmpty(headerParameter.CollectionHeaderPrefix)) + !string.IsNullOrEmpty(headerParameter.CollectionHeaderPrefix) && + requestApi.GetCollectionHeaderHelperType() is { } collectionHeaderHelperType) { - TryAddDependency(dependencies, dependencyNames, ScmCodeModelGenerator.Instance.PipelineRequestHeadersExtensionsDefinition.Type); + TryAddDependency(dependencies, dependencyNames, collectionHeaderHelperType); } } else if (type?.IsCollection == true) 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 97472c67514..bd229d4f9d9 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 @@ -7,6 +7,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; @@ -1600,6 +1601,43 @@ public void TestCollectionHeaderPrefix_UsesAddWithPrefixCall(bool hasPrefix) Assert.AreEqual(Helpers.GetExpectedFromFile(parameters: hasPrefix.ToString()), file.Content); } + [TestCase(true)] + [TestCase(false)] + public void TestCollectionHeaderPrefix_AddsHelperRequiredByRequestApi(bool useDefaultRequestApi) + { + if (!useDefaultRequestApi) + { + MockHelpers.LoadMockGenerator(httpRequestApi: TestHttpRequestApi.Instance); + } + + var metadataHeaderParam = InputFactory.HeaderParameter( + "metadata", + InputFactory.Dictionary(InputPrimitiveType.String), + isRequired: true, + serializedName: "x-ms-meta", + collectionHeaderPrefix: "x-ms-meta-"); + var inputServiceMethod = InputFactory.BasicServiceMethod( + "TestServiceMethod", + InputFactory.Operation( + "TestOperation", + parameters: [metadataHeaderParam]), + parameters: + [ + InputFactory.MethodParameter( + "metadata", + InputFactory.Dictionary(InputPrimitiveType.String), + isRequired: true, + location: InputRequestLocation.Header) + ]); + var restClient = new ClientProvider( + InputFactory.Client("TestClient", methods: [inputServiceMethod])).RestClient; + + Assert.AreEqual( + useDefaultRequestApi, + restClient.HelperDependencyTypes.Contains( + ScmCodeModelGenerator.Instance.PipelineRequestHeadersExtensionsDefinition.Type)); + } + private static void ValidateResponseClassifier(MethodBodyStatements bodyStatements, string parsedStatusCodes) { @@ -2379,5 +2417,32 @@ public void PageSizeParameterSerializedNameUsedInCreateRequestMethod() Assert.IsTrue(file.Content.Contains("uri.AppendQuery(\"maxpagesize\""), "Generated code should use the serialized name 'maxpagesize' in the query string"); } + + private sealed record TestHttpRequestApi : HttpRequestApi + { + public static TestHttpRequestApi Instance { get; } = new(Empty); + + public TestHttpRequestApi(ValueExpression original) + : base(typeof(object), original) + { + } + + public override Type UriBuilderType => typeof(UriBuilder); + + public override MethodBodyStatement SetHeaders(IReadOnlyList arguments) + => Original.Invoke("SetHeaders", arguments).Terminate(); + + public override MethodBodyStatement AddCollectionHeaders(ValueExpression prefix, ValueExpression headers) + => Original.Invoke("AddCollectionHeaders", [prefix, headers]).Terminate(); + + public override ValueExpression Content() => Original; + + public override ValueExpression ClientRequestId() => Original; + + public override HttpRequestApi FromExpression(ValueExpression original) + => new TestHttpRequestApi(original); + + public override HttpRequestApi ToExpression() => this; + } } } 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 index b209cd1a18c..79c388659e3 100644 --- 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 @@ -631,6 +631,23 @@ await GenerateAndAssertFiles( expectedFiles: [Path.Combine("src", "Generated", "Internal", "PipelineRequestHeadersExtensions.cs")]); } + [Test] + public async Task GeneratedScalarHeaderDoesNotKeepExtensions() + { + var header = InputFactory.HeaderParameter("x-ms-custom", 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: [], + unexpectedFiles: [Path.Combine("src", "Generated", "Internal", "PipelineRequestHeadersExtensions.cs")]); + } + [Test] public async Task BinaryDataBodyParameterDoesNotKeepBinaryContentHelpers() { 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 6c6e743ad89..7061abfefb1 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 @@ -72,6 +72,7 @@ public static Mock LoadMockGenerator( ClientResponseApi? clientResponseApi = null, ClientPipelineApi? clientPipelineApi = null, HttpMessageApi? httpMessageApi = null, + HttpRequestApi? httpRequestApi = null, RequestContentApi? requestContentApi = null, Func? auth = null, Func? createOutputLibrary = null, @@ -173,6 +174,11 @@ public static Mock LoadMockGenerator( mockTypeFactory.Setup(p => p.HttpMessageApi).Returns(httpMessageApi); } + if (httpRequestApi is not null) + { + mockTypeFactory.Setup(p => p.HttpRequestApi).Returns(httpRequestApi); + } + if (requestContentApi is not null) { mockTypeFactory.Setup(p => p.RequestContentApi).Returns(requestContentApi); 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 0852115ffe9..e15638aca1c 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 @@ -304,6 +304,7 @@ private static HashSet GetRemovalCandidates( includeAdditionalRoots: true, publicClientRootsOnly: false); + removeRoots.UnionWith(GetLastContractPublicRoots(generatedProviders, graph.Nodes)); removeRoots.UnionWith(customRemovalRoots); AddKeptNonRootNames(removeRoots, graph.Nodes); 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 17de7367faf..55ea9ed2a0a 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 @@ -476,6 +476,42 @@ private static HashSet GetRootNames( return roots; } + private static HashSet GetLastContractPublicRoots( + IReadOnlyList providers, + HashSet nodes) + { + var roots = new HashSet(StringComparer.Ordinal); + foreach (var provider in providers) + { + if (!IsPublicInLastContract(provider)) + { + continue; + } + + var providerName = GetProviderTypeName(provider.Type); + if (nodes.Contains(providerName)) + { + roots.Add(providerName); + } + } + + return roots; + } + + private static bool IsPublicInLastContract(TypeProvider provider) + { + for (TypeProvider? current = provider; current != null; current = current.DeclaringTypeProvider) + { + if (current.LastContractView is not { } lastContractView || + !lastContractView.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + return false; + } + } + + return true; + } + 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 34f7f9b17c1..9dbf2756b17 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 @@ -303,6 +303,7 @@ private static (HashSet InternalizeCandidates, HashSet PublicCan includeModelFactory: false, includeAdditionalRoots: true, publicClientRootsOnly: true); + internalizeRoots.UnionWith(GetLastContractPublicRoots(generatedProviders, graph.Nodes)); var generatedPublicReachable = GetReachableTypes(internalizeRoots, internalizeReferences); AddDerivedModelReferences(providers, publicGraph.Nodes, internalizeReferences, generatedPublicReachable, generatedDiscriminatorBaseNames); 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 ba19c627e24..5a9b0818083 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 @@ -953,6 +953,36 @@ public void PublicProviderMethodWithNamespaceLessSignatureKeepsNonRootTypePublic Assert.IsFalse(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); } + [Test] + public async Task LastContractPublicDeclarationRootsGeneratedType() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + public partial class GeneratedModel + { + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var generatedModel = new GeneratedModelTestTypeProvider( + "GeneratedModel", + TypeSignatureModifiers.Public, + "Sample.Models"); + await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(generatedModel), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}", + lastContractCompilation: () => Task.FromResult(lastContractCompilation)); + + using var session = ProviderReferenceMapAnalyzer.PrepareForGeneration([generatedModel]); + + Assert.IsTrue(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(generatedModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(generatedModel)); + } + [Test] public async Task LastContractGenericDeclarationDoesNotRootGeneratedType() { @@ -985,7 +1015,7 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public async Task LastContractNestedPublicDeclarationDoesNotRootGeneratedType() + public async Task LastContractNestedPublicDeclarationRootsGeneratedType() { var lastContractCompilation = CSharpCompilation.Create( "LastContract", @@ -1010,7 +1040,47 @@ await MockHelpers.LoadMockGeneratorAsync( configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}", lastContractCompilation: () => Task.FromResult(lastContractCompilation)); - ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([outerModel]); + Assert.IsNotNull(outerModel.LastContractView); + Assert.IsNotNull(innerModel.LastContractView); + Assert.IsTrue(outerModel.LastContractView!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(innerModel.LastContractView!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + using var session = ProviderReferenceMapAnalyzer.PrepareForGeneration([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(ProviderReferenceMapAnalyzer.ShouldWriteProvider(outerModel)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(innerModel)); + } + + [Test] + public async Task LastContractPublicNestedDeclarationInInternalTypeDoesNotRootGeneratedType() + { + var lastContractCompilation = CSharpCompilation.Create( + "LastContract", + [CSharpSyntaxTree.ParseText(""" + namespace Sample.Models + { + internal 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)); + + using var session = ProviderReferenceMapAnalyzer.PrepareForGeneration([outerModel]); Assert.IsTrue(outerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); Assert.IsFalse(outerModel.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); From 913fd332b00453776e85fd9b824407b53dc8a8a5 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 17 Jul 2026 14:43:11 +0000 Subject: [PATCH 5/6] fix(csharp): preserve serialization owner accessibility Co-authored-by: jolov Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../ProviderReferenceMapAnalyzer.Helpers.cs | 5 ++++ .../ProviderReferenceMapAnalyzerTests.cs | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+) 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 55ea9ed2a0a..14fac6b079b 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 @@ -918,6 +918,11 @@ private static HashSet GetGeneratedInternalTypeDeclarations( var nonInternalDeclarations = new HashSet(StringComparer.Ordinal); foreach (var provider in GetGeneratedProviders(providers)) { + if (IsModelSerializationProviderDeclaration(provider)) + { + continue; + } + if (provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) { AddTypeReference(declarations, provider.Type, generatedTypeNames); 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 5a9b0818083..f591a8278a0 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 @@ -1381,6 +1381,31 @@ public void InternalGeneratedPredecessorDoesNotPublicizeItsInternalDependency() Assert.IsFalse(dependency.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); } + [Test] + public void PublicSerializationPartialDoesNotPublicizeInternalOwner() + { + var model = new GeneratedModelTestTypeProvider( + "InternalModel", + TypeSignatureModifiers.Internal, + ns: "Sample.Models"); + var serialization = new TestTypeProvider( + "InternalModel", + TypeSignatureModifiers.Public, + ns: "Sample.Models"); + model.Update(serializations: [serialization]); + MockHelpers.LoadMockGenerator( + createOutputLibrary: () => new TestOutputLibrary(model), + configuration: "{\"unreferenced-types-handling\":\"removeOrInternalize\"}"); + CodeModelGenerator.Instance.AddTypeToKeep(model.Type.FullyQualifiedName); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility([model]); + + Assert.IsTrue(model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(serialization.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + Assert.IsFalse(serialization.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + } + [Test] public void InternalizedGenericModelRemovesAritylessModelFactoryMethod() { From 70a46e9b960c0179e329559e63c36017ee213b0b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 20 Jul 2026 02:29:43 +0000 Subject: [PATCH 6/6] revert(csharp): restore provider reference map Revert the main-branch rollback from #11282 while preserving the accessibility and SDK-compatibility fixes already on #11288. Keep the shared internal helper provider from #11285, but let provider-graph reachability determine whether helpers are emitted. This reverts commit 05406488bd77674c076139bd98360f6698964e92. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4b68173-2f2b-467d-9a94-9be9d14b5e17 --- .../BinaryContentHelperDefinition.cs | 3 + .../src/Providers/ClientOptionsProvider.cs | 75 +- .../src/Providers/ClientProvider.cs | 43 +- .../Providers/CollectionResultDefinition.cs | 16 + .../ModelReaderWriterContextDefinition.cs | 102 ++- .../ModelSerializationExtensionsDefinition.cs | 3 + .../MrwSerializationTypeDefinition.Xml.cs | 6 +- .../MrwSerializationTypeDefinition.cs | 74 +- ...ultipartFormDataSerializationDefinition.cs | 3 + ...elineRequestHeadersExtensionsDefinition.cs | 4 +- .../src/Providers/RestClientProvider.cs | 120 ++- .../Providers/ScmMethodProviderCollection.cs | 3 +- .../SerializationFormatDefinition.cs | 2 + .../src/ScmCodeModelGenerator.cs | 17 + .../src/ScmOutputLibrary.cs | 24 +- .../src/Snippets/HttpRequestApiSnippets.cs | 4 +- .../Providers/ClientOptionsProviderTests.cs | 107 +++ .../ClientProviderCustomizationTests.cs | 3 +- ...ModelReaderWriterContextDefinitionTests.cs | 331 ++++++++- .../ModelWithProjectedProperty.cs | 12 + .../SampleContext.cs | 9 + .../SystemObjectModelSerializationTests.cs | 34 - .../RestClientProviderTests.cs | 251 ++++++- .../TestClient.cs | 20 + .../Generated/TestClientOptions.cs | 16 + .../TestClientOptions.cs | 15 + .../ClientA.cs | 6 + .../ClientB.cs | 6 + .../InternalClient.cs | 6 + .../test/TestHelpers/MockHelpers.cs | 17 +- .../src/CSharpGen.cs | 97 ++- .../src/CodeModelGenerator.cs | 22 +- .../PostProcessing/GeneratedCodeWorkspace.cs | 29 +- .../src/PostProcessing/PostProcessor.cs | 686 ------------------ .../src/PostProcessing/ReferenceMap.cs | 70 -- .../src/PostProcessing/ReferenceMapBuilder.cs | 306 -------- .../src/Primitives/TypeProviderWriter.cs | 9 +- .../src/Providers/CanonicalTypeProvider.cs | 20 +- .../src/Providers/InternalHelperProvider.cs | 8 +- .../src/Providers/ModelFactoryProvider.cs | 2 + .../src/Providers/NamedTypeSymbolProvider.cs | 414 +++++++++++ .../src/Providers/TypeProvider.cs | 93 ++- .../ProviderReferenceMapResult.cs | 15 + .../ProviderReferenceMapSession.cs | 28 + .../src/SourceInput/SourceInputHelper.cs | 18 +- .../src/SourceInput/SourceInputModel.cs | 96 ++- .../src/Statements/XmlDocStatement.cs | 2 + .../src/TypeFactory.cs | 20 +- .../src/Utilities/TypeSymbolExtensions.cs | 61 +- .../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 | 7 +- .../ModelFactoriesCustomizationTests.cs | 26 + .../DerivedModel.cs | 9 + .../ClientCustomizationTests.cs | 2 +- .../ModelProviders/ModelProviderTests.cs | 16 +- .../NamedTypeSymbolProviderTests.cs | 89 +++ .../CustomClient.cs | 12 + .../CustomModel.cs | 6 + .../CustomApi.cs | 9 + .../CustomApi.cs | 12 + .../CustomApi.cs | 9 + .../NestedTypes.cs | 19 + .../test/Providers/TypeProviderTests.cs | 6 +- .../LastContract.cs | 17 + .../test/TestHelpers/TestOutputLibrary.cs | 5 + .../GenericContainer.cs | 6 + .../Utilities/TypeSymbolExtensionsTests.cs | 41 ++ .../Local.Tests/UnreferencedTypeTests.cs | 8 +- .../Generated/Internal/BinaryContentHelper.cs | 175 ----- .../PipelineRequestHeadersExtensions.cs | 48 -- .../Internal/Utf8JsonBinaryContent.cs | 61 -- .../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 - 102 files changed, 2331 insertions(+), 2153 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/Providers/TestData/ClientOptionsProviderTests/CustomizedPublicConstructorKeepsNestedServiceVersionPublic/Generated/TestClientOptions.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/CustomizedPublicConstructorKeepsNestedServiceVersionPublic/TestClientOptions.cs 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 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/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/TestData/ProviderReferenceMapAnalyzerTests/LastContractModelFactorySignaturesDoNotRootGeneratedModels/LastContract.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/TestData/TypeSymbolExtensionsTests/TypeParameterDoesNotResolveContainingGenericType/GenericContainer.cs delete mode 100644 packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/BinaryContentHelper.cs delete mode 100644 packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/PipelineRequestHeadersExtensions.cs delete mode 100644 packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/Utf8JsonBinaryContent.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 3a7194ee1c4..9118211f1a6 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 @@ -32,6 +32,9 @@ public class BinaryContentHelperDefinition : InternalHelperProvider 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..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,13 +27,20 @@ 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 = []; internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientProvider) { _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)]; @@ -45,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); @@ -53,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 @@ -62,7 +69,11 @@ internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientPro break; } } + + return serviceVersionsEnums; } + + return null; } /// @@ -77,12 +88,15 @@ public static ClientOptionsProvider CreateClientOptionsProvider(InputClient inpu { if (UseSingletonInstance(inputClient)) { - // Use singleton instance if (_singletonInstance == null) { - // Create singleton with namespace-based naming + _singletonClientProviders.Clear(); _singletonInstance = new ClientOptionsProvider(inputClient, clientProvider); } + if (!_singletonClientProviders.Contains(clientProvider)) + { + _singletonClientProviders.Add(clientProvider); + } return _singletonInstance; } @@ -160,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) @@ -177,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 @@ -205,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) @@ -222,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 @@ -248,6 +264,30 @@ 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) + { + foreach (var clientProvider in _singletonClientProviders) + { + if (clientProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)) + { + return TypeSignatureModifiers.Public; + } + } + + return TypeSignatureModifiers.Internal; + } + + 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)) @@ -289,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 b6e2d9cc78a..b3eec2a1aa7 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 @@ -25,6 +25,8 @@ namespace Microsoft.TypeSpec.Generator.ClientModel.Providers { public class ClientProvider : TypeProvider { + protected override bool IsClientProvider => true; + private record AuthFields(FieldProvider AuthField); private record ApiKeyFields(FieldProvider AuthField, FieldProvider AuthorizationHeaderField, FieldProvider? AuthorizationApiKeyPrefixField) : AuthFields(AuthField); private record OAuth2Fields(FieldProvider AuthField, FieldProvider AuthorizationScopesField) : AuthFields(AuthField); @@ -72,7 +74,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 +112,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) @@ -426,6 +429,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 [ScmCodeModelGenerator.Instance.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..7d5cc8976e6 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,30 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override CSharpType BuildBaseType() => typeof(ModelReaderWriterContext); + // Buildable attributes depend on the final set of providers selected by the reference map. + // They are rebuilt at write time while non-buildable attributes, including visitor updates, are preserved. + 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(); + 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 +73,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 +98,51 @@ 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); + 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 +154,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 +163,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 +261,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 +292,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 +503,23 @@ private static bool ImplementsModelReaderWriter(TypeProvider typeProvider) return false; } + private static bool ShouldAddStandaloneBuildableProvider(TypeProvider provider) + => IsResolvableBuildableType(provider.Type) + && ImplementsModelReaderWriter(provider) + && HasWritableModelReaderWriterSerialization(provider); + + 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 fe2d01a2a94..93321ae9d16 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 @@ -103,6 +103,9 @@ public ModelSerializationExtensionsDefinition() protected override string BuildName() => "ModelSerializationExtensions"; + protected override IReadOnlyList BuildBodyDependencyTypes() => + [ScmCodeModelGenerator.Instance.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 16ac19e14e9..adbb3026dda 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(); @@ -130,6 +121,13 @@ protected override IReadOnlyList BuildMethodsForBackCompatibilit protected override CSharpType? BuildBaseType() => _model.BaseType; + protected override IReadOnlyList BuildHelperDependencyTypes() => _rawDataField != null || _additionalProperties.Value.Length > 0 + ? [ScmCodeModelGenerator.Instance.TypeFactory.DictionaryInitializationType] + : []; + + protected override IReadOnlyList BuildBodyDependencyTypes() => + [ScmCodeModelGenerator.Instance.SystemOptionalDefinition.Type, ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition.Type]; + protected override SuppressionStatement[] BuildDisabledFileWarnings() { if (_model.CanonicalView.Properties.Any(p => ScmModelProvider.IsFileBinaryContentType(p.Type))) @@ -442,19 +440,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); } } @@ -484,7 +480,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 ); @@ -499,7 +495,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 ); @@ -515,7 +511,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 ); @@ -531,7 +527,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 ); @@ -545,7 +541,7 @@ internal MethodProvider BuildJsonModelWriteCoreMethod() MethodSignatureModifiers modifiers = _isStruct ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (ShouldOverrideMethods) + if (_shouldOverrideMethods) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -567,7 +563,7 @@ internal MethodProvider BuildPersistableModelWriteCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (ShouldOverrideMethods && !ShouldSkipSerializationMethodOverrides) + if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -591,7 +587,7 @@ internal MethodProvider BuildPersistableModelCreateCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (ShouldOverrideMethods && !ShouldSkipSerializationMethodOverrides) + if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -639,7 +635,7 @@ internal MethodProvider BuildJsonModelCreateCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (ShouldOverrideMethods && !ShouldSkipSerializationMethodOverrides) + if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -811,7 +807,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 ); @@ -1070,7 +1066,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 3ef2c7c4c62..bc9fa8eeceb 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 @@ -57,6 +57,9 @@ protected override string BuildRelativeFilePath() protected override SuppressionStatement[] BuildDisabledFileWarnings() => [new SuppressionStatement(null, Literal(ScmModelProvider.FileBinaryContentDiagnosticId), ScmModelProvider.ScmEvaluationTypeSuppressionJustification)]; + protected override IReadOnlyList BuildBodyDependencyTypes() => + [ScmCodeModelGenerator.Instance.SystemOptionalDefinition.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/PipelineRequestHeadersExtensionsDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/PipelineRequestHeadersExtensionsDefinition.cs index 12e90056ddb..f0e9d30d217 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/PipelineRequestHeadersExtensionsDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/PipelineRequestHeadersExtensionsDefinition.cs @@ -17,7 +17,7 @@ namespace Microsoft.TypeSpec.Generator.ClientModel.Providers { internal class PipelineRequestHeadersExtensionsDefinition : InternalHelperProvider { - private const string _setDelimited = "SetDelimited"; + internal const string SetDelimitedMethodName = "SetDelimited"; private const string _addWithPrefix = "Add"; private ParameterProvider _pipelineRequestHeadersParam; public PipelineRequestHeadersExtensionsDefinition() @@ -51,7 +51,7 @@ private MethodProvider BuildSetDelimited(bool hasFormat) ? new[] { _pipelineRequestHeadersParam, nameParameter, valueParameter, delimiterParameter, formatParameter } : new[] { _pipelineRequestHeadersParam, nameParameter, valueParameter, delimiterParameter }; MethodSignature signature = new MethodSignature( - Name: _setDelimited, + Name: SetDelimitedMethodName, Modifiers: modifiers, Parameters: parameters, ReturnType: null, 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 a98d5c10b26..aa917efa291 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) { @@ -333,7 +334,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)); } @@ -400,11 +401,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 @@ -977,7 +981,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) @@ -1288,7 +1294,17 @@ internal static List GetMethodParameters( } 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 && @@ -1307,8 +1323,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); } @@ -1350,12 +1366,42 @@ 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, ContentParameterName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + 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; } @@ -1363,15 +1409,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) @@ -1379,8 +1429,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) { @@ -1399,7 +1450,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; } @@ -1419,6 +1470,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, ContentParameterName, 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/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/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/ScmCodeModelGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ScmCodeModelGenerator.cs index a09c248b4f5..7322816975d 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 @@ -31,6 +31,22 @@ internal ModelSerializationExtensionsDefinition ModelSerializationExtensionsDefi internal SerializationFormatDefinition SerializationFormatDefinition { get; } = new SerializationFormatDefinition(); + private SystemOptionalDefinition? _systemOptionalDefinition; + internal SystemOptionalDefinition SystemOptionalDefinition => + _systemOptionalDefinition ??= new SystemOptionalDefinition(); + + private TypeFormattersDefinition? _typeFormattersDefinition; + 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. /// @@ -50,6 +66,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(PipelineRequestHeadersExtensionsDefinition.SetDelimitedMethodName, PipelineRequestHeadersExtensionsDefinition); AddTypeToKeep(ModelReaderWriterContextDefinition.s_name, isRoot: false); AddTypeToKeep(SerializationFormatDefinition, 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 dadaad84144..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) @@ -65,13 +80,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,14 +94,14 @@ protected override TypeProvider[] BuildTypeProviders() ..BuildClientTypes(), ScmCodeModelGenerator.Instance.ModelSerializationExtensionsDefinition, ScmCodeModelGenerator.Instance.SerializationFormatDefinition, - new TypeFormattersDefinition(), + ScmCodeModelGenerator.Instance.TypeFormattersDefinition, new ErrorResultDefinition(), 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/src/Snippets/HttpRequestApiSnippets.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Snippets/HttpRequestApiSnippets.cs index 588a2094b12..d27c45c1fe4 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(PipelineRequestHeadersExtensionsDefinition.SetDelimitedMethodName, parameters, typeArguments: null, callAsAsync: false, extensionType: ScmCodeModelGenerator.Instance.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..f119d0a52a5 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 @@ -383,6 +383,44 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsTrue(body?.Contains("ServiceVersion.V2024_01_01 => \"2024-01-01\"")); } + [Test] + public async Task CustomizedPublicConstructorKeepsNestedServiceVersionPublic() + { + string[] apiVersions = ["2023-10-01-preview-1", "2023-11-01", "2024-01-01"]; + var enumValues = apiVersions.Select((a, index) => (a, a)); + var inputEnum = InputFactory.StringEnum( + "ServiceVersion", + enumValues, + access: string.Empty, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "SampleNamespace"); + var inputClient = InputFactory.Client("RawClient", clientNamespace: "SampleNamespace"); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + apiVersions: () => apiVersions, + inputEnums: () => [inputEnum], + clients: () => [inputClient], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = mockGenerator.Object.OutputLibrary.TypeProviders + .OfType() + .Single(provider => provider.Name == "RawClient"); + var clientOptionsProvider = clientProvider.ClientOptions; + Assert.IsNotNull(clientOptionsProvider); + var serviceVersionProvider = clientOptionsProvider!.NestedTypes.Single(); + Assert.IsTrue(serviceVersionProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsNotNull(clientOptionsProvider.CustomCodeView); + var customConstructor = clientOptionsProvider.CustomCodeView!.Constructors.Single(); + var customServiceVersionType = customConstructor.Signature.Parameters.Single().Type; + Assert.IsEmpty(customServiceVersionType.Namespace); + Assert.IsNull(customServiceVersionType.DeclaringType); + + ProviderReferenceMapAnalyzer.ApplyPreWriteAccessibility(mockGenerator.Object.OutputLibrary.TypeProviders); + + Assert.IsTrue(serviceVersionProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsFalse(serviceVersionProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); + } + [Test] public void SingletonCreatedForMultipleClientsWithStandardParameters() { @@ -502,6 +540,75 @@ public void MultipleClientsWithRequiredCustomParametersShareSingletonOptions() Assert.AreEqual("SampleClientOptions", options1!.Name); } + [Test] + public async Task SharedSingletonOptionsArePublicWhenAnyClientIsPublic() + { + 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\"}"); + + 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] 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..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 @@ -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,113 @@ 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 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() + { + 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 +1153,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)]; } } @@ -1335,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 @@ -1554,6 +1731,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 +1979,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 cdfc4b96c40..bd229d4f9d9 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.Expressions; @@ -104,6 +105,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() { @@ -273,6 +289,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] @@ -363,6 +430,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() { @@ -739,38 +935,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() { @@ -1779,6 +1943,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/Providers/TestData/ClientOptionsProviderTests/CustomizedPublicConstructorKeepsNestedServiceVersionPublic/Generated/TestClientOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/CustomizedPublicConstructorKeepsNestedServiceVersionPublic/Generated/TestClientOptions.cs new file mode 100644 index 00000000000..b21ea1df8bf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/CustomizedPublicConstructorKeepsNestedServiceVersionPublic/Generated/TestClientOptions.cs @@ -0,0 +1,16 @@ +#nullable disable + +using System.ClientModel.Primitives; + +namespace SampleNamespace +{ + public partial class TestClientOptions : ClientPipelineOptions + { + public enum ServiceVersion + { + V2023_10_01_Preview_1 = 0, + V2023_11_01 = 1, + V2024_01_01 = 2 + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/CustomizedPublicConstructorKeepsNestedServiceVersionPublic/TestClientOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/CustomizedPublicConstructorKeepsNestedServiceVersionPublic/TestClientOptions.cs new file mode 100644 index 00000000000..5deb0d63350 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/CustomizedPublicConstructorKeepsNestedServiceVersionPublic/TestClientOptions.cs @@ -0,0 +1,15 @@ +#nullable disable + +using System.ClientModel.Primitives; +using Microsoft.TypeSpec.Generator.Customizations; + +namespace SampleNamespace +{ + [CodeGenType("RawClientOptions")] + public partial class TestClientOptions : ClientPipelineOptions + { + public TestClientOptions(ServiceVersion version) + { + } + } +} 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 + { + } +} 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 f258eba71f2..7061abfefb1 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; } @@ -77,7 +78,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() ?? []; @@ -151,7 +153,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 }; @@ -192,8 +194,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..94abf17e68e 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,69 @@ 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())); - - foreach (var serialization in outputType.SerializationProviders) + try + { + using var referenceMapSession = ProviderReferenceMapAnalyzer.PrepareForGeneration(output.TypeProviders); + 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 +173,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 +225,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 (!fileNamesToKeep.Contains(file.Name)) { file.Delete(); } 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..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); @@ -242,7 +262,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 c1c2678e1d3..740e000451c 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 9d84d3494c2..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/PostProcessor.cs +++ /dev/null @@ -1,686 +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) - || IsAdditionalNonRootType(symbol)) - { - 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))); - } - - private bool IsAdditionalNonRootType(INamedTypeSymbol symbol) - { - if (_additionalNonRootTypeNames.Contains(symbol.Name)) - { - return true; - } - - var fullyQualifiedName = symbol.GetFullyQualifiedName(); - if (_additionalNonRootTypeNames.Contains(fullyQualifiedName)) - { - return true; - } - - var genericArityIndex = fullyQualifiedName.IndexOf('`'); - return genericArityIndex > 0 - && _additionalNonRootTypeNames.Contains(fullyQualifiedName[..genericArityIndex]); - } - - 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..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 @@ -45,11 +45,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 _provider.GetAttributesForWrite()) { attribute.Write(writer); if (attribute is AttributeStatement) @@ -207,6 +207,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..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,7 +48,25 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu protected override IReadOnlyList BuildAttributes() { - return [.. _generatedTypeProvider.Attributes, .. _generatedTypeProvider.CustomCodeView?.Attributes ?? []]; + // TODO https://github.com/microsoft/typespec/issues/11232: Move this generated/custom attribute merge into a shared TypeProvider API. + return DeduplicateAttributes( + _generatedTypeProvider.Attributes, + _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 protected override CanonicalTypeProvider BuildCanonicalView() => this; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/InternalHelperProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/InternalHelperProvider.cs index d11a3490a4a..a945b299951 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/InternalHelperProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/InternalHelperProvider.cs @@ -1,21 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; namespace Microsoft.TypeSpec.Generator.Providers { /// - /// A base provider for generator-owned internal helper types that must not be removed during post-processing. + /// A base provider for generator-owned internal helper types. /// public abstract class InternalHelperProvider : TypeProvider { - protected InternalHelperProvider(InputType? inputType = default) : base(inputType) - { - CodeModelGenerator.Instance.AddTypeToKeep(this, isRoot: false); - } - protected override TypeSignatureModifiers BuildDeclarationModifiers() => TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; } 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 ae2495a64ba..f4fbf891762 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 @@ -30,6 +30,8 @@ protected internal ModelFactoryProvider(IEnumerable models) _models = models; } + internal bool PreserveLeadingMethodSeparator { get; set; } + 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 5f0faf734e7..34b7e129827 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,40 @@ 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; + protected override CSharpType[] GetTypeArguments() => + [.. _namedTypeSymbol.TypeParameters.Select(parameter => parameter.GetCSharpType())]; + + 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; @@ -314,6 +347,9 @@ protected internal override MethodProvider[] BuildMethods() GetNullableCSharpType(methodSymbol.ReturnType), GetSymbolXmlDoc(methodSymbol, "returns"), [.. methodSymbol.Parameters.Select(p => ConvertToParameterProvider(methodSymbol, p))], + GenericArguments: methodSymbol.TypeParameters.IsEmpty + ? null + : [.. methodSymbol.TypeParameters.Select(parameter => parameter.GetCSharpType())], ExplicitInterface: explicitInterface?.ContainingType?.GetCSharpType()); methods.Add(new MethodProvider(signature, MethodBodyStatement.Empty, this)); @@ -321,6 +357,384 @@ [.. 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()) + { + var invocationName = GetInvocationName(invocation); + if (invocationName == null) + { + continue; + } + + foreach (var provider in CodeModelGenerator.Instance.GetCustomCodeMethodDependencies(invocationName)) + { + dependencies.Add(provider.Type); + } + } + } + + 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 83380c4299b..31bc0d3fd63 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 @@ -22,6 +22,7 @@ public abstract class TypeProvider private Lazy _lastContractView; private Lazy _canonicalView; private Lazy _specView; + private Lazy _declaringTypeName; private readonly InputType? _inputType; protected TypeProvider(InputType? inputType = default) @@ -30,6 +31,7 @@ protected TypeProvider(InputType? inputType = default) _canonicalView = new(BuildCanonicalView); _lastContractView = new(() => BuildLastContractView()); _specView = new(BuildSpecView); + _declaringTypeName = new(() => GetDeclaringTypeName(DeclaringTypeProvider)); _inputType = inputType; } @@ -44,14 +46,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); + _declaringTypeName.Value); 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); + _declaringTypeName.Value); + + 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 +157,17 @@ public XmlDocProvider XmlDocs private set => _xmlDocs = value; } + internal bool PreserveTypeXmlDocs { get; private set; } + + protected internal virtual bool ShouldWriteTypeXmlDocs => false; + + protected internal virtual bool IsClientProvider => false; + + internal void PreserveXmlDocs() + { + PreserveTypeXmlDocs = true; + } + public string? Deprecated { get => _deprecated; @@ -300,7 +325,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; @@ -323,6 +362,31 @@ public IReadOnlyList Attributes internal IReadOnlyList GetAttributes() => _attributes ??= BuildAttributes(); + internal IReadOnlyList GetAttributesForWrite() => BuildAttributesForWrite(); + + /// + /// 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; + + /// + /// 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) @@ -505,6 +569,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() => []; @@ -548,8 +626,10 @@ public virtual void Reset() _serializationProviders = null; _nestedTypes = null; _xmlDocs = null; + PreserveTypeXmlDocs = false; _declarationModifiers = null; _relativeFilePath = null; + _declaringTypeName = new(() => GetDeclaringTypeName(DeclaringTypeProvider)); _customCodeView = new(() => BuildCustomCodeView()); _canonicalView = new(BuildCanonicalView); _lastContractView = new(() => BuildLastContractView()); @@ -622,7 +702,7 @@ public void Update( } if (serializations != null) { - _serializationProviders = (serializations as IReadOnlyList) ?? serializations.ToList(); + _serializationProviders = AssignSerializationProviderOwners(serializations); } if (nestedTypes != null) { @@ -642,7 +722,7 @@ public void Update( } if (attributes != null) { - _attributes = (attributes as IReadOnlyList) ?? [.. attributes]; + _attributes = [.. attributes]; } if (name != null) @@ -661,6 +741,7 @@ public void Update( private void ResetMembersBasedOnIdentityChange(string? name = null, string? @namespace = null) { + _declaringTypeName = new(() => GetDeclaringTypeName(DeclaringTypeProvider)); // Reset the custom code view to reflect the new namespace _customCodeView = new(BuildCustomCodeView(name ?? Type.Name, @namespace ?? Type.Namespace)); name = _customCodeView.Value?.Name ?? name ?? Type.Name; 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..30519d304e2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapResult.cs @@ -0,0 +1,15 @@ +// 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, + HashSet Nodes) + { + } +} 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..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 @@ -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,33 @@ 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) + private IReadOnlyList PopulateCustomizationTypeProviders() { - if (compilation == null) + 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 CustomizationTypeProviders => _customizationTypeProviders.Value; + private static INamedTypeSymbol? FindNamedTypeSymbol(Compilation compilation, bool includeReferencedAssemblies, string fullyQualifiedMetadataName) => includeReferencedAssemblies ? compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) @@ -108,7 +111,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 +121,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 +168,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/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) "", "", "", + "", + "", "", "", " _changeTrackingDictionaryProvider ??= new(); private ChangeTrackingDictionaryDefinition? _changeTrackingDictionaryProvider; + private OptionalDefinition? _optionalProvider; + private OptionalDefinition OptionalProvider => _optionalProvider ??= new(); + private Dictionary InputTypeToModelProvider { get; } = []; public IDictionary CSharpTypeMap { get; } = new Dictionary(CSharpType.IgnoreNullableComparer); @@ -42,8 +45,6 @@ private ChangeTrackingDictionaryDefinition ChangeTrackingDictionaryProvider private IReadOnlyList Visitors => CodeModelGenerator.Instance.Visitors; private Dictionary> SerializationsCache { get; } = []; - internal HashSet UnionVariantTypesToKeep { get; } = []; - protected internal TypeFactory() { } @@ -101,11 +102,6 @@ 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); - } } } type = CSharpType.FromUnion(unionInputs); @@ -303,15 +299,15 @@ protected virtual ModelFactoryProvider CreateModelFactoryCore(IEnumerable enumProvider, }; - if (enumType.Access == "public") - { - CodeModelGenerator.Instance.AddTypeToKeep(enumProvider); - } - EnumCache.Add(enumCacheKey, enumProvider); if (enumProvider != null) { + if (enumType.Access == "public") + { + CodeModelGenerator.Instance.AddTypeToKeep(enumProvider); + } + CSharpTypeMap[enumProvider.Type] = enumProvider; TypeProvidersByName[enumProvider.Type.Name] = enumProvider; } 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/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 4fd64c9b4c6..4a9a35dea67 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 @@ -334,7 +334,7 @@ public void ExtensibleFloatEnum_HasOnlyNonNullableImplicitOperator() } [Test] - public void PublicModelsAreIncludedInAdditionalRootTypes() + public void PublicEnumsAreIncludedInAdditionalRootTypes() { var inputEnum = InputFactory.StringEnum( "StringEnum", @@ -349,6 +349,11 @@ public void PublicModelsAreIncludedInAdditionalRootTypes() var rootTypes = CodeModelGenerator.Instance.AdditionalRootTypes; Assert.IsTrue(rootTypes.Contains("Sample.Models.StringEnum")); + + using var session = ProviderReferenceMapAnalyzer.PrepareForGeneration( + CodeModelGenerator.Instance.OutputLibrary.TypeProviders.ToList()); + Assert.IsTrue(enumProvider!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(enumProvider)); } [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 e637bd4e70a..88ab6c206b8 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 @@ -1558,6 +1558,11 @@ public void PublicModelsAreIncludedInAdditionalRootTypes() var rootTypes = CodeModelGenerator.Instance.AdditionalRootTypes; Assert.IsTrue(rootTypes.Contains("Sample.Models.MockInputModel")); + + using var session = ProviderReferenceMapAnalyzer.PrepareForGeneration( + CodeModelGenerator.Instance.OutputLibrary.TypeProviders.ToList()); + Assert.IsTrue(modelProvider!.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)); + Assert.IsTrue(ProviderReferenceMapAnalyzer.ShouldWriteProvider(modelProvider)); } [Test] @@ -1597,12 +1602,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..2310e2ea7b6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs @@ -6,9 +6,11 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; @@ -93,6 +95,31 @@ public async Task ValidateSelfReferentialGenericBaseType() Assert.IsNull(namedTypeSymbolProvider.Type.BaseType); } + [Test] + public void ValidateGenericTypeAndMethodArguments() + { + var compilation = CSharpCompilation.Create( + "Customization", + [CSharpSyntaxTree.ParseText(""" + namespace Sample + { + public class GenericType + { + public TMethod Convert(TType value, TMethod fallback) => fallback; + } + } + """)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + var symbol = compilation.GetTypeByMetadataName("Sample.GenericType`1"); + Assert.IsNotNull(symbol); + + var provider = new NamedTypeSymbolProvider(symbol!, compilation); + var method = provider.Methods.Single(method => method.Signature.Name == "Convert"); + + Assert.AreEqual("TType", provider.Type.Arguments.Single().Name); + Assert.AreEqual("TMethod", method.Signature.GenericArguments!.Single().Name); + } + [Test] public void ValidateNamespaceNestedType() { @@ -370,6 +397,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 00d37d01d8b..346e83dee11 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 @@ -900,7 +900,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); @@ -1224,14 +1224,14 @@ public void TestSpecViewIsNotNull() } [Test] - public void InternalHelperProviderIsKeptAsNonRoot() + public void InternalHelperProviderHasInternalStaticModifiers() { var typeProvider = new TestInternalHelperProvider(); Assert.IsTrue(typeProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)); Assert.IsTrue(typeProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static)); Assert.IsFalse(CodeModelGenerator.Instance.AdditionalRootTypes.Contains(typeProvider.Type.FullyQualifiedName)); - Assert.IsTrue(CodeModelGenerator.Instance.NonRootTypes.Contains(typeProvider.Type.FullyQualifiedName)); + Assert.IsFalse(CodeModelGenerator.Instance.NonRootTypes.Contains(typeProvider.Type.FullyQualifiedName)); } [Test] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/LastContractModelFactorySignaturesDoNotRootGeneratedModels/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/LastContractModelFactorySignaturesDoNotRootGeneratedModels/LastContract.cs new file mode 100644 index 00000000000..b91419eb56e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ReferenceMap/TestData/ProviderReferenceMapAnalyzerTests/LastContractModelFactorySignaturesDoNotRootGeneratedModels/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/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/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/TestProjects/Local.Tests/UnreferencedTypeTests.cs b/packages/http-client-csharp/generator/TestProjects/Local.Tests/UnreferencedTypeTests.cs index b664bf1d2c4..44eeaceec62 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local.Tests/UnreferencedTypeTests.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local.Tests/UnreferencedTypeTests.cs @@ -11,12 +11,12 @@ namespace TestProjects.Local.Tests public class UnreferencedTypeTests { [Test] - public void InternalHelperTypesAreKept() + public void UnreferencedTypesAreRemoved() { var types = Assembly.GetAssembly(typeof(SampleTypeSpecClient))!.GetTypes(); - Assert.IsTrue(types.Any(t => t.Name == "BinaryContentHelper")); - Assert.IsTrue(types.Any(t => t.Name == "PipelineRequestHeadersExtensions")); - Assert.IsTrue(types.Any(t => t.Name == "Utf8JsonBinaryContent")); + Assert.IsFalse(types.Any(t => t.Name == "BinaryContentHelper")); + Assert.IsFalse(types.Any(t => t.Name == "PipelineRequestHeadersExtensions")); + Assert.IsFalse(types.Any(t => t.Name == "Utf8JsonBinaryContent")); } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/BinaryContentHelper.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/BinaryContentHelper.cs deleted file mode 100644 index b76cf24aa37..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/BinaryContentHelper.cs +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT License. - -// - -#nullable disable - -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using System.Xml; - -namespace SampleTypeSpec -{ - internal static partial class BinaryContentHelper - { - /// - public static BinaryContent FromEnumerable(IEnumerable enumerable) - where T : notnull - { - Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); - content.JsonWriter.WriteStartArray(); - foreach (var item in enumerable) - { - content.JsonWriter.WriteObjectValue(item, ModelSerializationExtensions.WireOptions); - } - content.JsonWriter.WriteEndArray(); - - return content; - } - - /// - public static BinaryContent FromEnumerable(IEnumerable enumerable) - { - Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); - content.JsonWriter.WriteStartArray(); - foreach (var item in enumerable) - { - if (item == null) - { - content.JsonWriter.WriteNullValue(); - } - else - { -#if NET6_0_OR_GREATER - content.JsonWriter.WriteRawValue(item); -#else - using (JsonDocument document = JsonDocument.Parse(item)) - { - JsonSerializer.Serialize(content.JsonWriter, document.RootElement); - } -#endif - } - } - content.JsonWriter.WriteEndArray(); - - return content; - } - - /// - public static BinaryContent FromEnumerable(ReadOnlySpan span) - where T : notnull - { - Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); - content.JsonWriter.WriteStartArray(); - int i = 0; - for (; i < span.Length; i++) - { - content.JsonWriter.WriteObjectValue(span[i], ModelSerializationExtensions.WireOptions); - } - content.JsonWriter.WriteEndArray(); - - return content; - } - - /// - public static BinaryContent FromDictionary(IDictionary dictionary) - where TValue : notnull - { - Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); - content.JsonWriter.WriteStartObject(); - foreach (var item in dictionary) - { - content.JsonWriter.WritePropertyName(item.Key); - content.JsonWriter.WriteObjectValue(item.Value, ModelSerializationExtensions.WireOptions); - } - content.JsonWriter.WriteEndObject(); - - return content; - } - - /// - public static BinaryContent FromDictionary(IDictionary dictionary) - { - Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); - content.JsonWriter.WriteStartObject(); - foreach (var item in dictionary) - { - content.JsonWriter.WritePropertyName(item.Key); - if (item.Value == null) - { - content.JsonWriter.WriteNullValue(); - } - else - { -#if NET6_0_OR_GREATER - content.JsonWriter.WriteRawValue(item.Value); -#else - using (JsonDocument document = JsonDocument.Parse(item.Value)) - { - JsonSerializer.Serialize(content.JsonWriter, document.RootElement); - } -#endif - } - } - content.JsonWriter.WriteEndObject(); - - return content; - } - - /// - public static BinaryContent FromObject(object value) - { - Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); - content.JsonWriter.WriteObjectValue(value, ModelSerializationExtensions.WireOptions); - return content; - } - - /// - public static BinaryContent FromObject(BinaryData value) - { - Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); -#if NET6_0_OR_GREATER - content.JsonWriter.WriteRawValue(value); -#else - using (JsonDocument document = JsonDocument.Parse(value)) - { - JsonSerializer.Serialize(content.JsonWriter, document.RootElement); - } -#endif - return content; - } - - /// - /// - /// - public static BinaryContent FromEnumerable(IEnumerable enumerable, string rootNameHint, string childNameHint) - where T : notnull - { - using (MemoryStream stream = new MemoryStream(256)) - { - using (XmlWriter writer = XmlWriter.Create(stream, ModelSerializationExtensions.XmlWriterSettings)) - { - writer.WriteStartElement(rootNameHint); - foreach (var item in enumerable) - { - writer.WriteObjectValue(item, ModelSerializationExtensions.WireOptions, childNameHint); - } - writer.WriteEndElement(); - } - - if (stream.Position > int.MaxValue) - { - return BinaryContent.Create(BinaryData.FromStream(stream)); - } - else - { - return BinaryContent.Create(new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position))); - } - } - } - } -} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/PipelineRequestHeadersExtensions.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/PipelineRequestHeadersExtensions.cs deleted file mode 100644 index 69ddd4aee41..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/PipelineRequestHeadersExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT License. - -// - -#nullable disable - -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Linq; - -namespace SampleTypeSpec -{ - internal static partial class PipelineRequestHeadersExtensions - { - /// - /// The name. - /// The value. - /// The delimiter. - public static void SetDelimited(this PipelineRequestHeaders headers, string name, IEnumerable value, string delimiter) - { - IEnumerable stringValues = value.Select(v => TypeFormatters.ConvertToString(v)); - headers.Set(name, string.Join(delimiter, stringValues)); - } - - /// - /// The name. - /// The value. - /// The delimiter. - /// The format. - public static void SetDelimited(this PipelineRequestHeaders headers, string name, IEnumerable value, string delimiter, SerializationFormat format) - { - IEnumerable stringValues = value.Select(v => TypeFormatters.ConvertToString(v, format)); - headers.Set(name, string.Join(delimiter, stringValues)); - } - - /// - /// The prefix to prepend to each header key. - /// The dictionary of headers to add. - public static void Add(this PipelineRequestHeaders headers, string prefix, IDictionary value) - { - foreach (var header in value) - { - headers.Add(prefix + header.Key, header.Value); - } - } - } -} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/Utf8JsonBinaryContent.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/Utf8JsonBinaryContent.cs deleted file mode 100644 index f4586e305fc..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Internal/Utf8JsonBinaryContent.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT License. - -// - -#nullable disable - -using System.ClientModel; -using System.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace SampleTypeSpec -{ - internal partial class Utf8JsonBinaryContent : BinaryContent - { - private readonly MemoryStream _stream; - private readonly BinaryContent _content; - - public Utf8JsonBinaryContent() - { - _stream = new MemoryStream(); - _content = Create(_stream); - JsonWriter = new Utf8JsonWriter(_stream); - } - - /// Gets the JsonWriter. - public Utf8JsonWriter JsonWriter { get; } - - /// The stream containing the data to be written. - /// The cancellation token to use. - public override async Task WriteToAsync(Stream stream, CancellationToken cancellationToken = default) - { - await JsonWriter.FlushAsync().ConfigureAwait(false); - await _content.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); - } - - /// The stream containing the data to be written. - /// The cancellation token to use. - public override void WriteTo(Stream stream, CancellationToken cancellationToken = default) - { - JsonWriter.Flush(); - _content.WriteTo(stream, cancellationToken); - } - - /// - public override bool TryComputeLength(out long length) - { - length = JsonWriter.BytesCommitted + JsonWriter.BytesPending; - return true; - } - - public override void Dispose() - { - JsonWriter.Dispose(); - _content.Dispose(); - _stream.Dispose(); - } - } -} diff --git a/packages/http-client-csharp/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 {