Skip to content

Commit 2bd8055

Browse files
author
Julien Couvreur
committed
Extensions: check for marker collisions
1 parent d2699a1 commit 2bd8055

28 files changed

+1715
-41
lines changed

src/Compilers/CSharp/Portable/CSharpResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8218,4 +8218,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
82188218
<data name="ERR_InterpolatedStringHandlerArgumentDisallowed" xml:space="preserve">
82198219
<value>Interpolated string handler arguments are not allowed in this context.</value>
82208220
</data>
8221+
<data name="ERR_ExtensionBlockCollision" xml:space="preserve">
8222+
<value>This extension block collides with another extension block. They have different signatures, but result in the same grouping or marker type identifier.</value>
8223+
</data>
82218224
</root>

src/Compilers/CSharp/Portable/Errors/ErrorCode.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2420,6 +2420,7 @@ internal enum ErrorCode
24202420
ERR_InstanceOperatorExtensionWrongReceiverType = 9323,
24212421
ERR_ExpressionTreeContainsExtensionBasedConditionalLogicalOperator = 9324,
24222422
ERR_InterpolatedStringHandlerArgumentDisallowed = 9325,
2423+
ERR_ExtensionBlockCollision = 9326,
24232424

24242425
// Note: you will need to do the following after adding errors:
24252426
// 1) Update ErrorFacts.IsBuildOnlyDiagnostic (src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs)

src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2532,6 +2532,7 @@ or ErrorCode.ERR_InstanceOperatorStructExtensionWrongReceiverRefKind
25322532
or ErrorCode.ERR_InstanceOperatorExtensionWrongReceiverType
25332533
or ErrorCode.ERR_ExpressionTreeContainsExtensionBasedConditionalLogicalOperator
25342534
or ErrorCode.ERR_InterpolatedStringHandlerArgumentDisallowed
2535+
or ErrorCode.ERR_ExtensionBlockCollision
25352536
=> false,
25362537
};
25372538
#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.

src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs

Lines changed: 137 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,11 @@ internal sealed class MemberSignatureComparer : IEqualityComparer<Symbol>
135135
considerName: true,
136136
considerExplicitlyImplementedInterfaces: true,
137137
considerReturnType: true,
138-
considerTypeConstraints: false,
138+
considerTypeConstraints: false, // constraints are checked by caller instead
139139
considerCallingConvention: false,
140140
refKindCompareMode: RefKindCompareMode.ConsiderDifferences,
141-
typeComparison: TypeCompareKind.ObliviousNullableModifierMatchesAny);
141+
typeComparison: TypeCompareKind.ObliviousNullableModifierMatchesAny,
142+
considerParameterNames: true);
142143

143144
/// <summary>
144145
/// Determines if an interceptor has a compatible signature with an interceptable method.
@@ -354,6 +355,21 @@ internal sealed class MemberSignatureComparer : IEqualityComparer<Symbol>
354355
considerDefaultValues: true,
355356
typeComparison: TypeCompareKind.AllIgnoreOptions);
356357

358+
/// <summary>
359+
/// This instance is used to determine if two extension blocks match in C# sense and thus are allowed to share a marker type.
360+
/// It considers parameter and type parameter names, type constraints, nullability and attributes.
361+
/// </summary>
362+
public static readonly MemberSignatureComparer ExtensionSignatureComparer = new MemberSignatureComparer(
363+
considerName: false,
364+
considerExplicitlyImplementedInterfaces: false,
365+
considerReturnType: false,
366+
considerTypeConstraints: true,
367+
considerCallingConvention: false,
368+
refKindCompareMode: RefKindCompareMode.ConsiderDifferences,
369+
typeComparison: TypeCompareKind.ConsiderEverything,
370+
considerParameterNames: true,
371+
considerAttributes: true);
372+
357373
// Compare the "unqualified" part of the member name (no explicit part)
358374
private readonly bool _considerName;
359375

@@ -380,6 +396,11 @@ internal sealed class MemberSignatureComparer : IEqualityComparer<Symbol>
380396
// Equality options for parameter types and return types (if return is considered).
381397
private readonly TypeCompareKind _typeComparison;
382398

399+
// Compare parameter and type parameter names
400+
private readonly bool _considerParameterNames;
401+
402+
private readonly bool _considerAttributes;
403+
383404
private MemberSignatureComparer(
384405
bool considerName,
385406
bool considerExplicitlyImplementedInterfaces,
@@ -389,7 +410,9 @@ private MemberSignatureComparer(
389410
RefKindCompareMode refKindCompareMode,
390411
bool considerArity = true,
391412
bool considerDefaultValues = false,
392-
TypeCompareKind typeComparison = TypeCompareKind.IgnoreDynamic | TypeCompareKind.IgnoreNativeIntegers)
413+
TypeCompareKind typeComparison = TypeCompareKind.IgnoreDynamic | TypeCompareKind.IgnoreNativeIntegers,
414+
bool considerParameterNames = false,
415+
bool considerAttributes = false)
393416
{
394417
Debug.Assert(!considerExplicitlyImplementedInterfaces || considerName, "Doesn't make sense to consider interfaces separately from name.");
395418
Debug.Assert(!considerTypeConstraints || considerArity, "If you consider type constraints, you must also consider arity");
@@ -412,6 +435,9 @@ private MemberSignatureComparer(
412435
{
413436
_typeComparison |= TypeCompareKind.FunctionPointerRefMatchesOutInRefReadonly;
414437
}
438+
439+
_considerParameterNames = considerParameterNames;
440+
_considerAttributes = considerAttributes;
415441
}
416442

417443
#region IEqualityComparer<Symbol> Members
@@ -447,9 +473,28 @@ public bool Equals(Symbol? member1, Symbol? member2)
447473

448474
// NB: up to, and including, this check, we have not actually forced the (type) parameters
449475
// to be expanded - we're only using the counts.
450-
if (_considerArity && (member1.GetMemberArity() != member2.GetMemberArity()))
476+
if (_considerArity)
451477
{
452-
return false;
478+
if (member1.GetMemberArity() != member2.GetMemberArity())
479+
{
480+
return false;
481+
}
482+
483+
if (member1.GetMemberArity() > 0 && (_considerParameterNames || _considerAttributes))
484+
{
485+
ImmutableArray<TypeParameterSymbol> typeParams1 = member1.GetMemberTypeParameters();
486+
ImmutableArray<TypeParameterSymbol> typeParams2 = member2.GetMemberTypeParameters();
487+
488+
if (_considerParameterNames && typeParams1.Length > 0 && !haveSameTypeParameterNames(typeParams1, typeParams2))
489+
{
490+
return false;
491+
}
492+
493+
if (_considerAttributes && !haveSameTypeParameterAttributes(typeParams1, typeParams2))
494+
{
495+
return false;
496+
}
497+
}
453498
}
454499

455500
if (member1.GetParameterCount() != member2.GetParameterCount())
@@ -465,10 +510,25 @@ public bool Equals(Symbol? member1, Symbol? member2)
465510
return false;
466511
}
467512

468-
if (member1.GetParameterCount() > 0 && !HaveSameParameterTypes(member1.GetParameters().AsSpan(), typeMap1, member2.GetParameters().AsSpan(), typeMap2,
469-
_refKindCompareMode, considerDefaultValues: _considerDefaultValues, _typeComparison))
513+
if (member1.GetParameterCount() > 0)
470514
{
471-
return false;
515+
var params1 = member1.GetParameters();
516+
var params2 = member2.GetParameters();
517+
if (!HaveSameParameterTypes(params1.AsSpan(), typeMap1, params2.AsSpan(), typeMap2,
518+
_refKindCompareMode, considerDefaultValues: _considerDefaultValues, _typeComparison))
519+
{
520+
return false;
521+
}
522+
523+
if (_considerParameterNames && !haveSameParameterNames(params1, params2))
524+
{
525+
return false;
526+
}
527+
528+
if (_considerAttributes && !haveSameParameterAttributes(params1, params2))
529+
{
530+
return false;
531+
}
472532
}
473533

474534
if (_considerCallingConvention)
@@ -526,7 +586,63 @@ public bool Equals(Symbol? member1, Symbol? member2)
526586
}
527587
}
528588

529-
return !_considerTypeConstraints || HaveSameConstraints(member1, typeMap1, member2, typeMap2);
589+
return !_considerTypeConstraints || HaveSameConstraints(member1, typeMap1, member2, typeMap2, ignoreNullability: (_typeComparison & TypeCompareKind.IgnoreNullableModifiersForReferenceTypes) != 0);
590+
591+
static bool haveSameParameterNames(ImmutableArray<ParameterSymbol> params1, ImmutableArray<ParameterSymbol> params2)
592+
{
593+
return params1.SequenceEqual(params2, (p1, p2) => p1.Name == p2.Name);
594+
}
595+
596+
static bool haveSameTypeParameterNames(ImmutableArray<TypeParameterSymbol> typeParams1, ImmutableArray<TypeParameterSymbol> typeParams2)
597+
{
598+
return typeParams1.SequenceEqual(typeParams2, (p1, p2) => p1.Name == p2.Name);
599+
}
600+
601+
static bool haveSameParameterAttributes(ImmutableArray<ParameterSymbol> params1, ImmutableArray<ParameterSymbol> params2)
602+
{
603+
return params1.SequenceEqual(params2, (p1, p2) => hasSameAttribute(p1.GetAttributes(), p2.GetAttributes()));
604+
}
605+
606+
static bool haveSameTypeParameterAttributes(ImmutableArray<TypeParameterSymbol> typeParams1, ImmutableArray<TypeParameterSymbol> typeParams2)
607+
{
608+
return typeParams1.SequenceEqual(typeParams2, (p1, p2) => hasSameAttribute(p1.GetAttributes(), p2.GetAttributes()));
609+
}
610+
611+
static bool hasSameAttribute(ImmutableArray<CSharpAttributeData> attributes1, ImmutableArray<CSharpAttributeData> attributes2)
612+
{
613+
if (attributes1.Length != attributes2.Length)
614+
{
615+
return false;
616+
}
617+
618+
var comparer = CommonAttributeDataComparer.InstanceIgnoringNamedArgumentOrder;
619+
620+
if (attributes1 is [var single1] && attributes2 is [var single2])
621+
{
622+
// Fast path for single attribute case
623+
return comparer.Equals(single1, single2);
624+
}
625+
626+
// Tracked by https://github.com/dotnet/roslyn/issues/78827 : optimization, consider using a pool
627+
var counts = new Dictionary<CSharpAttributeData, int>(comparer);
628+
629+
foreach (var attribute in attributes1)
630+
{
631+
counts[attribute] = counts.TryGetValue(attribute, out var foundCount) ? foundCount + 1 : 1;
632+
}
633+
634+
foreach (var attribute in attributes2)
635+
{
636+
if (!counts.TryGetValue(attribute, out var foundCount) || foundCount == 0)
637+
{
638+
return false;
639+
}
640+
641+
counts[attribute] = foundCount - 1;
642+
}
643+
644+
return counts.Values.All(c => c == 0);
645+
}
530646
}
531647

532648
public int GetHashCode(Symbol? member)
@@ -623,7 +739,7 @@ public static bool HaveSameReturnTypes(Symbol member1, TypeMap? typeMap1, Symbol
623739
true);
624740
}
625741

626-
private static bool HaveSameConstraints(Symbol member1, TypeMap? typeMap1, Symbol member2, TypeMap? typeMap2)
742+
private static bool HaveSameConstraints(Symbol member1, TypeMap? typeMap1, Symbol member2, TypeMap? typeMap2, bool ignoreNullability = true)
627743
{
628744
Debug.Assert(member1.GetMemberArity() == member2.GetMemberArity());
629745

@@ -635,17 +751,17 @@ private static bool HaveSameConstraints(Symbol member1, TypeMap? typeMap1, Symbo
635751

636752
var typeParameters1 = member1.GetMemberTypeParameters();
637753
var typeParameters2 = member2.GetMemberTypeParameters();
638-
return HaveSameConstraints(typeParameters1, typeMap1, typeParameters2, typeMap2);
754+
return HaveSameConstraints(typeParameters1, typeMap1, typeParameters2, typeMap2, ignoreNullability);
639755
}
640756

641-
public static bool HaveSameConstraints(ImmutableArray<TypeParameterSymbol> typeParameters1, TypeMap? typeMap1, ImmutableArray<TypeParameterSymbol> typeParameters2, TypeMap? typeMap2)
757+
public static bool HaveSameConstraints(ImmutableArray<TypeParameterSymbol> typeParameters1, TypeMap? typeMap1, ImmutableArray<TypeParameterSymbol> typeParameters2, TypeMap? typeMap2, bool ignoreNullability = true)
642758
{
643759
Debug.Assert(typeParameters1.Length == typeParameters2.Length);
644760

645761
int arity = typeParameters1.Length;
646762
for (int i = 0; i < arity; i++)
647763
{
648-
if (!HaveSameConstraints(typeParameters1[i], typeMap1, typeParameters2[i], typeMap2))
764+
if (!HaveSameConstraints(typeParameters1[i], typeMap1, typeParameters2[i], typeMap2, ignoreNullability))
649765
{
650766
return false;
651767
}
@@ -654,10 +770,16 @@ public static bool HaveSameConstraints(ImmutableArray<TypeParameterSymbol> typeP
654770
return true;
655771
}
656772

657-
public static bool HaveSameConstraints(TypeParameterSymbol typeParameter1, TypeMap? typeMap1, TypeParameterSymbol typeParameter2, TypeMap? typeMap2)
773+
public static bool HaveSameConstraints(TypeParameterSymbol typeParameter1, TypeMap? typeMap1, TypeParameterSymbol typeParameter2, TypeMap? typeMap2, bool ignoreNullability = true)
658774
{
659775
// Spec 13.4.3: Implementation of generic methods.
660776

777+
if (!ignoreNullability &&
778+
typeParameter1.HasNotNullConstraint != typeParameter2.HasNotNullConstraint)
779+
{
780+
return false;
781+
}
782+
661783
if ((typeParameter1.HasConstructorConstraint != typeParameter2.HasConstructorConstraint) ||
662784
(typeParameter1.HasReferenceTypeConstraint != typeParameter2.HasReferenceTypeConstraint) ||
663785
(typeParameter1.HasValueTypeConstraint != typeParameter2.HasValueTypeConstraint) ||
@@ -903,7 +1025,7 @@ internal static bool ConsideringTupleNamesCreatesDifference(Symbol member1, Symb
9031025
internal enum RefKindCompareMode
9041026
{
9051027
/// <summary>
906-
/// Ref parameter modifiers are ignored.
1028+
/// All ref modifiers are considered equivalent.
9071029
/// </summary>
9081030
DoNotConsiderDifferences = 0,
9091031

src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ internal static ImmutableArray<ParameterSymbol> GetParameters(this Symbol member
4242
return ((PropertySymbol)member).Parameters;
4343
case SymbolKind.Event:
4444
return ImmutableArray<ParameterSymbol>.Empty;
45+
case SymbolKind.NamedType:
46+
var namedType = (NamedTypeSymbol)member;
47+
if (namedType.IsExtension)
48+
{
49+
return namedType.ExtensionParameter is not null
50+
? [namedType.ExtensionParameter]
51+
: [];
52+
}
53+
goto default;
4554
default:
4655
throw ExceptionUtilities.UnexpectedValue(member.Kind);
4756
}
@@ -341,6 +350,13 @@ internal static int GetParameterCount(this Symbol member)
341350
case SymbolKind.Event:
342351
case SymbolKind.Field:
343352
return 0;
353+
case SymbolKind.NamedType:
354+
var namedType = (NamedTypeSymbol)member;
355+
if (namedType.IsExtension)
356+
{
357+
return namedType.ExtensionParameter is not null ? 1 : 0;
358+
}
359+
goto default;
344360
default:
345361
throw ExceptionUtilities.UnexpectedValue(member.Kind);
346362
}

src/Compilers/CSharp/Portable/Symbols/Source/ExtensionGroupingInfo.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,33 @@ internal IEnumerable<ImmutableArray<SourceNamedTypeSymbol>> EnumerateMergedExten
184184
}
185185
}
186186

187+
/// <summary>
188+
/// Reports a diagnostic when two extension blocks grouped into a single marker type have different C#-level signatures.
189+
/// Note: It's fine for two extension blocks with different IL-level signatures to be grouped into the same grouping type, so we don't check for collisions there.
190+
/// </summary>
191+
internal void CheckSignatureCollisions(BindingDiagnosticBag diagnostics)
192+
{
193+
foreach (ImmutableArray<SourceNamedTypeSymbol> mergedBlocks in EnumerateMergedExtensionBlocks())
194+
{
195+
checkCollisions(mergedBlocks, diagnostics);
196+
}
197+
198+
return;
199+
200+
static void checkCollisions(ImmutableArray<SourceNamedTypeSymbol> mergedBlocks, BindingDiagnosticBag diagnostics)
201+
{
202+
SourceNamedTypeSymbol first = mergedBlocks[0];
203+
204+
for (int i = 1; i < mergedBlocks.Length; i++)
205+
{
206+
if (!MemberSignatureComparer.ExtensionSignatureComparer.Equals(first, (SourceNamedTypeSymbol?)mergedBlocks[i]))
207+
{
208+
diagnostics.Add(ErrorCode.ERR_ExtensionBlockCollision, mergedBlocks[i].Locations[0]);
209+
}
210+
}
211+
}
212+
}
213+
187214
private abstract class ExtensionGroupingOrMarkerType : Cci.INestedTypeDefinition
188215
{
189216
ushort ITypeDefinition.Alignment => 0;

src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbol.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,7 @@ private void PartialConstructorChecks(SourceConstructorSymbol implementation, Bi
287287
{
288288
diagnostics.Add(ErrorCode.ERR_PartialMemberInconsistentTupleNames, implementation.GetFirstLocation(), this, implementation);
289289
}
290-
else if (!MemberSignatureComparer.PartialMethodsStrictComparer.Equals(this, implementation)
291-
|| !Parameters.SequenceEqual(implementation.Parameters, static (a, b) => a.Name == b.Name))
290+
else if (!MemberSignatureComparer.PartialMethodsStrictComparer.Equals(this, implementation))
292291
{
293292
diagnostics.Add(ErrorCode.WRN_PartialMemberSignatureDifference, implementation.GetFirstLocation(),
294293
new FormattedSymbol(this, SymbolDisplayFormat.MinimallyQualifiedFormat),

src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2362,6 +2362,7 @@ private void CheckMemberNameConflictsAndUnmatchedOperators(BindingDiagnosticBag
23622362
if (this.declaration.ContainsExtensionDeclarations)
23632363
{
23642364
checkMemberNameConflictsInExtensions(diagnostics);
2365+
this.GetExtensionGroupingInfo().CheckSignatureCollisions(diagnostics);
23652366
}
23662367

23672368
checkMemberNameConflicts(GetMembersByName(), GetTypeMembersDictionary(), GetMembersUnordered(), diagnostics);
@@ -6000,7 +6001,6 @@ internal ExtensionGroupingInfo GetExtensionGroupingInfo()
60006001

60016002
if (_lazyExtensionGroupingInfo is null)
60026003
{
6003-
// PROTOTYPE: Find the right place and perform checks for conflicting declarations getting into the same group or marker, and reporting appropriate errors.
60046004
Interlocked.CompareExchange(ref _lazyExtensionGroupingInfo, new ExtensionGroupingInfo(this), null);
60056005
}
60066006

src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol_Extension.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -795,9 +795,12 @@ static void appendAttributes(ImmutableArray<CSharpAttributeData> attributes, Str
795795

796796
foreach (CSharpAttributeData attribute in attributes)
797797
{
798-
var stringBuilder = PooledStringBuilder.GetInstance();
799-
appendAttribute(attribute, stringBuilder.Builder);
800-
attributesBuilder.Add(stringBuilder.ToStringAndFree());
798+
if (!attribute.IsConditionallyOmitted)
799+
{
800+
var stringBuilder = PooledStringBuilder.GetInstance();
801+
appendAttribute(attribute, stringBuilder.Builder);
802+
attributesBuilder.Add(stringBuilder.ToStringAndFree());
803+
}
801804
}
802805

803806
attributesBuilder.Sort(StringComparer.Ordinal); // Actual order doesn't matter - just want to be deterministic

0 commit comments

Comments
 (0)