Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -128,6 +128,15 @@ protected internal sealed override IReadOnlyList<MethodProvider> BuildMethodsFor
continue;
}

var unavailableTypes = GetUnavailableSignatureTypes(previousMethod.Signature);
if (unavailableTypes.Count > 0)
{
CodeModelGenerator.Instance.Emitter.ReportDiagnostic(
DiagnosticCodes.UnavailableBackcompatType,
$"Skipped backward compatible model factory method '{previousMethod.Signature.FullMethodName}' because its signature references unavailable type(s): {string.Join(", ", unavailableTypes)}.");
continue;
}

List<MethodSignature> currentOverloads = [];
bool foundCompatibleOverload = false;

Expand Down Expand Up @@ -209,6 +218,53 @@ protected internal sealed override IReadOnlyList<MethodProvider> BuildMethodsFor
return [.. factoryMethods];
}

private static IReadOnlyList<string> GetUnavailableSignatureTypes(MethodSignature signature)
{
var unavailableTypes = new HashSet<string>(StringComparer.Ordinal);
if (signature.ReturnType != null)
{
CollectUnavailableTypes(signature.ReturnType, unavailableTypes);
}

foreach (var parameter in signature.Parameters)
{
CollectUnavailableTypes(parameter.Type, unavailableTypes);
}

return [.. unavailableTypes.OrderBy(type => type, StringComparer.Ordinal)];
}

private static void CollectUnavailableTypes(CSharpType type, ISet<string> unavailableTypes)
{
foreach (var argument in type.Arguments)
{
CollectUnavailableTypes(argument, unavailableTypes);
}

if (type.IsFrameworkType)
{
return;
}

var typeFactory = CodeModelGenerator.Instance.TypeFactory;
if (typeFactory.CSharpTypeMap.Keys.Any(type.AreNamesEqual))
{
return;
}

var declaringTypeName = type.DeclaringType?.Name;
if (CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(

@jorgerangel-msft jorgerangel-msft Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Unrelated to this PR but I'm wondering if we should eventually rename this method? We've been using it to find types in both the generated + customization compilation, including myself. Maybe FindForTypeinCurrentCompilation or FindForTypeinCompilation ?

type.Namespace,
type.Name,
declaringTypeName,
includeReferencedAssemblies: true) != null)
{
return;
}

unavailableTypes.Add(type.FullyQualifiedName);
}
Comment on lines +237 to +266

private bool TryBuildCompatibleMethodForPreviousContract(
MethodProvider previousMethod,
MethodSignature? currentMethodSignature,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ internal static class DiagnosticCodes
public const string BaselineContractMissing = "baseline-contract-missing";
public const string InvalidAccessModifier = "invalid-access-modifier";
public const string PluginBuildFailed = "plugin-build-failed";
public const string UnavailableBackcompatType = "unavailable-backcompat-type";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,94 @@ public async Task BackCompatibility_NewPropertyAddedWithRenamedParam()
StringAssert.Contains("return new global::Sample.Models.PublicModel1(default, default, listProp.ToList(), default, additionalBinaryDataProperties: null);", bodyString);
}

[Test]
public async Task BackCompatibility_SkipsMethodWithUnavailableParameterType()
{
var externalTool = InputFactory.Model(
"Tool",
external: new InputExternalTypeMetadata("System.Uri", null, null));
var hostedAgentDefinition = InputFactory.Model(
"HostedAgentDefinition",
properties:
[
InputFactory.Property("Tool", externalTool)
]);

_instance = (await MockHelpers.LoadMockGeneratorAsync(
inputNamespaceName: "Sample.Namespace",
inputModelTypes: [externalTool, hostedAgentDefinition],
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object;

var modelFactory = _instance.OutputLibrary.ModelFactory.Value;
modelFactory.ProcessTypeForBackCompatibility();

var methods = modelFactory.Methods
.Where(method => method.Signature.Name == "HostedAgentDefinition")
.ToList();
Assert.AreEqual(1, methods.Count);
Assert.AreEqual(typeof(Uri), methods[0].Signature.Parameters.Single().Type.FrameworkType);

var content = new TypeProviderWriter(modelFactory).Write().Content;
StringAssert.DoesNotContain("ProjectsAgentTool", content);
}

[Test]
public async Task BackCompatibility_SkipsMethodWithUnavailableGenericParameterType()
{
var externalTool = InputFactory.Model(
"Tool",
external: new InputExternalTypeMetadata("System.Uri", null, null));
var hostedAgentDefinition = InputFactory.Model(
"HostedAgentDefinition",
properties:
[
InputFactory.Property("Tools", InputFactory.Array(externalTool))
]);

_instance = (await MockHelpers.LoadMockGeneratorAsync(
inputNamespaceName: "Sample.Namespace",
inputModelTypes: [externalTool, hostedAgentDefinition],
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object;

var modelFactory = _instance.OutputLibrary.ModelFactory.Value;
modelFactory.ProcessTypeForBackCompatibility();

var methods = modelFactory.Methods
.Where(method => method.Signature.Name == "HostedAgentDefinition")
.ToList();
Assert.AreEqual(1, methods.Count);

var toolsParameter = methods[0].Signature.Parameters.Single();
Assert.AreEqual(typeof(IEnumerable<>), toolsParameter.Type.FrameworkType);
Assert.AreEqual(typeof(Uri), toolsParameter.Type.Arguments.Single().FrameworkType);

var content = new TypeProviderWriter(modelFactory).Write().Content;
StringAssert.DoesNotContain("ProjectsAgentTool", content);
}

[Test]
public async Task BackCompatibility_SkipsMethodWithUnavailableReturnType()
{
var hostedAgentDefinition = InputFactory.Model("HostedAgentDefinition");

_instance = (await MockHelpers.LoadMockGeneratorAsync(
inputNamespaceName: "Sample.Namespace",
inputModelTypes: [hostedAgentDefinition],
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object;

var modelFactory = _instance.OutputLibrary.ModelFactory.Value;
modelFactory.ProcessTypeForBackCompatibility();

var methods = modelFactory.Methods
.Where(method => method.Signature.Name == "HostedAgentDefinition")
.ToList();
Assert.AreEqual(1, methods.Count);
Assert.AreEqual("HostedAgentDefinition", methods[0].Signature.ReturnType?.Name);

var content = new TypeProviderWriter(modelFactory).Write().Content;
StringAssert.DoesNotContain("ProjectsAgentTool", content);
}

[Test]
public void ModelWithNestedDiscriminators()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;

namespace Sample.Namespace
{
public class HostedAgentDefinition
{
}

public class ProjectsAgentTool
{
}

public static partial class SampleNamespaceModelFactory
{
public static HostedAgentDefinition HostedAgentDefinition(IEnumerable<ProjectsAgentTool> tools)
{
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Sample.Namespace
{
public class HostedAgentDefinition
{
}

public class ProjectsAgentTool
{
}

public static partial class SampleNamespaceModelFactory
{
public static HostedAgentDefinition HostedAgentDefinition(ProjectsAgentTool tool)
{
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Sample.Namespace
{
public class HostedAgentDefinition
{
}

public class ProjectsAgentTool
{
}

public static partial class SampleNamespaceModelFactory
{
public static ProjectsAgentTool HostedAgentDefinition()
{
return null;
}
}
}
Loading