Generate readonlyattribute if it does not exist#18715
Generate readonlyattribute if it does not exist#18715OmarTawfik merged 23 commits intodotnet:features/readonly-reffrom OmarTawfik:generate-readonlyattribute-if-it-does-not-exist
Conversation
| /// Custom attributes associated with the method's return value. | ||
| /// </summary> | ||
| IEnumerable<ICustomAttribute> ReturnValueAttributes { get; } | ||
| IEnumerable<ICustomAttribute> GetReturnValueAttributes(CommonPEModuleBuilder moduleBuilder); |
There was a problem hiding this comment.
Consider passing an EmitContext argument instead, for consistency with other Cci interface methods. #Resolved
| internal override ImmutableArray<NamedTypeSymbol> GetAdditionalTopLevelTypes(DiagnosticBag diagnostics) | ||
| { | ||
| return _additionalTypes; | ||
| Interlocked.CompareExchange(ref _embeddedAttributesConstructors, CreateEmbeddedAttributesIfNeeded(diagnostics), null); |
There was a problem hiding this comment.
Interlocked.CompareExchange [](start = 12, length = 27)
GetAdditionalTopLevelTypes should be called once (or at most once per emit) so Interlocked.CompareExchange shouldn't be necessary. #Resolved
There was a problem hiding this comment.
Yes. the identity of the dictionary is unimportant. Even if we assign optimistically on multiple threads a dictionary with same contents. interlocked would not be needed. Simple assignment is sufficient.
In reply to: 113524192 [](ancestors = 113524192)
|
|
||
| internal virtual SynthesizedAttributeData SynthesizeEmbeddedAttribute() | ||
| { | ||
| // For modules, this attribute should be present. Only assemblies generate and embedd this type. |
There was a problem hiding this comment.
Typo here and method below: embed #Resolved
| private NamedTypeSymbol _baseType; | ||
| private MissingNamespaceSymbol _namespace; | ||
| private ImmutableArray<Symbol> _members; | ||
| private ModuleSymbol _module; |
There was a problem hiding this comment.
readonly for all fields. #Resolved
| _module = compilation.SourceModule; | ||
|
|
||
| _namespace = new MissingNamespaceSymbol(compilation.GlobalNamespace, nameParts[0]); | ||
| for (int i = 1; i + 1 < nameParts.Length; i++) |
There was a problem hiding this comment.
_namespace = compilation.GlobalNamespace;
for (int i = 0; ...
``` #Resolved
|
|
||
| System_Runtime_CompilerServices_ReadOnlyAttribute, | ||
| System_Runtime_CompilerServices_IsReadOnlyAttribute, | ||
| Microsoft_CodeAnalysis_EmbeddedAttribute, |
There was a problem hiding this comment.
Microsoft_CodeAnalysis_EmbeddedAttribute [](start = 8, length = 40)
I am just started looking at this change, but the fact that we added this type to the well-known types list feels very surprising to me. Well-known types are not used to identify attribute applications, and, when we are embedding types we are using the types that we embed, which aren't well-known (they are hidden from everyone, but the compiler). #Closed
| public SourceEmbeddedAttributeSymbol(WellKnownType wellKnownType, CSharpCompilation compilation) | ||
| { | ||
| var nameParts = WellKnownTypes.GetMetadataName(wellKnownType).Split('.'); | ||
| Debug.Assert(nameParts.Length > 1, "Attribute cannot be in global namespace?"); |
There was a problem hiding this comment.
Is the assert necessary? #ByDesign
There was a problem hiding this comment.
I'd say yes, since the code below operates on that fact.
In reply to: 113527748 [](ancestors = 113527748)
| /// Custom attributes associated with the method's return value. | ||
| /// </summary> | ||
| IEnumerable<ICustomAttribute> ReturnValueAttributes { get; } | ||
| IEnumerable<ICustomAttribute> GetReturnValueAttributes(CommonPEModuleBuilder moduleBuilder); |
There was a problem hiding this comment.
IEnumerable GetReturnValueAttributes(CommonPEModuleBuilder moduleBuilder); [](start = 8, length = 92)
Consider passing an
EmitContextargument instead, for consistency with otherCciinterface methods.
I agree with this suggestion, contracts in interfaces should use EmitContext. #Closed
| <value>Do not use 'System.Runtime.CompilerServices.IsReadOnlyAttribute'. This is reserved for compiler usage.</value> | ||
| </data> | ||
| <data name="ERR_TypeReserved" xml:space="preserve"> | ||
| <value>The type '{0}' is reserved to be used by the compiler.</value> |
There was a problem hiding this comment.
type [](start = 15, length = 4)
"type name" vs. "type" ? #Closed
|
|
||
| if (WrongArity(symbol, arity, diagnose, options, out diagInfo)) | ||
| // Check for external symbols marked with 'Microsoft.CodeAnalysis.Embedded' attribute | ||
| if (!this.Compilation.SourceModule.Equals(unwrappedSymbol.ContainingModule) && unwrappedSymbol.IsHiddenByCodeAnalysisEmbeddedAttribute()) |
There was a problem hiding this comment.
!this.Compilation.SourceModule.Equals(unwrappedSymbol.ContainingModule) [](start = 16, length = 71)
Do we have tests covering significance of this condition? #Closed
There was a problem hiding this comment.
Curious - why "external" part makes a difference? I thought the embedded symbols are not bindable regardless of where defined.
In reply to: 113533730 [](ancestors = 113533730)
There was a problem hiding this comment.
We have tests that cover one condition. Adding more tests now.
edit: @VSadov I'm correcting this now. Lookup would fail if on embedded attributes defined somewhere else. If you define your own embedded attribute we would ignore it. UNLESS the compiler needs to generate one, then we will produce an error.
In reply to: 113549567 [](ancestors = 113549567,113533730)
There was a problem hiding this comment.
Could you point me to a test that demonstrates significance of the first condition?
In reply to: 114024707 [](ancestors = 114024707,113549567,113533730)
There was a problem hiding this comment.
Please check AttributeTests_Embedded.cs:
ReferencingEmbeddedAttributesFromADifferentAssemblyFails_Internal
ReferencingEmbeddedAttributesFromADifferentAssemblyFails_Public
and
EmbeddedAttributeInSourceShouldTriggerAnErrorIfCompilerNeedsToGenerateOne
In reply to: 115050349 [](ancestors = 115050349,114024707,113549567,113533730)
| /// </summary> | ||
| private Symbol[] _lazyWellKnownTypeMembers; | ||
|
|
||
| internal bool NeedsGeneratedIsReadOnlyAttribute { get; private set; } |
There was a problem hiding this comment.
internal bool NeedsGeneratedIsReadOnlyAttribute { get; private set; } [](start = 8, length = 69)
It would be good to have a comment when one should expect this property to have actual value. We should probably have a way to "freeze" the value and a way to assert an attempt to set the value it after it was frozen and an attempt to read the value before it is frozen. #Closed
| member: WellKnownMember.System_Runtime_CompilerServices_IsReadOnlyAttribute__ctor, | ||
| diagnostics: DeclarationDiagnostics, | ||
| location: readOnlySymbol.Locations.FirstOrDefault() ?? Location.None, | ||
| isOptional: Options.OutputKind != OutputKind.NetModule); |
There was a problem hiding this comment.
isOptional: Options.OutputKind != OutputKind.NetModule [](start = 16, length = 54)
It feels like this doesn't give us the behavior we want. I think we want to embed the attribute only if the type cannot be found, the lack of the required constructor should result in an error rather than in an embedded attribute. #Closed
There was a problem hiding this comment.
If the attribute defined in current module and does not have right constructor, embedding another attribute would cause a conflict. Is that the reason for giving an error?
In reply to: 113537098 [](ancestors = 113537098)
There was a problem hiding this comment.
Good point. I think the lookup should be modified to give a different error if the type was found but it was lacking the correct signature
In reply to: 113554955 [](ancestors = 113554955,113537098)
There was a problem hiding this comment.
Good point. I think the lookup should be modified to give a different error if the type was found but it was lacking the correct signature
I think the existing error for a missing well-known member will work just fine.
In reply to: 114047541 [](ancestors = 114047541,113554955,113537098)
| var attributeSymbol = Binder.GetWellKnownTypeMember( | ||
| compilation: this, | ||
| member: WellKnownMember.System_Runtime_CompilerServices_IsReadOnlyAttribute__ctor, | ||
| diagnostics: DeclarationDiagnostics, |
There was a problem hiding this comment.
DeclarationDiagnostics [](start = 29, length = 22)
Rather than adding diagnostics to DeclarationDiagnostics directly, we probably should use Symbol.AddDeclarationDiagnostics API instead as some symbols customize where declaration diagnostics should go. #Closed
There was a problem hiding this comment.
Moving to a model where diagnosticsbag is defined by the syntax needing it whenever possible.
In reply to: 113538171 [](ancestors = 113538171)
| } | ||
|
|
||
| internal SynthesizedAttributeData SynthesizeReadOnlyAttribute() | ||
| internal void EnsureIsReadOnlyAttributeExists(Symbol readOnlySymbol) |
There was a problem hiding this comment.
EnsureIsReadOnlyAttributeExists [](start = 22, length = 31)
Please ensure we have tests for local functions. #Closed
There was a problem hiding this comment.
I've a test for the positive case. Will add a test for the negative case as well.
In reply to: 113539714 [](ancestors = 113539714)
| /// <summary> | ||
| /// Lazily initiated HasEmbeddedAttribute | ||
| /// </summary> | ||
| private ThreeState _lazyHasEmbeddedAttribute = ThreeState.Unknown; |
There was a problem hiding this comment.
Should this be part of UncommonProperties rather than a field directly on PENamedTypeSymbol? #Closed
There was a problem hiding this comment.
There was a problem hiding this comment.
There was a problem hiding this comment.
UncommonProperties are only instantiated for PENamedTypeSymbols with uncommon attributes. The instances of UncommonProperties are not shared. #Resolved
There was a problem hiding this comment.
There was a problem hiding this comment.
Is there still a case where symbol can be null? If not, remove the null check and perhaps return symbol rather than using a ref parameter.
There was a problem hiding this comment.
This is possibly going to be rewritten in Vlad's next PR, as there are more attributes to be generated. His feature will possibly need to generate some attributes that are generated in this PR, so the check will be necessary.
I see the future of embedded attributes going in one of two directions:
- keeping the current design, having each feature calling
CreateEmbeddedAttributeIfNeededon the attributes it needs (possibly overlapping), and thus the current design protects against that. - If the features that need embedded attributes grow in numbers, there might be a need for an independent type manager to hold/manage them.
There was a problem hiding this comment.
For now we are adding only IsByRefLike, so there is no need for #2 just yet.
The current code seems ok to me for now.
There was a problem hiding this comment.
Minor point: factory.Block() has overload with params array so these could be inlined below. #Resolved
There was a problem hiding this comment.
Consider reporting the fully qualified type name so it's clear we're not reserving the type name regardless of namespace. #Resolved
There was a problem hiding this comment.
No. because the compiler is trying to synthesize two attributes here (Embedded and IsReadOnly).
There was a problem hiding this comment.
No. because the compiler is trying to synthesize two attributes here (Embedded and IsReadOnly).
|
LGTM |
|
@cston @AlekseyTs addressed the latest set of comments. |
AlekseyTs
left a comment
There was a problem hiding this comment.
Please wait for sign-off from me.
| builder.AddRange(_additionalTypes); | ||
|
|
||
| CreateEmbeddedAttributesIfNeeded(); | ||
| diagnostics.AddRange(_lazyEmbeddedAttributesDiagnostics); |
There was a problem hiding this comment.
diagnostics.AddRange(_lazyEmbeddedAttributesDiagnostics); [](start = 12, length = 57)
This is likely to cause duplication of errors. We should avoid that, we specifically avoid reporting duplicate errors in PEModuleBuilder, see _reportedErrorTypesMap for example. Please revert to the previous approach.
| { | ||
| private int value; | ||
|
|
||
| public override ref readonly int Method(ref readonly int x) => ref value; |
There was a problem hiding this comment.
public override ref readonly int Method(ref readonly int x) => ref value; [](start = 3, length = 74)
The override scenarios should be tested inside IsReadOnlyAttribute type. This test could be valuable too, but it doesn't test most interesting scenario.
There was a problem hiding this comment.
Similar for explicit interface implementations. In these tests, test both scenarios when base type/interface is defined in the same assembly and when it is defined in a different assembly.
| } | ||
|
|
||
| [Fact] | ||
| public void RefReadOnlyDefinitionsInsideUserDefinedIsReadOnlyAttribute_Class_CorrectParent() |
There was a problem hiding this comment.
RefReadOnlyDefinitionsInsideUserDefinedIsReadOnlyAttribute_Class_CorrectParent [](start = 20, length = 78)
Added tests for indexers, operators, properties, and overrides. There is already a test that verifies we produce errors if IsReadOnly was an interface, because it is lacking the constructor.
I don't see any tests added for explicit interface implementations. I believe I asked for them in the previous review pass.
|
Done with review pass. I believe the remaining issues that I track are summarized in the last three comments above. At the very least the requested tests should be added before the PR can be merged, I think. |
|
@AlekseyTs added the tests requested in your last comment in 40748ee About your other comment:
This contradicts with the point @cston pointed out. Basically, if I reverted, first call to |
Yes, I believe we should revert the behavior. There is no requirement for this API to produce the same errors on every call. In fact, I believe it is preferred that this API wouldn't produce errors due to the same reasons on the second and subsequent calls. |
| _explicitInterfaceType.CheckAllConstraints(conversions, new SourceLocation(explicitInterfaceSpecifier.Name), diagnostics); | ||
| } | ||
|
|
||
| if (_refKind == RefKind.RefReadOnly) |
There was a problem hiding this comment.
if (_refKind == RefKind.RefReadOnly) [](start = 12, length = 36)
It feels like we should be moving similar checks for all source symbols into an override of AfterAddingTypeMembersChecks API. This should ensure that the checks won't cause circularity issues in the future due to some changes in the compiler's behavior.
There was a problem hiding this comment.
I don't think the general rule applies, because for some symbols like SourceConstructorMethod we need the parameters to be computed first before we perform that check, and calling Parameters in AfterAddingTypeMembersChecks would call MethodChecks in turn.
For SourcePropertySymbol, it wasn't necessary since it computes parameters without calling MethodChecks. Some symbols I believe don't even use this API like LocalFunctionSymbol.
|
|
||
| if (_refKind == RefKind.RefReadOnly) | ||
| { | ||
| DeclaringCompilation.EnsureIsReadOnlyAttributeExists(diagnostics, _location, modifyCompilationForRefReadOnly: true); |
There was a problem hiding this comment.
_location [](start = 82, length = 9)
I didn't notice any location changes for errors produced by this call. Is location not changed in iteration 18, or do we have a test gap?
There was a problem hiding this comment.
We have tests covering these diagnostics, like IsReadOnlyAttributeExistsWithWrongConstructorSignature_Assembly. However, Covering every permutation for every symbol type and error type and location (parameter vs return type) I believe should be left to the final test plan (lot's of moving pieces right now in this feature).
However, I've added 5 more tests for symbols for that specific error location for this PR.
|
@AlekseyTs I reverted the change and added a few more tests. Also answered the last comment about |
|
@AlekseyTs moved calls of |
AlekseyTs
left a comment
There was a problem hiding this comment.
LGTM, modulo the failing tests and merge conflicts.
Things done in this PR:
GetAttributes()andGetReturnTypeAttributes()of various helpers through out the code base to accept a parameter of typePEModuleBuilderinstead ofCompilationState.IsReadOnlyattribute on the compilation object.PEAssemblyBuilderuses that fact and either generates that type during emitting, or gives an error if the user also defines that type in the same compilation.Microsoft.CodeAnalysis.EmbeddedAttributethat both C# and VB compilers recognize and ignore types annotated by it during look up. All types embedded will have both this attribute andCompilerGeneratedAttribute.cc @dotnet/roslyn-compiler @VSadov @AlekseyTs