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,28 @@
// 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;

namespace Microsoft.DotNet.ApiCompatibility
{
internal static class ApiStabilityClassifier
{
internal const string ExperimentalAttributeMetadataName = "System.Diagnostics.CodeAnalysis.ExperimentalAttribute";

public static ApiStability Classify(ISymbol? symbol)
{
for (ISymbol? current = symbol; current != null; current = current.ContainingType)
{
if (current.GetAttributes().Any(IsExperimentalAttribute))
{
return ApiStability.Experimental;
}
}

return ApiStability.Stable;
}

public static bool IsExperimentalAttribute(AttributeData attribute) =>
attribute.AttributeClass?.ToDisplayString() == ExperimentalAttributeMetadataName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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>
public readonly struct CompatDifference(MetadataInformation left, MetadataInformation right, string diagnosticId, string message, DifferenceType type, string? memberId, DifferenceSeverity severity = DifferenceSeverity.Error) : IDiagnostic, IEquatable<CompatDifference>
{
/// <inheritdoc />
public string DiagnosticId { get; } = diagnosticId;
Comment on lines +18 to 21
Expand All @@ -25,6 +25,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 ISymbol? LeftSymbol { get; }

internal ISymbol? RightSymbol { get; }

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

Expand Down Expand Up @@ -55,6 +64,19 @@ public CompatDifference(MetadataInformation left, MetadataInformation right, str
{
}

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

internal CompatDifference WithSymbols(ISymbol? leftSymbol, ISymbol? rightSymbol) =>
new(Left, Right, DiagnosticId, Message, Type, ReferenceId, Severity, leftSymbol, rightSymbol);

private CompatDifference(MetadataInformation left, MetadataInformation right, string diagnosticId, string message, DifferenceType type, string? memberId, DifferenceSeverity severity, ISymbol? leftSymbol, ISymbol? rightSymbol)
: this(left, right, diagnosticId, message, type, memberId, severity)
{
LeftSymbol = leftSymbol;
RightSymbol = rightSymbol;
}

/// <summary>
/// Create a compatibility difference object with default left and right metadata for which the difference occurred.
/// </summary>
Expand All @@ -78,6 +100,7 @@ public bool Equals(CompatDifference other) =>
Right.Equals(other.Right) &&
DiagnosticId.Equals(other.DiagnosticId, StringComparison.InvariantCultureIgnoreCase) &&
Type.Equals(other.Type) &&
Severity.Equals(other.Severity) &&
string.Equals(ReferenceId, other.ReferenceId, StringComparison.InvariantCultureIgnoreCase);

/// <inheritdoc />
Expand All @@ -91,6 +114,7 @@ public override int GetHashCode()
hashCode = hashCode * -1521134295 + Right.GetHashCode();
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(DiagnosticId.ToLowerInvariant());
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Type.ToString().ToLowerInvariant());
hashCode = hashCode * -1521134295 + Severity.GetHashCode();
if (ReferenceId != null)
{
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(ReferenceId.ToLowerInvariant());
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 @@ -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,7 +92,26 @@ public void Visit(ITypeMapper type)
/// <inheritdoc />
public void Visit(IMemberMapper member)
{
AddDifferences(member);
AddSymbolDifferences(member);
}

private void AddSymbolDifferences<T>(IElementMapper<T> mapper)
where T : ISymbol
{
foreach (CompatDifference item in mapper.GetDifferences())
{
ApiStability leftStability = ApiStabilityClassifier.Classify(item.LeftSymbol);
ApiStability rightStability = ApiStabilityClassifier.Classify(item.RightSymbol);

bool isExperimentalDifference =
(item.LeftSymbol is null && rightStability == ApiStability.Experimental) ||
(item.RightSymbol is null && leftStability == ApiStability.Experimental) ||
(leftStability == ApiStability.Experimental && rightStability == ApiStability.Experimental);

_compatDifferences.Add(isExperimentalDifference
? item.WithSeverity(DifferenceSeverity.Informational)
: item);
}
}

private void AddDifferences<T>(IElementMapper<T> mapper)
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 @@ -146,6 +146,11 @@ private void ReportAttributeDifferences(ISymbol containing,
// Loop over left and issue "removed" diagnostic for each one.
foreach (AttributeData leftAttribute in leftGroup.Attributes)
{
if (ApiStabilityClassifier.IsExperimentalAttribute(leftAttribute))
{
continue;
}

AddDifference(differences, DifferenceType.Removed, leftMetadata, rightMetadata, containing, itemRef, leftAttribute);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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.ApiSymbolExtensions;

namespace Microsoft.DotNet.ApiCompatibility.Rules
{
public class ExperimentalApiBecomesStable : IRule
{
public ExperimentalApiBecomesStable(IRuleRegistrationContext context)
{
context.RegisterOnTypeSymbolAction(RunOnTypeSymbol);
context.RegisterOnMemberSymbolAction(RunOnMemberSymbol);
}

private static void RunOnTypeSymbol(ITypeSymbol? left,
ITypeSymbol? right,
MetadataInformation leftMetadata,
MetadataInformation rightMetadata,
IList<CompatDifference> differences) =>
AddDifference(left, right, leftMetadata, rightMetadata, differences);

private static void RunOnMemberSymbol(ISymbol? left,
ISymbol? right,
ITypeSymbol leftContainingType,
ITypeSymbol rightContainingType,
MetadataInformation leftMetadata,
MetadataInformation rightMetadata,
IList<CompatDifference> differences) =>
AddDifference(left, right, leftMetadata, rightMetadata, differences);

private static void AddDifference(ISymbol? left,
ISymbol? right,
MetadataInformation leftMetadata,
MetadataInformation rightMetadata,
IList<CompatDifference> differences)
{
if (left is null || right is null ||
ApiStabilityClassifier.Classify(left) != ApiStability.Experimental ||
ApiStabilityClassifier.Classify(right) != ApiStability.Stable)
{
return;
}

differences.Add(new CompatDifference(
leftMetadata,
rightMetadata,
DiagnosticIds.ExperimentalApiBecomesStable,
string.Format(Resources.ExperimentalApiBecomesStable, right.ToDisplayString(SymbolExtensions.DisplayFormat)),
DifferenceType.Changed,
right.GetDocumentationCommentId(),
DifferenceSeverity.Error));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public IRule[] CreateRules(IRuleSettings settings, IRuleRegistrationContext cont
new CannotRemoveBaseTypeOrInterface(settings, context),
new CannotSealType(settings, context),
new EnumsMustMatch(settings, context),
new ExperimentalApiBecomesStable(context),
new MembersMustExist(settings, context),
new CannotChangeVisibility(settings, context),
new CannotChangeGenericConstraints(settings, context),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

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

namespace Microsoft.DotNet.ApiCompatibility.Rules
Expand All @@ -26,6 +27,10 @@ public IEnumerable<CompatDifference> Run<T>(IElementMapper<T> mapper)
int rightLength = mapper.Right.Length;
for (int rightIndex = 0; rightIndex < rightLength; rightIndex++)
{
int initialDifferenceCount = differences.Count;
ISymbol? leftSymbol = null;
ISymbol? rightSymbol = null;

if (mapper is AssemblyMapper am)
{
// Ignore assembly mappings which are null on both sides, i.e. when different assembly identities are marked as compatible.
Expand All @@ -37,8 +42,11 @@ public IEnumerable<CompatDifference> Run<T>(IElementMapper<T> mapper)
the assembly mapper is directly visited. */
bool containsSingleAssembly = am.ContainingAssemblySet == null || am.ContainingAssemblySet.AssemblyCount < 2;

context.RunOnAssemblySymbolActions(am.Left?.Element,
am.Right[rightIndex]?.Element,
leftSymbol = am.Left?.Element;
rightSymbol = am.Right[rightIndex]?.Element;

context.RunOnAssemblySymbolActions(leftSymbol as IAssemblySymbol,
rightSymbol as IAssemblySymbol,
am.Left?.MetadataInformation ?? MetadataInformation.DefaultLeft,
am.Right[rightIndex]?.MetadataInformation ?? MetadataInformation.DefaultRight,
containsSingleAssembly,
Expand All @@ -48,6 +56,8 @@ the assembly mapper is directly visited. */
{
if (tm.ShouldDiffElement(rightIndex))
{
leftSymbol = tm.Left;
rightSymbol = tm.Right[rightIndex];
context.RunOnTypeSymbolActions(tm.Left,
tm.Right[rightIndex],
tm.ContainingNamespace.ContainingAssembly.Left?.MetadataInformation ?? MetadataInformation.DefaultLeft,
Expand All @@ -63,6 +73,9 @@ the assembly mapper is directly visited. */
Debug.Assert(mm.ContainingType.Left != null);
Debug.Assert(mm.ContainingType.Right[rightIndex] != null);

leftSymbol = mm.Left;
rightSymbol = mm.Right[rightIndex];

context.RunOnMemberSymbolActions(
mm.Left,
mm.Right[rightIndex],
Expand All @@ -77,6 +90,11 @@ the assembly mapper is directly visited. */
{
throw new ArgumentOutOfRangeException(nameof(mapper));
}

for (int differenceIndex = initialDifferenceCount; differenceIndex < differences.Count; differenceIndex++)
{
differences[differenceIndex] = differences[differenceIndex].WithSymbols(leftSymbol, rightSymbol);
}
}

return differences;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ public void ExecuteWorkItems()

foreach (CompatDifference difference in differenceGroup)
{
if (difference.Severity == DifferenceSeverity.Informational)
{
log.LogMessage(MessageImportance.Normal, difference.ToString());
continue;
}

Suppression suppression = new(difference.DiagnosticId)
{
Target = difference.ReferenceId,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading