Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.ApiCompatibility
{
internal enum ApiStability
{
Stable,
Experimental,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;

namespace Microsoft.DotNet.ApiCompatibility
{
internal static class ApiStabilityClassifier
{
private const string ExperimentalAttributeName = "ExperimentalAttribute";

private static readonly ConditionalWeakTable<ISymbol, CacheEntry> s_classifications = new();

public static ApiStability Classify(ISymbol? symbol)
{
if (symbol is null)
{
return ApiStability.Stable;
}

if (s_classifications.TryGetValue(symbol, out CacheEntry? cached))
{
return cached.Stability;
}

ApiStability stability = symbol.GetAttributes().Any(IsExperimentalAttribute) ||
Classify(symbol.ContainingSymbol) == ApiStability.Experimental
? ApiStability.Experimental
: ApiStability.Stable;

return s_classifications.GetValue(symbol, _ => new(stability)).Stability;
}

public static bool IsExperimentalAttribute(AttributeData attribute) =>
attribute.AttributeClass is
{
MetadataName: ExperimentalAttributeName,
ContainingNamespace:
{
Name: "CodeAnalysis",
ContainingNamespace:
{
Name: "Diagnostics",
ContainingNamespace:
{
Name: "System",
ContainingNamespace.IsGlobalNamespace: true
}
}
}
};

private sealed class CacheEntry(ApiStability stability)
{
public ApiStability Stability { get; } = stability;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ namespace Microsoft.DotNet.ApiCompatibility
/// <param name="message"><see cref="string"/> message describing the difference.</param>
/// <param name="type"><see cref="DifferenceType"/> to describe the type of the difference.</param>
/// <param name="memberId"><see cref="string"/> containing the member ID for which the difference is associated to.</param>
public readonly struct CompatDifference(MetadataInformation left, MetadataInformation right, string diagnosticId, string message, DifferenceType type, string? memberId) : IDiagnostic, IEquatable<CompatDifference>
/// <param name="severity">The severity of the compatibility difference.</param>
public readonly struct CompatDifference(MetadataInformation left, MetadataInformation right, string diagnosticId, string message, DifferenceType type, string? memberId, DifferenceSeverity severity) : IDiagnostic, IEquatable<CompatDifference>
{
/// <inheritdoc />
public string DiagnosticId { get; } = diagnosticId;
Expand All @@ -25,6 +26,15 @@ public readonly struct CompatDifference(MetadataInformation left, MetadataInform
/// </summary>
public DifferenceType Type { get; } = type;

/// <summary>
/// The severity of the compatibility difference.
/// </summary>
public DifferenceSeverity Severity { get; } = severity;

internal ApiStability? LeftStability { get; }

internal ApiStability? RightStability { get; }

/// <inheritdoc />
public string Message { get; } = message;

Expand All @@ -41,6 +51,20 @@ public readonly struct CompatDifference(MetadataInformation left, MetadataInform
/// </summary>
public MetadataInformation Right { get; } = right;

/// <summary>
/// Instantiate a new object representing the compatibility difference.
/// </summary>
/// <param name="left">The metadata information of the left comparison side.</param>
/// <param name="right">The metadata information of the right comparison side.</param>
/// <param name="diagnosticId"><see cref="string"/> representing the diagnostic ID.</param>
/// <param name="message"><see cref="string"/> message describing the difference.</param>
/// <param name="type"><see cref="DifferenceType"/> to describe the type of the difference.</param>
/// <param name="memberId"><see cref="string"/> containing the member ID for which the difference is associated to.</param>
public CompatDifference(MetadataInformation left, MetadataInformation right, string diagnosticId, string message, DifferenceType type, string? memberId)
: this(left, right, diagnosticId, message, type, memberId, DifferenceSeverity.Error)
{
}

/// <summary>
/// Instantiate a new object representing the compatibility difference.
/// </summary>
Expand All @@ -55,6 +79,19 @@ public CompatDifference(MetadataInformation left, MetadataInformation right, str
{
}

internal CompatDifference WithSeverity(DifferenceSeverity newSeverity) =>
new(Left, Right, DiagnosticId, Message, Type, ReferenceId, newSeverity, LeftStability, RightStability);

internal CompatDifference WithStabilities(ApiStability? leftStability, ApiStability? rightStability) =>
new(Left, Right, DiagnosticId, Message, Type, ReferenceId, Severity, leftStability, rightStability);

private CompatDifference(MetadataInformation left, MetadataInformation right, string diagnosticId, string message, DifferenceType type, string? memberId, DifferenceSeverity severity, ApiStability? leftStability, ApiStability? rightStability)
: this(left, right, diagnosticId, message, type, memberId, severity)
{
LeftStability = leftStability;
RightStability = rightStability;
}

/// <summary>
/// Create a compatibility difference object with default left and right metadata for which the difference occurred.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ public static class DiagnosticIds
public const string CannotReduceVisibility = "CP0019";
public const string CannotExpandVisibility = "CP0020";
public const string CannotChangeGenericConstraint = "CP0021";
public const string ExperimentalApiBecomesStable = "CP0022";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.ApiCompatibility
{
public enum DifferenceSeverity
{
Error,
Informational,
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;
using Microsoft.DotNet.ApiCompatibility.Mapping;

namespace Microsoft.DotNet.ApiCompatibility
Expand Down Expand Up @@ -52,7 +53,7 @@ public void Visit(IAssemblySetMapper mapper)
/// <inheritdoc />
public void Visit(IAssemblyMapper assembly)
{
AddDifferences(assembly);
AddSymbolDifferences(assembly.GetDifferences());

foreach (INamespaceMapper @namespace in assembly.GetNamespaces())
{
Expand All @@ -72,7 +73,7 @@ public void Visit(INamespaceMapper @namespace)
/// <inheritdoc />
public void Visit(ITypeMapper type)
{
AddDifferences(type);
AddSymbolDifferences(type);

if (type.ShouldDiffMembers)
{
Expand All @@ -91,14 +92,39 @@ public void Visit(ITypeMapper type)
/// <inheritdoc />
public void Visit(IMemberMapper member)
{
AddDifferences(member);
AddSymbolDifferences(member);
}

private void AddDifferences<T>(IElementMapper<T> mapper)
private void AddSymbolDifferences<T>(IElementMapper<T> mapper)
where T : ISymbol
=> AddSymbolDifferences(mapper.GetDifferences());

private void AddSymbolDifferences(IEnumerable<CompatDifference> differences)
{
foreach (CompatDifference item in mapper.GetDifferences())
foreach (CompatDifference item in differences)
{
_compatDifferences.Add(item);
bool isExperimentalDifference =
(item.LeftStability is null && item.RightStability == ApiStability.Experimental) ||
(item.RightStability is null && item.LeftStability == ApiStability.Experimental) ||
(item.LeftStability == ApiStability.Experimental && item.RightStability == ApiStability.Experimental);

CompatDifference difference = isExperimentalDifference
? item.WithSeverity(DifferenceSeverity.Informational)
: item;

if (_compatDifferences.TryGetValue(difference, out CompatDifference existingDifference))
{
if (existingDifference.Severity == DifferenceSeverity.Informational &&
difference.Severity == DifferenceSeverity.Error)
{
_compatDifferences.Remove(existingDifference);
_compatDifferences.Add(difference);
}
}
else
{
_compatDifferences.Add(difference);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@
<data name="EnumTypesMustMatch" xml:space="preserve">
<value>Underlying type of enum '{0}' changed from '{1}' to '{2}'.</value>
</data>
<data name="ExperimentalApiBecomesStable" xml:space="preserve">
<value>API '{0}' was previously marked experimental and is now stable. Treat this as a new stable API and complete the required API documentation and review.</value>
</data>
<data name="CannotAddVirtualToMember" xml:space="preserve">
<value>Cannot add virtual keyword to member '{0}'.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ private void AddDifference(IList<CompatDifference> differences,
differences.Add(difference);
}

private void ReportAttributeDifferences(ISymbol containing,
// Attribute diagnostics retain the left containing symbol for compatibility with existing behavior.
// The right containing symbol is only used when handing an experimental API promotion to CP0022.
private void ReportAttributeDifferences(ISymbol leftContaining,
ISymbol rightContaining,
MetadataInformation leftMetadata,
MetadataInformation rightMetadata,
string itemRef,
Expand Down Expand Up @@ -119,7 +122,7 @@ private void ReportAttributeDifferences(ISymbol containing,
{
// Attribute arguments exist on left but not right.
// Issue "changed" diagnostic.
AddDifference(differences, DifferenceType.Changed, leftMetadata, rightMetadata, containing, itemRef, leftAttribute);
AddDifference(differences, DifferenceType.Changed, leftMetadata, rightMetadata, leftContaining, itemRef, leftAttribute);
}
}

Expand All @@ -136,7 +139,7 @@ private void ReportAttributeDifferences(ISymbol containing,
// [Foo("b")]
// void F()
// Issue "changed" diagnostic when in strict mode.
AddDifference(differences, DifferenceType.Changed, leftMetadata, rightMetadata, containing, itemRef, rightGroup.Attributes[i]);
AddDifference(differences, DifferenceType.Changed, leftMetadata, rightMetadata, leftContaining, itemRef, rightGroup.Attributes[i]);
}
}
}
Expand All @@ -146,7 +149,18 @@ private void ReportAttributeDifferences(ISymbol containing,
// Loop over left and issue "removed" diagnostic for each one.
foreach (AttributeData leftAttribute in leftGroup.Attributes)
{
AddDifference(differences, DifferenceType.Removed, leftMetadata, rightMetadata, containing, itemRef, leftAttribute);
if (ApiStabilityClassifier.IsExperimentalAttribute(leftAttribute))
{
ExperimentalApiBecomesStable.AddDifference(
leftContaining,
rightContaining,
leftMetadata,
rightMetadata,
differences);
continue;
}

AddDifference(differences, DifferenceType.Removed, leftMetadata, rightMetadata, leftContaining, itemRef, leftAttribute);
}
}
}
Expand All @@ -162,7 +176,7 @@ private void ReportAttributeDifferences(ISymbol containing,
// Loop over right and issue "added" diagnostic for each one.
foreach (AttributeData rightAttribute in rightGroup.Attributes)
{
AddDifference(differences, DifferenceType.Added, leftMetadata, rightMetadata, containing, itemRef, rightAttribute);
AddDifference(differences, DifferenceType.Added, leftMetadata, rightMetadata, leftContaining, itemRef, rightAttribute);
}
}
}
Expand All @@ -186,6 +200,7 @@ private void RunOnTypeSymbol(ITypeSymbol? left,
for (int i = 0; i < leftNamed.TypeParameters.Length; i++)
{
ReportAttributeDifferences(left,
right,
leftMetadata,
rightMetadata,
left.GetDocumentationCommentId() + $"<{i}>",
Expand All @@ -197,6 +212,7 @@ private void RunOnTypeSymbol(ITypeSymbol? left,
}

ReportAttributeDifferences(left,
right,
leftMetadata,
rightMetadata,
left.GetDocumentationCommentId() ?? "",
Expand All @@ -223,6 +239,7 @@ private void RunOnMemberSymbol(ISymbol? left,
// If member is a method,
// compare return type attributes,
ReportAttributeDifferences(left,
right,
leftMetadata,
rightMetadata,
left.GetDocumentationCommentId() + "->" + leftMethod.ReturnType,
Expand All @@ -236,6 +253,7 @@ private void RunOnMemberSymbol(ISymbol? left,
for (int i = 0; i < leftMethod.Parameters.Length; i++)
{
ReportAttributeDifferences(left,
right,
leftMetadata,
rightMetadata,
left.GetDocumentationCommentId() + $"${i}",
Expand All @@ -251,6 +269,7 @@ private void RunOnMemberSymbol(ISymbol? left,
for (int i = 0; i < leftMethod.TypeParameters.Length; i++)
{
ReportAttributeDifferences(left,
right,
leftMetadata,
rightMetadata,
left.GetDocumentationCommentId() + $"<{i}>",
Expand All @@ -262,6 +281,7 @@ private void RunOnMemberSymbol(ISymbol? left,
}

ReportAttributeDifferences(left,
right,
leftMetadata,
rightMetadata,
left.GetDocumentationCommentId() ?? "",
Expand Down
Loading
Loading