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..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
@@ -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,9 @@ 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 const string EnumValueMustMatchDiagnostic = "CP0011";
private readonly HashSet _suppressedTypes;
private readonly HashSet _suppressedMembers;
@@ -62,7 +67,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 +136,64 @@ 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:
+ case EnumValueMustMatchDiagnostic:
+ 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 +398,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 +590,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..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,16 +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 ParsesSuppressions()
+ {
+ 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.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.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));
}
@@ -48,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",
@@ -69,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));
}
@@ -77,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));
}
@@ -85,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));
@@ -94,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));
}
@@ -102,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));
@@ -111,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));
@@ -122,7 +148,7 @@ public void ParsesEnumValuesMustMatch()
[Test]
public void IgnoresUnknownRulesAndMalformedLines()
{
- var baseline = Helpers.GetApiCompatBaselineFromFile();
+ var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension);
Assert.IsTrue(baseline.IsEmpty);
}
@@ -130,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.
@@ -145,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).
@@ -166,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.
@@ -179,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.
@@ -192,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[]))]));
@@ -207,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)));
@@ -221,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.
@@ -240,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(
@@ -264,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", []));
@@ -276,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.
@@ -292,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.
@@ -309,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.
@@ -325,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.
@@ -342,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))));
@@ -351,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)));
@@ -368,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
new file mode 100644
index 00000000000..6b8132093b2
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml
@@ -0,0 +1,83 @@
+
+
+
+ CP0014
+ T:Azure.AI.Projects.Agents.HostedAgentDefinition
+
+
+ CP0001
+ T:Azure.AI.Projects.Agents.ProjectsAgentProtocol
+
+
+ CP0002
+ M:Azure.AI.Projects.Agents.ProjectsAgentsModelFactory.ProtocolVersionRecord(Azure.AI.Projects.Agents.ProjectsAgentProtocol,System.String)
+
+
+ CP0002
+ M:Ns.Foo.Reset
+
+
+ CP0002
+ M:Ns.Foo.#ctor(Ns.Kind,System.String)
+
+
+ CP0002
+ M:Ns.Foo.get_Kind
+
+
+ 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
+
+
+ CP0011
+ F:Ns.CapacityLevel.FiftyThousand
+
+
+ CP0002
+ M:Ns.Overloads.Get(System.String)
+
+
+ CP0002
+ M:Ns.Types.WithNullable(System.Nullable{System.Int32})
+
+
+ CP0002
+ M:Ns.Types.WithArray(System.String[])
+
+
+ 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
+
+
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(