Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ff55509
Profile C# post-processing benchmarks
live1206 Jun 11, 2026
f022e78
Add provider reference map shadow replacement
live1206 Jun 12, 2026
f88a64a
Revert "Profile C# post-processing benchmarks"
live1206 Jun 12, 2026
2d57265
Remove hybrid map diagnostics from generator
live1206 Jun 12, 2026
dc145f4
Rename hybrid reference map components
live1206 Jun 12, 2026
0cc3abf
Document hybrid reference map decisions
live1206 Jun 12, 2026
c2abe79
Fix hybrid reference map CI failures
live1206 Jun 12, 2026
c463995
Track serialization providers in hybrid reference map
live1206 Jun 12, 2026
9a1d1a1
Handle model factory subclasses in reference map
live1206 Jun 12, 2026
2917111
Track generated body type references
live1206 Jun 15, 2026
2cda184
Match Roslyn cleanup for body references
live1206 Jun 15, 2026
ce16922
Remove unused serialization providers
live1206 Jun 15, 2026
7a32814
Merge remote-tracking branch 'origin/main' into mtg-hybrid-reference-map
live1206 Jun 15, 2026
5a8248f
Preserve public discriminator subtypes
live1206 Jun 15, 2026
43529f3
Use explicit serialization helper dependencies
live1206 Jun 15, 2026
21ecc1a
Use explicit collection result dependencies
live1206 Jun 15, 2026
f3259a4
Use explicit client body dependencies
live1206 Jun 15, 2026
b6e8aad
Track rest client helper dependencies
live1206 Jun 15, 2026
48885cb
Skip stubbed client body dependencies
live1206 Jun 15, 2026
c6539de
Internalize models from final API reachability
live1206 Jun 16, 2026
f676243
Update model root visibility test
live1206 Jun 16, 2026
0a37522
Detect custom model factory providers
live1206 Jun 16, 2026
582fa84
Preserve file header when removing usings
live1206 Jun 16, 2026
74be26d
Limit derived public reachability to discriminators
live1206 Jun 16, 2026
dc6f0f2
Ignore custom roots for derived public reachability
live1206 Jun 16, 2026
7c20ef2
Propagate public discriminator reachability transitively
live1206 Jun 17, 2026
2e4af08
Keep enum serialization helpers reachable
live1206 Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,60 @@ private IReadOnlyList<ParameterProvider> GetClientParameters()

protected override string BuildName() => _inputClient.IsExactName ? _inputClient.Name : _inputClient.Name.ToIdentifierName();

protected override IReadOnlyList<CSharpType> BuildBodyDependencyTypes()
{
var dependencies = new List<CSharpType>();
foreach (var method in Methods.OfType<ScmMethodProvider>())
{
if (method.BodyStatements == null)
{
continue;
}

if (method.CollectionDefinition != null)
{
dependencies.Add(method.CollectionDefinition.Type);
}

if (method.ServiceMethod == null)
{
continue;
}

AddInputTypeDependency(dependencies, method.ServiceMethod.Response.Type);
AddInputTypeDependency(dependencies, method.ServiceMethod.Exception?.Type);
foreach (var parameter in method.ServiceMethod.Parameters)
{
AddInputTypeDependency(dependencies, parameter.Type);
}

foreach (var parameter in method.ServiceMethod.Operation.Parameters)
{
AddInputTypeDependency(dependencies, parameter.Type);
}

foreach (var response in method.ServiceMethod.Operation.Responses)
{
AddInputTypeDependency(dependencies, response.BodyType);
foreach (var header in response.Headers)
{
AddInputTypeDependency(dependencies, header.Type);
}
}
}

return dependencies;
}

private static void AddInputTypeDependency(List<CSharpType> dependencies, InputType? inputType)
{
var type = inputType == null ? null : ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputType);
if (type != null)
{
dependencies.Add(type);
}
}

protected override FieldProvider[] BuildFields()
{
List<FieldProvider> fields = [EndpointField];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,22 @@ private bool HasPagingOperationNameCollision(string operationName)
protected override TypeSignatureModifiers BuildDeclarationModifiers()
=> TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class;

protected override IReadOnlyList<CSharpType> BuildBodyDependencyTypes()
{
var dependencies = new List<CSharpType> { 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() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m

protected override CSharpType? BuildBaseType() => _model.BaseType;

protected override IReadOnlyList<string> BuildHelperDependencyNames() => _rawDataField != null || _additionalProperties.Value.Length > 0
? ["ChangeTrackingDictionary"]
: [];

protected override SuppressionStatement[] BuildDisabledFileWarnings()
{
if (_model.CanonicalView.Properties.Any(p => ScmModelProvider.IsFileBinaryContentType(p.Type)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ protected override string BuildRelativeFilePath()
return Path.Combine("src", "Generated", "Models", $"{Name}.Serialization.Multipart.cs");
}

protected override IReadOnlyList<string> BuildHelperDependencyNames() => _model.Properties.Any(
prop => prop.WireInfo != null && !prop.WireInfo.IsRequired &&
(prop.Type is { IsCollection: true, IsReadOnlyMemory: false } || prop.Type.IsDictionary))
? ["Optional"]
: [];

protected override SuppressionStatement[] BuildDisabledFileWarnings()
=> [new SuppressionStatement(null, Literal(ScmModelProvider.FileBinaryContentDiagnosticId), ScmModelProvider.ScmEvaluationTypeSuppressionJustification)];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,33 @@ protected override FieldProvider[] BuildFields()
return [.. pipelineMessage20xClassifiersFields];
}

protected override IReadOnlyList<string> BuildHelperDependencyNames()
{
var dependencies = new HashSet<string>(StringComparer.Ordinal);
foreach (var serviceMethod in _inputClient.Methods)
{
foreach (var parameter in serviceMethod.Operation.Parameters)
{
if (parameter is not InputHeaderParameter and not InputQueryParameter)
{
continue;
}

var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(parameter.Type);
if (type?.IsDictionary == true)
{
dependencies.Add("ChangeTrackingDictionary");
}
else if (type?.IsCollection == true)
{
dependencies.Add("ChangeTrackingList");
}
}
}

return [.. dependencies];
}

protected override ScmMethodProvider[] BuildMethods()
{
List<ScmMethodProvider> methods = new List<ScmMethodProvider>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Tests.Common;
using NUnit.Framework;

namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.PostProcessing
{
public class ClientBodyDependencyPostProcessingTests
{
[Test]
public async Task OperationBodyParameterModelDoesNotBecomePublic()
{
var requestModel = InputFactory.Model("RequestBody");
var parameter = InputFactory.BodyParameter("body", requestModel, isRequired: true);
var operation = InputFactory.Operation("Create", parameters: [parameter], httpMethod: "POST");
var method = InputFactory.BasicServiceMethod("Create", operation);
var client = InputFactory.Client("TestClient", methods: [method]);

await GenerateAndAssertInternalModels([requestModel], [client], ["RequestBody"]);
}

[Test]
public async Task OperationResponseBodyModelRemainsPublicAsRootOutputModel()
{
var responseModel = InputFactory.Model("ResponseBody");
var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: responseModel)]);
var method = InputFactory.BasicServiceMethod(
"Get",
operation,
response: InputFactory.ServiceMethodResponse(InputPrimitiveType.String, []));
var client = InputFactory.Client("TestClient", methods: [method]);

await GenerateAndAssertPublicModels([responseModel], [client], ["ResponseBody"]);
}

[Test]
public async Task NestedBodyModelGraphDoesNotBecomePublic()
{
var nestedModel = InputFactory.Model("NestedToolParameter");
var toolModel = InputFactory.Model(
"ToolConfig",
properties: [InputFactory.Property("Parameter", nestedModel)]);
var parameter = InputFactory.BodyParameter("tool", toolModel, isRequired: true);
var operation = InputFactory.Operation("Configure", parameters: [parameter], httpMethod: "POST");
var method = InputFactory.BasicServiceMethod("Configure", operation);
var client = InputFactory.Client("TestClient", methods: [method]);

await GenerateAndAssertInternalModels([toolModel, nestedModel], [client], ["ToolConfig", "NestedToolParameter"]);
}

[Test]
public async Task NonDiscriminatorDerivedBodyModelDoesNotBecomePublicFromPublicBase()
{
var baseTool = InputFactory.Model("BaseTool");
var concreteTool = InputFactory.Model(
"ConcreteTool",
properties: [InputFactory.Property("Name", InputPrimitiveType.String)],
baseModel: baseTool);
var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: baseTool)]);
var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(baseTool, []));
var client = InputFactory.Client("TestClient", methods: [method]);

await GenerateAndAssertMixedModels(
[baseTool, concreteTool],
[client],
publicModelNames: ["BaseTool"],
internalModelNames: ["ConcreteTool"]);
}

[Test]
public async Task CustomizedEnumSerializationProviderIsKeptWhenModelSerializationUsesEnum()
{
var statusEnum = InputFactory.StringEnum(
"Status",
[("Succeeded", "succeeded"), ("Failed", "failed")],
clientNamespace: "Sample");
var resultModel = InputFactory.Model(
"OperationResult",
properties: [InputFactory.Property("Status", statusEnum, isRequired: true)],
@namespace: "Sample");
var operation = InputFactory.Operation("Get", responses: [InputFactory.OperationResponse(bodytype: resultModel)]);
var method = InputFactory.BasicServiceMethod("Get", operation, response: InputFactory.ServiceMethodResponse(resultModel, []));
var client = InputFactory.Client("TestClient", methods: [method], clientNamespace: "Sample");

await GenerateAndAssertFiles(
enums: [statusEnum],
models: [resultModel],
clients: [client],
customFiles: [
(Path.Combine("src", "Custom", "Status.cs"), """
namespace Sample;

[CodeGenType("Status")]
public enum Status
{
Succeeded,
Failed
}
""")
],
expectedFiles: [Path.Combine("src", "Generated", "Models", "Status.Serialization.cs")]);
}

private static async Task GenerateAndAssertInternalModels(
InputModelType[] models,
InputClient[] clients,
string[] modelNames)
=> await GenerateAndAssertModels(models, clients, modelNames, shouldBePublic: false);

private static async Task GenerateAndAssertPublicModels(
InputModelType[] models,
InputClient[] clients,
string[] modelNames)
=> await GenerateAndAssertModels(models, clients, modelNames, shouldBePublic: true);

private static async Task GenerateAndAssertMixedModels(
InputModelType[] models,
InputClient[] clients,
string[] publicModelNames,
string[] internalModelNames)
=> await GenerateAndAssertModels(models, clients, publicModelNames, internalModelNames);

private static async Task GenerateAndAssertModels(
InputModelType[] models,
InputClient[] clients,
string[] modelNames,
bool shouldBePublic)
=> await GenerateAndAssertModels(
models,
clients,
shouldBePublic ? modelNames : [],
shouldBePublic ? [] : modelNames);

private static async Task GenerateAndAssertModels(
InputModelType[] models,
InputClient[] clients,
string[] publicModelNames,
string[] internalModelNames)
{
await GenerateAndAssertFiles(
enums: [],
models: models,
clients: clients,
customFiles: [],
publicModelNames: publicModelNames,
internalModelNames: internalModelNames,
expectedFiles: []);
}

private static async Task GenerateAndAssertFiles(
InputEnumType[] enums,
InputModelType[] models,
InputClient[] clients,
(string Path, string Content)[] customFiles,
string[] expectedFiles,
string[] publicModelNames = null!,
string[] internalModelNames = null!)
{
publicModelNames ??= [];
internalModelNames ??= [];

var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(outputPath);
try
{
foreach (var customFile in customFiles)
{
var customPath = Path.Combine(outputPath, customFile.Path);
Directory.CreateDirectory(Path.GetDirectoryName(customPath)!);
File.WriteAllText(customPath, customFile.Content);
}

await MockHelpers.LoadMockGeneratorAsync(
inputEnums: () => enums,
inputModels: () => models,
clients: () => clients,
configuration: "{\"package-name\": \"Sample\", \"disable-xml-docs\": true}",
outputPath: outputPath);

await new CSharpGen().ExecuteAsync();

foreach (var modelName in publicModelNames)
{
var modelPath = Path.Combine(outputPath, "src", "Generated", "Models", $"{modelName}.cs");
Assert.IsTrue(File.Exists(modelPath), $"Expected generated model file '{modelPath}'.");
var text = File.ReadAllText(modelPath);
StringAssert.Contains($"public partial class {modelName}", text, $"{modelName} should be public.");
}

foreach (var modelName in internalModelNames)
{
var modelPath = Path.Combine(outputPath, "src", "Generated", "Models", $"{modelName}.cs");
Assert.IsTrue(File.Exists(modelPath), $"Expected generated model file '{modelPath}'.");
var text = File.ReadAllText(modelPath);
StringAssert.Contains($"internal partial class {modelName}", text, $"{modelName} should be internal.");
StringAssert.DoesNotContain($"public partial class {modelName}", text, $"{modelName} should not be public.");
}

var modelFactoryPath = Path.Combine(outputPath, "src", "Generated", "SampleModelFactory.cs");
if (File.Exists(modelFactoryPath))
{
var modelFactoryText = File.ReadAllText(modelFactoryPath);
foreach (var modelName in publicModelNames)
{
StringAssert.Contains($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should be generated.");
}

foreach (var modelName in internalModelNames)
{
StringAssert.DoesNotContain($" {modelName}(", modelFactoryText, $"Model factory method for {modelName} should not be generated.");
}
}

foreach (var expectedFile in expectedFiles)
{
var filePath = Path.Combine(outputPath, expectedFile);
Assert.IsTrue(File.Exists(filePath), $"Expected generated file '{filePath}'.");
}
}
finally
{
if (Directory.Exists(outputPath))
{
Directory.Delete(outputPath, recursive: true);
}
}
}
}
}
Loading
Loading