From 66771e8c98229562324082b6ff1846a746561e7a Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 24 Jul 2026 15:43:20 -0700 Subject: [PATCH 1/2] Support XML ApiCompat baselines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 883420ab-f215-4afc-91a2-3433270335bb --- .../PostProcessing/GeneratedCodeWorkspace.cs | 18 +- .../src/SourceInput/ApiCompatBaseline.cs | 182 +++++++++++++++++- .../SourceInput/ApiCompatBaselineTests.cs | 29 +++ .../ApiCompatBaselineTests/Baseline.xml | 43 +++++ .../test/common/Helpers.cs | 7 +- 5 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 740e000451c..81ee9fcd288 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -342,8 +342,10 @@ internal static async Task AddPackageReferencesFromProject() /// /// Locates and parses the ApiCompat baseline (suppression) file for the current library, if - /// present. The file is expected at eng/apicompatbaselines/<AssemblyName>.txt - /// relative to a repository root discovered by walking up from the project directory. + /// present. The file is expected at eng/apicompatbaselines/<AssemblyName>.xml or + /// eng/apicompatbaselines/<AssemblyName>.txt relative to a repository root + /// discovered by walking up from the project directory. The XML format is preferred when both + /// files exist. /// Returns when no baseline file is found. /// internal static ApiCompatBaseline LoadApiCompatBaseline() @@ -353,11 +355,15 @@ internal static ApiCompatBaseline LoadApiCompatBaseline() while (directory != null) { - var candidate = Path.Combine(directory.FullName, "eng", "apicompatbaselines", $"{packageName}.txt"); - if (File.Exists(candidate)) + var baselineDirectory = Path.Combine(directory.FullName, "eng", "apicompatbaselines"); + foreach (var extension in new[] { ".xml", ".txt" }) { - CodeModelGenerator.Instance.Emitter.Debug($"Loading ApiCompat baseline from {candidate}"); - return ApiCompatBaseline.FromFile(candidate); + var candidate = Path.Combine(baselineDirectory, $"{packageName}{extension}"); + if (File.Exists(candidate)) + { + CodeModelGenerator.Instance.Emitter.Debug($"Loading ApiCompat baseline from {candidate}"); + return ApiCompatBaseline.FromFile(candidate); + } } directory = directory.Parent; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/ApiCompatBaseline.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/ApiCompatBaseline.cs index 86e1fb24fa2..be48bd0f247 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/ApiCompatBaseline.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/ApiCompatBaseline.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Xml.Linq; using Microsoft.TypeSpec.Generator.Primitives; namespace Microsoft.TypeSpec.Generator.SourceInput @@ -12,6 +13,7 @@ namespace Microsoft.TypeSpec.Generator.SourceInput /// /// Represents the set of intentional, already-accepted breaking changes recorded in an /// ApiCompat baseline (suppression) file (for example the files under + /// eng/apicompatbaselines/<AssemblyName>.xml or /// eng/apicompatbaselines/<AssemblyName>.txt). /// /// The backward-compatibility system resurrects any public member that exists in the previous @@ -28,6 +30,8 @@ public sealed class ApiCompatBaseline private const string TypesMustExist = "TypesMustExist"; private const string MembersMustExist = "MembersMustExist"; private const string EnumValuesMustMatch = "EnumValuesMustMatch"; + private const string TypeMustExistDiagnostic = "CP0001"; + private const string MemberMustExistDiagnostic = "CP0002"; private readonly HashSet _suppressedTypes; private readonly HashSet _suppressedMembers; @@ -62,7 +66,9 @@ public static ApiCompatBaseline FromFile(string path) return Empty; } - return Parse(File.ReadAllLines(path)); + return string.Equals(Path.GetExtension(path), ".xml", StringComparison.OrdinalIgnoreCase) + ? ParseXml(XDocument.Load(path)) + : Parse(File.ReadAllLines(path)); } /// @@ -129,6 +135,63 @@ public static ApiCompatBaseline Parse(IEnumerable lines) return new ApiCompatBaseline(suppressedTypes, suppressedMembers, suppressedMethods); } + private static ApiCompatBaseline ParseXml(XDocument document) + { + var suppressedTypes = new HashSet(StringComparer.Ordinal); + var suppressedMembers = new HashSet(); + var suppressedMethods = new HashSet(); + + foreach (var suppression in document.Descendants()) + { + if (suppression.Name.LocalName != "Suppression") + { + continue; + } + + string? diagnosticId = null; + string? target = null; + foreach (var element in suppression.Elements()) + { + switch (element.Name.LocalName) + { + case "DiagnosticId": + diagnosticId = element.Value.Trim(); + break; + case "Target": + target = element.Value.Trim(); + break; + } + } + + if (string.IsNullOrEmpty(target)) + { + continue; + } + + switch (diagnosticId) + { + case TypeMustExistDiagnostic: + if (target.StartsWith("T:", StringComparison.Ordinal)) + { + suppressedTypes.Add(RemoveGenericArity(target.Substring(2))); + } + break; + case MemberMustExistDiagnostic: + if (TryParseXmlMember(target, out var memberKey, out var methodKey)) + { + suppressedMembers.Add(memberKey); + if (methodKey.HasValue) + { + suppressedMethods.Add(methodKey.Value); + } + } + break; + } + } + + return new ApiCompatBaseline(suppressedTypes, suppressedMembers, suppressedMethods); + } + /// /// Determines whether the removal of the type with the given fully-qualified name has been /// accepted in the baseline. @@ -333,6 +396,116 @@ private static bool TryParseField(string signature, out MemberKey memberKey) return true; } + private static bool TryParseXmlMember(string target, out MemberKey memberKey, out MethodKey? methodKey) + { + memberKey = default; + methodKey = null; + + if (target.Length < 3 || target[1] != ':') + { + return false; + } + + var symbolKind = target[0]; + var symbol = target.Substring(2); + if (symbolKind == 'M') + { + var parenIndex = symbol.IndexOf('('); + if (parenIndex >= 0 && !symbol.EndsWith(")", StringComparison.Ordinal)) + { + return false; + } + + var memberPath = parenIndex >= 0 ? symbol.Substring(0, parenIndex) : symbol; + var parameterList = parenIndex >= 0 + ? symbol.Substring(parenIndex + 1, symbol.Length - parenIndex - 2) + : string.Empty; + + if (!TrySplitXmlMemberPath(memberPath, out var declaringTypeFullName, out var memberName)) + { + return false; + } + + memberName = NormalizeXmlMemberName(memberName); + var normalizedParameters = NormalizeXmlParameterList(parameterList); + memberKey = new MemberKey(declaringTypeFullName, memberName, CountParameters(normalizedParameters)); + methodKey = new MethodKey(declaringTypeFullName, memberName, normalizedParameters); + return true; + } + + if (symbolKind is 'F' or 'P' or 'E' + && TrySplitXmlMemberPath(symbol, out var declaringType, out var name)) + { + memberKey = new MemberKey(declaringType, NormalizeXmlMemberName(name), 0); + return true; + } + + return false; + } + + private static bool TrySplitXmlMemberPath( + string memberPath, + out string declaringTypeFullName, + out string memberName) + { + declaringTypeFullName = string.Empty; + memberName = string.Empty; + + var lastDot = memberPath.LastIndexOf('.'); + if (lastDot <= 0 || lastDot == memberPath.Length - 1) + { + return false; + } + + declaringTypeFullName = RemoveGenericArity(memberPath.Substring(0, lastDot)); + memberName = memberPath.Substring(lastDot + 1); + return true; + } + + private static string NormalizeXmlMemberName(string memberName) + { + if (memberName.StartsWith('#')) + { + memberName = $".{memberName.Substring(1)}"; + } + + if (memberName.StartsWith("get_", StringComparison.Ordinal) + || memberName.StartsWith("set_", StringComparison.Ordinal) + || memberName.StartsWith("add_", StringComparison.Ordinal) + || memberName.StartsWith("remove_", StringComparison.Ordinal)) + { + memberName = memberName.Substring(memberName.IndexOf('_') + 1); + } + + return RemoveGenericArity(memberName); + } + + private static string NormalizeXmlParameterList(string parameterList) + => NormalizeParameterList(parameterList) + .Replace('{', '<') + .Replace('}', '>') + .Replace('+', '.'); + + private static string RemoveGenericArity(string name) + { + var builder = new StringBuilder(name.Length); + for (int i = 0; i < name.Length; i++) + { + if (name[i] == '`') + { + while (i + 1 < name.Length && char.IsDigit(name[i + 1])) + { + i++; + } + continue; + } + + builder.Append(name[i] == '+' ? '.' : name[i]); + } + + return builder.ToString(); + } + private static int CountParameters(string parameterList) { var trimmed = parameterList.Trim(); @@ -415,6 +588,13 @@ private static string BuildParameterSignature(IReadOnlyList paramete private static void AppendTypeName(StringBuilder builder, CSharpType type) { + if (type.IsFrameworkType && type.FrameworkType.IsGenericParameter) + { + builder.Append('`'); + builder.Append(type.FrameworkType.GenericParameterPosition); + return; + } + // Nullable value types are rendered by ApiCompat as System.Nullable. if (type.IsNullable && type.IsValueType) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs index bddec2f1797..c6836217ba2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs @@ -37,6 +37,35 @@ public void ParsesTypesMustExist() Assert.IsFalse(baseline.IsTypeSuppressed("Azure.AI.Projects.Agents.SomethingElse")); } + [Test] + public void ParsesXmlSuppressions() + { + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: ".xml", method: "Baseline"); + + Assert.IsTrue(baseline.IsTypeSuppressed("Azure.AI.Projects.Agents.ProjectsAgentProtocol")); + Assert.IsTrue(baseline.IsMemberSuppressed( + "Azure.AI.Projects.Agents.ProjectsAgentsModelFactory", + "ProtocolVersionRecord", + 2)); + Assert.IsTrue(baseline.IsMethodRemovalSuppressed( + "Azure.AI.Projects.Agents.ProjectsAgentsModelFactory", + "ProtocolVersionRecord", + [ + new CSharpType(typeof(string)), + new CSharpType(typeof(IEnumerable)) + ])); + Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", ".ctor", 0)); + Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "Kind", 0)); + Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "Kind", 1)); + Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "LegacyField", 0)); + Assert.IsTrue(baseline.IsTypeSuppressed("Ns.Outer.Inner")); + Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Outer.Inner", "Reset", 0)); + + var genericParameter = new CSharpType(typeof(IEnumerable<>).GetGenericArguments()[0]); + Assert.IsTrue(baseline.IsMethodRemovalSuppressed("Ns.Foo", "Generic", [genericParameter])); + Assert.IsFalse(baseline.IsMemberSuppressed("Ns.Foo", "ParameterRename", 1)); + } + [Test] public void TypeSuppressionImpliesAllMembersSuppressed() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml new file mode 100644 index 00000000000..238621f2647 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml @@ -0,0 +1,43 @@ + + + + CP0001 + T:Azure.AI.Projects.Agents.ProjectsAgentProtocol + + + CP0002 + M:Azure.AI.Projects.Agents.ProjectsAgentsModelFactory.ProtocolVersionRecord(System.String,System.Collections.Generic.IEnumerable{System.String}) + + + CP0002 + M:Ns.Foo.#ctor + + + CP0002 + M:Ns.Foo.get_Kind + + + CP0002 + M:Ns.Foo.set_Kind(Ns.Kind) + + + CP0002 + F:Ns.Foo.LegacyField + + + CP0001 + T:Ns.Outer`1+Inner + + + CP0002 + M:Ns.Outer`1+Inner.Reset + + + CP0002 + M:Ns.Foo.Generic`1(`0) + + + CP0017 + M:Ns.Foo.ParameterRename(System.String)$0 + + diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/Helpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/Helpers.cs index bbab4c9d5f3..7380d81af8b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/Helpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/common/Helpers.cs @@ -27,16 +27,17 @@ public static string GetExpectedFromFile( } /// - /// Loads an from a .txt baseline asset file located at - /// TestData/<CallingClass>/<method>.txt next to the calling test. + /// Loads an from a baseline asset file located under + /// TestData/<CallingClass> next to the calling test. /// public static ApiCompatBaseline GetApiCompatBaselineFromFile( string? parameters = null, + string fileExtension = ".txt", [CallerMemberName] string method = "", [CallerFilePath] string filePath = "") { return ApiCompatBaseline.FromFile( - GetAssetFileOrDirectoryPath(true, parameters, method, filePath, fileExtension: ".txt")); + GetAssetFileOrDirectoryPath(true, parameters, method, filePath, fileExtension)); } public static string GetAssetFileOrDirectoryPath( From e6d961d6bb915936123962b9061e5057e64202d7 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 27 Jul 2026 09:07:52 -0700 Subject: [PATCH 2/2] Add ApiCompat baseline format parity tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 883420ab-f215-4afc-91a2-3433270335bb --- .../src/SourceInput/ApiCompatBaseline.cs | 2 + .../SourceInput/ApiCompatBaselineTests.cs | 101 +++++++++++------- .../ApiCompatBaselineTests/Baseline.xml | 58 ++++++++-- .../IgnoresUnknownRulesAndMalformedLines.xml | 13 +++ .../SuppressedString.xml | 7 ++ .../XmlDocumentationSymbols.xml | 19 ++++ 6 files changed, 150 insertions(+), 50 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/IgnoresUnknownRulesAndMalformedLines.xml create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/SuppressedString.xml create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/XmlDocumentationSymbols.xml diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/ApiCompatBaseline.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/ApiCompatBaseline.cs index be48bd0f247..fc9d6316bf0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/ApiCompatBaseline.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/ApiCompatBaseline.cs @@ -32,6 +32,7 @@ public sealed class ApiCompatBaseline private const string EnumValuesMustMatch = "EnumValuesMustMatch"; private const string TypeMustExistDiagnostic = "CP0001"; private const string MemberMustExistDiagnostic = "CP0002"; + private const string EnumValueMustMatchDiagnostic = "CP0011"; private readonly HashSet _suppressedTypes; private readonly HashSet _suppressedMembers; @@ -177,6 +178,7 @@ private static ApiCompatBaseline ParseXml(XDocument document) } break; case MemberMustExistDiagnostic: + case EnumValueMustMatchDiagnostic: if (TryParseXmlMember(target, out var memberKey, out var methodKey)) { suppressedMembers.Add(memberKey); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs index c6836217ba2..0d0cfddde73 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs @@ -10,8 +10,17 @@ namespace Microsoft.TypeSpec.Generator.Tests.SourceInput { + [TestFixture(".txt")] + [TestFixture(".xml")] public class ApiCompatBaselineTests { + private readonly string _fileExtension; + + public ApiCompatBaselineTests(string fileExtension) + { + _fileExtension = fileExtension; + } + [SetUp] public void Setup() { @@ -31,45 +40,33 @@ public void EmptyBaselineSuppressesNothing() [Test] public void ParsesTypesMustExist() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); Assert.IsTrue(baseline.IsTypeSuppressed("Azure.AI.Projects.Agents.ProjectsAgentProtocol")); Assert.IsFalse(baseline.IsTypeSuppressed("Azure.AI.Projects.Agents.SomethingElse")); } [Test] - public void ParsesXmlSuppressions() + public void ParsesSuppressions() { - var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: ".xml", method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); Assert.IsTrue(baseline.IsTypeSuppressed("Azure.AI.Projects.Agents.ProjectsAgentProtocol")); Assert.IsTrue(baseline.IsMemberSuppressed( "Azure.AI.Projects.Agents.ProjectsAgentsModelFactory", "ProtocolVersionRecord", 2)); - Assert.IsTrue(baseline.IsMethodRemovalSuppressed( - "Azure.AI.Projects.Agents.ProjectsAgentsModelFactory", - "ProtocolVersionRecord", - [ - new CSharpType(typeof(string)), - new CSharpType(typeof(IEnumerable)) - ])); - Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", ".ctor", 0)); + Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", ".ctor", 2)); Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "Kind", 0)); Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "Kind", 1)); Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "LegacyField", 0)); - Assert.IsTrue(baseline.IsTypeSuppressed("Ns.Outer.Inner")); - Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Outer.Inner", "Reset", 0)); - - var genericParameter = new CSharpType(typeof(IEnumerable<>).GetGenericArguments()[0]); - Assert.IsTrue(baseline.IsMethodRemovalSuppressed("Ns.Foo", "Generic", [genericParameter])); - Assert.IsFalse(baseline.IsMemberSuppressed("Ns.Foo", "ParameterRename", 1)); + Assert.IsTrue(baseline.IsMemberSuppressed("Ns.CapacityLevel", "FiftyThousand", 0)); } [Test] public void TypeSuppressionImpliesAllMembersSuppressed() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); Assert.IsTrue(baseline.IsMemberSuppressed("Azure.AI.Projects.Agents.ProjectsAgentProtocol", "AnyMember", 3)); } @@ -77,7 +74,7 @@ public void TypeSuppressionImpliesAllMembersSuppressed() [Test] public void ParsesMembersMustExistMethod() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); Assert.IsTrue(baseline.IsMemberSuppressed( "Azure.AI.Projects.Agents.ProjectsAgentsModelFactory", @@ -98,7 +95,7 @@ public void ParsesMembersMustExistMethod() [Test] public void ParsesParameterlessMember() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "Reset", 0)); } @@ -106,7 +103,7 @@ public void ParsesParameterlessMember() [Test] public void ParsesConstructor() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", ".ctor", 2)); } @@ -114,7 +111,7 @@ public void ParsesConstructor() [Test] public void ParsesPropertyAccessorOntoOwner() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "Kind", 0)); Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "Kind", 1)); @@ -123,7 +120,7 @@ public void ParsesPropertyAccessorOntoOwner() [Test] public void CountsGenericParametersAsSingleArgument() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "Configure", 2)); } @@ -131,7 +128,7 @@ public void CountsGenericParametersAsSingleArgument() [Test] public void ParsesMembersMustExistField() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // A removed field/enum member has no parameter list; it is recorded with arity 0. Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Foo", "LegacyField", 0)); @@ -140,7 +137,7 @@ public void ParsesMembersMustExistField() [Test] public void ParsesEnumValuesMustMatch() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // An accepted enum value difference suppresses back-compat handling for that member. Assert.IsTrue(baseline.IsMemberSuppressed("Ns.CapacityLevel", "FiftyThousand", 0)); @@ -151,7 +148,7 @@ public void ParsesEnumValuesMustMatch() [Test] public void IgnoresUnknownRulesAndMalformedLines() { - var baseline = Helpers.GetApiCompatBaselineFromFile(); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension); Assert.IsTrue(baseline.IsEmpty); } @@ -159,7 +156,7 @@ public void IgnoresUnknownRulesAndMalformedLines() [Test] public void IsMethodRemovalSuppressedDistinguishesOverloadsByParameterType() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // The baseline accepts removal of Get(string). Only that overload is suppressed; a Get(int) // overload with the same arity must NOT be treated as suppressed. @@ -174,7 +171,7 @@ public void IsMethodRemovalSuppressedDistinguishesOverloadsByParameterType() [Test] public void IsMethodRemovalSuppressedMatchesGenericParameterTypes() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Foo.Configure(IDictionary, string) is suppressed; the query builds the same // canonical signature from CSharpType parameters (including nested generic arguments). @@ -195,7 +192,7 @@ public void IsMethodRemovalSuppressedMatchesGenericParameterTypes() [Test] public void IsMethodRemovalSuppressedHonorsTypeSuppression() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // The whole type is suppressed via TypesMustExist, so any method on it is suppressed // regardless of parameter types. @@ -208,7 +205,7 @@ public void IsMethodRemovalSuppressedHonorsTypeSuppression() [Test] public void IsMethodRemovalSuppressedMatchesNullableValueTypeParameter() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithNullable(System.Nullable) is suppressed. A nullable value type // is rendered as System.Nullable on both sides. @@ -221,7 +218,7 @@ public void IsMethodRemovalSuppressedMatchesNullableValueTypeParameter() [Test] public void IsMethodRemovalSuppressedMatchesArrayParameter() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithArray(System.String[]) is suppressed. Assert.IsTrue(baseline.IsMethodRemovalSuppressed("Ns.Types", "WithArray", [new CSharpType(typeof(string[]))])); @@ -236,7 +233,7 @@ public void IsMethodRemovalSuppressedMatchesArrayParameter() [Test] public void IsMethodRemovalSuppressedMatchesGenericListParameter() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithList(System.Collections.Generic.List) is suppressed. var listOfString = new CSharpType(typeof(List<>), new CSharpType(typeof(string))); @@ -250,7 +247,7 @@ public void IsMethodRemovalSuppressedMatchesGenericListParameter() [Test] public void IsMethodRemovalSuppressedMatchesNestedGenericParameter() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithNestedGeneric(IList>) is suppressed; nested generic // arguments are rendered recursively on both sides. @@ -269,7 +266,7 @@ public void IsMethodRemovalSuppressedMatchesNestedGenericParameter() [Test] public void IsMethodRemovalSuppressedMatchesMultipleParametersInOrder() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithMany(int, string, bool) is suppressed. Assert.IsTrue(baseline.IsMethodRemovalSuppressed( @@ -293,7 +290,7 @@ public void IsMethodRemovalSuppressedMatchesMultipleParametersInOrder() [Test] public void IsMethodRemovalSuppressedMatchesParameterlessOverload() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Foo.Reset() has no parameters; the canonical signature is empty on both sides. Assert.IsTrue(baseline.IsMethodRemovalSuppressed("Ns.Foo", "Reset", [])); @@ -305,7 +302,7 @@ public void IsMethodRemovalSuppressedMatchesParameterlessOverload() [Test] public void IsMethodRemovalSuppressedMatchesEnumParameter() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithEnum(Sample.Models.MyKind) is suppressed. The enum's output type is produced // by the TypeFactory from the input enum and renders as its fully-qualified name. @@ -321,7 +318,7 @@ public void IsMethodRemovalSuppressedMatchesEnumParameter() [Test] public void IsMethodRemovalSuppressedMatchesNullableEnumParameter() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithNullableEnum(System.Nullable) is suppressed. A nullable // enum renders as System.Nullable. @@ -338,7 +335,7 @@ public void IsMethodRemovalSuppressedMatchesNullableEnumParameter() [Test] public void IsMethodRemovalSuppressedMatchesModelParameter() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithModel(Sample.Models.MyModel) is suppressed. The model's output type is produced // by the TypeFactory and renders as its fully-qualified name. @@ -354,7 +351,7 @@ public void IsMethodRemovalSuppressedMatchesModelParameter() [Test] public void IsMethodRemovalSuppressedMatchesDictionaryWithModelValue() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "Baseline"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); // Ns.Types.WithDictionary(Dictionary) is suppressed. The model // appears as a generic argument and is rendered recursively. @@ -371,7 +368,7 @@ public void IsMethodRemovalSuppressedMatchesDictionaryWithModelValue() [Test] public void ReferencesSuppressedTypeMatchesDirectType() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "SuppressedString"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "SuppressedString"); Assert.IsTrue(baseline.ReferencesSuppressedType(new CSharpType(typeof(string)))); Assert.IsFalse(baseline.ReferencesSuppressedType(new CSharpType(typeof(int)))); @@ -380,7 +377,7 @@ public void ReferencesSuppressedTypeMatchesDirectType() [Test] public void ReferencesSuppressedTypeMatchesNestedGenericArgument() { - var baseline = Helpers.GetApiCompatBaselineFromFile(method: "SuppressedString"); + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "SuppressedString"); // IList -- the suppressed type is nested as a generic argument. var listOfString = new CSharpType(typeof(IList<>), new CSharpType(typeof(string))); @@ -397,4 +394,26 @@ public void ReferencesSuppressedTypeReturnsFalseForNullOrEmptyBaseline() Assert.IsFalse(ApiCompatBaseline.Empty.ReferencesSuppressedType(null)); } } + + public class ApiCompatBaselineXmlTests + { + [SetUp] + public void Setup() + { + MockHelpers.LoadMockGenerator(); + } + + [Test] + public void ParsesXmlDocumentationSymbolFormats() + { + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: ".xml", method: "XmlDocumentationSymbols"); + + Assert.IsTrue(baseline.IsTypeSuppressed("Ns.Outer.Inner")); + Assert.IsTrue(baseline.IsMemberSuppressed("Ns.Outer.Inner", "Reset", 0)); + + var genericParameter = new CSharpType(typeof(IEnumerable<>).GetGenericArguments()[0]); + Assert.IsTrue(baseline.IsMethodRemovalSuppressed("Ns.Foo", "Generic", [genericParameter])); + Assert.IsFalse(baseline.IsMemberSuppressed("Ns.Foo", "ParameterRename", 1)); + } + } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml index 238621f2647..6b8132093b2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml @@ -1,16 +1,24 @@ - + + + CP0014 + T:Azure.AI.Projects.Agents.HostedAgentDefinition + CP0001 T:Azure.AI.Projects.Agents.ProjectsAgentProtocol CP0002 - M:Azure.AI.Projects.Agents.ProjectsAgentsModelFactory.ProtocolVersionRecord(System.String,System.Collections.Generic.IEnumerable{System.String}) + M:Azure.AI.Projects.Agents.ProjectsAgentsModelFactory.ProtocolVersionRecord(Azure.AI.Projects.Agents.ProjectsAgentProtocol,System.String) + + + CP0002 + M:Ns.Foo.Reset CP0002 - M:Ns.Foo.#ctor + M:Ns.Foo.#ctor(Ns.Kind,System.String) CP0002 @@ -20,24 +28,56 @@ CP0002 M:Ns.Foo.set_Kind(Ns.Kind) + + CP0002 + M:Ns.Foo.Configure(System.Collections.Generic.IDictionary{System.String,System.Int32},System.String) + CP0002 F:Ns.Foo.LegacyField - CP0001 - T:Ns.Outer`1+Inner + CP0011 + F:Ns.CapacityLevel.FiftyThousand + + + CP0002 + M:Ns.Overloads.Get(System.String) CP0002 - M:Ns.Outer`1+Inner.Reset + M:Ns.Types.WithNullable(System.Nullable{System.Int32}) CP0002 - M:Ns.Foo.Generic`1(`0) + M:Ns.Types.WithArray(System.String[]) - CP0017 - M:Ns.Foo.ParameterRename(System.String)$0 + CP0002 + M:Ns.Types.WithList(System.Collections.Generic.List{System.String}) + + + CP0002 + M:Ns.Types.WithNestedGeneric(System.Collections.Generic.IList{System.Collections.Generic.IDictionary{System.String,System.Int32}}) + + + CP0002 + M:Ns.Types.WithMany(System.Int32,System.String,System.Boolean) + + + CP0002 + M:Ns.Types.WithEnum(Sample.Models.MyKind) + + + CP0002 + M:Ns.Types.WithNullableEnum(System.Nullable{Sample.Models.MyKind}) + + + CP0002 + M:Ns.Types.WithModel(Sample.Models.MyModel) + + + CP0002 + M:Ns.Types.WithDictionary(System.Collections.Generic.Dictionary{System.String,Sample.Models.MyModel}) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/IgnoresUnknownRulesAndMalformedLines.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/IgnoresUnknownRulesAndMalformedLines.xml new file mode 100644 index 00000000000..b6ebcdea5fc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/IgnoresUnknownRulesAndMalformedLines.xml @@ -0,0 +1,13 @@ + + + + CP0014 + T:Ns.Foo + + + CP0002 + + + M:Ns.Foo.Reset + + diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/SuppressedString.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/SuppressedString.xml new file mode 100644 index 00000000000..150fe0266e3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/SuppressedString.xml @@ -0,0 +1,7 @@ + + + + CP0001 + T:System.String + + diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/XmlDocumentationSymbols.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/XmlDocumentationSymbols.xml new file mode 100644 index 00000000000..9a710a29668 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/XmlDocumentationSymbols.xml @@ -0,0 +1,19 @@ + + + + CP0001 + T:Ns.Outer`1+Inner + + + CP0002 + M:Ns.Outer`1+Inner.Reset + + + CP0002 + M:Ns.Foo.Generic`1(`0) + + + CP0017 + M:Ns.Foo.ParameterRename(System.String)$0 + +