Skip to content

Commit 7aec589

Browse files
live1206Copilot
andcommitted
Filter members inherited from custom base types
Avoid emitting duplicate generated properties or fields when custom partial code changes a model base type to a type that already exposes those members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f692a94 commit 7aec589

6 files changed

Lines changed: 104 additions & 25 deletions

File tree

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,14 @@ private IReadOnlyList<ModelProvider> BuildDerivedModels()
142142
return existingProvider;
143143
}
144144

145-
// Try to find the type in the customization compilation (excluding referenced assemblies)
145+
// Try to find the type in the customization compilation. Referenced assemblies are
146+
// included so custom bases from framework or external packages are represented by
147+
// normal symbol-backed providers.
146148
var baseTypeProvider = CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(
147149
baseType.Namespace,
148150
baseType.Name,
149-
baseType.DeclaringType?.Name);
151+
baseType.DeclaringType?.Name,
152+
includeReferencedAssemblies: true);
150153

151154
if (baseTypeProvider != null)
152155
{
@@ -155,8 +158,8 @@ private IReadOnlyList<ModelProvider> BuildDerivedModels()
155158
return baseTypeProvider;
156159
}
157160

158-
// If we couldn't find the type symbol (e.g., type is from a referenced assembly),
159-
// create a SystemObjectTypeProvider that represents the external type
161+
// If we couldn't find the type symbol, create a SystemObjectTypeProvider that
162+
// represents the external type without member metadata.
160163
var systemObjectTypeProvider = new SystemObjectTypeProvider(baseType);
161164
// Cache it in CSharpTypeMap for future lookups
162165
CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = systemObjectTypeProvider;

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ internal sealed class NamedTypeSymbolProvider : TypeProvider
2222
{
2323
private INamedTypeSymbol _namedTypeSymbol;
2424
private readonly Compilation _compilation;
25+
private TypeProvider? _baseTypeProvider;
2526

2627
public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation)
2728
{
@@ -41,21 +42,36 @@ public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation com
4142
protected override IReadOnlyList<AttributeStatement> BuildAttributes()
4243
=> [.._namedTypeSymbol.GetAttributes().Select(a => new AttributeStatement(a))];
4344

45+
internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider();
46+
4447
protected override CSharpType? BuildBaseType()
4548
{
46-
if (_namedTypeSymbol.BaseType == null
47-
|| _namedTypeSymbol.BaseType.SpecialType == SpecialType.System_Object
48-
|| _namedTypeSymbol.BaseType.SpecialType == SpecialType.System_ValueType
49-
|| _namedTypeSymbol.BaseType.SpecialType == SpecialType.System_Array
50-
|| _namedTypeSymbol.BaseType.SpecialType == SpecialType.System_Enum
51-
|| TypeSymbolExtensions.ContainsTypeAsArgument(_namedTypeSymbol.BaseType, _namedTypeSymbol))
49+
if (ShouldSkipBaseType(_namedTypeSymbol.BaseType))
5250
{
5351
return null;
5452
}
5553

56-
return _namedTypeSymbol.BaseType.GetCSharpType();
54+
return _namedTypeSymbol.BaseType!.GetCSharpType();
5755
}
5856

57+
private TypeProvider? BuildBaseTypeProvider()
58+
{
59+
if (ShouldSkipBaseType(_namedTypeSymbol.BaseType))
60+
{
61+
return null;
62+
}
63+
64+
return new NamedTypeSymbolProvider(_namedTypeSymbol.BaseType!, _compilation);
65+
}
66+
67+
private bool ShouldSkipBaseType(INamedTypeSymbol? baseType)
68+
=> baseType == null
69+
|| baseType.SpecialType == SpecialType.System_Object
70+
|| baseType.SpecialType == SpecialType.System_ValueType
71+
|| baseType.SpecialType == SpecialType.System_Array
72+
|| baseType.SpecialType == SpecialType.System_Enum
73+
|| TypeSymbolExtensions.ContainsTypeAsArgument(baseType, _namedTypeSymbol);
74+
5975
protected override TypeSignatureModifiers BuildDeclarationModifiers()
6076
{
6177
var declaredModifiers = GetAccessModifiers(_namedTypeSymbol.DeclaredAccessibility);

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,18 @@ private IReadOnlyList<PropertyProvider> BuildAllCustomProperties()
6464
var allCustomProperties = CustomCodeView?.Properties != null
6565
? new List<PropertyProvider>(CustomCodeView.Properties)
6666
: [];
67-
var baseTypeCustomCodeView = BaseTypeProvider?.CustomCodeView;
67+
var baseTypeProvider = BaseTypeProvider;
68+
var visited = new HashSet<TypeProvider>();
6869

6970
// add all custom properties from base types
70-
while (baseTypeCustomCodeView != null)
71+
while (baseTypeProvider != null && visited.Add(baseTypeProvider))
7172
{
72-
allCustomProperties.AddRange(baseTypeCustomCodeView.Properties);
73-
baseTypeCustomCodeView = baseTypeCustomCodeView.BaseTypeProvider?.CustomCodeView;
73+
allCustomProperties.AddRange(baseTypeProvider.Properties);
74+
if (baseTypeProvider.CustomCodeView is { } customCodeView)
75+
{
76+
allCustomProperties.AddRange(customCodeView.Properties);
77+
}
78+
baseTypeProvider = baseTypeProvider.BaseTypeProvider;
7479
}
7580

7681
return allCustomProperties;
@@ -81,13 +86,18 @@ private IReadOnlyList<FieldProvider> BuildAllCustomFields()
8186
var allCustomFields = CustomCodeView?.Fields != null
8287
? new List<FieldProvider>(CustomCodeView.Fields)
8388
: [];
84-
var baseTypeCustomCodeView = BaseTypeProvider?.CustomCodeView;
89+
var baseTypeProvider = BaseTypeProvider;
90+
var visited = new HashSet<TypeProvider>();
8591

8692
// add all custom fields from base types
87-
while (baseTypeCustomCodeView != null)
93+
while (baseTypeProvider != null && visited.Add(baseTypeProvider))
8894
{
89-
allCustomFields.AddRange(baseTypeCustomCodeView.Fields);
90-
baseTypeCustomCodeView = baseTypeCustomCodeView.BaseTypeProvider?.CustomCodeView;
95+
allCustomFields.AddRange(baseTypeProvider.Fields);
96+
if (baseTypeProvider.CustomCodeView is { } customCodeView)
97+
{
98+
allCustomFields.AddRange(customCodeView.Fields);
99+
}
100+
baseTypeProvider = baseTypeProvider.BaseTypeProvider;
91101
}
92102

93103
return allCustomFields;

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ private IReadOnlyDictionary<string, INamedTypeSymbol> PopulateNameMap()
6060
return nameMap;
6161
}
6262

63-
public TypeProvider? FindForTypeInCustomization(string ns, string name, string? declaringTypeName = null)
63+
public TypeProvider? FindForTypeInCustomization(string ns, string name, string? declaringTypeName = null, bool includeReferencedAssemblies = false)
6464
{
65-
return FindTypeInCustomization(Customization, ns, name, false, declaringTypeName);
65+
return FindTypeInCustomization(Customization, ns, name, includeReferencedAssemblies, declaringTypeName);
6666
}
6767

6868
public TypeProvider? FindForTypeInLastContract(string ns, string name, string? declaringTypeName = null)

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,10 +1765,50 @@ public async Task CanCustomizeBaseModelToSystemType()
17651765
// The BaseModelProvider should be null since the base is not a generated model
17661766
Assert.IsNull(modelProvider.BaseModelProvider);
17671767

1768-
// System types from referenced assemblies are NOT found by FindForTypeInCustomization
1769-
// (which only searches the customization assembly, not references), so they use SystemObjectTypeProvider
1770-
Assert.IsInstanceOf<SystemObjectTypeProvider>(modelProvider.BaseTypeProvider,
1771-
"System.Exception is from a referenced assembly and should use SystemObjectTypeProvider");
1768+
// System types from referenced assemblies are found in the customization compilation
1769+
// so inherited members can be represented by normal property providers.
1770+
Assert.IsInstanceOf<NamedTypeSymbolProvider>(modelProvider.BaseTypeProvider,
1771+
"System.Exception is from a referenced assembly and should use NamedTypeSymbolProvider");
1772+
}
1773+
1774+
[Test]
1775+
public async Task CanCustomizeSpecBaseModelToSystemType()
1776+
{
1777+
// This verifies that a custom partial base type wins even when the input model
1778+
// has a TypeSpec base model. Otherwise the generated partial would keep the
1779+
// TypeSpec base and conflict with the custom partial declaration.
1780+
var specBaseModel = InputFactory.Model(
1781+
"specBaseModel",
1782+
properties: [InputFactory.Property("specBaseProp", InputPrimitiveType.String)],
1783+
usage: InputModelTypeUsage.Json);
1784+
var childModel = InputFactory.Model(
1785+
"mockInputModel",
1786+
properties: [
1787+
InputFactory.Property("message", InputPrimitiveType.String),
1788+
InputFactory.Property("childProp", InputPrimitiveType.String),
1789+
],
1790+
baseModel: specBaseModel,
1791+
usage: InputModelTypeUsage.Json);
1792+
1793+
var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
1794+
inputModelTypes: [childModel, specBaseModel],
1795+
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
1796+
1797+
var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel") as ModelProvider;
1798+
1799+
Assert.IsNotNull(modelProvider);
1800+
Assert.IsNotNull(modelProvider!.BaseType);
1801+
Assert.AreEqual("Exception", modelProvider.BaseType!.Name);
1802+
Assert.AreEqual("System", modelProvider.BaseType!.Namespace);
1803+
Assert.IsNull(modelProvider.BaseModelProvider, "The TypeSpec base model should not be used when custom code declares a system base type.");
1804+
Assert.IsInstanceOf<NamedTypeSymbolProvider>(modelProvider.BaseTypeProvider);
1805+
Assert.That(modelProvider.Properties.Select(p => p.Name), Does.Not.Contain("Message"));
1806+
Assert.That(modelProvider.Properties.Select(p => p.Name), Does.Contain("ChildProp"));
1807+
1808+
var modelContent = new TypeProviderWriter(modelProvider).Write().Content;
1809+
Assert.That(modelContent, Does.Contain("public partial class MockInputModel : global::System.Exception"));
1810+
Assert.That(modelContent, Does.Not.Contain("SpecBaseModel"));
1811+
Assert.That(modelContent, Does.Not.Contain("public string Message"));
17721812
}
17731813

17741814
[Test]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#nullable disable
2+
3+
using System;
4+
5+
namespace Sample.Models
6+
{
7+
public partial class MockInputModel : Exception
8+
{
9+
}
10+
}

0 commit comments

Comments
 (0)