Skip to content

Commit 7f543e0

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

23 files changed

+1562
-25
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 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: 134 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,21 @@ internal sealed class MemberSignatureComparer : IEqualityComparer<Symbol>
354354
considerDefaultValues: true,
355355
typeComparison: TypeCompareKind.AllIgnoreOptions);
356356

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

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

398+
// Compare parameter and type parameter names
399+
private readonly bool _considerParameterNames;
400+
401+
private readonly bool _considerAttributes;
402+
383403
private MemberSignatureComparer(
384404
bool considerName,
385405
bool considerExplicitlyImplementedInterfaces,
@@ -389,7 +409,9 @@ private MemberSignatureComparer(
389409
RefKindCompareMode refKindCompareMode,
390410
bool considerArity = true,
391411
bool considerDefaultValues = false,
392-
TypeCompareKind typeComparison = TypeCompareKind.IgnoreDynamic | TypeCompareKind.IgnoreNativeIntegers)
412+
TypeCompareKind typeComparison = TypeCompareKind.IgnoreDynamic | TypeCompareKind.IgnoreNativeIntegers,
413+
bool considerParameterNames = false,
414+
bool considerAttributes = false)
393415
{
394416
Debug.Assert(!considerExplicitlyImplementedInterfaces || considerName, "Doesn't make sense to consider interfaces separately from name.");
395417
Debug.Assert(!considerTypeConstraints || considerArity, "If you consider type constraints, you must also consider arity");
@@ -412,6 +434,9 @@ private MemberSignatureComparer(
412434
{
413435
_typeComparison |= TypeCompareKind.FunctionPointerRefMatchesOutInRefReadonly;
414436
}
437+
438+
_considerParameterNames = considerParameterNames;
439+
_considerAttributes = considerAttributes;
415440
}
416441

417442
#region IEqualityComparer<Symbol> Members
@@ -447,9 +472,28 @@ public bool Equals(Symbol? member1, Symbol? member2)
447472

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

455499
if (member1.GetParameterCount() != member2.GetParameterCount())
@@ -465,10 +509,25 @@ public bool Equals(Symbol? member1, Symbol? member2)
465509
return false;
466510
}
467511

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

474533
if (_considerCallingConvention)
@@ -526,7 +585,63 @@ public bool Equals(Symbol? member1, Symbol? member2)
526585
}
527586
}
528587

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

532647
public int GetHashCode(Symbol? member)
@@ -623,7 +738,7 @@ public static bool HaveSameReturnTypes(Symbol member1, TypeMap? typeMap1, Symbol
623738
true);
624739
}
625740

626-
private static bool HaveSameConstraints(Symbol member1, TypeMap? typeMap1, Symbol member2, TypeMap? typeMap2)
741+
private static bool HaveSameConstraints(Symbol member1, TypeMap? typeMap1, Symbol member2, TypeMap? typeMap2, bool ignoreNullability = true)
627742
{
628743
Debug.Assert(member1.GetMemberArity() == member2.GetMemberArity());
629744

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

636751
var typeParameters1 = member1.GetMemberTypeParameters();
637752
var typeParameters2 = member2.GetMemberTypeParameters();
638-
return HaveSameConstraints(typeParameters1, typeMap1, typeParameters2, typeMap2);
753+
return HaveSameConstraints(typeParameters1, typeMap1, typeParameters2, typeMap2, ignoreNullability);
639754
}
640755

641-
public static bool HaveSameConstraints(ImmutableArray<TypeParameterSymbol> typeParameters1, TypeMap? typeMap1, ImmutableArray<TypeParameterSymbol> typeParameters2, TypeMap? typeMap2)
756+
public static bool HaveSameConstraints(ImmutableArray<TypeParameterSymbol> typeParameters1, TypeMap? typeMap1, ImmutableArray<TypeParameterSymbol> typeParameters2, TypeMap? typeMap2, bool ignoreNullability = true)
642757
{
643758
Debug.Assert(typeParameters1.Length == typeParameters2.Length);
644759

645760
int arity = typeParameters1.Length;
646761
for (int i = 0; i < arity; i++)
647762
{
648-
if (!HaveSameConstraints(typeParameters1[i], typeMap1, typeParameters2[i], typeMap2))
763+
if (!HaveSameConstraints(typeParameters1[i], typeMap1, typeParameters2[i], typeMap2, ignoreNullability))
649764
{
650765
return false;
651766
}
@@ -654,10 +769,16 @@ public static bool HaveSameConstraints(ImmutableArray<TypeParameterSymbol> typeP
654769
return true;
655770
}
656771

657-
public static bool HaveSameConstraints(TypeParameterSymbol typeParameter1, TypeMap? typeMap1, TypeParameterSymbol typeParameter2, TypeMap? typeMap2)
772+
public static bool HaveSameConstraints(TypeParameterSymbol typeParameter1, TypeMap? typeMap1, TypeParameterSymbol typeParameter2, TypeMap? typeMap2, bool ignoreNullability = true)
658773
{
659774
// Spec 13.4.3: Implementation of generic methods.
660775

776+
if (!ignoreNullability &&
777+
typeParameter1.HasNotNullConstraint != typeParameter2.HasNotNullConstraint)
778+
{
779+
return false;
780+
}
781+
661782
if ((typeParameter1.HasConstructorConstraint != typeParameter2.HasConstructorConstraint) ||
662783
(typeParameter1.HasReferenceTypeConstraint != typeParameter2.HasReferenceTypeConstraint) ||
663784
(typeParameter1.HasValueTypeConstraint != typeParameter2.HasValueTypeConstraint) ||
@@ -903,7 +1024,7 @@ internal static bool ConsideringTupleNamesCreatesDifference(Symbol member1, Symb
9031024
internal enum RefKindCompareMode
9041025
{
9051026
/// <summary>
906-
/// Ref parameter modifiers are ignored.
1027+
/// All ref modifiers are considered equivalent.
9071028
/// </summary>
9081029
DoNotConsiderDifferences = 0,
9091030

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/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

src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf

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

src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf

Lines changed: 5 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)