Skip to content
Merged
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,93 @@ protected internal sealed override IReadOnlyList<MethodProvider> BuildMethodsFor
return [.. factoryMethods];
}

internal 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;
}

if (TryGetArrayElementName(type, out var elementName))
{
if (!IsTypeAvailable(type.Namespace, elementName, type.DeclaringType?.Name))
{
unavailableTypes.Add(string.IsNullOrEmpty(type.Namespace) ? elementName : $"{type.Namespace}.{elementName}");
}
return;
}

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

var declaringTypeName = type.DeclaringType?.Name;
if (CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(
Comment thread
JoshLove-msft marked this conversation as resolved.
type.Namespace,
type.Name,
declaringTypeName,
includeReferencedAssemblies: true) != null)
{
return;
}

unavailableTypes.Add(type.FullyQualifiedName);
}

private static bool TryGetArrayElementName(CSharpType type, [NotNullWhen(true)] out string? elementName)
{
var bracketIndex = type.Name.IndexOf('[');
if (bracketIndex <= 0)
{
elementName = null;
return false;
}

elementName = type.Name[..bracketIndex];
return true;
}

private static bool IsTypeAvailable(string ns, string name, string? declaringTypeName)
{
var typeFactory = CodeModelGenerator.Instance.TypeFactory;
if (typeFactory.CSharpTypeMap.Keys.Any(
type => type.Name == name &&
(string.IsNullOrEmpty(ns) || type.Namespace == ns) &&
type.DeclaringType?.Name == declaringTypeName))
{
return true;
}

return CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(
ns,
name,
declaringTypeName,
includeReferencedAssemblies: true) != null;
}

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 @@ -196,7 +196,11 @@ private static CSharpType ConstructCSharpTypeFromSymbol(
bool isNullable = fullyQualifiedName.StartsWith(NullableTypeName);
bool isEnum = typeSymbol.TypeKind == TypeKind.Enum || (isNullable && typeArg?.TypeKind == TypeKind.Enum);
bool isNullableUnknownType = isNullable && typeArg?.TypeKind == TypeKind.Error;
string name = isNullableUnknownType ? fullyQualifiedName : typeSymbol.Name;
string name = isNullableUnknownType
? fullyQualifiedName
: typeSymbol is IArrayTypeSymbol
? fullyQualifiedName[(fullyQualifiedName.LastIndexOf('.') + 1)..]
: typeSymbol.Name;
// get everything before ` in case of generics
string[] pieces = fullyQualifiedName.Split('`')[0].Split('.');
List<CSharpType> arguments = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,152 @@ 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_PreservesMethodWithAvailableArrayParameterType()
{
var tool = InputFactory.Model("Tool");
var hostedAgentDefinition = InputFactory.Model(
"HostedAgentDefinition",
properties:
[
InputFactory.Property("Tools", InputFactory.Array(tool))
]);

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

var modelFactory = _instance.OutputLibrary.TypeProviders.OfType<ModelFactoryProvider>().Single();
var previousMethod = modelFactory.LastContractView!.Methods
.Single(method => method.Signature.Name == "HostedAgentDefinition");
Assert.IsEmpty(ModelFactoryProvider.GetUnavailableSignatureTypes(previousMethod.Signature));
}

[Test]
public async Task BackCompatibility_SkipsMethodWithUnavailableArrayParameterType()
{
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.TypeProviders.OfType<ModelFactoryProvider>().Single();
var previousMethod = modelFactory.LastContractView!.Methods
.Single(method => method.Signature.Name == "HostedAgentDefinition");
CollectionAssert.AreEqual(
new[] { "Sample.Models.ProjectsAgentTool" },
ModelFactoryProvider.GetUnavailableSignatureTypes(previousMethod.Signature));

modelFactory.ProcessTypeForBackCompatibility();

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

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,21 @@
namespace Sample.Models
{
public class Tool
{
}

public class HostedAgentDefinition
{
}
}

namespace Sample.Namespace
{
public static partial class SampleNamespaceModelFactory
{
public static Models.HostedAgentDefinition HostedAgentDefinition(Models.Tool[] tools)
{
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Sample.Models
{
public class HostedAgentDefinition
{
}

public class ProjectsAgentTool
{
}
}

namespace Sample.Namespace
{
public static partial class SampleNamespaceModelFactory
{
public static Models.HostedAgentDefinition HostedAgentDefinition(Models.ProjectsAgentTool[] tools)
{
return null;
}
}
}
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