From eddfa11a4098c36d6057377e6754e3ad73d9bb6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:33:18 +0000 Subject: [PATCH 01/14] Initial plan From ba03862cfce6e2935808857d6587d689e7584fab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:46:34 +0000 Subject: [PATCH 02/14] fix(csharp): skip back compatibility for released experimental types Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/SourceInput/SourceInputModel.cs | 5 ++- .../EnumProviders/EnumProviderTests.cs | 35 +++++++++++++++++++ .../Choice.cs | 21 +++++++++++ .../Choice.cs | 12 +++++++ .../Types.cs | 30 ++++++++++++++++ .../test/Providers/TypeProviderTests.cs | 35 +++++++++++++++++++ 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompatibilityIgnoresExperimentalEnum(Extensible)/Choice.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompatibilityIgnoresExperimentalEnum(Fixed)/Choice.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalLastContract/Types.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index 18ed06828ff..c6ac40c0290 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Utilities; namespace Microsoft.TypeSpec.Generator.SourceInput { @@ -69,7 +71,8 @@ private IReadOnlyDictionary PopulateNameMap() public TypeProvider? FindForTypeInLastContract(string ns, string name, string? declaringTypeName = null) { - return FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); + var type = FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); + return type?.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute) == true ? null : type; } private IReadOnlyList PopulateCustomizationTypeProviders() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs index 04425b4e4c4..4f6fbc74326 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/EnumProviderTests.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; @@ -23,6 +25,39 @@ public class EnumProviderTests { internal const string NewLine = "\n"; + [Test] + public async Task BackCompatibilityIgnoresExperimentalEnum( + [Values("Fixed", "Extensible")] string shape, + [Values] bool previouslyExperimental, + [Values] bool currentlyExperimental) + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => + { + var compilation = await Helpers.GetCompilationFromDirectoryAsync(shape); + if (previouslyExperimental) + { + return compilation; + } + var tree = compilation.SyntaxTrees.Single(t => t.FilePath.EndsWith("Choice.cs", StringComparison.Ordinal)); + var root = tree.GetRoot(); + var updatedRoot = root.ReplaceNodes(root.DescendantNodes().OfType(), + (_, declaration) => declaration.WithAttributeLists(default)); + return compilation.ReplaceSyntaxTree(tree, tree.WithRootAndOptions(updatedRoot, tree.Options)); + }); + var input = InputFactory.Int32Enum("Choice", [("Preview", 10)], isExtensible: shape == "Extensible"); + if (currentlyExperimental) + { + InputFactory.Experimental(input, "CURRENT001"); + } + var provider = EnumProvider.Create(input); + provider.EnsureBuilt(); + provider.ProcessTypeForBackCompatibility(); + + Assert.AreEqual(previouslyExperimental ? 1 : 2, provider.EnumValues.Count); + Assert.AreEqual(previouslyExperimental || shape == "Extensible" ? 10 : 42, + provider.EnumValues.Single(v => v.Name == "Preview").Value); + } + [Test] public async Task ExperimentalFixedEnumDependenciesSurviveBackCompatibility( [Values] bool numeric, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompatibilityIgnoresExperimentalEnum(Extensible)/Choice.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompatibilityIgnoresExperimentalEnum(Extensible)/Choice.cs new file mode 100644 index 00000000000..d427c1ddc92 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompatibilityIgnoresExperimentalEnum(Extensible)/Choice.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Sample.Models +{ + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public readonly partial struct Choice + { + private readonly int _value; + private const int PreviewValue = 42; + private const int RemovedValue = 99; + + public Choice(int value) + { + _value = value; + } + + public static Choice Preview { get; } = new Choice(PreviewValue); + public static Choice Removed { get; } = new Choice(RemovedValue); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompatibilityIgnoresExperimentalEnum(Fixed)/Choice.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompatibilityIgnoresExperimentalEnum(Fixed)/Choice.cs new file mode 100644 index 00000000000..c191e1a1af0 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/BackCompatibilityIgnoresExperimentalEnum(Fixed)/Choice.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Sample.Models +{ + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public enum Choice + { + Preview = 42, + Removed = 99 + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalLastContract/Types.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalLastContract/Types.cs new file mode 100644 index 00000000000..95666ca6fa6 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalLastContract/Types.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Test +{ + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public class ExperimentalType + { + public void DoWork(int value) { } + } + + public class StableType + { + public void DoWork(int value) { } + } + + [Unrelated.Experimental("PREVIOUS001")] + public class UnrelatedExperimentalType + { + public void DoWork(int value) { } + } +} + +namespace Unrelated +{ + internal sealed class ExperimentalAttribute : System.Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index 05fae1279ce..306166bc073 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -302,6 +302,41 @@ public async Task TestLoadLastContractView() Assert.AreEqual("p1", signature.Parameters[0].Name); } + [Test] + public async Task BackCompatibilityIgnoresExperimentalLastContract( + [Values("ExperimentalType", "StableType", "UnrelatedExperimentalType")] string name, + [Values] bool currentlyExperimental) + { + await MockHelpers.LoadMockGeneratorAsync( + lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync()); + var typeProvider = new TestTypeProvider( + name: name, + declarationModifiers: TypeSignatureModifiers.Public | TypeSignatureModifiers.Abstract | TypeSignatureModifiers.Class); + typeProvider.Update( + attributes: ExperimentalApiHelpers.BuildAttributes( + currentlyExperimental ? new InputExperimentalDetails("CURRENT001") : null), + methods: + [ + new MethodProvider( + new MethodSignature("DoWork", null, MethodSignatureModifiers.Public, null, null, + [ + new ParameterProvider("value", $"", typeof(int)), + new ParameterProvider("added", $"", typeof(bool), defaultValue: Snippet.Default, location: ParameterLocation.Query) + ]), + MethodBodyStatement.Empty, + typeProvider) + ]); + + var previouslyExperimental = name == "ExperimentalType"; + Assert.AreEqual(previouslyExperimental, typeProvider.LastContractView is null); + Assert.AreEqual(previouslyExperimental, typeProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract)); + + typeProvider.ProcessTypeForBackCompatibility(); + + Assert.AreEqual(previouslyExperimental ? 1 : 2, typeProvider.Methods.Count); + Assert.AreEqual(2, typeProvider.Methods[0].Signature.Parameters.Count); + } + // Validates that a custom TypeProvider inherits the parameter-reordering back-compat behavior // from the base TypeProvider. [Test] From 54a35345a624f40dbd8bdb913a41c9f2b2f08bf8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:16:31 +0000 Subject: [PATCH 03/14] fix(csharp): document experimental back-compat behavior Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/NamedTypeSymbolProvider.cs | 3 +++ .../src/SourceInput/SourceInputModel.cs | 2 +- .../Types.cs | 19 +++++++++++++++++++ .../test/Providers/TypeProviderTests.cs | 10 ++++++++++ .../generator/docs/backward-compatibility.md | 7 +++++++ 5 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresPolyfilledExperimentalAttribute/Types.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs index 0ae3540d45a..e5ff47b8cb6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs @@ -51,6 +51,9 @@ internal string MetadataName internal string MetadataSimpleName => _metadataSimpleName ??= _namedTypeSymbol.Name; + internal bool IsExperimental => _namedTypeSymbol.GetAttributes() + .Any(a => a.AttributeClass?.ToDisplayString() == typeof(ExperimentalAttribute).FullName); + // Nested declarations are not materialized as providers for this symbol-backed view. internal bool HasPublicApiNestedTypes => _namedTypeSymbol.GetTypeMembers().Any(type => type.DeclaredAccessibility is Accessibility.Public or Accessibility.Protected or diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index c6ac40c0290..31a425bc11b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -72,7 +72,7 @@ private IReadOnlyDictionary PopulateNameMap() public TypeProvider? FindForTypeInLastContract(string ns, string name, string? declaringTypeName = null) { var type = FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); - return type?.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute) == true ? null : type; + return type is NamedTypeSymbolProvider { IsExperimental: true } ? null : type; } private IReadOnlyList PopulateCustomizationTypeProviders() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresPolyfilledExperimentalAttribute/Types.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresPolyfilledExperimentalAttribute/Types.cs new file mode 100644 index 00000000000..96db1ce191f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresPolyfilledExperimentalAttribute/Types.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace System.Diagnostics.CodeAnalysis +{ + internal sealed class ExperimentalAttribute : System.Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + } +} + +namespace Test +{ + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public class ExperimentalType + { + public void DoWork(int value) { } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index 306166bc073..d28b8494ef9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -337,6 +337,16 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.AreEqual(2, typeProvider.Methods[0].Signature.Parameters.Count); } + [Test] + public async Task BackCompatibilityIgnoresPolyfilledExperimentalAttribute() + { + await MockHelpers.LoadMockGeneratorAsync( + lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync()); + var typeProvider = new TestTypeProvider(name: "ExperimentalType"); + + Assert.IsNull(typeProvider.LastContractView); + } + // Validates that a custom TypeProvider inherits the parameter-reordering back-compat behavior // from the base TypeProvider. [Test] diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index e71cc90f943..158e1531063 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -4,6 +4,7 @@ - [Overview](#overview) - [How It Works](#how-it-works) +- [Experimental Types](#experimental-types) - [Supported Scenarios](#supported-scenarios) - [Model Factory Methods](#model-factory-methods) - [New Model Property Added](#scenario-new-model-property-added) @@ -51,6 +52,12 @@ When generating code, the generator can optionally receive a compiled assembly f 2. Compares the generated types against the types in the last contract 3. Automatically generates compatibility methods, properties, or enum members where differences are detected +### Experimental Types + +Types in the last released contract marked with `System.Diagnostics.CodeAnalysis.ExperimentalAttribute` are excluded from backward-compatibility lookup. The generator does not preserve their previous type shape or restore their previous members as compatibility shims, allowing those types to change when they graduate to stable or are removed. + +This decision uses the attribute on the last released type, including polyfilled definitions of the attribute. Marking a previously stable type experimental in the current generation does not remove its compatibility guarantees. Experimental attributes on individual members continue to use the existing member-level handling. + ### ApiCompat Baseline Awareness Sometimes a breaking change (such as removing a model or a model factory method) is intentional and has already been reviewed and accepted. In the Azure SDK, such accepted breaking changes are recorded in an [ApiCompat](https://github.com/dotnet/sdk/tree/main/src/Compatibility) baseline (suppression) file, conventionally located at `eng/apicompatbaselines/.txt`. From 907ad2888e79ea6f2a873d1afb960f45737a7ed2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:55:54 +0000 Subject: [PATCH 04/14] fix(csharp): ignore experimental prior contract members Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/NamedTypeSymbolProvider.cs | 26 +++++++++++++--- .../src/SourceInput/SourceInputModel.cs | 14 +++++++-- .../Types.cs | 31 +++++++++++++++++++ .../test/Providers/TypeProviderTests.cs | 22 ++++++++++++- 4 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalMembersLastContract/Types.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs index e5ff47b8cb6..58c6224aa4a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs @@ -26,11 +26,14 @@ internal sealed class NamedTypeSymbolProvider : TypeProvider private string? _metadataName; private string? _metadataSimpleName; private TypeProvider? _baseTypeProvider; + private readonly bool _excludeExperimentalMembers; - public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation) + public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation, bool excludeExperimentalMembers = false) { _namedTypeSymbol = namedTypeSymbol; _compilation = compilation; + _excludeExperimentalMembers = excludeExperimentalMembers; + IsExperimental = HasExperimentalAttribute(namedTypeSymbol); } internal string MetadataName @@ -51,8 +54,10 @@ internal string MetadataName internal string MetadataSimpleName => _metadataSimpleName ??= _namedTypeSymbol.Name; - internal bool IsExperimental => _namedTypeSymbol.GetAttributes() - .Any(a => a.AttributeClass?.ToDisplayString() == typeof(ExperimentalAttribute).FullName); + internal bool IsExperimental { get; } + + private static bool HasExperimentalAttribute(ISymbol symbol) + => symbol.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == typeof(ExperimentalAttribute).FullName); // Nested declarations are not materialized as providers for this symbol-backed view. internal bool HasPublicApiNestedTypes => _namedTypeSymbol.GetTypeMembers().Any(type => @@ -96,7 +101,7 @@ protected internal override CSharpType[] BuildImplements() return null; } - return new NamedTypeSymbolProvider(_namedTypeSymbol.BaseType!, _compilation); + return new NamedTypeSymbolProvider(_namedTypeSymbol.BaseType!, _compilation, _excludeExperimentalMembers); } protected override CSharpType? BuildBaseType() @@ -208,6 +213,11 @@ protected internal override PropertyProvider[] BuildProperties() List properties = new List(); foreach (var propertySymbol in _namedTypeSymbol.GetMembers().OfType()) { + if (_excludeExperimentalMembers && HasExperimentalAttribute(propertySymbol)) + { + continue; + } + var modifiers = GetAccessModifier(propertySymbol.DeclaredAccessibility); if (propertySymbol.IsStatic) { @@ -325,6 +335,10 @@ protected internal override ConstructorProvider[] BuildConstructors() { continue; } + if (_excludeExperimentalMembers && HasExperimentalAttribute(constructorSymbol)) + { + continue; + } var initializer = ExtractConstructorInitializer(constructorSymbol); var signature = new ConstructorSignature( @@ -354,6 +368,10 @@ protected internal override MethodProvider[] BuildMethods() { continue; } + if (_excludeExperimentalMembers && HasExperimentalAttribute(methodSymbol)) + { + continue; + } var modifiers = GetAccessModifier(methodSymbol.DeclaredAccessibility); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index 31a425bc11b..8bb75ae6fa4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -71,7 +71,14 @@ private IReadOnlyDictionary PopulateNameMap() public TypeProvider? FindForTypeInLastContract(string ns, string name, string? declaringTypeName = null) { - var type = FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); + var type = FindTypeInCompilation( + LastContract, + ns, + name, + true, + declaringTypeName, + includeInternal: false, + excludeExperimentalMembers: true); return type is NamedTypeSymbolProvider { IsExperimental: true } ? null : type; } @@ -115,7 +122,8 @@ private static string GetFullyQualifiedMetadataName(string ns, string name, stri string name, bool includeReferencedAssemblies, string? declaringTypeName = null, - bool includeInternal = true) + bool includeInternal = true, + bool excludeExperimentalMembers = false) { if (compilation == null) { @@ -144,7 +152,7 @@ private static string GetFullyQualifiedMetadataName(string ns, string name, stri type = null; } - return type != null ? new NamedTypeSymbolProvider(type, compilation) : null; + return type != null ? new NamedTypeSymbolProvider(type, compilation, excludeExperimentalMembers) : null; } private bool TryGetName(ISymbol symbol, [NotNullWhen(true)] out string? name) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalMembersLastContract/Types.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalMembersLastContract/Types.cs new file mode 100644 index 00000000000..14672d3f51d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalMembersLastContract/Types.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Test +{ + public class ExperimentalMembersType + { + public ExperimentalMembersType() { } + + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public ExperimentalMembersType(int value) { } + + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public int ExperimentalProperty { get; set; } + + public int StableProperty { get; set; } + + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public void ExperimentalMethod() { } + + public void StableMethod() { } + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + internal sealed class ExperimentalAttribute : System.Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index d28b8494ef9..89a0c0881b8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -152,7 +152,7 @@ await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => owner.ProcessTypeForBackCompatibility(); var shims = owner.Methods.Where(m => m.Signature.Attributes.Any(a => a.Type.Equals(typeof(EditorBrowsableAttribute)))).ToArray(); - Assert.AreEqual(3, shims.Length); + Assert.AreEqual(previouslyExperimental ? 0 : 3, shims.Length); foreach (var shim in shims) { var experiment = shim.Signature.Attributes.SingleOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute))); @@ -347,6 +347,26 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsNull(typeProvider.LastContractView); } + [Test] + public async Task BackCompatibilityIgnoresExperimentalMembersLastContract() + { + await MockHelpers.LoadMockGeneratorAsync( + lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync()); + var typeProvider = new TestTypeProvider(name: "ExperimentalMembersType"); + var lastContractView = typeProvider.LastContractView; + + Assert.IsNotNull(lastContractView); + CollectionAssert.AreEqual( + new[] { "StableMethod" }, + lastContractView!.Methods.Select(method => method.Signature.Name).ToArray()); + CollectionAssert.AreEqual( + new[] { "StableProperty" }, + lastContractView.Properties.Select(property => property.Name).ToArray()); + CollectionAssert.AreEqual( + new[] { 0 }, + lastContractView.Constructors.Select(constructor => constructor.Signature.Parameters.Count).ToArray()); + } + // Validates that a custom TypeProvider inherits the parameter-reordering back-compat behavior // from the base TypeProvider. [Test] From 52dc9771866c424d776c32e23e894f6ca5358252 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:20:44 +0000 Subject: [PATCH 05/14] fix(http-client-csharp): ignore experimental members in back compat Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ApiVersionEnumProvider.cs | 6 +- .../src/Providers/ExtensibleEnumProvider.cs | 7 ++- .../src/Providers/FixedEnumProvider.cs | 15 +++-- .../Providers/ModelBaseTypeCompatibility.cs | 22 ++++--- .../src/Providers/ModelFactoryProvider.cs | 2 +- .../src/Providers/ModelProvider.cs | 8 +-- .../src/Providers/NamedTypeSymbolProvider.cs | 22 ++----- .../src/Providers/PropertyProvider.cs | 2 +- .../Providers/SystemObjectModelProvider.cs | 22 +++---- .../src/Providers/TypeProvider.cs | 4 +- .../src/Shared/MethodSignatureHelper.cs | 12 ++++ .../src/SourceInput/SourceInputModel.cs | 14 +---- .../src/Utilities/BackCompatHelper.cs | 6 +- .../EnumProviders/EnumProviderTests.cs | 16 +++++ .../ExperimentalExtensibleMembers/Choice.cs | 24 ++++++++ .../ExperimentalFixedMembers/Choice.cs | 20 ++++++ .../ModelProviders/ModelProviderTests.cs | 22 +++++++ .../MockInputModel.cs | 19 ++++++ .../Types.cs | 5 +- .../test/Providers/TypeProviderTests.cs | 61 +++++++++++++++++-- 20 files changed, 230 insertions(+), 79 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalExtensibleMembers/Choice.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalFixedMembers/Choice.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ExperimentalPropertyInLastContractIsIgnored/MockInputModel.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs index c6c3446deaa..764aa30e46a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs @@ -137,14 +137,14 @@ private List BuildCustomEnumMembers(IReadOnlyList private List BuildApiVersionEnumValuesForBackwardCompatibility(List currentApiVersions) { - var lastContractFields = LastContractView?.Fields; - if (lastContractFields == null || lastContractFields.Count == 0) + var lastContractFields = LastContractView?.Fields.Where(MethodSignatureHelper.IsPublicApi).ToArray(); + if (lastContractFields == null || lastContractFields.Length == 0) { return currentApiVersions; } var currentVersionsLookup = currentApiVersions.ToDictionary(v => v.Name, StringComparer.OrdinalIgnoreCase); - var allMembers = new List(currentApiVersions.Count + lastContractFields.Count); + var allMembers = new List(currentApiVersions.Count + lastContractFields.Length); bool addedPreviousApiVersion = false; // First, add all missing backward compatibility versions in their original order diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs index 7ab35ce7e77..f06f3196406 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs @@ -58,7 +58,10 @@ private protected override bool IsCustomizedValueName(string name) protected override IReadOnlyList BuildEnumValues() { - var lastContractNames = LastContractView?.Properties.Select(p => p.Name).ToArray() ?? []; + var lastContractNames = LastContractView?.Properties + .Where(MethodSignatureHelper.IsPublicApi) + .Select(p => p.Name) + .ToArray() ?? []; var generatedNames = GetGeneratedValueNames(_allowedValues, lastContractNames); var values = new EnumTypeMember[_allowedValues.Count]; @@ -288,7 +291,7 @@ protected override TypeProvider[] BuildSerializationProviders() protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) { var lastContractProperties = LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)); + .Where(MethodSignatureHelper.IsPublicApi); if (lastContractProperties == null || !lastContractProperties.Any()) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs index 8a3d341b1c4..3f644d1d396 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs @@ -78,7 +78,7 @@ protected override TypeProvider[] BuildSerializationProviders() protected override IReadOnlyList BuildEnumValues() { var customMembers = new HashSet(CustomCodeView?.Fields ?? []); - var lastContractFields = LastContractView?.Fields ?? []; + var lastContractFields = LastContractView?.Fields.Where(MethodSignatureHelper.IsPublicApi).ToArray() ?? []; var lastContractNames = lastContractFields.Select(f => f.Name).ToArray(); var generatedNames = GetGeneratedValueNames(AllowedValues, lastContractNames); @@ -126,8 +126,8 @@ protected override IReadOnlyList BuildEnumValues() protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) { - var lastContractFields = LastContractView?.Fields; - if (lastContractFields == null || lastContractFields.Count == 0) + var lastContractFields = LastContractView?.Fields.Where(MethodSignatureHelper.IsPublicApi).ToArray(); + if (lastContractFields == null || lastContractFields.Length == 0) { return null; } @@ -212,7 +212,10 @@ private HashSet GetCustomMemberLastContractNames(IReadOnlyList f.OriginalName != null) .Select(f => f.OriginalName!) ?? [], StringComparer.Ordinal); - var lastContractNames = lastContractFields.Select(f => f.Name).ToArray(); + var lastContractNames = lastContractFields + .Where(MethodSignatureHelper.IsPublicApi) + .Select(f => f.Name) + .ToArray(); var generatedNames = GetGeneratedValueNames(AllowedValues, lastContractNames); var customMemberLastContractNames = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -255,7 +258,9 @@ private protected static void AppendMembersNotInLastContract( IReadOnlyList lastContractFields, List allMembers) { - var lastContractNames = new HashSet(lastContractFields.Select(f => f.Name), StringComparer.OrdinalIgnoreCase); + var lastContractNames = new HashSet( + lastContractFields.Where(MethodSignatureHelper.IsPublicApi).Select(f => f.Name), + StringComparer.OrdinalIgnoreCase); foreach (var current in currentValues) { if (!lastContractNames.Contains(current.Name)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs index e4487477eec..b5d1bcc3317 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs @@ -91,19 +91,17 @@ private static bool HasNoUnsupportedLastContractMembers(ModelProvider candidate) return lastContract.Implements.All(previous => currentInterfaces.Any(current => ModelBaseMemberCompatibility.AreTypesCompatible(previous, current))) && lastContract.Constructors - .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers)) + .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature)) .All(constructor => SystemObjectModelProvider.HasSupportedConstructorParameters(constructor.Signature.Parameters) && constructor.Signature.Parameters.All(parameter => candidate.Properties.Any(property => property.AsParameter.Name == parameter.Name && (ModelBaseMemberCompatibility.AreTypesCompatible(parameter.Type, property.Type) || ModelBaseMemberCompatibility.AreTypesCompatible(parameter.Type, property.Type.InputType))))) && lastContract.Methods - .Where(method => MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers)) + .Where(method => MethodSignatureHelper.IsPublicApi(method.Signature)) .All(previous => currentGeneratedMethods.Any(current => AreGeneratedMethodSignaturesEquivalent(previous.Signature, current.Signature))) && - !lastContract.Fields.Any(field => - field.Modifiers.HasFlag(FieldModifiers.Public) || - field.Modifiers.HasFlag(FieldModifiers.Protected)); + !lastContract.Fields.Any(MethodSignatureHelper.IsPublicApi); } private static bool AreGeneratedMethodSignaturesEquivalent( @@ -128,8 +126,8 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) return true; } - var currentProperties = GetUniquePublicProperties(candidate.Properties); - var previousProperties = GetUniquePublicProperties(lastContract.Properties); + var currentProperties = GetUniquePublicProperties(candidate.Properties, isLastContract: false); + var previousProperties = GetUniquePublicProperties(lastContract.Properties, isLastContract: true); if (currentProperties is null || previousProperties is null) { return false; @@ -143,10 +141,14 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) !LastContractDerivedConstructorHasRequiredParameter(property))); } - private static Dictionary? GetUniquePublicProperties(IReadOnlyList properties) + private static Dictionary? GetUniquePublicProperties( + IReadOnlyList properties, + bool isLastContract) { var result = new Dictionary(StringComparer.Ordinal); - foreach (var property in properties.Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers))) + foreach (var property in properties.Where(property => isLastContract + ? MethodSignatureHelper.IsPublicApi(property) + : MethodSignatureHelper.IsPublicApi(property.Modifiers))) { // Overloaded indexers can share a symbol name. They are unsupported contracts, not // malformed input that should abort generation with a duplicate-key exception. @@ -164,7 +166,7 @@ private static bool IsRequiredInitializationProperty(PropertyProvider property) private bool LastContractDerivedConstructorHasRequiredParameter(PropertyProvider property) => _model.LastContractView?.Constructors.Any(constructor => - MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && + MethodSignatureHelper.IsPublicApi(constructor.Signature) && constructor.Signature.Parameters.Any(parameter => parameter.Name == property.AsParameter.Name && parameter.Type.Equals(property.Type, ignoreNullable: true) && diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index f60171bd37d..d9bbad0580c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -127,7 +127,7 @@ protected internal sealed override IReadOnlyList BuildMethodsFor foreach (var previousMethod in LastContractView.Methods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature)) { continue; } 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 f0cedac065f..f0e77855169 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 @@ -107,7 +107,7 @@ _inputModel.BaseModel is not null private IDictionary LastContractPropertiesMap => _lastContractPropertiesMap ??= LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) + .Where(MethodSignatureHelper.IsPublicApi) .ToDictionary(p => p.Name, p => p.Type) ?? []; private IDictionary? _lastContractPropertiesMap; @@ -1111,7 +1111,7 @@ protected internal override IReadOnlyList BuildConstructors foreach (var previousConstructor in restorablePreviousConstructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) { continue; } @@ -1183,7 +1183,7 @@ private void RestorePreviousConstructorParameterNames( const MethodSignatureModifiers privateProtected = MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected; foreach (var previousConstructor in previousConstructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) { continue; } @@ -2056,7 +2056,7 @@ private bool ShouldUseObjectAdditionalProperties() // Check if the property exists in the last contract by name var lastContractProperty = LastContractView.Properties.FirstOrDefault(p => - MethodSignatureHelper.IsPublicApi(p.Modifiers) && + MethodSignatureHelper.IsPublicApi(p) && p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName); if (lastContractProperty == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs index 58c6224aa4a..5903522f740 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs @@ -26,13 +26,11 @@ internal sealed class NamedTypeSymbolProvider : TypeProvider private string? _metadataName; private string? _metadataSimpleName; private TypeProvider? _baseTypeProvider; - private readonly bool _excludeExperimentalMembers; - public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation, bool excludeExperimentalMembers = false) + public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation) { _namedTypeSymbol = namedTypeSymbol; _compilation = compilation; - _excludeExperimentalMembers = excludeExperimentalMembers; IsExperimental = HasExperimentalAttribute(namedTypeSymbol); } @@ -101,7 +99,7 @@ protected internal override CSharpType[] BuildImplements() return null; } - return new NamedTypeSymbolProvider(_namedTypeSymbol.BaseType!, _compilation, _excludeExperimentalMembers); + return new NamedTypeSymbolProvider(_namedTypeSymbol.BaseType!, _compilation); } protected override CSharpType? BuildBaseType() @@ -213,11 +211,6 @@ protected internal override PropertyProvider[] BuildProperties() List properties = new List(); foreach (var propertySymbol in _namedTypeSymbol.GetMembers().OfType()) { - if (_excludeExperimentalMembers && HasExperimentalAttribute(propertySymbol)) - { - continue; - } - var modifiers = GetAccessModifier(propertySymbol.DeclaredAccessibility); if (propertySymbol.IsStatic) { @@ -335,10 +328,6 @@ protected internal override ConstructorProvider[] BuildConstructors() { continue; } - if (_excludeExperimentalMembers && HasExperimentalAttribute(constructorSymbol)) - { - continue; - } var initializer = ExtractConstructorInitializer(constructorSymbol); var signature = new ConstructorSignature( @@ -346,6 +335,9 @@ protected internal override ConstructorProvider[] BuildConstructors() GetSymbolXmlDoc(constructorSymbol, "summary"), GetAccessModifier(constructorSymbol.DeclaredAccessibility), [.. constructorSymbol.Parameters.Select(p => ConvertToParameterProvider(constructorSymbol, p))], + attributes: [.. constructorSymbol.GetAttributes() + .Where(a => a.AttributeClass?.ToDisplayString() == typeof(ExperimentalAttribute).FullName) + .Select(a => new AttributeStatement(a))], initializer: initializer); constructors.Add(new ConstructorProvider(signature, MethodBodyStatement.Empty, this)); } @@ -368,10 +360,6 @@ protected internal override MethodProvider[] BuildMethods() { continue; } - if (_excludeExperimentalMembers && HasExperimentalAttribute(methodSymbol)) - { - continue; - } var modifiers = GetAccessModifier(methodSymbol.DeclaredAccessibility); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index f060d44cf5f..9d47e072373 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -113,7 +113,7 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T var canonicalName = identifierName.NormalizeCSharpAcronyms(isDateTime); var enclosingTypeName = enclosingType.Name; var lastContractProperties = enclosingType.LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) + .Where(MethodSignatureHelper.IsPublicApi) .ToList(); // An exact input-identifier match is authoritative: it is the name this property would have had diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs index df21d9faed0..8d5dad819d2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs @@ -159,7 +159,7 @@ private bool HasCallableInitializationConstructor() } foreach (var constructor in constructors.Where(constructor => - MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && + MethodSignatureHelper.IsPublicApi(constructor.Signature) && HasSupportedConstructorParameters(constructor.Signature.Parameters))) { var matches = constructor.Signature.Parameters @@ -269,7 +269,7 @@ private bool TryGetLastContractConstructor( { var properties = Properties; foreach (var candidate in _lastContractType!.Constructors - .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && + .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature) && HasSupportedConstructorParameters(constructor.Signature.Parameters)) .OrderByDescending(constructor => constructor.Signature.Parameters.Count)) { @@ -316,7 +316,7 @@ protected internal override PropertyProvider[] BuildProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property.Modifiers) && seenPropertyNames.Add(property.Name))) + MethodSignatureHelper.IsPublicApi(property) && seenPropertyNames.Add(property.Name))) { if (ApplyCurrentInputMetadata(property) is { } projectedProperty) { @@ -342,7 +342,7 @@ internal bool HasCompatibleLastContractProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { if (provider.Properties - .Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers)) + .Where(MethodSignatureHelper.IsPublicApi) .Any(property => FindEffectiveFrameworkProperty(property.Name) is not { } candidate || !IsCompatibleProperty(property, candidate))) { @@ -391,7 +391,7 @@ internal bool HasCompatibleLastContractNonPropertyMembers() } foreach (var method in provider.Methods.Where(method => - MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers))) + MethodSignatureHelper.IsPublicApi(method.Signature))) { var signature = method.Signature; if (overriddenMethods.Any(overrideSignature => @@ -407,7 +407,7 @@ internal bool HasCompatibleLastContractNonPropertyMembers() overriddenMethods.AddRange(provider.Methods.Select(method => method.Signature) .Where(signature => signature.Modifiers.HasFlag(MethodSignatureModifiers.Override))); if (provider.Fields - .Where(field => IsPublicApiField(field.Modifiers)) + .Where(MethodSignatureHelper.IsPublicApi) .Any(field => FindEffectiveFrameworkMember(field.Name) is not FieldInfo candidate || !IsCompatibleField(field, candidate))) { @@ -425,16 +425,16 @@ internal IEnumerable GetPublicApiMemberNames() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property.Modifiers))) + MethodSignatureHelper.IsPublicApi(property))) { yield return property.Name; } foreach (var method in provider.Methods.Where(method => - MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers))) + MethodSignatureHelper.IsPublicApi(method.Signature))) { yield return GetPublicApiMemberName(method.Signature.Name); } - foreach (var field in provider.Fields.Where(field => IsPublicApiField(field.Modifiers))) + foreach (var field in provider.Fields.Where(MethodSignatureHelper.IsPublicApi)) { yield return field.Name; } @@ -669,10 +669,6 @@ private static bool RequiresOverridableMethod(MethodSignatureModifiers modifiers modifiers.HasFlag(MethodSignatureModifiers.Abstract) || modifiers.HasFlag(MethodSignatureModifiers.Override)); - private static bool IsPublicApiField(FieldModifiers modifiers) - => modifiers.HasFlag(FieldModifiers.Public) || - modifiers.HasFlag(FieldModifiers.Protected) && !modifiers.HasFlag(FieldModifiers.Private); - private static bool IsCompatibleField(FieldProvider previous, FieldInfo current) // Historical constant expressions are not necessarily available (e.g. metadata symbols). // Constant-value reconciliation is outside this narrow restoration path. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 807643f3ea8..9a2fbf8a31b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -1218,7 +1218,7 @@ protected internal virtual IReadOnlyList BuildMethodsForBackComp foreach (var previousMethod in previousMethods) { if (currentMethodSignatures.ContainsKey(previousMethod.Signature) - || !MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers) + || !MethodSignatureHelper.IsPublicApi(previousMethod.Signature) || BackCompatHelper.IsMethodRemovalAcceptedInBaseline(this, previousMethod.Signature)) { continue; @@ -1283,7 +1283,7 @@ protected internal virtual IReadOnlyList BuildConstructorsF // Check if the last contract had a public constructor with matching parameters foreach (var previousConstructor in LastContractView.Constructors) { - if (!previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) { continue; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index 5ea3e2058a2..496b8221bb0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -10,6 +10,7 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.TypeSpec.Generator.Utilities; namespace Microsoft.TypeSpec.Generator { @@ -19,6 +20,17 @@ internal static bool IsPublicApi(MethodSignatureModifiers modifiers) => (modifiers.HasFlag(MethodSignatureModifiers.Public) || modifiers.HasFlag(MethodSignatureModifiers.Protected)) && !modifiers.HasFlag(MethodSignatureModifiers.Private); + internal static bool IsPublicApi(MethodSignatureBase signature) + => IsPublicApi(signature.Modifiers) && !signature.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); + + internal static bool IsPublicApi(PropertyProvider property) + => IsPublicApi(property.Modifiers) && !property.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); + + internal static bool IsPublicApi(FieldProvider field) + => (field.Modifiers.HasFlag(FieldModifiers.Public) || field.Modifiers.HasFlag(FieldModifiers.Protected)) + && !field.Modifiers.HasFlag(FieldModifiers.Private) + && !field.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); + internal static bool ContainsSameParameters(MethodSignature method1, MethodSignature method2) { var count = method1.Parameters.Count; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index 8bb75ae6fa4..31a425bc11b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -71,14 +71,7 @@ private IReadOnlyDictionary PopulateNameMap() public TypeProvider? FindForTypeInLastContract(string ns, string name, string? declaringTypeName = null) { - var type = FindTypeInCompilation( - LastContract, - ns, - name, - true, - declaringTypeName, - includeInternal: false, - excludeExperimentalMembers: true); + var type = FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false); return type is NamedTypeSymbolProvider { IsExperimental: true } ? null : type; } @@ -122,8 +115,7 @@ private static string GetFullyQualifiedMetadataName(string ns, string name, stri string name, bool includeReferencedAssemblies, string? declaringTypeName = null, - bool includeInternal = true, - bool excludeExperimentalMembers = false) + bool includeInternal = true) { if (compilation == null) { @@ -152,7 +144,7 @@ private static string GetFullyQualifiedMetadataName(string ns, string name, stri type = null; } - return type != null ? new NamedTypeSymbolProvider(type, compilation, excludeExperimentalMembers) : null; + return type != null ? new NamedTypeSymbolProvider(type, compilation) : null; } private bool TryGetName(ISymbol symbol, [NotNullWhen(true)] out string? name) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index a0feb39b122..2ea0b03eb78 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -144,7 +144,7 @@ public static bool ParametersMatch(IReadOnlyList params1, IRe } IEnumerable scopedMethods = lastContractMethods - .Where(m => MethodSignatureHelper.IsPublicApi(m.Signature.Modifiers)); + .Where(m => MethodSignatureHelper.IsPublicApi(m.Signature)); if (methodName != null) { scopedMethods = scopedMethods.Where(m => @@ -252,7 +252,7 @@ public static void RestorePreviousParameterNames( { foreach (var previousMethod in previousMethods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature)) { continue; } @@ -388,7 +388,7 @@ public static void AddBackCompatOverloads(TypeProvider enclosingType, List provider.EnumValues.Single(v => v.Name == "Preview").Value); } + [TestCase(false, "ExperimentalFixedMembers")] + [TestCase(true, "ExperimentalExtensibleMembers")] + public async Task BackCompatibilityIgnoresExperimentalEnumMembers(bool isExtensible, string previousContract) + { + await MockHelpers.LoadMockGeneratorAsync( + lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync(method: previousContract)); + var input = isExtensible + ? InputFactory.StringEnum("Choice", [], isExtensible: true) + : InputFactory.Int32Enum("Choice", []); + var provider = EnumProvider.Create(input); + provider.EnsureBuilt(); + provider.ProcessTypeForBackCompatibility(); + + CollectionAssert.AreEqual(new[] { "Stable" }, provider.EnumValues.Select(value => value.Name).ToArray()); + } + [Test] public async Task ExperimentalFixedEnumDependenciesSurviveBackCompatibility( [Values] bool numeric, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalExtensibleMembers/Choice.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalExtensibleMembers/Choice.cs new file mode 100644 index 00000000000..043b7a2f0fe --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalExtensibleMembers/Choice.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Sample.Models +{ + public readonly struct Choice + { + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public static Choice Experimental => default; + + public static Choice Stable => default; + + private const string ExperimentalValue = "experimental"; + private const string StableValue = "stable"; + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + internal sealed class ExperimentalAttribute : System.Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalFixedMembers/Choice.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalFixedMembers/Choice.cs new file mode 100644 index 00000000000..bae2888d85b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalFixedMembers/Choice.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Sample.Models +{ + public enum Choice + { + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + Experimental = 1, + Stable = 2 + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + internal sealed class ExperimentalAttribute : System.Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + } +} 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 47c533030f3..c770fdfe369 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 @@ -3228,6 +3228,28 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsTrue(countProperty!.Type.Equals(typeof(int))); } + [Test] + public async Task BackCompat_ExperimentalPropertyInLastContractIsIgnored() + { + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders + .OfType() + .Single(model => model.Name == "MockInputModel"); + modelProvider.ProcessTypeForBackCompatibility(); + + Assert.IsTrue(modelProvider.Properties.Single(property => property.Name == "Count").Type.Equals(typeof(int))); + } + [Test] public async Task BackCompat_NonAbstractTypeIsRespected() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ExperimentalPropertyInLastContractIsIgnored/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ExperimentalPropertyInLastContractIsIgnored/MockInputModel.cs new file mode 100644 index 00000000000..e8069792ea6 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ExperimentalPropertyInLastContractIsIgnored/MockInputModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Sample.Models +{ + public class MockInputModel + { + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public string Count { get; set; } + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + internal sealed class ExperimentalAttribute : System.Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalMembersLastContract/Types.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalMembersLastContract/Types.cs index 14672d3f51d..cceb91cfcf8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalMembersLastContract/Types.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityIgnoresExperimentalMembersLastContract/Types.cs @@ -3,7 +3,7 @@ namespace Test { - public class ExperimentalMembersType + public abstract class ExperimentalMembersType { public ExperimentalMembersType() { } @@ -15,6 +15,9 @@ public ExperimentalMembersType(int value) { } public int StableProperty { get; set; } + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] + public const int ExperimentalField = 1; + [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] public void ExperimentalMethod() { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index 89a0c0881b8..4daa80d6364 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -356,15 +356,64 @@ await MockHelpers.LoadMockGeneratorAsync( var lastContractView = typeProvider.LastContractView; Assert.IsNotNull(lastContractView); - CollectionAssert.AreEqual( - new[] { "StableMethod" }, + CollectionAssert.AreEquivalent( + new[] { "ExperimentalMethod", "StableMethod" }, lastContractView!.Methods.Select(method => method.Signature.Name).ToArray()); - CollectionAssert.AreEqual( - new[] { "StableProperty" }, + CollectionAssert.AreEquivalent( + new[] { "ExperimentalProperty", "StableProperty" }, lastContractView.Properties.Select(property => property.Name).ToArray()); - CollectionAssert.AreEqual( - new[] { 0 }, + CollectionAssert.AreEquivalent( + new[] { 0, 1 }, lastContractView.Constructors.Select(constructor => constructor.Signature.Parameters.Count).ToArray()); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi( + lastContractView.Methods.Single(method => method.Signature.Name == "ExperimentalMethod").Signature)); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi( + lastContractView.Methods.Single(method => method.Signature.Name == "StableMethod").Signature)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi( + lastContractView.Properties.Single(property => property.Name == "ExperimentalProperty"))); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi( + lastContractView.Properties.Single(property => property.Name == "StableProperty"))); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi( + lastContractView.Constructors.Single(constructor => constructor.Signature.Parameters.Count == 1).Signature)); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi( + lastContractView.Constructors.Single(constructor => constructor.Signature.Parameters.Count == 0).Signature)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi( + lastContractView.Fields.Single(field => field.Name == "ExperimentalField"))); + } + + [Test] + public async Task BackCompatibilityDoesNotPromoteExperimentalConstructor() + { + await MockHelpers.LoadMockGeneratorAsync( + lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync( + method: nameof(BackCompatibilityIgnoresExperimentalMembersLastContract))); + var typeProvider = new TestTypeProvider( + name: "ExperimentalMembersType", + declarationModifiers: TypeSignatureModifiers.Public | TypeSignatureModifiers.Abstract | TypeSignatureModifiers.Class); + var experimentalConstructor = new ConstructorProvider( + new ConstructorSignature( + typeProvider.Type, + null, + MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected, + [new ParameterProvider("value", $"", typeof(int))]), + MethodBodyStatement.Empty, + typeProvider); + var stableConstructor = new ConstructorProvider( + new ConstructorSignature( + typeProvider.Type, + null, + MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected, + []), + MethodBodyStatement.Empty, + typeProvider); + typeProvider.Update(constructors: [experimentalConstructor, stableConstructor]); + + typeProvider.ProcessTypeForBackCompatibility(); + + Assert.AreEqual( + MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected, + experimentalConstructor.Signature.Modifiers); + Assert.AreEqual(MethodSignatureModifiers.Public, stableConstructor.Signature.Modifiers); } // Validates that a custom TypeProvider inherits the parameter-reordering back-compat behavior From 59115ba00d1941905bbddb5066101c6684f96c20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:30:45 +0000 Subject: [PATCH 06/14] fix(http-client-csharp): skip experimental mapped base properties Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/TypeFactory.cs | 3 ++- .../SystemObjectModelProviderTests.cs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index 1cf21325f0e..24f6698da12 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -69,7 +69,8 @@ internal bool IsLastContractModelBasePropertyCompatible( CSharpType mappedBase, InputModelProperty currentProperty, PropertyProvider lastContractProperty) - => !lastContractProperty.Modifiers.HasFlag(MethodSignatureModifiers.Static) && + => MethodSignatureHelper.IsPublicApi(lastContractProperty) && + !lastContractProperty.Modifiers.HasFlag(MethodSignatureModifiers.Static) && IsLastContractModelBasePropertyCompatibleCore(mappedBase, currentProperty, lastContractProperty); /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs index 64c7a280f7d..209076afe56 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs @@ -1079,6 +1079,25 @@ public void DefaultLastContractMappingIgnoresReferenceNullabilityAnnotations() new CSharpType(typeof(InitOnlyPropertyTarget)), currentProperty, historicalProperty), Is.True); } + [Test] + public void LastContractMappingRejectsExperimentalHistoricalProperty() + { + var currentProperty = InputFactory.Property("value", InputPrimitiveType.String); + var historicalProperty = new PropertyProvider( + $"", + MethodSignatureModifiers.Public, + typeof(string), + "Value", + new AutoPropertyBody(true), + new TestTypeProvider(), + attributes: [new AttributeStatement( + typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute), + Snippet.Literal("TEST001"))]); + + Assert.That(CodeModelGenerator.Instance.TypeFactory.IsLastContractModelBasePropertyCompatible( + new CSharpType(typeof(InitOnlyPropertyTarget)), currentProperty, historicalProperty), Is.False); + } + [Test] public void DownstreamLastContractMappingCanExplicitlyReconcileValueNullability() { From bd7893fa446ffc62e03e7ae2e374ead885ad7826 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:47:59 +0000 Subject: [PATCH 07/14] fix(http-client-csharp): preserve member back-compat behavior Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ApiVersionEnumProvider.cs | 6 +-- .../src/Providers/ExtensibleEnumProvider.cs | 7 +-- .../src/Providers/FixedEnumProvider.cs | 15 +++---- .../Providers/ModelBaseTypeCompatibility.cs | 21 +++++---- .../src/Providers/ModelFactoryProvider.cs | 2 +- .../src/Providers/ModelProvider.cs | 8 ++-- .../src/Providers/PropertyProvider.cs | 2 +- .../Providers/SystemObjectModelProvider.cs | 22 ++++++---- .../src/Providers/TypeProvider.cs | 4 +- .../src/Shared/MethodSignatureHelper.cs | 12 ------ .../src/TypeFactory.cs | 2 +- .../src/Utilities/BackCompatHelper.cs | 6 +-- .../EnumProviders/EnumProviderTests.cs | 16 ------- .../ExperimentalExtensibleMembers/Choice.cs | 24 ----------- .../ExperimentalFixedMembers/Choice.cs | 20 --------- .../ModelProviders/ModelProviderTests.cs | 22 ---------- .../MockInputModel.cs | 19 -------- .../NamedTypeSymbolProviderTests.cs | 27 ++++++++++++ .../ValidateAttributes/Types.cs | 25 +++++++++++ .../Types.cs | 10 +++++ .../test/Providers/TypeProviderTests.cs | 43 ++++++------------- 21 files changed, 121 insertions(+), 192 deletions(-) delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalExtensibleMembers/Choice.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalFixedMembers/Choice.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ExperimentalPropertyInLastContractIsIgnored/MockInputModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/ValidateAttributes/Types.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityDoesNotPromoteProtectedConstructor/Types.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs index 764aa30e46a..c6c3446deaa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs @@ -137,14 +137,14 @@ private List BuildCustomEnumMembers(IReadOnlyList private List BuildApiVersionEnumValuesForBackwardCompatibility(List currentApiVersions) { - var lastContractFields = LastContractView?.Fields.Where(MethodSignatureHelper.IsPublicApi).ToArray(); - if (lastContractFields == null || lastContractFields.Length == 0) + var lastContractFields = LastContractView?.Fields; + if (lastContractFields == null || lastContractFields.Count == 0) { return currentApiVersions; } var currentVersionsLookup = currentApiVersions.ToDictionary(v => v.Name, StringComparer.OrdinalIgnoreCase); - var allMembers = new List(currentApiVersions.Count + lastContractFields.Length); + var allMembers = new List(currentApiVersions.Count + lastContractFields.Count); bool addedPreviousApiVersion = false; // First, add all missing backward compatibility versions in their original order diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs index f06f3196406..7ab35ce7e77 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs @@ -58,10 +58,7 @@ private protected override bool IsCustomizedValueName(string name) protected override IReadOnlyList BuildEnumValues() { - var lastContractNames = LastContractView?.Properties - .Where(MethodSignatureHelper.IsPublicApi) - .Select(p => p.Name) - .ToArray() ?? []; + var lastContractNames = LastContractView?.Properties.Select(p => p.Name).ToArray() ?? []; var generatedNames = GetGeneratedValueNames(_allowedValues, lastContractNames); var values = new EnumTypeMember[_allowedValues.Count]; @@ -291,7 +288,7 @@ protected override TypeProvider[] BuildSerializationProviders() protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) { var lastContractProperties = LastContractView?.Properties - .Where(MethodSignatureHelper.IsPublicApi); + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)); if (lastContractProperties == null || !lastContractProperties.Any()) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs index 3f644d1d396..8a3d341b1c4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/FixedEnumProvider.cs @@ -78,7 +78,7 @@ protected override TypeProvider[] BuildSerializationProviders() protected override IReadOnlyList BuildEnumValues() { var customMembers = new HashSet(CustomCodeView?.Fields ?? []); - var lastContractFields = LastContractView?.Fields.Where(MethodSignatureHelper.IsPublicApi).ToArray() ?? []; + var lastContractFields = LastContractView?.Fields ?? []; var lastContractNames = lastContractFields.Select(f => f.Name).ToArray(); var generatedNames = GetGeneratedValueNames(AllowedValues, lastContractNames); @@ -126,8 +126,8 @@ protected override IReadOnlyList BuildEnumValues() protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) { - var lastContractFields = LastContractView?.Fields.Where(MethodSignatureHelper.IsPublicApi).ToArray(); - if (lastContractFields == null || lastContractFields.Length == 0) + var lastContractFields = LastContractView?.Fields; + if (lastContractFields == null || lastContractFields.Count == 0) { return null; } @@ -212,10 +212,7 @@ private HashSet GetCustomMemberLastContractNames(IReadOnlyList f.OriginalName != null) .Select(f => f.OriginalName!) ?? [], StringComparer.Ordinal); - var lastContractNames = lastContractFields - .Where(MethodSignatureHelper.IsPublicApi) - .Select(f => f.Name) - .ToArray(); + var lastContractNames = lastContractFields.Select(f => f.Name).ToArray(); var generatedNames = GetGeneratedValueNames(AllowedValues, lastContractNames); var customMemberLastContractNames = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -258,9 +255,7 @@ private protected static void AppendMembersNotInLastContract( IReadOnlyList lastContractFields, List allMembers) { - var lastContractNames = new HashSet( - lastContractFields.Where(MethodSignatureHelper.IsPublicApi).Select(f => f.Name), - StringComparer.OrdinalIgnoreCase); + var lastContractNames = new HashSet(lastContractFields.Select(f => f.Name), StringComparer.OrdinalIgnoreCase); foreach (var current in currentValues) { if (!lastContractNames.Contains(current.Name)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs index b5d1bcc3317..a56d9c8ea0c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs @@ -91,17 +91,19 @@ private static bool HasNoUnsupportedLastContractMembers(ModelProvider candidate) return lastContract.Implements.All(previous => currentInterfaces.Any(current => ModelBaseMemberCompatibility.AreTypesCompatible(previous, current))) && lastContract.Constructors - .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature)) + .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers)) .All(constructor => SystemObjectModelProvider.HasSupportedConstructorParameters(constructor.Signature.Parameters) && constructor.Signature.Parameters.All(parameter => candidate.Properties.Any(property => property.AsParameter.Name == parameter.Name && (ModelBaseMemberCompatibility.AreTypesCompatible(parameter.Type, property.Type) || ModelBaseMemberCompatibility.AreTypesCompatible(parameter.Type, property.Type.InputType))))) && lastContract.Methods - .Where(method => MethodSignatureHelper.IsPublicApi(method.Signature)) + .Where(method => MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers)) .All(previous => currentGeneratedMethods.Any(current => AreGeneratedMethodSignaturesEquivalent(previous.Signature, current.Signature))) && - !lastContract.Fields.Any(MethodSignatureHelper.IsPublicApi); + !lastContract.Fields.Any(field => + field.Modifiers.HasFlag(FieldModifiers.Public) || + field.Modifiers.HasFlag(FieldModifiers.Protected)); } private static bool AreGeneratedMethodSignaturesEquivalent( @@ -126,8 +128,8 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) return true; } - var currentProperties = GetUniquePublicProperties(candidate.Properties, isLastContract: false); - var previousProperties = GetUniquePublicProperties(lastContract.Properties, isLastContract: true); + var currentProperties = GetUniquePublicProperties(candidate.Properties); + var previousProperties = GetUniquePublicProperties(lastContract.Properties); if (currentProperties is null || previousProperties is null) { return false; @@ -142,13 +144,10 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) } private static Dictionary? GetUniquePublicProperties( - IReadOnlyList properties, - bool isLastContract) + IReadOnlyList properties) { var result = new Dictionary(StringComparer.Ordinal); - foreach (var property in properties.Where(property => isLastContract - ? MethodSignatureHelper.IsPublicApi(property) - : MethodSignatureHelper.IsPublicApi(property.Modifiers))) + foreach (var property in properties.Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers))) { // Overloaded indexers can share a symbol name. They are unsupported contracts, not // malformed input that should abort generation with a duplicate-key exception. @@ -166,7 +165,7 @@ private static bool IsRequiredInitializationProperty(PropertyProvider property) private bool LastContractDerivedConstructorHasRequiredParameter(PropertyProvider property) => _model.LastContractView?.Constructors.Any(constructor => - MethodSignatureHelper.IsPublicApi(constructor.Signature) && + MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && constructor.Signature.Parameters.Any(parameter => parameter.Name == property.AsParameter.Name && parameter.Type.Equals(property.Type, ignoreNullable: true) && diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index d9bbad0580c..f60171bd37d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -127,7 +127,7 @@ protected internal sealed override IReadOnlyList BuildMethodsFor foreach (var previousMethod in LastContractView.Methods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers)) { continue; } 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 f0e77855169..f0cedac065f 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 @@ -107,7 +107,7 @@ _inputModel.BaseModel is not null private IDictionary LastContractPropertiesMap => _lastContractPropertiesMap ??= LastContractView?.Properties - .Where(MethodSignatureHelper.IsPublicApi) + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) .ToDictionary(p => p.Name, p => p.Type) ?? []; private IDictionary? _lastContractPropertiesMap; @@ -1111,7 +1111,7 @@ protected internal override IReadOnlyList BuildConstructors foreach (var previousConstructor in restorablePreviousConstructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers)) { continue; } @@ -1183,7 +1183,7 @@ private void RestorePreviousConstructorParameterNames( const MethodSignatureModifiers privateProtected = MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected; foreach (var previousConstructor in previousConstructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers)) { continue; } @@ -2056,7 +2056,7 @@ private bool ShouldUseObjectAdditionalProperties() // Check if the property exists in the last contract by name var lastContractProperty = LastContractView.Properties.FirstOrDefault(p => - MethodSignatureHelper.IsPublicApi(p) && + MethodSignatureHelper.IsPublicApi(p.Modifiers) && p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName); if (lastContractProperty == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index 9d47e072373..f060d44cf5f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -113,7 +113,7 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T var canonicalName = identifierName.NormalizeCSharpAcronyms(isDateTime); var enclosingTypeName = enclosingType.Name; var lastContractProperties = enclosingType.LastContractView?.Properties - .Where(MethodSignatureHelper.IsPublicApi) + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) .ToList(); // An exact input-identifier match is authoritative: it is the name this property would have had diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs index 8d5dad819d2..df21d9faed0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs @@ -159,7 +159,7 @@ private bool HasCallableInitializationConstructor() } foreach (var constructor in constructors.Where(constructor => - MethodSignatureHelper.IsPublicApi(constructor.Signature) && + MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && HasSupportedConstructorParameters(constructor.Signature.Parameters))) { var matches = constructor.Signature.Parameters @@ -269,7 +269,7 @@ private bool TryGetLastContractConstructor( { var properties = Properties; foreach (var candidate in _lastContractType!.Constructors - .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature) && + .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && HasSupportedConstructorParameters(constructor.Signature.Parameters)) .OrderByDescending(constructor => constructor.Signature.Parameters.Count)) { @@ -316,7 +316,7 @@ protected internal override PropertyProvider[] BuildProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property) && seenPropertyNames.Add(property.Name))) + MethodSignatureHelper.IsPublicApi(property.Modifiers) && seenPropertyNames.Add(property.Name))) { if (ApplyCurrentInputMetadata(property) is { } projectedProperty) { @@ -342,7 +342,7 @@ internal bool HasCompatibleLastContractProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { if (provider.Properties - .Where(MethodSignatureHelper.IsPublicApi) + .Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers)) .Any(property => FindEffectiveFrameworkProperty(property.Name) is not { } candidate || !IsCompatibleProperty(property, candidate))) { @@ -391,7 +391,7 @@ internal bool HasCompatibleLastContractNonPropertyMembers() } foreach (var method in provider.Methods.Where(method => - MethodSignatureHelper.IsPublicApi(method.Signature))) + MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers))) { var signature = method.Signature; if (overriddenMethods.Any(overrideSignature => @@ -407,7 +407,7 @@ internal bool HasCompatibleLastContractNonPropertyMembers() overriddenMethods.AddRange(provider.Methods.Select(method => method.Signature) .Where(signature => signature.Modifiers.HasFlag(MethodSignatureModifiers.Override))); if (provider.Fields - .Where(MethodSignatureHelper.IsPublicApi) + .Where(field => IsPublicApiField(field.Modifiers)) .Any(field => FindEffectiveFrameworkMember(field.Name) is not FieldInfo candidate || !IsCompatibleField(field, candidate))) { @@ -425,16 +425,16 @@ internal IEnumerable GetPublicApiMemberNames() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property))) + MethodSignatureHelper.IsPublicApi(property.Modifiers))) { yield return property.Name; } foreach (var method in provider.Methods.Where(method => - MethodSignatureHelper.IsPublicApi(method.Signature))) + MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers))) { yield return GetPublicApiMemberName(method.Signature.Name); } - foreach (var field in provider.Fields.Where(MethodSignatureHelper.IsPublicApi)) + foreach (var field in provider.Fields.Where(field => IsPublicApiField(field.Modifiers))) { yield return field.Name; } @@ -669,6 +669,10 @@ private static bool RequiresOverridableMethod(MethodSignatureModifiers modifiers modifiers.HasFlag(MethodSignatureModifiers.Abstract) || modifiers.HasFlag(MethodSignatureModifiers.Override)); + private static bool IsPublicApiField(FieldModifiers modifiers) + => modifiers.HasFlag(FieldModifiers.Public) || + modifiers.HasFlag(FieldModifiers.Protected) && !modifiers.HasFlag(FieldModifiers.Private); + private static bool IsCompatibleField(FieldProvider previous, FieldInfo current) // Historical constant expressions are not necessarily available (e.g. metadata symbols). // Constant-value reconciliation is outside this narrow restoration path. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 9a2fbf8a31b..807643f3ea8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -1218,7 +1218,7 @@ protected internal virtual IReadOnlyList BuildMethodsForBackComp foreach (var previousMethod in previousMethods) { if (currentMethodSignatures.ContainsKey(previousMethod.Signature) - || !MethodSignatureHelper.IsPublicApi(previousMethod.Signature) + || !MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers) || BackCompatHelper.IsMethodRemovalAcceptedInBaseline(this, previousMethod.Signature)) { continue; @@ -1283,7 +1283,7 @@ protected internal virtual IReadOnlyList BuildConstructorsF // Check if the last contract had a public constructor with matching parameters foreach (var previousConstructor in LastContractView.Constructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) + if (!previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) { continue; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index 496b8221bb0..5ea3e2058a2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -10,7 +10,6 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; -using Microsoft.TypeSpec.Generator.Utilities; namespace Microsoft.TypeSpec.Generator { @@ -20,17 +19,6 @@ internal static bool IsPublicApi(MethodSignatureModifiers modifiers) => (modifiers.HasFlag(MethodSignatureModifiers.Public) || modifiers.HasFlag(MethodSignatureModifiers.Protected)) && !modifiers.HasFlag(MethodSignatureModifiers.Private); - internal static bool IsPublicApi(MethodSignatureBase signature) - => IsPublicApi(signature.Modifiers) && !signature.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); - - internal static bool IsPublicApi(PropertyProvider property) - => IsPublicApi(property.Modifiers) && !property.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); - - internal static bool IsPublicApi(FieldProvider field) - => (field.Modifiers.HasFlag(FieldModifiers.Public) || field.Modifiers.HasFlag(FieldModifiers.Protected)) - && !field.Modifiers.HasFlag(FieldModifiers.Private) - && !field.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); - internal static bool ContainsSameParameters(MethodSignature method1, MethodSignature method2) { var count = method1.Parameters.Count; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index 24f6698da12..cb2fdf43237 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -69,7 +69,7 @@ internal bool IsLastContractModelBasePropertyCompatible( CSharpType mappedBase, InputModelProperty currentProperty, PropertyProvider lastContractProperty) - => MethodSignatureHelper.IsPublicApi(lastContractProperty) && + => MethodSignatureHelper.IsPublicApi(lastContractProperty.Modifiers) && !lastContractProperty.Modifiers.HasFlag(MethodSignatureModifiers.Static) && IsLastContractModelBasePropertyCompatibleCore(mappedBase, currentProperty, lastContractProperty); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index 2ea0b03eb78..a0feb39b122 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -144,7 +144,7 @@ public static bool ParametersMatch(IReadOnlyList params1, IRe } IEnumerable scopedMethods = lastContractMethods - .Where(m => MethodSignatureHelper.IsPublicApi(m.Signature)); + .Where(m => MethodSignatureHelper.IsPublicApi(m.Signature.Modifiers)); if (methodName != null) { scopedMethods = scopedMethods.Where(m => @@ -252,7 +252,7 @@ public static void RestorePreviousParameterNames( { foreach (var previousMethod in previousMethods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers)) { continue; } @@ -388,7 +388,7 @@ public static void AddBackCompatOverloads(TypeProvider enclosingType, List provider.EnumValues.Single(v => v.Name == "Preview").Value); } - [TestCase(false, "ExperimentalFixedMembers")] - [TestCase(true, "ExperimentalExtensibleMembers")] - public async Task BackCompatibilityIgnoresExperimentalEnumMembers(bool isExtensible, string previousContract) - { - await MockHelpers.LoadMockGeneratorAsync( - lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync(method: previousContract)); - var input = isExtensible - ? InputFactory.StringEnum("Choice", [], isExtensible: true) - : InputFactory.Int32Enum("Choice", []); - var provider = EnumProvider.Create(input); - provider.EnsureBuilt(); - provider.ProcessTypeForBackCompatibility(); - - CollectionAssert.AreEqual(new[] { "Stable" }, provider.EnumValues.Select(value => value.Name).ToArray()); - } - [Test] public async Task ExperimentalFixedEnumDependenciesSurviveBackCompatibility( [Values] bool numeric, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalExtensibleMembers/Choice.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalExtensibleMembers/Choice.cs deleted file mode 100644 index 043b7a2f0fe..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalExtensibleMembers/Choice.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Sample.Models -{ - public readonly struct Choice - { - [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] - public static Choice Experimental => default; - - public static Choice Stable => default; - - private const string ExperimentalValue = "experimental"; - private const string StableValue = "stable"; - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - internal sealed class ExperimentalAttribute : System.Attribute - { - public ExperimentalAttribute(string diagnosticId) { } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalFixedMembers/Choice.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalFixedMembers/Choice.cs deleted file mode 100644 index bae2888d85b..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/TestData/EnumProviderTests/ExperimentalFixedMembers/Choice.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Sample.Models -{ - public enum Choice - { - [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] - Experimental = 1, - Stable = 2 - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - internal sealed class ExperimentalAttribute : System.Attribute - { - public ExperimentalAttribute(string diagnosticId) { } - } -} 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 c770fdfe369..47c533030f3 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 @@ -3228,28 +3228,6 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsTrue(countProperty!.Type.Equals(typeof(int))); } - [Test] - public async Task BackCompat_ExperimentalPropertyInLastContractIsIgnored() - { - var inputModel = InputFactory.Model( - "MockInputModel", - properties: - [ - InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: true), - ]); - - await MockHelpers.LoadMockGeneratorAsync( - inputModelTypes: [inputModel], - lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync()); - - var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders - .OfType() - .Single(model => model.Name == "MockInputModel"); - modelProvider.ProcessTypeForBackCompatibility(); - - Assert.IsTrue(modelProvider.Properties.Single(property => property.Name == "Count").Type.Equals(typeof(int))); - } - [Test] public async Task BackCompat_NonAbstractTypeIsRespected() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ExperimentalPropertyInLastContractIsIgnored/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ExperimentalPropertyInLastContractIsIgnored/MockInputModel.cs deleted file mode 100644 index e8069792ea6..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ExperimentalPropertyInLastContractIsIgnored/MockInputModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Sample.Models -{ - public class MockInputModel - { - [System.Diagnostics.CodeAnalysis.Experimental("PREVIOUS001")] - public string Count { get; set; } - } -} - -namespace System.Diagnostics.CodeAnalysis -{ - internal sealed class ExperimentalAttribute : System.Attribute - { - public ExperimentalAttribute(string diagnosticId) { } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs index ecddb0eadce..2daad41fa1c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/NamedTypeSymbolProviderTests.cs @@ -120,6 +120,33 @@ public class GenericType Assert.AreEqual("TMethod", method.Signature.GenericArguments!.Single().Name); } + [Test] + public async Task ValidateAttributes() + { + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + var compilation = mockGenerator.Object.SourceInputModel.Customization!; + var symbol = compilation.GetTypeByMetadataName("Sample.ExperimentalType"); + Assert.IsNotNull(symbol); + + var provider = new NamedTypeSymbolProvider(symbol!, compilation); + var constructor = provider.Constructors.Single(); + + Assert.IsTrue(provider.IsExperimental); + Assert.AreEqual( + Literal("TYPE001").ToDisplayString(), + provider.Attributes.Single(attribute => attribute.Type.Equals(typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute))) + .Arguments.Single().ToDisplayString()); + Assert.AreEqual( + Literal("CTOR001").ToDisplayString(), + constructor.Signature.Attributes.Single(attribute => attribute.Type.Equals(typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute))) + .Arguments.Single().ToDisplayString()); + + var stableSymbol = compilation.GetTypeByMetadataName("Sample.StableType"); + Assert.IsNotNull(stableSymbol); + Assert.IsFalse(new NamedTypeSymbolProvider(stableSymbol!, compilation).IsExperimental); + } + [Test] public void ValidateNamespaceNestedType() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/ValidateAttributes/Types.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/ValidateAttributes/Types.cs new file mode 100644 index 00000000000..7b693e881da --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/TestData/NamedTypeSymbolProviderTests/ValidateAttributes/Types.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace System.Diagnostics.CodeAnalysis +{ + [System.AttributeUsage(System.AttributeTargets.All)] + internal sealed class ExperimentalAttribute : System.Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + } +} + +namespace Sample +{ + [System.Diagnostics.CodeAnalysis.Experimental("TYPE001")] + public class ExperimentalType + { + [System.Diagnostics.CodeAnalysis.Experimental("CTOR001")] + public ExperimentalType() { } + } + + public class StableType + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityDoesNotPromoteProtectedConstructor/Types.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityDoesNotPromoteProtectedConstructor/Types.cs new file mode 100644 index 00000000000..161410ee1d1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BackCompatibilityDoesNotPromoteProtectedConstructor/Types.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Test +{ + public abstract class ProtectedConstructorType + { + protected ProtectedConstructorType(int value) { } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index 4daa80d6364..83bcb5b669f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -152,7 +152,7 @@ await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => owner.ProcessTypeForBackCompatibility(); var shims = owner.Methods.Where(m => m.Signature.Attributes.Any(a => a.Type.Equals(typeof(EditorBrowsableAttribute)))).ToArray(); - Assert.AreEqual(previouslyExperimental ? 0 : 3, shims.Length); + Assert.AreEqual(3, shims.Length); foreach (var shim in shims) { var experiment = shim.Signature.Attributes.SingleOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute))); @@ -348,10 +348,11 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public async Task BackCompatibilityIgnoresExperimentalMembersLastContract() + public async Task ExperimentalAttributesDoNotChangePublicApiDetection() { await MockHelpers.LoadMockGeneratorAsync( - lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync()); + lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync( + method: "BackCompatibilityIgnoresExperimentalMembersLastContract")); var typeProvider = new TestTypeProvider(name: "ExperimentalMembersType"); var lastContractView = typeProvider.LastContractView; @@ -365,32 +366,25 @@ await MockHelpers.LoadMockGeneratorAsync( CollectionAssert.AreEquivalent( new[] { 0, 1 }, lastContractView.Constructors.Select(constructor => constructor.Signature.Parameters.Count).ToArray()); - Assert.IsFalse(MethodSignatureHelper.IsPublicApi( - lastContractView.Methods.Single(method => method.Signature.Name == "ExperimentalMethod").Signature)); Assert.IsTrue(MethodSignatureHelper.IsPublicApi( - lastContractView.Methods.Single(method => method.Signature.Name == "StableMethod").Signature)); - Assert.IsFalse(MethodSignatureHelper.IsPublicApi( - lastContractView.Properties.Single(property => property.Name == "ExperimentalProperty"))); + lastContractView.Methods.Single(method => method.Signature.Name == "ExperimentalMethod").Signature.Modifiers)); Assert.IsTrue(MethodSignatureHelper.IsPublicApi( - lastContractView.Properties.Single(property => property.Name == "StableProperty"))); - Assert.IsFalse(MethodSignatureHelper.IsPublicApi( - lastContractView.Constructors.Single(constructor => constructor.Signature.Parameters.Count == 1).Signature)); + lastContractView.Properties.Single(property => property.Name == "ExperimentalProperty").Modifiers)); Assert.IsTrue(MethodSignatureHelper.IsPublicApi( - lastContractView.Constructors.Single(constructor => constructor.Signature.Parameters.Count == 0).Signature)); - Assert.IsFalse(MethodSignatureHelper.IsPublicApi( - lastContractView.Fields.Single(field => field.Name == "ExperimentalField"))); + lastContractView.Constructors.Single(constructor => constructor.Signature.Parameters.Count == 1).Signature.Modifiers)); + Assert.IsTrue(lastContractView.Fields.Single(field => field.Name == "ExperimentalField").Modifiers.HasFlag(FieldModifiers.Public)); } [Test] - public async Task BackCompatibilityDoesNotPromoteExperimentalConstructor() + public async Task BackCompatibilityDoesNotPromoteProtectedConstructor() { await MockHelpers.LoadMockGeneratorAsync( lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync( - method: nameof(BackCompatibilityIgnoresExperimentalMembersLastContract))); + method: nameof(BackCompatibilityDoesNotPromoteProtectedConstructor))); var typeProvider = new TestTypeProvider( - name: "ExperimentalMembersType", + name: "ProtectedConstructorType", declarationModifiers: TypeSignatureModifiers.Public | TypeSignatureModifiers.Abstract | TypeSignatureModifiers.Class); - var experimentalConstructor = new ConstructorProvider( + var privateProtectedConstructor = new ConstructorProvider( new ConstructorSignature( typeProvider.Type, null, @@ -398,22 +392,13 @@ await MockHelpers.LoadMockGeneratorAsync( [new ParameterProvider("value", $"", typeof(int))]), MethodBodyStatement.Empty, typeProvider); - var stableConstructor = new ConstructorProvider( - new ConstructorSignature( - typeProvider.Type, - null, - MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected, - []), - MethodBodyStatement.Empty, - typeProvider); - typeProvider.Update(constructors: [experimentalConstructor, stableConstructor]); + typeProvider.Update(constructors: [privateProtectedConstructor]); typeProvider.ProcessTypeForBackCompatibility(); Assert.AreEqual( MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected, - experimentalConstructor.Signature.Modifiers); - Assert.AreEqual(MethodSignatureModifiers.Public, stableConstructor.Signature.Modifiers); + privateProtectedConstructor.Signature.Modifiers); } // Validates that a custom TypeProvider inherits the parameter-reordering back-compat behavior From 684dd0edc06004d5864c4143001e9277fcd04488 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:07:39 +0000 Subject: [PATCH 08/14] test(http-client-csharp): preserve experimental member boundaries Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../SystemObjectModelProviderTests.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs index 209076afe56..64c7a280f7d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs @@ -1079,25 +1079,6 @@ public void DefaultLastContractMappingIgnoresReferenceNullabilityAnnotations() new CSharpType(typeof(InitOnlyPropertyTarget)), currentProperty, historicalProperty), Is.True); } - [Test] - public void LastContractMappingRejectsExperimentalHistoricalProperty() - { - var currentProperty = InputFactory.Property("value", InputPrimitiveType.String); - var historicalProperty = new PropertyProvider( - $"", - MethodSignatureModifiers.Public, - typeof(string), - "Value", - new AutoPropertyBody(true), - new TestTypeProvider(), - attributes: [new AttributeStatement( - typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute), - Snippet.Literal("TEST001"))]); - - Assert.That(CodeModelGenerator.Instance.TypeFactory.IsLastContractModelBasePropertyCompatible( - new CSharpType(typeof(InitOnlyPropertyTarget)), currentProperty, historicalProperty), Is.False); - } - [Test] public void DownstreamLastContractMappingCanExplicitlyReconcileValueNullability() { From 9219c3593f192dc40143262ea6740dcb63ebc6dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:20:02 +0000 Subject: [PATCH 09/14] test(http-client-csharp): apply attribute-aware compatibility checks Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ExtensibleEnumProvider.cs | 2 +- .../Providers/ModelBaseTypeCompatibility.cs | 21 ++++++++-------- .../src/Providers/ModelFactoryProvider.cs | 2 +- .../src/Providers/ModelProvider.cs | 8 +++---- .../src/Providers/PropertyProvider.cs | 2 +- .../Providers/SystemObjectModelProvider.cs | 23 +++++++++--------- .../src/Providers/TypeProvider.cs | 5 ++-- .../src/Shared/MethodSignatureHelper.cs | 15 ++++++++++-- .../src/TypeFactory.cs | 2 +- .../src/Utilities/BackCompatHelper.cs | 6 ++--- .../SystemObjectModelProviderTests.cs | 19 +++++++++++++++ .../test/Providers/TypeProviderTests.cs | 24 ++++++++++++------- .../generator/docs/backward-compatibility.md | 2 +- 13 files changed, 84 insertions(+), 47 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs index 7ab35ce7e77..406da6c37fa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs @@ -288,7 +288,7 @@ protected override TypeProvider[] BuildSerializationProviders() protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) { var lastContractProperties = LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)); + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)); if (lastContractProperties == null || !lastContractProperties.Any()) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs index a56d9c8ea0c..9eb48bb14f5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs @@ -91,19 +91,17 @@ private static bool HasNoUnsupportedLastContractMembers(ModelProvider candidate) return lastContract.Implements.All(previous => currentInterfaces.Any(current => ModelBaseMemberCompatibility.AreTypesCompatible(previous, current))) && lastContract.Constructors - .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers)) + .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers, constructor.Signature.Attributes)) .All(constructor => SystemObjectModelProvider.HasSupportedConstructorParameters(constructor.Signature.Parameters) && constructor.Signature.Parameters.All(parameter => candidate.Properties.Any(property => property.AsParameter.Name == parameter.Name && (ModelBaseMemberCompatibility.AreTypesCompatible(parameter.Type, property.Type) || ModelBaseMemberCompatibility.AreTypesCompatible(parameter.Type, property.Type.InputType))))) && lastContract.Methods - .Where(method => MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers)) + .Where(method => MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers, method.Signature.Attributes)) .All(previous => currentGeneratedMethods.Any(current => AreGeneratedMethodSignaturesEquivalent(previous.Signature, current.Signature))) && - !lastContract.Fields.Any(field => - field.Modifiers.HasFlag(FieldModifiers.Public) || - field.Modifiers.HasFlag(FieldModifiers.Protected)); + !lastContract.Fields.Any(field => MethodSignatureHelper.IsPublicApi(field.Modifiers, field.Attributes)); } private static bool AreGeneratedMethodSignaturesEquivalent( @@ -128,8 +126,8 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) return true; } - var currentProperties = GetUniquePublicProperties(candidate.Properties); - var previousProperties = GetUniquePublicProperties(lastContract.Properties); + var currentProperties = GetUniquePublicProperties(candidate.Properties, isLastContract: false); + var previousProperties = GetUniquePublicProperties(lastContract.Properties, isLastContract: true); if (currentProperties is null || previousProperties is null) { return false; @@ -144,10 +142,13 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) } private static Dictionary? GetUniquePublicProperties( - IReadOnlyList properties) + IReadOnlyList properties, + bool isLastContract) { var result = new Dictionary(StringComparer.Ordinal); - foreach (var property in properties.Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers))) + foreach (var property in properties.Where(property => isLastContract + ? MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes) + : MethodSignatureHelper.IsPublicApi(property.Modifiers))) { // Overloaded indexers can share a symbol name. They are unsupported contracts, not // malformed input that should abort generation with a duplicate-key exception. @@ -165,7 +166,7 @@ private static bool IsRequiredInitializationProperty(PropertyProvider property) private bool LastContractDerivedConstructorHasRequiredParameter(PropertyProvider property) => _model.LastContractView?.Constructors.Any(constructor => - MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && + MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers, constructor.Signature.Attributes) && constructor.Signature.Parameters.Any(parameter => parameter.Name == property.AsParameter.Name && parameter.Type.Equals(property.Type, ignoreNullable: true) && diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index f60171bd37d..7bb7b4ff8fc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -127,7 +127,7 @@ protected internal sealed override IReadOnlyList BuildMethodsFor foreach (var previousMethod in LastContractView.Methods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers, previousMethod.Signature.Attributes)) { continue; } 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 f0cedac065f..9594c3b32e2 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 @@ -107,7 +107,7 @@ _inputModel.BaseModel is not null private IDictionary LastContractPropertiesMap => _lastContractPropertiesMap ??= LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)) .ToDictionary(p => p.Name, p => p.Type) ?? []; private IDictionary? _lastContractPropertiesMap; @@ -1111,7 +1111,7 @@ protected internal override IReadOnlyList BuildConstructors foreach (var previousConstructor in restorablePreviousConstructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers, previousConstructor.Signature.Attributes)) { continue; } @@ -1183,7 +1183,7 @@ private void RestorePreviousConstructorParameterNames( const MethodSignatureModifiers privateProtected = MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected; foreach (var previousConstructor in previousConstructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers, previousConstructor.Signature.Attributes)) { continue; } @@ -2056,7 +2056,7 @@ private bool ShouldUseObjectAdditionalProperties() // Check if the property exists in the last contract by name var lastContractProperty = LastContractView.Properties.FirstOrDefault(p => - MethodSignatureHelper.IsPublicApi(p.Modifiers) && + MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes) && p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName); if (lastContractProperty == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index f060d44cf5f..d3a14740a97 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -113,7 +113,7 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T var canonicalName = identifierName.NormalizeCSharpAcronyms(isDateTime); var enclosingTypeName = enclosingType.Name; var lastContractProperties = enclosingType.LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)) .ToList(); // An exact input-identifier match is authoritative: it is the name this property would have had diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs index df21d9faed0..d8699af8614 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs @@ -159,7 +159,7 @@ private bool HasCallableInitializationConstructor() } foreach (var constructor in constructors.Where(constructor => - MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && + MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers, constructor.Signature.Attributes) && HasSupportedConstructorParameters(constructor.Signature.Parameters))) { var matches = constructor.Signature.Parameters @@ -269,7 +269,7 @@ private bool TryGetLastContractConstructor( { var properties = Properties; foreach (var candidate in _lastContractType!.Constructors - .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers) && + .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers, constructor.Signature.Attributes) && HasSupportedConstructorParameters(constructor.Signature.Parameters)) .OrderByDescending(constructor => constructor.Signature.Parameters.Count)) { @@ -316,7 +316,7 @@ protected internal override PropertyProvider[] BuildProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property.Modifiers) && seenPropertyNames.Add(property.Name))) + MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes) && seenPropertyNames.Add(property.Name))) { if (ApplyCurrentInputMetadata(property) is { } projectedProperty) { @@ -342,7 +342,7 @@ internal bool HasCompatibleLastContractProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { if (provider.Properties - .Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers)) + .Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes)) .Any(property => FindEffectiveFrameworkProperty(property.Name) is not { } candidate || !IsCompatibleProperty(property, candidate))) { @@ -391,7 +391,7 @@ internal bool HasCompatibleLastContractNonPropertyMembers() } foreach (var method in provider.Methods.Where(method => - MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers))) + MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers, method.Signature.Attributes))) { var signature = method.Signature; if (overriddenMethods.Any(overrideSignature => @@ -407,7 +407,7 @@ internal bool HasCompatibleLastContractNonPropertyMembers() overriddenMethods.AddRange(provider.Methods.Select(method => method.Signature) .Where(signature => signature.Modifiers.HasFlag(MethodSignatureModifiers.Override))); if (provider.Fields - .Where(field => IsPublicApiField(field.Modifiers)) + .Where(field => IsPublicApiField(field)) .Any(field => FindEffectiveFrameworkMember(field.Name) is not FieldInfo candidate || !IsCompatibleField(field, candidate))) { @@ -425,16 +425,16 @@ internal IEnumerable GetPublicApiMemberNames() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property.Modifiers))) + MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes))) { yield return property.Name; } foreach (var method in provider.Methods.Where(method => - MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers))) + MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers, method.Signature.Attributes))) { yield return GetPublicApiMemberName(method.Signature.Name); } - foreach (var field in provider.Fields.Where(field => IsPublicApiField(field.Modifiers))) + foreach (var field in provider.Fields.Where(IsPublicApiField)) { yield return field.Name; } @@ -669,9 +669,8 @@ private static bool RequiresOverridableMethod(MethodSignatureModifiers modifiers modifiers.HasFlag(MethodSignatureModifiers.Abstract) || modifiers.HasFlag(MethodSignatureModifiers.Override)); - private static bool IsPublicApiField(FieldModifiers modifiers) - => modifiers.HasFlag(FieldModifiers.Public) || - modifiers.HasFlag(FieldModifiers.Protected) && !modifiers.HasFlag(FieldModifiers.Private); + private static bool IsPublicApiField(FieldProvider field) + => MethodSignatureHelper.IsPublicApi(field.Modifiers, field.Attributes); private static bool IsCompatibleField(FieldProvider previous, FieldInfo current) // Historical constant expressions are not necessarily available (e.g. metadata symbols). diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 807643f3ea8..02b17f965c1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -1218,7 +1218,7 @@ protected internal virtual IReadOnlyList BuildMethodsForBackComp foreach (var previousMethod in previousMethods) { if (currentMethodSignatures.ContainsKey(previousMethod.Signature) - || !MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers) + || !MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers, previousMethod.Signature.Attributes) || BackCompatHelper.IsMethodRemovalAcceptedInBaseline(this, previousMethod.Signature)) { continue; @@ -1283,7 +1283,8 @@ protected internal virtual IReadOnlyList BuildConstructorsF // Check if the last contract had a public constructor with matching parameters foreach (var previousConstructor in LastContractView.Constructors) { - if (!previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) + if (!previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) || + !MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers, previousConstructor.Signature.Attributes)) { continue; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index 5ea3e2058a2..97a2f7b48dc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -10,14 +10,25 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.TypeSpec.Generator.Utilities; namespace Microsoft.TypeSpec.Generator { internal class MethodSignatureHelper { - internal static bool IsPublicApi(MethodSignatureModifiers modifiers) + internal static bool IsPublicApi( + MethodSignatureModifiers modifiers, + IReadOnlyList? attributes = null) => (modifiers.HasFlag(MethodSignatureModifiers.Public) || modifiers.HasFlag(MethodSignatureModifiers.Protected)) - && !modifiers.HasFlag(MethodSignatureModifiers.Private); + && !modifiers.HasFlag(MethodSignatureModifiers.Private) + && attributes?.Any(ExperimentalApiHelpers.IsExperimentalAttribute) != true; + + internal static bool IsPublicApi( + FieldModifiers modifiers, + IReadOnlyList? attributes = null) + => (modifiers.HasFlag(FieldModifiers.Public) || modifiers.HasFlag(FieldModifiers.Protected)) + && !modifiers.HasFlag(FieldModifiers.Private) + && attributes?.Any(ExperimentalApiHelpers.IsExperimentalAttribute) != true; internal static bool ContainsSameParameters(MethodSignature method1, MethodSignature method2) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index cb2fdf43237..efaa8b260d5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -69,7 +69,7 @@ internal bool IsLastContractModelBasePropertyCompatible( CSharpType mappedBase, InputModelProperty currentProperty, PropertyProvider lastContractProperty) - => MethodSignatureHelper.IsPublicApi(lastContractProperty.Modifiers) && + => MethodSignatureHelper.IsPublicApi(lastContractProperty.Modifiers, lastContractProperty.Attributes) && !lastContractProperty.Modifiers.HasFlag(MethodSignatureModifiers.Static) && IsLastContractModelBasePropertyCompatibleCore(mappedBase, currentProperty, lastContractProperty); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index a0feb39b122..cc48ae89583 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -144,7 +144,7 @@ public static bool ParametersMatch(IReadOnlyList params1, IRe } IEnumerable scopedMethods = lastContractMethods - .Where(m => MethodSignatureHelper.IsPublicApi(m.Signature.Modifiers)); + .Where(m => MethodSignatureHelper.IsPublicApi(m.Signature.Modifiers, m.Signature.Attributes)); if (methodName != null) { scopedMethods = scopedMethods.Where(m => @@ -252,7 +252,7 @@ public static void RestorePreviousParameterNames( { foreach (var previousMethod in previousMethods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers, previousMethod.Signature.Attributes)) { continue; } @@ -388,7 +388,7 @@ public static void AddBackCompatOverloads(TypeProvider enclosingType, List owner.ProcessTypeForBackCompatibility(); var shims = owner.Methods.Where(m => m.Signature.Attributes.Any(a => a.Type.Equals(typeof(EditorBrowsableAttribute)))).ToArray(); - Assert.AreEqual(3, shims.Length); + Assert.AreEqual(previouslyExperimental ? 0 : 3, shims.Length); foreach (var shim in shims) { var experiment = shim.Signature.Attributes.SingleOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute))); @@ -348,7 +348,7 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public async Task ExperimentalAttributesDoNotChangePublicApiDetection() + public async Task ExperimentalAttributesAreExcludedFromPublicApiDetection() { await MockHelpers.LoadMockGeneratorAsync( lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync( @@ -366,13 +366,19 @@ await MockHelpers.LoadMockGeneratorAsync( CollectionAssert.AreEquivalent( new[] { 0, 1 }, lastContractView.Constructors.Select(constructor => constructor.Signature.Parameters.Count).ToArray()); - Assert.IsTrue(MethodSignatureHelper.IsPublicApi( - lastContractView.Methods.Single(method => method.Signature.Name == "ExperimentalMethod").Signature.Modifiers)); - Assert.IsTrue(MethodSignatureHelper.IsPublicApi( - lastContractView.Properties.Single(property => property.Name == "ExperimentalProperty").Modifiers)); - Assert.IsTrue(MethodSignatureHelper.IsPublicApi( - lastContractView.Constructors.Single(constructor => constructor.Signature.Parameters.Count == 1).Signature.Modifiers)); - Assert.IsTrue(lastContractView.Fields.Single(field => field.Name == "ExperimentalField").Modifiers.HasFlag(FieldModifiers.Public)); + var experimentalMethod = lastContractView.Methods.Single(method => method.Signature.Name == "ExperimentalMethod"); + var stableMethod = lastContractView.Methods.Single(method => method.Signature.Name == "StableMethod"); + var experimentalProperty = lastContractView.Properties.Single(property => property.Name == "ExperimentalProperty"); + var stableProperty = lastContractView.Properties.Single(property => property.Name == "StableProperty"); + var experimentalConstructor = lastContractView.Constructors.Single(constructor => constructor.Signature.Parameters.Count == 1); + var experimentalField = lastContractView.Fields.Single(field => field.Name == "ExperimentalField"); + + Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalMethod.Signature.Modifiers, experimentalMethod.Signature.Attributes)); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableMethod.Signature.Modifiers, stableMethod.Signature.Attributes)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalProperty.Modifiers, experimentalProperty.Attributes)); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableProperty.Modifiers, stableProperty.Attributes)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalConstructor.Signature.Modifiers, experimentalConstructor.Signature.Attributes)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalField.Modifiers, experimentalField.Attributes)); } [Test] diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 158e1531063..44e74ff2451 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -56,7 +56,7 @@ When generating code, the generator can optionally receive a compiled assembly f Types in the last released contract marked with `System.Diagnostics.CodeAnalysis.ExperimentalAttribute` are excluded from backward-compatibility lookup. The generator does not preserve their previous type shape or restore their previous members as compatibility shims, allowing those types to change when they graduate to stable or are removed. -This decision uses the attribute on the last released type, including polyfilled definitions of the attribute. Marking a previously stable type experimental in the current generation does not remove its compatibility guarantees. Experimental attributes on individual members continue to use the existing member-level handling. +This decision uses the attribute on the last released type, including polyfilled definitions of the attribute. Marking a previously stable type experimental in the current generation does not remove its compatibility guarantees. Experimental attributes on individual members in the last released contract also exclude those members from compatibility handling; attributes added only in the current generation do not remove guarantees for previously stable members. ### ApiCompat Baseline Awareness From 11f79c7abb1de6b6fe172bc00ada256bb46f9b3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:29:29 +0000 Subject: [PATCH 10/14] refactor(csharp): pass signatures to experimental API check Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../Providers/ModelBaseTypeCompatibility.cs | 6 +++--- .../src/Providers/ModelFactoryProvider.cs | 2 +- .../src/Providers/ModelProvider.cs | 4 ++-- .../Providers/SystemObjectModelProvider.cs | 8 ++++---- .../src/Providers/TypeProvider.cs | 4 ++-- .../src/Shared/MethodSignatureHelper.cs | 3 +++ .../src/Utilities/BackCompatHelper.cs | 6 +++--- .../test/Shared/MethodSignatureHelperTests.cs | 20 +++++++++++++++++++ 8 files changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs index 9eb48bb14f5..f4f8a2b903c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs @@ -91,14 +91,14 @@ private static bool HasNoUnsupportedLastContractMembers(ModelProvider candidate) return lastContract.Implements.All(previous => currentInterfaces.Any(current => ModelBaseMemberCompatibility.AreTypesCompatible(previous, current))) && lastContract.Constructors - .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers, constructor.Signature.Attributes)) + .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature)) .All(constructor => SystemObjectModelProvider.HasSupportedConstructorParameters(constructor.Signature.Parameters) && constructor.Signature.Parameters.All(parameter => candidate.Properties.Any(property => property.AsParameter.Name == parameter.Name && (ModelBaseMemberCompatibility.AreTypesCompatible(parameter.Type, property.Type) || ModelBaseMemberCompatibility.AreTypesCompatible(parameter.Type, property.Type.InputType))))) && lastContract.Methods - .Where(method => MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers, method.Signature.Attributes)) + .Where(method => MethodSignatureHelper.IsPublicApi(method.Signature)) .All(previous => currentGeneratedMethods.Any(current => AreGeneratedMethodSignaturesEquivalent(previous.Signature, current.Signature))) && !lastContract.Fields.Any(field => MethodSignatureHelper.IsPublicApi(field.Modifiers, field.Attributes)); @@ -166,7 +166,7 @@ private static bool IsRequiredInitializationProperty(PropertyProvider property) private bool LastContractDerivedConstructorHasRequiredParameter(PropertyProvider property) => _model.LastContractView?.Constructors.Any(constructor => - MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers, constructor.Signature.Attributes) && + MethodSignatureHelper.IsPublicApi(constructor.Signature) && constructor.Signature.Parameters.Any(parameter => parameter.Name == property.AsParameter.Name && parameter.Type.Equals(property.Type, ignoreNullable: true) && diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index 7bb7b4ff8fc..d9bbad0580c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -127,7 +127,7 @@ protected internal sealed override IReadOnlyList BuildMethodsFor foreach (var previousMethod in LastContractView.Methods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers, previousMethod.Signature.Attributes)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature)) { continue; } 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 9594c3b32e2..0ffb630c947 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 @@ -1111,7 +1111,7 @@ protected internal override IReadOnlyList BuildConstructors foreach (var previousConstructor in restorablePreviousConstructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers, previousConstructor.Signature.Attributes)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) { continue; } @@ -1183,7 +1183,7 @@ private void RestorePreviousConstructorParameterNames( const MethodSignatureModifiers privateProtected = MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected; foreach (var previousConstructor in previousConstructors) { - if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers, previousConstructor.Signature.Attributes)) + if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) { continue; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs index d8699af8614..9c4ab1ffa10 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs @@ -159,7 +159,7 @@ private bool HasCallableInitializationConstructor() } foreach (var constructor in constructors.Where(constructor => - MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers, constructor.Signature.Attributes) && + MethodSignatureHelper.IsPublicApi(constructor.Signature) && HasSupportedConstructorParameters(constructor.Signature.Parameters))) { var matches = constructor.Signature.Parameters @@ -269,7 +269,7 @@ private bool TryGetLastContractConstructor( { var properties = Properties; foreach (var candidate in _lastContractType!.Constructors - .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature.Modifiers, constructor.Signature.Attributes) && + .Where(constructor => MethodSignatureHelper.IsPublicApi(constructor.Signature) && HasSupportedConstructorParameters(constructor.Signature.Parameters)) .OrderByDescending(constructor => constructor.Signature.Parameters.Count)) { @@ -391,7 +391,7 @@ internal bool HasCompatibleLastContractNonPropertyMembers() } foreach (var method in provider.Methods.Where(method => - MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers, method.Signature.Attributes))) + MethodSignatureHelper.IsPublicApi(method.Signature))) { var signature = method.Signature; if (overriddenMethods.Any(overrideSignature => @@ -430,7 +430,7 @@ internal IEnumerable GetPublicApiMemberNames() yield return property.Name; } foreach (var method in provider.Methods.Where(method => - MethodSignatureHelper.IsPublicApi(method.Signature.Modifiers, method.Signature.Attributes))) + MethodSignatureHelper.IsPublicApi(method.Signature))) { yield return GetPublicApiMemberName(method.Signature.Name); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 02b17f965c1..73d7b9d3207 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -1218,7 +1218,7 @@ protected internal virtual IReadOnlyList BuildMethodsForBackComp foreach (var previousMethod in previousMethods) { if (currentMethodSignatures.ContainsKey(previousMethod.Signature) - || !MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers, previousMethod.Signature.Attributes) + || !MethodSignatureHelper.IsPublicApi(previousMethod.Signature) || BackCompatHelper.IsMethodRemovalAcceptedInBaseline(this, previousMethod.Signature)) { continue; @@ -1284,7 +1284,7 @@ protected internal virtual IReadOnlyList BuildConstructorsF foreach (var previousConstructor in LastContractView.Constructors) { if (!previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) || - !MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers, previousConstructor.Signature.Attributes)) + !MethodSignatureHelper.IsPublicApi(previousConstructor.Signature)) { continue; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index 97a2f7b48dc..2cfed63b4da 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -16,6 +16,9 @@ namespace Microsoft.TypeSpec.Generator { internal class MethodSignatureHelper { + internal static bool IsPublicApi(MethodSignatureBase signature) + => IsPublicApi(signature.Modifiers, signature.Attributes); + internal static bool IsPublicApi( MethodSignatureModifiers modifiers, IReadOnlyList? attributes = null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index cc48ae89583..2ea0b03eb78 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -144,7 +144,7 @@ public static bool ParametersMatch(IReadOnlyList params1, IRe } IEnumerable scopedMethods = lastContractMethods - .Where(m => MethodSignatureHelper.IsPublicApi(m.Signature.Modifiers, m.Signature.Attributes)); + .Where(m => MethodSignatureHelper.IsPublicApi(m.Signature)); if (methodName != null) { scopedMethods = scopedMethods.Where(m => @@ -252,7 +252,7 @@ public static void RestorePreviousParameterNames( { foreach (var previousMethod in previousMethods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers, previousMethod.Signature.Attributes)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature)) { continue; } @@ -388,7 +388,7 @@ public static void AddBackCompatOverloads(TypeProvider enclosingType, List Date: Tue, 13 Oct 2026 19:36:27 +0000 Subject: [PATCH 11/14] refactor(csharp): make public API checks signature based Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ExtensibleEnumProvider.cs | 2 +- .../src/Providers/ModelBaseTypeCompatibility.cs | 2 +- .../src/Providers/ModelProvider.cs | 4 ++-- .../src/Providers/PropertyProvider.cs | 2 +- .../src/Providers/SystemObjectModelProvider.cs | 6 +++--- .../src/Shared/MethodSignatureHelper.cs | 14 ++++++++------ .../src/TypeFactory.cs | 2 +- .../test/Providers/TypeProviderTests.cs | 10 +++++----- 8 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs index 406da6c37fa..0521b21632b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs @@ -288,7 +288,7 @@ protected override TypeProvider[] BuildSerializationProviders() protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) { var lastContractProperties = LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)); + .Where(p => MethodSignatureHelper.IsPublicApi(p)); if (lastContractProperties == null || !lastContractProperties.Any()) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs index f4f8a2b903c..2b66f1dba24 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs @@ -147,7 +147,7 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) { var result = new Dictionary(StringComparer.Ordinal); foreach (var property in properties.Where(property => isLastContract - ? MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes) + ? MethodSignatureHelper.IsPublicApi(property) : MethodSignatureHelper.IsPublicApi(property.Modifiers))) { // Overloaded indexers can share a symbol name. They are unsupported contracts, not 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 0ffb630c947..4e2b3e31dac 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 @@ -107,7 +107,7 @@ _inputModel.BaseModel is not null private IDictionary LastContractPropertiesMap => _lastContractPropertiesMap ??= LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)) + .Where(p => MethodSignatureHelper.IsPublicApi(p)) .ToDictionary(p => p.Name, p => p.Type) ?? []; private IDictionary? _lastContractPropertiesMap; @@ -2056,7 +2056,7 @@ private bool ShouldUseObjectAdditionalProperties() // Check if the property exists in the last contract by name var lastContractProperty = LastContractView.Properties.FirstOrDefault(p => - MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes) && + MethodSignatureHelper.IsPublicApi(p) && p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName); if (lastContractProperty == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index d3a14740a97..cabc08adb48 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -113,7 +113,7 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T var canonicalName = identifierName.NormalizeCSharpAcronyms(isDateTime); var enclosingTypeName = enclosingType.Name; var lastContractProperties = enclosingType.LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)) + .Where(p => MethodSignatureHelper.IsPublicApi(p)) .ToList(); // An exact input-identifier match is authoritative: it is the name this property would have had diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs index 9c4ab1ffa10..607213833c6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs @@ -316,7 +316,7 @@ protected internal override PropertyProvider[] BuildProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes) && seenPropertyNames.Add(property.Name))) + MethodSignatureHelper.IsPublicApi(property) && seenPropertyNames.Add(property.Name))) { if (ApplyCurrentInputMetadata(property) is { } projectedProperty) { @@ -342,7 +342,7 @@ internal bool HasCompatibleLastContractProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { if (provider.Properties - .Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes)) + .Where(property => MethodSignatureHelper.IsPublicApi(property)) .Any(property => FindEffectiveFrameworkProperty(property.Name) is not { } candidate || !IsCompatibleProperty(property, candidate))) { @@ -425,7 +425,7 @@ internal IEnumerable GetPublicApiMemberNames() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes))) + MethodSignatureHelper.IsPublicApi(property))) { yield return property.Name; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index 2cfed63b4da..095cc6a1d68 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -17,14 +17,16 @@ namespace Microsoft.TypeSpec.Generator internal class MethodSignatureHelper { internal static bool IsPublicApi(MethodSignatureBase signature) - => IsPublicApi(signature.Modifiers, signature.Attributes); + => IsPublicApi(signature.Modifiers) + && !signature.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); - internal static bool IsPublicApi( - MethodSignatureModifiers modifiers, - IReadOnlyList? attributes = null) + internal static bool IsPublicApi(MethodSignatureModifiers modifiers) => (modifiers.HasFlag(MethodSignatureModifiers.Public) || modifiers.HasFlag(MethodSignatureModifiers.Protected)) - && !modifiers.HasFlag(MethodSignatureModifiers.Private) - && attributes?.Any(ExperimentalApiHelpers.IsExperimentalAttribute) != true; + && !modifiers.HasFlag(MethodSignatureModifiers.Private); + + internal static bool IsPublicApi(PropertyProvider property) + => IsPublicApi(property.Modifiers) + && !property.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); internal static bool IsPublicApi( FieldModifiers modifiers, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index efaa8b260d5..24f6698da12 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -69,7 +69,7 @@ internal bool IsLastContractModelBasePropertyCompatible( CSharpType mappedBase, InputModelProperty currentProperty, PropertyProvider lastContractProperty) - => MethodSignatureHelper.IsPublicApi(lastContractProperty.Modifiers, lastContractProperty.Attributes) && + => MethodSignatureHelper.IsPublicApi(lastContractProperty) && !lastContractProperty.Modifiers.HasFlag(MethodSignatureModifiers.Static) && IsLastContractModelBasePropertyCompatibleCore(mappedBase, currentProperty, lastContractProperty); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index c8164d35511..41455bb6b03 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -373,11 +373,11 @@ await MockHelpers.LoadMockGeneratorAsync( var experimentalConstructor = lastContractView.Constructors.Single(constructor => constructor.Signature.Parameters.Count == 1); var experimentalField = lastContractView.Fields.Single(field => field.Name == "ExperimentalField"); - Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalMethod.Signature.Modifiers, experimentalMethod.Signature.Attributes)); - Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableMethod.Signature.Modifiers, stableMethod.Signature.Attributes)); - Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalProperty.Modifiers, experimentalProperty.Attributes)); - Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableProperty.Modifiers, stableProperty.Attributes)); - Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalConstructor.Signature.Modifiers, experimentalConstructor.Signature.Attributes)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalMethod.Signature)); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableMethod.Signature)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalProperty)); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableProperty)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalConstructor.Signature)); Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalField.Modifiers, experimentalField.Attributes)); } From 06aca1f0b2dd9f845880088541478b78f83f12cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:44:05 +0000 Subject: [PATCH 12/14] refactor(csharp): use modifier check for properties Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ExtensibleEnumProvider.cs | 2 +- .../src/Providers/ModelBaseTypeCompatibility.cs | 2 +- .../src/Providers/ModelProvider.cs | 4 ++-- .../src/Providers/PropertyProvider.cs | 2 +- .../src/Providers/SystemObjectModelProvider.cs | 6 +++--- .../src/Shared/MethodSignatureHelper.cs | 4 ---- .../Microsoft.TypeSpec.Generator/src/TypeFactory.cs | 2 +- .../test/Providers/TypeProviderTests.cs | 6 +++--- 8 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs index 0521b21632b..7ab35ce7e77 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs @@ -288,7 +288,7 @@ protected override TypeProvider[] BuildSerializationProviders() protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) { var lastContractProperties = LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p)); + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)); if (lastContractProperties == null || !lastContractProperties.Any()) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs index 2b66f1dba24..a05330f885f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs @@ -147,7 +147,7 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) { var result = new Dictionary(StringComparer.Ordinal); foreach (var property in properties.Where(property => isLastContract - ? MethodSignatureHelper.IsPublicApi(property) + ? MethodSignatureHelper.IsPublicApi(property.Modifiers) : MethodSignatureHelper.IsPublicApi(property.Modifiers))) { // Overloaded indexers can share a symbol name. They are unsupported contracts, not 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 4e2b3e31dac..da9af5e4ba7 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 @@ -107,7 +107,7 @@ _inputModel.BaseModel is not null private IDictionary LastContractPropertiesMap => _lastContractPropertiesMap ??= LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p)) + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) .ToDictionary(p => p.Name, p => p.Type) ?? []; private IDictionary? _lastContractPropertiesMap; @@ -2056,7 +2056,7 @@ private bool ShouldUseObjectAdditionalProperties() // Check if the property exists in the last contract by name var lastContractProperty = LastContractView.Properties.FirstOrDefault(p => - MethodSignatureHelper.IsPublicApi(p) && + MethodSignatureHelper.IsPublicApi(p.Modifiers) && p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName); if (lastContractProperty == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index cabc08adb48..f060d44cf5f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -113,7 +113,7 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T var canonicalName = identifierName.NormalizeCSharpAcronyms(isDateTime); var enclosingTypeName = enclosingType.Name; var lastContractProperties = enclosingType.LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p)) + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) .ToList(); // An exact input-identifier match is authoritative: it is the name this property would have had diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs index 607213833c6..3c94b4280a6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs @@ -316,7 +316,7 @@ protected internal override PropertyProvider[] BuildProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property) && seenPropertyNames.Add(property.Name))) + MethodSignatureHelper.IsPublicApi(property.Modifiers) && seenPropertyNames.Add(property.Name))) { if (ApplyCurrentInputMetadata(property) is { } projectedProperty) { @@ -342,7 +342,7 @@ internal bool HasCompatibleLastContractProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { if (provider.Properties - .Where(property => MethodSignatureHelper.IsPublicApi(property)) + .Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers)) .Any(property => FindEffectiveFrameworkProperty(property.Name) is not { } candidate || !IsCompatibleProperty(property, candidate))) { @@ -425,7 +425,7 @@ internal IEnumerable GetPublicApiMemberNames() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property))) + MethodSignatureHelper.IsPublicApi(property.Modifiers))) { yield return property.Name; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index 095cc6a1d68..cb30a8d5f41 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -24,10 +24,6 @@ internal static bool IsPublicApi(MethodSignatureModifiers modifiers) => (modifiers.HasFlag(MethodSignatureModifiers.Public) || modifiers.HasFlag(MethodSignatureModifiers.Protected)) && !modifiers.HasFlag(MethodSignatureModifiers.Private); - internal static bool IsPublicApi(PropertyProvider property) - => IsPublicApi(property.Modifiers) - && !property.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); - internal static bool IsPublicApi( FieldModifiers modifiers, IReadOnlyList? attributes = null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index 24f6698da12..cb2fdf43237 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -69,7 +69,7 @@ internal bool IsLastContractModelBasePropertyCompatible( CSharpType mappedBase, InputModelProperty currentProperty, PropertyProvider lastContractProperty) - => MethodSignatureHelper.IsPublicApi(lastContractProperty) && + => MethodSignatureHelper.IsPublicApi(lastContractProperty.Modifiers) && !lastContractProperty.Modifiers.HasFlag(MethodSignatureModifiers.Static) && IsLastContractModelBasePropertyCompatibleCore(mappedBase, currentProperty, lastContractProperty); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index 41455bb6b03..296ccd6a4ea 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -348,7 +348,7 @@ await MockHelpers.LoadMockGeneratorAsync( } [Test] - public async Task ExperimentalAttributesAreExcludedFromPublicApiDetection() + public async Task ExperimentalAttributesAreExcludedForMethodConstructorAndFieldDetection() { await MockHelpers.LoadMockGeneratorAsync( lastContractCompilation: () => Helpers.GetCompilationFromDirectoryAsync( @@ -375,8 +375,8 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalMethod.Signature)); Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableMethod.Signature)); - Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalProperty)); - Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableProperty)); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi(experimentalProperty.Modifiers)); + Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableProperty.Modifiers)); Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalConstructor.Signature)); Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalField.Modifiers, experimentalField.Attributes)); } From 44c0f884605f8f9d343d824ba71a71fec6680067 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:49:33 +0000 Subject: [PATCH 13/14] fix(csharp): check optional attributes in public API helper Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ExtensibleEnumProvider.cs | 2 +- .../src/Providers/ModelBaseTypeCompatibility.cs | 2 +- .../src/Providers/ModelProvider.cs | 4 ++-- .../src/Providers/PropertyProvider.cs | 2 +- .../src/Providers/SystemObjectModelProvider.cs | 6 +++--- .../src/Shared/MethodSignatureHelper.cs | 10 ++++++---- .../Microsoft.TypeSpec.Generator/src/TypeFactory.cs | 2 +- .../test/Providers/TypeProviderTests.cs | 2 +- 8 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs index 7ab35ce7e77..406da6c37fa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ExtensibleEnumProvider.cs @@ -288,7 +288,7 @@ protected override TypeProvider[] BuildSerializationProviders() protected internal override IReadOnlyList? BuildEnumValuesForBackCompatibility(IReadOnlyList currentValues) { var lastContractProperties = LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)); + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)); if (lastContractProperties == null || !lastContractProperties.Any()) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs index a05330f885f..f4f8a2b903c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelBaseTypeCompatibility.cs @@ -147,7 +147,7 @@ private bool HasCompatibleLastContractProperties(ModelProvider candidate) { var result = new Dictionary(StringComparer.Ordinal); foreach (var property in properties.Where(property => isLastContract - ? MethodSignatureHelper.IsPublicApi(property.Modifiers) + ? MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes) : MethodSignatureHelper.IsPublicApi(property.Modifiers))) { // Overloaded indexers can share a symbol name. They are unsupported contracts, not 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 da9af5e4ba7..0ffb630c947 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 @@ -107,7 +107,7 @@ _inputModel.BaseModel is not null private IDictionary LastContractPropertiesMap => _lastContractPropertiesMap ??= LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)) .ToDictionary(p => p.Name, p => p.Type) ?? []; private IDictionary? _lastContractPropertiesMap; @@ -2056,7 +2056,7 @@ private bool ShouldUseObjectAdditionalProperties() // Check if the property exists in the last contract by name var lastContractProperty = LastContractView.Properties.FirstOrDefault(p => - MethodSignatureHelper.IsPublicApi(p.Modifiers) && + MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes) && p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName); if (lastContractProperty == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index f060d44cf5f..d3a14740a97 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -113,7 +113,7 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T var canonicalName = identifierName.NormalizeCSharpAcronyms(isDateTime); var enclosingTypeName = enclosingType.Name; var lastContractProperties = enclosingType.LastContractView?.Properties - .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers, p.Attributes)) .ToList(); // An exact input-identifier match is authoritative: it is the name this property would have had diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs index 3c94b4280a6..9c4ab1ffa10 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs @@ -316,7 +316,7 @@ protected internal override PropertyProvider[] BuildProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property.Modifiers) && seenPropertyNames.Add(property.Name))) + MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes) && seenPropertyNames.Add(property.Name))) { if (ApplyCurrentInputMetadata(property) is { } projectedProperty) { @@ -342,7 +342,7 @@ internal bool HasCompatibleLastContractProperties() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { if (provider.Properties - .Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers)) + .Where(property => MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes)) .Any(property => FindEffectiveFrameworkProperty(property.Name) is not { } candidate || !IsCompatibleProperty(property, candidate))) { @@ -425,7 +425,7 @@ internal IEnumerable GetPublicApiMemberNames() for (var provider = _lastContractType; provider is not null; provider = provider.BaseTypeProvider) { foreach (var property in provider.Properties.Where(property => - MethodSignatureHelper.IsPublicApi(property.Modifiers))) + MethodSignatureHelper.IsPublicApi(property.Modifiers, property.Attributes))) { yield return property.Name; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index cb30a8d5f41..2cfed63b4da 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -17,12 +17,14 @@ namespace Microsoft.TypeSpec.Generator internal class MethodSignatureHelper { internal static bool IsPublicApi(MethodSignatureBase signature) - => IsPublicApi(signature.Modifiers) - && !signature.Attributes.Any(ExperimentalApiHelpers.IsExperimentalAttribute); + => IsPublicApi(signature.Modifiers, signature.Attributes); - internal static bool IsPublicApi(MethodSignatureModifiers modifiers) + internal static bool IsPublicApi( + MethodSignatureModifiers modifiers, + IReadOnlyList? attributes = null) => (modifiers.HasFlag(MethodSignatureModifiers.Public) || modifiers.HasFlag(MethodSignatureModifiers.Protected)) - && !modifiers.HasFlag(MethodSignatureModifiers.Private); + && !modifiers.HasFlag(MethodSignatureModifiers.Private) + && attributes?.Any(ExperimentalApiHelpers.IsExperimentalAttribute) != true; internal static bool IsPublicApi( FieldModifiers modifiers, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index cb2fdf43237..efaa8b260d5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -69,7 +69,7 @@ internal bool IsLastContractModelBasePropertyCompatible( CSharpType mappedBase, InputModelProperty currentProperty, PropertyProvider lastContractProperty) - => MethodSignatureHelper.IsPublicApi(lastContractProperty.Modifiers) && + => MethodSignatureHelper.IsPublicApi(lastContractProperty.Modifiers, lastContractProperty.Attributes) && !lastContractProperty.Modifiers.HasFlag(MethodSignatureModifiers.Static) && IsLastContractModelBasePropertyCompatibleCore(mappedBase, currentProperty, lastContractProperty); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index 296ccd6a4ea..37321fd9398 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -375,7 +375,7 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalMethod.Signature)); Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableMethod.Signature)); - Assert.IsTrue(MethodSignatureHelper.IsPublicApi(experimentalProperty.Modifiers)); + Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalProperty.Modifiers, experimentalProperty.Attributes)); Assert.IsTrue(MethodSignatureHelper.IsPublicApi(stableProperty.Modifiers)); Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalConstructor.Signature)); Assert.IsFalse(MethodSignatureHelper.IsPublicApi(experimentalField.Modifiers, experimentalField.Attributes)); From 8e77a2f1a9e1cfbaccc3bf35c868aedea63d06d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:58:22 +0000 Subject: [PATCH 14/14] chore(csharp): remove unused LINQ import Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/SourceInput/SourceInputModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index 31a425bc11b..71f096d5f78 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Utilities;