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/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 8ece3a31fa3..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 @@ -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,21 @@ 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); - if (type.IsUnion) + 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 && + type.UnionItemTypeReferenceKind != UnionItemTypeReferenceKind.MetadataOnly && + (unionItemTypeExclusions == null || + type.UnionItemTypeReferenceKind == UnionItemTypeReferenceKind.PublicSurface)) { 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.Candidates.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Candidates.cs index 622aa980a89..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); @@ -422,13 +423,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 +505,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/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/ReferenceMap/ProviderReferenceMapAnalyzer.Helpers.cs index ac67a94f7c7..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 @@ -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) @@ -573,7 +609,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 +623,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 +678,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 +696,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 +713,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 +731,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 +747,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 +768,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); @@ -874,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); @@ -888,6 +937,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..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 @@ -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( @@ -297,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); @@ -373,7 +380,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 +406,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 +429,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 +450,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 +460,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 +475,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 +489,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 +504,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/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 945f24e2d9f..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 @@ -193,6 +193,116 @@ 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 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() + { + 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() { @@ -270,6 +380,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() { @@ -739,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() { @@ -771,7 +1015,7 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public async Task LastContractNestedPublicDeclarationDoesNotRootGeneratedType() + public async Task LastContractNestedPublicDeclarationRootsGeneratedType() { var lastContractCompilation = CSharpCompilation.Create( "LastContract", @@ -796,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)); @@ -1097,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() {