Skip to content

Commit 170fa21

Browse files
authored
Add APICompat support for checking generic constraints (#40230)
1 parent 01cb016 commit 170fa21

18 files changed

+539
-1
lines changed

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/DiagnosticIds.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public static class DiagnosticIds
2828
public const string CannotAddSealedToInterfaceMember = "CP0018";
2929
public const string CannotReduceVisibility = "CP0019";
3030
public const string CannotExpandVisibility = "CP0020";
31+
public const string CannotChangeGenericConstraint = "CP0021";
3132

3233
// Assembly loading ids
3334
public const string AssemblyReferenceNotFound = "CP1002";

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/Resources.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@
210210
<data name="CannotRemoveAttribute" xml:space="preserve">
211211
<value>Cannot remove attribute '{0}' from '{1}'.</value>
212212
</data>
213+
<data name="CannotAddGenericConstraint" xml:space="preserve">
214+
<value>Cannot add constraint '{0}' on type parameter '{1}' of '{2}'.</value>
215+
</data>
216+
<data name="CannotRemoveGenericConstraint" xml:space="preserve">
217+
<value>Cannot remove constraint '{0}' on type parameter '{1}' of '{2}'.</value>
218+
</data>
213219
<data name="CannotChangeParameterName" xml:space="preserve">
214220
<value>Parameter name on member '{0}' changed from '{1}' to '{2}'.</value>
215221
</data>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Immutable;
5+
using System.Diagnostics;
6+
using System.Runtime;
7+
using Microsoft.CodeAnalysis;
8+
9+
namespace Microsoft.DotNet.ApiCompatibility.Rules
10+
{
11+
/// <summary>
12+
/// This class implements a rule to check that generic constraints on type parameters do not change.
13+
/// TypeParameter names should not change in strict mode, or when specified
14+
/// strict mode: no changes at all
15+
/// sealed types and non-virtual methods: no additions, removals permitted
16+
/// non-sealed types and virtual methods: no changes
17+
/// </summary>
18+
public class CannotChangeGenericConstraints : IRule
19+
{
20+
private readonly IRuleSettings _settings;
21+
22+
public CannotChangeGenericConstraints(IRuleSettings settings, IRuleRegistrationContext context)
23+
{
24+
_settings = settings;
25+
context.RegisterOnMemberSymbolAction(RunOnMemberSymbol);
26+
context.RegisterOnTypeSymbolAction(RunOnTypeSymbol);
27+
}
28+
29+
private void RunOnTypeSymbol(ITypeSymbol? left,
30+
ITypeSymbol? right,
31+
MetadataInformation leftMetadata,
32+
MetadataInformation rightMetadata,
33+
IList<CompatDifference> differences)
34+
{
35+
if (left is not INamedTypeSymbol leftType || right is not INamedTypeSymbol rightType)
36+
{
37+
return;
38+
}
39+
40+
var leftTypeParameters = leftType.TypeParameters;
41+
var rightTypeParameters = rightType.TypeParameters;
42+
43+
// can remove constraints on sealed classes since no code should observe broader set of type parameters
44+
bool permitConstraintRemoval = !_settings.StrictMode && leftType.IsSealed;
45+
46+
CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, permitConstraintRemoval, leftMetadata, rightMetadata, differences);
47+
}
48+
49+
private void RunOnMemberSymbol(
50+
ISymbol? left,
51+
ISymbol? right,
52+
ITypeSymbol leftContainingType,
53+
ITypeSymbol rightContainingType,
54+
MetadataInformation leftMetadata,
55+
MetadataInformation rightMetadata,
56+
IList<CompatDifference> differences)
57+
{
58+
if (left is not IMethodSymbol leftMethod || right is not IMethodSymbol rightMethod)
59+
{
60+
return;
61+
}
62+
63+
var leftTypeParameters = leftMethod.TypeParameters;
64+
var rightTypeParameters = rightMethod.TypeParameters;
65+
66+
bool permitConstraintRemoval = !_settings.StrictMode && !leftMethod.IsVirtual;
67+
68+
CompareTypeParameters(leftTypeParameters, rightTypeParameters, left, permitConstraintRemoval, leftMetadata, rightMetadata, differences);
69+
}
70+
71+
private void CompareTypeParameters(
72+
ImmutableArray<ITypeParameterSymbol> leftTypeParameters,
73+
ImmutableArray<ITypeParameterSymbol> rightTypeParameters,
74+
ISymbol left,
75+
bool permitConstraintRemoval,
76+
MetadataInformation leftMetadata,
77+
MetadataInformation rightMetadata,
78+
IList<CompatDifference> differences)
79+
{
80+
Debug.Assert(leftTypeParameters.Length == rightTypeParameters.Length);
81+
for (int i = 0; i < leftTypeParameters.Length; i++)
82+
{
83+
ITypeParameterSymbol leftTypeParam = leftTypeParameters[i];
84+
ITypeParameterSymbol rightTypeParam = rightTypeParameters[i];
85+
86+
List<string> addedConstraints = new();
87+
List<string> removedConstraints = new();
88+
89+
CompareBoolConstraint(typeParam => typeParam.HasConstructorConstraint, "new()");
90+
CompareBoolConstraint(typeParam => typeParam.HasNotNullConstraint, "notnull");
91+
CompareBoolConstraint(typeParam => typeParam.HasReferenceTypeConstraint, "class");
92+
CompareBoolConstraint(typeParam => typeParam.HasUnmanagedTypeConstraint, "unmanaged");
93+
// unmanaged implies struct
94+
CompareBoolConstraint(typeParam => typeParam.HasValueTypeConstraint & !typeParam.HasUnmanagedTypeConstraint, "struct");
95+
96+
HashSet<ISymbol> rightOnlyConstraints = rightTypeParam.ConstraintTypes.ToHashSet(_settings.SymbolEqualityComparer);
97+
rightOnlyConstraints.ExceptWith(leftTypeParam.ConstraintTypes);
98+
99+
// we could allow an addition if removals are allowed, and the addition is a less-derived base type or interface
100+
// for example: changing a constraint from MemoryStream to Stream on a sealed type, or non-virtual member
101+
// but we'll leave this to suppressions
102+
103+
addedConstraints.AddRange(rightOnlyConstraints.Select(x => x.GetDocumentationCommentId()!));
104+
105+
// additions
106+
foreach(var addedConstraint in addedConstraints)
107+
{
108+
differences.Add(new CompatDifference(
109+
leftMetadata,
110+
rightMetadata,
111+
DiagnosticIds.CannotChangeGenericConstraint,
112+
string.Format(Resources.CannotAddGenericConstraint, addedConstraint, leftTypeParam, left),
113+
DifferenceType.Added,
114+
$"{left.GetDocumentationCommentId()}``{i}:{addedConstraint}"));
115+
}
116+
117+
// removals
118+
// we could allow a removal in the case of reducing to more-derived interfaces if those interfaces were previous constraints
119+
// for example if IB : IA and a type is constrained by both IA and IB, it's safe to remove IA since it's implied by IB
120+
// but we'll leave this to suppressions
121+
122+
if (!permitConstraintRemoval)
123+
{
124+
HashSet<ISymbol> leftOnlyConstraints = leftTypeParam.ConstraintTypes.ToHashSet(_settings.SymbolEqualityComparer);
125+
leftOnlyConstraints.ExceptWith(rightTypeParam.ConstraintTypes);
126+
removedConstraints.AddRange(leftOnlyConstraints.Select(x => x.GetDocumentationCommentId()!));
127+
128+
foreach (var removedConstraint in removedConstraints)
129+
{
130+
differences.Add(new CompatDifference(
131+
leftMetadata,
132+
rightMetadata,
133+
DiagnosticIds.CannotChangeGenericConstraint,
134+
string.Format(Resources.CannotRemoveGenericConstraint, removedConstraint, leftTypeParam, left),
135+
DifferenceType.Removed,
136+
$"{left.GetDocumentationCommentId()}``{i}:{removedConstraint}"));
137+
}
138+
}
139+
140+
void CompareBoolConstraint(Func<ITypeParameterSymbol, bool> boolConstraint, string constraintName)
141+
{
142+
bool leftBoolConstraint = boolConstraint(leftTypeParam);
143+
bool rightBoolConstraint = boolConstraint(rightTypeParam);
144+
145+
// addition
146+
if (!leftBoolConstraint && rightBoolConstraint)
147+
{
148+
addedConstraints.Add(constraintName);
149+
}
150+
// removal
151+
else if (!permitConstraintRemoval && leftBoolConstraint && !rightBoolConstraint)
152+
{
153+
removedConstraints.Add(constraintName);
154+
}
155+
}
156+
}
157+
}
158+
}
159+
}

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/Rules/RuleFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public IRule[] CreateRules(IRuleSettings settings, IRuleRegistrationContext cont
2525
new CannotSealType(settings, context),
2626
new EnumsMustMatch(settings, context),
2727
new MembersMustExist(settings, context),
28-
new CannotChangeVisibility(settings, context)
28+
new CannotChangeVisibility(settings, context),
29+
new CannotChangeGenericConstraints(settings, context),
2930
};
3031

3132
if (enableRuleAttributesMustMatch)

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/xlf/Resources.cs.xlf

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/xlf/Resources.de.xlf

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/xlf/Resources.es.xlf

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/xlf/Resources.fr.xlf

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/xlf/Resources.it.xlf

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompatibility/xlf/Resources.ja.xlf

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)