-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Extensions: check for collisions #79702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extensions: check for collisions #79702
Conversation
4e1d448 to
9e42d84
Compare
|
|
||
| /// <summary> | ||
| /// Reports a diagnostic when two extension blocks grouped into a single marker type have different C#-level signatures. | ||
| /// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 This is illustrated by #ClosedGrouping_30
2bd8055 to
ea0615f
Compare
ea0615f to
7f543e0
Compare
Are we certain this step doesn't need to pay attention to Refers to: src/Compilers/Core/Portable/Symbols/Attributes/CommonAttributeDataComparer.cs:52 in 7f543e0. [](commit_id = 7f543e0, deletion_comment = False) |
| attr1.IsConditionallyOmitted == attr2.IsConditionallyOmitted && | ||
| attr1.CommonConstructorArguments.SequenceEqual(attr2.CommonConstructorArguments) && | ||
| attr1.NamedArguments.SequenceEqual(attr2.NamedArguments); | ||
| (_considerNamedArgumentsOrder ? attr1.NamedArguments.SequenceEqual(attr2.NamedArguments) : attr1.NamedArguments.SetEquals(attr2.NamedArguments)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They were compared with Default/AllNullableIgnoreOptions. I updated this to use AllIgnore and showed the impact on assembly attributes with tuples in a test.
I considered filing an issue for the order of comparison. It seems strange that we would be order-dependent for this. But I don't think we'd get to fixing this (low-pri)
| <value>Interpolated string handler arguments are not allowed in this context.</value> | ||
| </data> | ||
| <data name="ERR_ExtensionBlockCollision" xml:space="preserve"> | ||
| <value>This extension block collides with another extension block. They have different signatures, but result in the same marker type identifier.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| <value>Interpolated string handler arguments are not allowed in this context.</value> | ||
| </data> | ||
| <data name="ERR_ExtensionBlockCollision" xml:space="preserve"> | ||
| <value>This extension block collides with another extension block. They have different signatures, but result in the same marker type identifier.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ImmutableArray<TypeParameterSymbol> typeParams1 = member1.GetMemberTypeParameters(); | ||
| ImmutableArray<TypeParameterSymbol> typeParams2 = member2.GetMemberTypeParameters(); | ||
|
|
||
| if (_considerParameterNames && typeParams1.Length > 0 && !haveSameTypeParameterNames(typeParams1, typeParams2)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thread is marked as resolved, but I do not see any response to the question. It was about typeParams1.Length > 0 condition, BTW.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You commented on commit 3, but this was fixed later on
| bool considerDefaultValues = false, | ||
| TypeCompareKind typeComparison = TypeCompareKind.IgnoreDynamic | TypeCompareKind.IgnoreNativeIntegers) | ||
| TypeCompareKind typeComparison = TypeCompareKind.IgnoreDynamic | TypeCompareKind.IgnoreNativeIntegers, | ||
| bool considerParameterNames = false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider renaming
| } | ||
|
|
||
| return !_considerTypeConstraints || HaveSameConstraints(member1, typeMap1, member2, typeMap2); | ||
| return !_considerTypeConstraints || HaveSameConstraints(member1, typeMap1, member2, typeMap2, ignoreNullability: (_typeComparison & TypeCompareKind.IgnoreNullableModifiersForReferenceTypes) != 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of the existing comparers used considerTypeConstraints (they all use false)
| return false; | ||
| } | ||
|
|
||
| var comparer = CommonAttributeDataComparer.InstanceIgnoringNamedArgumentOrder; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| } | ||
|
|
||
| private static bool HaveSameConstraints(Symbol member1, TypeMap? typeMap1, Symbol member2, TypeMap? typeMap2) | ||
| private static bool HaveSameConstraints(Symbol member1, TypeMap? typeMap1, Symbol member2, TypeMap? typeMap2, bool ignoreNullability = true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this value be adjusted? #Closed Refers to: src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs:792 in 7f543e0. [](commit_id = 7f543e0, deletion_comment = False) |
|
Are grouping collisions going to be handled in a separate PR? #Closed |
| return ((PropertySymbol)member).Parameters; | ||
| case SymbolKind.Event: | ||
| return ImmutableArray<ParameterSymbol>.Empty; | ||
| case SymbolKind.NamedType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like change is done for the benefit of one call site, that can conditionally special case extensions (based on explicit request). It is, however, not obvious that all other call sites would want this behavior. I suggest leaving this general helper unchanged. #Closed
| case SymbolKind.Event: | ||
| case SymbolKind.Field: | ||
| return 0; | ||
| case SymbolKind.NamedType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| /// <summary> | ||
| /// Reports a diagnostic when two extension blocks grouped into a single marker type have different C#-level signatures. | ||
| /// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| var stringBuilder = PooledStringBuilder.GetInstance(); | ||
| appendAttribute(attribute, stringBuilder.Builder); | ||
| attributesBuilder.Add(stringBuilder.ToStringAndFree()); | ||
| if (!attribute.IsConditionallyOmitted) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is considered in CommonAttributeDataComparer.Equals (that's what brought this issue to my attention). Grouping_39 illustrates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is considered in
CommonAttributeDataComparer.Equals
It looks like it is considered as part of equality of the attribute. But for our purposes, it feels like conditionally omitted attributes should be out of collision check. Since they are not going to be emitted, no real difference that can come from them, or we should not be ignoring them here. I am leaning towards ignoring though.
|
Done with review pass (commit 1), tests are not reviewed #Closed |
| if (parameter1 is null || parameter2 is null) | ||
| { | ||
| return parameter1 is null && parameter2 is null; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, think this would be more clearly expressed as:
| if (parameter1 is null || parameter2 is null) | |
| { | |
| return parameter1 is null && parameter2 is null; | |
| } | |
| if (parameter1 is null) | |
| { | |
| return parameter2 is null; | |
| } | |
| else if (parameter2 is null) | |
| { | |
| return false; | |
| } | |
| ``` #Resolved |
| return parameter1 is null && parameter2 is null; | ||
| } | ||
|
|
||
| if (!MemberSignatureComparer.HaveSameParameterType(parameter1, typeMap1, parameter2, typeMap2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we try and order this after the cheap checks, like name/declared scope? #Resolved
| <value>Interpolated string handler arguments are not allowed in this context.</value> | ||
| </data> | ||
| <data name="ERR_ExtensionBlockCollision" xml:space="preserve"> | ||
| <value>This extension block collides with another extension block. They result in conflicting content-based type names in metadata.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have an immediate suggestion for the wording, but is there something we can tell users to try in this case? #Resolved
| [Fact] | ||
| public void Grouping_09() | ||
| { | ||
| // attribute on parameter vs. different attribute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // attribute on parameter vs. different attribute | |
| // attribute on parameter vs. different parameter value | |
| ``` #Resolved |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider testing object vs dynamic #Resolved
| var typeMap2 = new TypeMap(typeParameters2, indexedTypeParameters, allowAlpha: true); | ||
|
|
||
| // Report any mismatched method constraints. | ||
| var compareKind = TypeCompareKind.IgnoreDynamicAndTupleNames | TypeCompareKind.IgnoreNullableModifiersForReferenceTypes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| var containingTypeMap = new TypeMap(containingTypeParameters, IndexedTypeParameterSymbol.Take(n), allowAlpha: false); | ||
| var nestedTypeMap = new TypeMap(nestedTypeParameters, IndexedTypeParameterSymbol.Take(nestedTypeParameters.Length), allowAlpha: false); | ||
|
|
||
| var compareKind = TypeCompareKind.IgnoreDynamicAndTupleNames | TypeCompareKind.IgnoreNullableModifiersForReferenceTypes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| TypeMap? typeMap1 = MemberSignatureComparer.GetTypeMap(extension1); | ||
| TypeMap? typeMap2 = MemberSignatureComparer.GetTypeMap(extension2); | ||
| if (extension1.Arity != extension2.Arity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| TypeMap? typeMap1 = MemberSignatureComparer.GetTypeMap(extension1); | ||
| TypeMap? typeMap2 = MemberSignatureComparer.GetTypeMap(extension2); | ||
| if (extension1.Arity != extension2.Arity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| { | ||
| var stringBuilder = PooledStringBuilder.GetInstance(); | ||
| appendTypeWithAnnotation(contraintTypes[i], stringBuilder.Builder); | ||
| appendType(constraintTypes[i].Type, stringBuilder.Builder); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we don't care about top-level nullability annotations on each type constraint.
We don't emit one annotation per type constraint, we only emit one for the whole type parameter.
Similarly MemberSignatureComparer.HaveSameConstraints performs a single top-level nullability check per type parameter.
And that's why I changed the raw marker name to use "notnull", "maybenull" (invented keyword) or nothing (for oblivious).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we don't care about top-level nullability annotations on each type constraint.
I am not sure about that. For example, given the following C# code:
class C<T> where T : C1?, I1, I2? {}
class C1{}
interface I1{}
interface I2{}
The following metadata are generated:
.class private auto ansi beforefieldinit C`1<(C1, I1, I2) T>
extends [mscorlib]System.Object
{
.param constraint T, C1
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
01 00 02 00 00
)
.param constraint T, I1
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
01 00 01 00 00
)
.param constraint T, I2
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
01 00 02 00 00
)
It looks like top level nullability of type constraints affects metadata, therefore, we are supposed to care about it.
We also should care about notnull constraint, but that is a different thing and it has its own impact on metadata.
That said, I think a change to this line should be reverted unless you can demonstrate that the old behavior leads to an incorrect result.
| var typeMap2 = new TypeMap(typeParameters2, indexedTypeParameters, allowAlpha: true); | ||
|
|
||
| // Report any mismatched method constraints. | ||
| var typeComparison = TypeCompareKind.IgnoreDynamicAndTupleNames | TypeCompareKind.IgnoreNullableModifiersForReferenceTypes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| var desiredTypeMap = new TypeMap(desiredTypeParameters, indexedTypeParameters, allowAlpha: true); | ||
|
|
||
| return MemberSignatureComparer.HaveSameConstraints(candidateTypeParameters, candidateTypeMap, desiredTypeParameters, desiredTypeMap); | ||
| var typeComparison = TypeCompareKind.IgnoreDynamicAndTupleNames | TypeCompareKind.IgnoreNullableModifiersForReferenceTypes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
Done with review pass (commit 10), tests are not reviewed #Closed |
| { | ||
| if (x.Kind == TypedConstantKind.Type && y.Kind == TypedConstantKind.Type) | ||
| { | ||
| return x.ValueInternal is ISymbolInternal xType && y.ValueInternal is ISymbolInternal yType && xType.Equals(yType, TypeCompareKind.AllIgnoreOptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looked at TypeNameDecoder (the decoding counterpart to TypeNameSerializer) and didn't see any handle for modifiers. Thanks
| } | ||
|
|
||
| public static bool HaveSameNullabilityInConstraints(TypeParameterSymbol typeParameter1, TypeMap typeMap1, TypeParameterSymbol typeParameter2, TypeMap typeMap2) | ||
| public static bool HaveSameNullabilityInConstraints(TypeParameterSymbol typeParameter1, TypeMap? typeMap1, TypeParameterSymbol typeParameter2, TypeMap? typeMap2, TypeCompareKind typeComparison) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this method more, I realized that it is probably not appropriate to let user control the comparison type used by this method because the supplied value might not be suitable for the task that the method is supposed to perform. I suggest reverting changes to this method and not using it for the purpose of this PR. Instead the top level nullability comparison can be done explicitly in HaveSameCSharpSignature helper. It is already doing the other checks by calling HaveSameConstraints. #Closed
|
Done with review pass (commit 11), tests are not reviewed #Closed |
AlekseyTs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (commit 13). I didn't do a thorough review of tests, relying on Fred's sign-off in this regard.
| TypeMap? typeMap1 = MemberSignatureComparer.GetTypeMap(extension1); | ||
| TypeMap? typeMap2 = MemberSignatureComparer.GetTypeMap(extension2); | ||
| if (extension1.Arity > 0 | ||
| && !MemberSignatureComparer.HaveSameConstraints(extension1.TypeParameters, typeMap1, extension2.TypeParameters, typeMap2, TypeCompareKind.AllIgnoreOptions)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| if (!MemberSignatureComparer.HaveSameParameterType(parameter1, typeMap1, parameter2, typeMap2, | ||
| refKindCompareMode: MemberSignatureComparer.RefKindCompareMode.IgnoreRefKind, | ||
| considerDefaultValues: false, TypeCompareKind.AllIgnoreOptions)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overview:
For extension blocks, we produce grouping and marker type names. They have content-based hashes.
There are two kinds of possible collisions:
This PR implements a symbol-level check that ensures that we only allow extension blocks with identical IL-level signatures into a grouping type, and those with identical C#-level signatures into a marker type.
It is useful to look at
ComputeExtensionGroupingRawName()andComputeExtensionMarkerRawName()to see what information counts in IL-level vs. C#-level signature.The only quirk (by design, but keeps surprising me) is that we decided that refkind doesn't count as part of IL-level signature.
Addresses one of the metadata follow-ups in #78963
Relates to test plan #76130