diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/FieldSymbolWithAttributesAndModifiers.cs b/src/Compilers/CSharp/Portable/Symbols/Source/FieldSymbolWithAttributesAndModifiers.cs index 67d199cf3ff56..bfec737594640 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/FieldSymbolWithAttributesAndModifiers.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/FieldSymbolWithAttributesAndModifiers.cs @@ -26,7 +26,7 @@ internal abstract class FieldSymbolWithAttributesAndModifiers : FieldSymbol, IAt /// /// Gets the syntax list of custom attributes applied on the symbol. /// - protected abstract SyntaxList AttributeDeclarationSyntaxList { get; } + protected abstract OneOrMany> GetAttributeDeclarations(); protected abstract IAttributeTargetSymbol AttributeOwner { get; } @@ -86,7 +86,7 @@ private CustomAttributesBag GetAttributesBag() return bag; } - if (LoadAndValidateAttributes(OneOrMany.Create(this.AttributeDeclarationSyntaxList), ref _lazyCustomAttributesBag)) + if (LoadAndValidateAttributes(this.GetAttributeDeclarations(), ref _lazyCustomAttributesBag)) { var completed = state.NotePartComplete(CompletionPart.Attributes); Debug.Assert(completed); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/GlobalExpressionVariable.cs b/src/Compilers/CSharp/Portable/Symbols/Source/GlobalExpressionVariable.cs index 64b9a72c48268..3403885c3079f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/GlobalExpressionVariable.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/GlobalExpressionVariable.cs @@ -56,7 +56,7 @@ internal static GlobalExpressionVariable Create( : new GlobalExpressionVariable(containingType, modifiers, typeSyntax, name, syntaxReference, locationSpan); } - protected override SyntaxList AttributeDeclarationSyntaxList => default(SyntaxList); + protected override OneOrMany> GetAttributeDeclarations() => OneOrMany>.Empty; protected override TypeSyntax TypeSyntax => (TypeSyntax)_typeSyntaxOpt?.GetSyntax(); protected override SyntaxTokenList ModifiersTokenList => default(SyntaxTokenList); public override bool HasInitializer => false; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs index 27167398d80fe..707da3435ec61 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs @@ -454,30 +454,57 @@ AttributeLocation IAttributeTargetSymbol.AllowedAttributeLocations } } +#nullable enable /// /// Symbol to copy bound attributes from, or null if the attributes are not shared among multiple source parameter symbols. /// /// - /// Used for parameters of partial implementation. We bind the attributes only on the definition - /// part and copy them over to the implementation. + /// This is inconsistent with analogous 'BoundAttributesSource' on other symbols. + /// Usually the definition part is the source, but for parameters the implementation part is the source. + /// This affects the location of diagnostics among other things. /// - private SourceParameterSymbol BoundAttributesSource + private SourceParameterSymbol? BoundAttributesSource + => PartialImplementationPart; + + protected SourceParameterSymbol? PartialImplementationPart { get { - var sourceMethod = this.ContainingSymbol as SourceOrdinaryMethodSymbol; - if ((object)sourceMethod == null) + ImmutableArray implParameters = this.ContainingSymbol switch + { + SourceMemberMethodSymbol { PartialImplementationPart.Parameters: { } parameters } => parameters, + SourcePropertySymbol { PartialImplementationPart.Parameters: { } parameters } => parameters, + _ => default + }; + + if (implParameters.IsDefault) { return null; } - var impl = sourceMethod.SourcePartialImplementation; - if ((object)impl == null) + Debug.Assert(!this.ContainingSymbol.IsPartialImplementation()); + return (SourceParameterSymbol)implParameters[this.Ordinal]; + } + } + + protected SourceParameterSymbol? PartialDefinitionPart + { + get + { + ImmutableArray defParameters = this.ContainingSymbol switch + { + SourceMemberMethodSymbol { PartialDefinitionPart.Parameters: { } parameters } => parameters, + SourcePropertySymbol { PartialDefinitionPart.Parameters: { } parameters } => parameters, + _ => default + }; + + if (defParameters.IsDefault) { return null; } - return (SourceParameterSymbol)impl.Parameters[this.Ordinal]; + Debug.Assert(!this.ContainingSymbol.IsPartialDefinition()); + return (SourceParameterSymbol)defParameters[this.Ordinal]; } } @@ -495,44 +522,22 @@ internal sealed override SyntaxList AttributeDeclarationLis /// internal virtual OneOrMany> GetAttributeDeclarations() { - // C# spec: - // The attributes on the parameters of the resulting method declaration - // are the combined attributes of the corresponding parameters of the defining - // and the implementing partial method declaration in unspecified order. - // Duplicates are not removed. - - SyntaxList attributes = AttributeDeclarationList; - - var sourceMethod = this.ContainingSymbol as SourceOrdinaryMethodSymbol; - if ((object)sourceMethod == null) - { - return OneOrMany.Create(attributes); - } + // Attributes on parameters in partial members are owned by the parameter in the implementation part. + // If this symbol has a non-null PartialImplementationPart, we should have accessed this method through that implementation symbol. + Debug.Assert(PartialImplementationPart is null); - SyntaxList otherAttributes; - - // if this is a definition get the implementation and vice versa - SourceOrdinaryMethodSymbol otherPart = sourceMethod.OtherPartOfPartial; - if ((object)otherPart != null) + if (PartialDefinitionPart is { } definitionPart) { - otherAttributes = ((SourceParameterSymbol)otherPart.Parameters[this.Ordinal]).AttributeDeclarationList; + return OneOrMany.Create( + AttributeDeclarationList, + definitionPart.AttributeDeclarationList); } else { - otherAttributes = default(SyntaxList); - } - - if (attributes.Equals(default(SyntaxList))) - { - return OneOrMany.Create(otherAttributes); - } - else if (otherAttributes.Equals(default(SyntaxList))) - { - return OneOrMany.Create(attributes); + return OneOrMany.Create(AttributeDeclarationList); } - - return OneOrMany.Create(ImmutableArray.Create(attributes, otherAttributes)); } +#nullable disable /// /// Returns data decoded from well-known attributes applied to the symbol or null if there are no applied attributes. @@ -1030,33 +1035,26 @@ private bool IsValidCallerInfoContext(AttributeSyntax node) => !ContainingSymbol && !ContainingSymbol.IsOperator() && !IsOnPartialImplementation(node); +#nullable enable /// /// Is the attribute syntax appearing on a parameter of a partial method implementation part? /// Since attributes are merged between the parts of a partial, we need to look at the syntax where the /// attribute appeared in the source to see if it corresponds to a partial method implementation part. /// - /// - /// private bool IsOnPartialImplementation(AttributeSyntax node) { - var method = ContainingSymbol as MethodSymbol; - if ((object)method == null) return false; - var impl = method.IsPartialImplementation() ? method : method.PartialImplementationPart; - if ((object)impl == null) return false; - var paramList = - node // AttributeSyntax - .Parent // AttributeListSyntax - .Parent // ParameterSyntax - .Parent as ParameterListSyntax; // ParameterListSyntax - if (paramList == null) return false; - var methDecl = paramList.Parent as MethodDeclarationSyntax; - if (methDecl == null) return false; - foreach (var r in impl.DeclaringSyntaxReferences) - { - if (r.GetSyntax() == methDecl) return true; - } - return false; + // If we are asking this, the candidate attribute had better be contained in *some* attribute associated with this parameter syntactically + Debug.Assert(this.GetAttributeDeclarations().Any(attrLists => attrLists.Any(attrList => attrList.Contains(node)))); + + var implParameter = this.ContainingSymbol.IsPartialImplementation() ? this : PartialImplementationPart; + if (implParameter?.AttributeDeclarationList is not { } implParameterAttributeList) + { + return false; + } + + return implParameterAttributeList.Any(attrList => attrList.Attributes.Contains(node)); } +#nullable disable private void ValidateCallerLineNumberAttribute(AttributeSyntax node, BindingDiagnosticBag diagnostics) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEnumConstantSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEnumConstantSymbol.cs index 4216680ce2790..a2f5dd94a71f9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEnumConstantSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEnumConstantSymbol.cs @@ -86,17 +86,14 @@ protected sealed override DeclarationModifiers Modifiers } } - protected override SyntaxList AttributeDeclarationSyntaxList + protected override OneOrMany> GetAttributeDeclarations() { - get + if (this.containingType.AnyMemberHasAttributes) { - if (this.containingType.AnyMemberHasAttributes) - { - return this.SyntaxNode.AttributeLists; - } - - return default(SyntaxList); + return OneOrMany.Create(this.SyntaxNode.AttributeLists); } + + return OneOrMany>.Empty; } #nullable enable diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs index 804ac34303400..13a1bda935111 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs @@ -408,17 +408,14 @@ private static BaseFieldDeclarationSyntax GetFieldDeclaration(CSharpSyntaxNode d return (BaseFieldDeclarationSyntax)declarator.Parent.Parent; } - protected override SyntaxList AttributeDeclarationSyntaxList + protected override OneOrMany> GetAttributeDeclarations() { - get + if (this.containingType.AnyMemberHasAttributes) { - if (this.containingType.AnyMemberHasAttributes) - { - return GetFieldDeclaration(this.SyntaxNode).AttributeLists; - } - - return default(SyntaxList); + return OneOrMany.Create(GetFieldDeclaration(this.SyntaxNode).AttributeLists); } + + return OneOrMany>.Empty; } public sealed override RefKind RefKind => GetTypeAndRefKind(ConsList.Empty).RefKind; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs index db0e62af2e2c6..2b14c397e27bf 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs @@ -335,10 +335,6 @@ internal bool IsPartialWithoutImplementation } } - /// - /// Returns the implementation part of a partial method definition, - /// or null if this is not a partial method or it is the definition part. - /// internal SourceOrdinaryMethodSymbol SourcePartialDefinition { get @@ -347,10 +343,6 @@ internal SourceOrdinaryMethodSymbol SourcePartialDefinition } } - /// - /// Returns the definition part of a partial method implementation, - /// or null if this is not a partial method or it is the implementation part. - /// internal SourceOrdinaryMethodSymbol SourcePartialImplementation { get @@ -401,6 +393,10 @@ protected sealed override SourceMemberMethodSymbol BoundAttributesSource internal sealed override OneOrMany> GetAttributeDeclarations() { + // Attributes on partial methods are owned by the definition part. + // If this symbol has a non-null PartialDefinitionPart, we should have accessed this method through that definition symbol instead + Debug.Assert(PartialDefinitionPart is null); + if ((object)this.SourcePartialImplementation != null) { return OneOrMany.Create(ImmutableArray.Create(AttributeDeclarationSyntaxList, this.SourcePartialImplementation.AttributeDeclarationSyntaxList)); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs index 5d19c49bfd0cc..c625366d197a9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs @@ -631,16 +631,38 @@ public sealed override ImmutableArray ExplicitInterfaceImplementat internal sealed override OneOrMany> GetAttributeDeclarations() { - var syntax = this.GetSyntax(); - switch (syntax.Kind()) + if (PartialImplementationPart is { } implementation) { - case SyntaxKind.GetAccessorDeclaration: - case SyntaxKind.SetAccessorDeclaration: - case SyntaxKind.InitAccessorDeclaration: - return OneOrMany.Create(((AccessorDeclarationSyntax)syntax).AttributeLists); + return OneOrMany.Create(AttributeDeclarationList, ((SourcePropertyAccessorSymbol)implementation).AttributeDeclarationList); } - return base.GetAttributeDeclarations(); + // If we are asking this question on a partial implementation symbol, + // it must be from a context which prefers to order implementation attributes before definition attributes. + // For example, the 'value' parameter of a set accessor. + if (PartialDefinitionPart is { } definition) + { + Debug.Assert(MethodKind == MethodKind.PropertySet); + return OneOrMany.Create(AttributeDeclarationList, ((SourcePropertyAccessorSymbol)definition).AttributeDeclarationList); + } + + return OneOrMany.Create(AttributeDeclarationList); + } + + private SyntaxList AttributeDeclarationList + { + get + { + var syntax = this.GetSyntax(); + switch (syntax.Kind()) + { + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.InitAccessorDeclaration: + return ((AccessorDeclarationSyntax)syntax).AttributeLists; + } + + return default; + } } #nullable enable @@ -805,6 +827,8 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r } #nullable enable + protected sealed override SourceMemberMethodSymbol? BoundAttributesSource => (SourceMemberMethodSymbol?)PartialDefinitionPart; + public sealed override MethodSymbol? PartialImplementationPart => _property is SourcePropertySymbol { IsPartialDefinition: true, OtherPartOfPartial: { } other } ? (MethodKind == MethodKind.PropertyGet ? other.GetMethod : other.SetMethod) : null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs index d9f9672b670d5..46d4da363c7a8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs @@ -171,8 +171,25 @@ private static SyntaxTokenList GetModifierTokensSyntax(SyntaxNode syntax) private static bool HasInitializer(SyntaxNode syntax) => syntax is PropertyDeclarationSyntax { Initializer: { } }; - public override SyntaxList AttributeDeclarationSyntaxList - => ((BasePropertyDeclarationSyntax)CSharpSyntaxNode).AttributeLists; + public override OneOrMany> GetAttributeDeclarations() + { + // Attributes on partial properties are owned by the definition part. + // If this symbol has a non-null PartialDefinitionPart, we should have accessed this method through that definition symbol instead + Debug.Assert(PartialDefinitionPart is null); + + if (PartialImplementationPart is { } implementationPart) + { + return OneOrMany.Create( + ((BasePropertyDeclarationSyntax)CSharpSyntaxNode).AttributeLists, + ((BasePropertyDeclarationSyntax)implementationPart.CSharpSyntaxNode).AttributeLists); + } + else + { + return OneOrMany.Create(((BasePropertyDeclarationSyntax)CSharpSyntaxNode).AttributeLists); + } + } + + protected override SourcePropertySymbolBase? BoundAttributesSource => PartialDefinitionPart; public override IAttributeTargetSymbol AttributesOwner => this; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs index 1749008b67547..a003de34a608b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs @@ -1060,7 +1060,13 @@ private SynthesizedSealedPropertyAccessor MakeSynthesizedSealedAccessor() #region Attributes - public abstract SyntaxList AttributeDeclarationSyntaxList { get; } + public abstract OneOrMany> GetAttributeDeclarations(); + + /// + /// Symbol to copy bound attributes from, or null if the attributes are not shared among multiple source property symbols. + /// Analogous to . + /// + protected abstract SourcePropertySymbolBase BoundAttributesSource { get; } public abstract IAttributeTargetSymbol AttributesOwner { get; } @@ -1087,10 +1093,29 @@ private CustomAttributesBag GetAttributesBag() return bag; } - // The property is responsible for completion of the backing field - _ = BackingField?.GetAttributes(); + var copyFrom = this.BoundAttributesSource; + + // prevent infinite recursion: + Debug.Assert(!ReferenceEquals(copyFrom, this)); + + bool bagCreatedOnThisThread; + if (copyFrom is not null) + { + // When partial properties get the ability to have a backing field, + // the implementer will have to decide how the BackingField symbol works in 'copyFrom' scenarios. + Debug.Assert(BackingField is null); + + var attributesBag = copyFrom.GetAttributesBag(); + bagCreatedOnThisThread = Interlocked.CompareExchange(ref _lazyCustomAttributesBag, attributesBag, null) == null; + } + else + { + // The property is responsible for completion of the backing field + _ = BackingField?.GetAttributes(); + bagCreatedOnThisThread = LoadAndValidateAttributes(GetAttributeDeclarations(), ref _lazyCustomAttributesBag); + } - if (LoadAndValidateAttributes(OneOrMany.Create(AttributeDeclarationSyntaxList), ref _lazyCustomAttributesBag)) + if (bagCreatedOnThisThread) { var completed = _state.NotePartComplete(CompletionPart.Attributes); Debug.Assert(completed); diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedPrimaryConstructorParameterBackingFieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedPrimaryConstructorParameterBackingFieldSymbol.cs index 6552a867d6ae9..5f3d127451425 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedPrimaryConstructorParameterBackingFieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedPrimaryConstructorParameterBackingFieldSymbol.cs @@ -33,8 +33,8 @@ protected override IAttributeTargetSymbol AttributeOwner internal override Location ErrorLocation => ParameterSymbol.TryGetFirstLocation() ?? NoLocation.Singleton; - protected override SyntaxList AttributeDeclarationSyntaxList - => default; + protected override OneOrMany> GetAttributeDeclarations() + => OneOrMany>.Empty; public override Symbol? AssociatedSymbol => null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs index 63afef8d2f803..1dc7444d03efb 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs @@ -49,8 +49,10 @@ public SynthesizedRecordEqualityContractProperty(SourceMemberContainerTypeSymbol public override ImmutableArray DeclaringSyntaxReferences => ImmutableArray.Empty; - public override SyntaxList AttributeDeclarationSyntaxList - => new SyntaxList(); + public override OneOrMany> GetAttributeDeclarations() + => OneOrMany>.Empty; + + protected override SourcePropertySymbolBase? BoundAttributesSource => null; public override IAttributeTargetSymbol AttributesOwner => this; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs index 1a3554d59e402..2d905f6daf0f8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Symbols { @@ -42,13 +43,15 @@ public SynthesizedRecordPropertySymbol( BackingParameter = (SourceParameterSymbol)backingParameter; } + protected override SourcePropertySymbolBase? BoundAttributesSource => null; + public override IAttributeTargetSymbol AttributesOwner => BackingParameter as IAttributeTargetSymbol ?? this; protected override Location TypeLocation => ((ParameterSyntax)CSharpSyntaxNode).Type!.Location; - public override SyntaxList AttributeDeclarationSyntaxList - => BackingParameter.AttributeDeclarationList; + public override OneOrMany> GetAttributeDeclarations() + => OneOrMany.Create(BackingParameter.AttributeDeclarationList); protected override SourcePropertyAccessorSymbol CreateGetAccessorSymbol(bool isAutoPropertyAccessor, BindingDiagnosticBag diagnostics) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedBackingFieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedBackingFieldSymbol.cs index cf1ddb35d1427..323447d1aa226 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedBackingFieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedBackingFieldSymbol.cs @@ -100,8 +100,8 @@ protected override IAttributeTargetSymbol AttributeOwner internal override Location ErrorLocation => _property.Location; - protected override SyntaxList AttributeDeclarationSyntaxList - => _property.AttributeDeclarationSyntaxList; + protected override OneOrMany> GetAttributeDeclarations() + => _property.GetAttributeDeclarations(); public override Symbol AssociatedSymbol => _property; @@ -163,15 +163,18 @@ private void CheckForFieldTargetedAttribute(BindingDiagnosticBag diagnostics) return; } - foreach (var attribute in AttributeDeclarationSyntaxList) + foreach (var attributeList in GetAttributeDeclarations()) { - if (attribute.Target?.GetAttributeLocation() == AttributeLocation.Field) + foreach (var attribute in attributeList) { - diagnostics.Add( - new CSDiagnosticInfo(ErrorCode.WRN_AttributesOnBackingFieldsNotAvailable, - languageVersion.ToDisplayString(), - new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureAttributesOnBackingFields.RequiredVersion())), - attribute.Target.Location); + if (attribute.Target?.GetAttributeLocation() == AttributeLocation.Field) + { + diagnostics.Add( + new CSDiagnosticInfo(ErrorCode.WRN_AttributesOnBackingFieldsNotAvailable, + languageVersion.ToDisplayString(), + new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureAttributesOnBackingFields.RequiredVersion())), + attribute.Target.Location); + } } } } diff --git a/src/Compilers/CSharp/Test/Emit2/Attributes/AttributeTests_CallerInfoAttributes.cs b/src/Compilers/CSharp/Test/Emit2/Attributes/AttributeTests_CallerInfoAttributes.cs index dd9319f97b459..bf4053c6fd9f2 100644 --- a/src/Compilers/CSharp/Test/Emit2/Attributes/AttributeTests_CallerInfoAttributes.cs +++ b/src/Compilers/CSharp/Test/Emit2/Attributes/AttributeTests_CallerInfoAttributes.cs @@ -3231,6 +3231,19 @@ public static void Main() // (14,10): warning CS4025: The CallerFilePathAttribute applied to parameter 'path' will have no effect because it applies to a member that is used in contexts that do not allow optional arguments // [CallerFilePath] string path) { } Diagnostic(ErrorCode.WRN_CallerFilePathParamForUnconsumedLocation, "CallerFilePath").WithArguments("path")); + + CompileAndVerify(source, options: TestOptions.DebugExe.WithMetadataImportOptions(MetadataImportOptions.All), symbolValidator: verify); + + void verify(ModuleSymbol module) + { + // https://github.com/dotnet/roslyn/issues/73482 + // These are ignored in source but they still get written out to metadata. + // This means if the method is accessible from another compilation, then the attribute will be respected there, but not in the declaring compilation. + var goo = module.GlobalNamespace.GetMember("D.Goo"); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerLineNumberAttribute"], goo.Parameters[0].GetAttributes().SelectAsArray(attr => attr.ToString())); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerMemberNameAttribute"], goo.Parameters[1].GetAttributes().SelectAsArray(attr => attr.ToString())); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerFilePathAttribute"], goo.Parameters[2].GetAttributes().SelectAsArray(attr => attr.ToString())); + } } [Fact] @@ -5836,5 +5849,106 @@ public CallerArgumentExpressionAttribute([CallerArgumentExpression(nameof(parame // public CallerArgumentExpressionAttribute([CallerArgumentExpression(nameof(parameterName))] string parameterName) Diagnostic(ErrorCode.ERR_BadCallerArgumentExpressionParamWithoutDefaultValue, "CallerArgumentExpression").WithLocation(5, 51)); } + + [Fact] + public void CallerMemberName_SetterValueParam() + { + // There is no way in C# to call a setter without passing an argument for the value, so the CallerMemberName effectively does nothing. + var source = """ + using System; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + public partial class C + { + public static void Main() + { + var c = new C(); + c[1] = "1"; + } + + public string this[int x] + { + [param: Optional, DefaultParameterValue("0")] + [param: CallerMemberName] + set + { + Console.Write(value); + } + } + } + """; + + var verifier = CompileAndVerify(source, expectedOutput: "1"); + verifier.VerifyDiagnostics(); + + var source1 = """ + class D + { + void M() + { + var c = new C(); + c.set_Item(1); + } + } + """; + var comp1 = CreateCompilation(source1, references: [verifier.Compilation.EmitToImageReference()]); + comp1.VerifyEmitDiagnostics( + // (6,11): error CS0571: 'C.this[int].set': cannot explicitly call operator or accessor + // c.set_Item(1); + Diagnostic(ErrorCode.ERR_CantCallSpecialMethod, "set_Item").WithArguments("C.this[int].set").WithLocation(6, 11)); + } + + [Fact] + public void CallerArgumentExpression_SetterValueParam() + { + var source = """ + using System; + using System.Runtime.CompilerServices; + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = false)] + public sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + public string ParameterName { get; } + } + } + + partial class C + { + public static void Main() + { + var c = new C(); + c[1] = GetNumber(); + } + + public static int GetNumber() => 1; + + public int this[int x, [CallerArgumentExpression("value")] string argumentExpression = "0"] + { + set + { + Console.Write(argumentExpression); + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "0", symbolValidator: verify); + verifier.VerifyDiagnostics( + // (27,29): warning CS8963: The CallerArgumentExpressionAttribute applied to parameter 'argumentExpression' will have no effect. It is applied with an invalid parameter name. + // public int this[int x, [CallerArgumentExpression("value")] string argumentExpression = "0"] + Diagnostic(ErrorCode.WRN_CallerArgumentExpressionAttributeHasInvalidParameterName, "CallerArgumentExpression").WithArguments("argumentExpression").WithLocation(27, 29)); + + void verify(ModuleSymbol module) + { + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["""System.Runtime.CompilerServices.CallerArgumentExpressionAttribute("value")"""], indexer.Parameters[1].GetAttributes().SelectAsArray(attr => attr.ToString())); + } + } } } diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs index 1be88ef4740e1..e4d5d591b05a2 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs @@ -3017,11 +3017,1175 @@ partial class C Diagnostic(ErrorCode.ERR_PartialMemberInconsistentTupleNames, "this").WithArguments("C.this[(int x, int y, int z)]", "C.this[(int a, int b, int c)]").WithLocation(13, 37)); } + [Theory] + [InlineData("A(1)", "B(2)")] + [InlineData("B(2)", "A(1)")] + public void Attributes_Property_01(string declAttribute, string implAttribute) + { + // Name or arguments to attributes doesn't affect order of emit. + // Attributes on the declaration part precede attributes on the implementation part. + var source = $$""" + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System; + + class A(int i) : Attribute { } + class B(int i) : Attribute { } + + partial class C + { + [{{declAttribute}}] + public partial int P { get; set; } + + [{{implAttribute}}] + public partial int P { get => 1; set { } } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + + var property = comp.GetMember("C.P"); + AssertEx.Equal([declAttribute, implAttribute], property.GetAttributes().ToStrings()); + AssertEx.Equal([declAttribute, implAttribute], property.PartialImplementationPart!.GetAttributes().ToStrings()); + } + + [Theory] + [InlineData("A(1)", "B(2)")] + [InlineData("B(2)", "A(1)")] + public void Attributes_Property_02(string declAttribute, string implAttribute) + { + // Lexical order of the partial declarations themselves doesn't affect order that attributes are emitted. + var source = $$""" + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System; + + class A(int i) : Attribute { } + class B(int i) : Attribute { } + + partial class C + { + [{{implAttribute}}] + public partial int P { get => 1; set { } } + + [{{declAttribute}}] + public partial int P { get; set; } + } + """; + + var verifier = CompileAndVerify(source, symbolValidator: module => verify(module, isSource: false), sourceSymbolValidator: module => verify(module, isSource: true)); + verifier.VerifyDiagnostics(); + + void verify(ModuleSymbol module, bool isSource) + { + var property = module.GlobalNamespace.GetMember("C.P"); + AssertEx.Equal([declAttribute, implAttribute], property.GetAttributes().ToStrings()); + if (isSource) + { + AssertEx.Equal([declAttribute, implAttribute], ((SourcePropertySymbol)property).PartialImplementationPart!.GetAttributes().ToStrings()); + } + } + } + + [Fact] + public void Attributes_Property_03() + { + // Order of attributes within a part is preserved. + var source = """ + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class A(int i) : Attribute { } + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class B(int i) : Attribute { } + + partial class C + { + [A(2), B(2)] + public partial int P { get => 1; set { } } + + [A(1), B(1)] + public partial int P { get; set; } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + + var property = comp.GetMember("C.P"); + AssertEx.Equal(["A(1)", "B(1)", "A(2)", "B(2)"], property.GetAttributes().ToStrings()); + AssertEx.Equal(["A(1)", "B(1)", "A(2)", "B(2)"], property.PartialImplementationPart!.GetAttributes().ToStrings()); + } + + [Fact] + public void Attributes_GetAccessor() + { + // Order of attributes within a part is preserved. + var source = """ + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class A(int i) : Attribute { } + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class B(int i) : Attribute { } + + partial class C + { + public partial int P + { + [A(2), B(2)] + get => 1; + set { } + } + + public partial int P + { + [A(1), B(1)] + get; + set; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + + var accessor = comp.GetMember("C.get_P"); + AssertEx.Equal(["A(1)", "B(1)", "A(2)", "B(2)"], accessor.GetAttributes().ToStrings()); + AssertEx.Equal(["A(1)", "B(1)", "A(2)", "B(2)"], accessor.PartialImplementationPart!.GetAttributes().ToStrings()); + } + + [Theory] + [CombinatorialData] + public void Attributes_SetAccessor(bool definitionFirst) + { + var definitionPart = """ + public partial int P + { + get; + [A(1), B(1)] + set; + } + """; + + var implementationPart = """ + public partial int P + { + get => 1; + [A(2), B(2)] + set { } + } + """; + + var source = $$""" + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System; + + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class A(int i) : Attribute { } + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class B(int i) : Attribute { } + + partial class C + { + {{(definitionFirst ? definitionPart : implementationPart)}} + + {{(definitionFirst ? implementationPart : definitionPart)}} + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + + var accessor = comp.GetMember("C.set_P"); + AssertEx.Equal(["A(1)", "B(1)", "A(2)", "B(2)"], accessor.GetAttributes().ToStrings()); + AssertEx.Equal(["A(1)", "B(1)", "A(2)", "B(2)"], accessor.PartialImplementationPart!.GetAttributes().ToStrings()); + } + + [Fact] + public void Attributes_SetValueParam() + { + // Just as with parameter attributes on partial methods, + // the implementation part attributes are emitted before the definition part attributes. + var source = """ + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class A(int i) : Attribute { } + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class B(int i) : Attribute { } + + partial class C + { + public partial int P + { + get => 1; + [param: A(2), B(2)] + set { } + } + + public partial int P + { + get; + [param: A(1), B(1)] + set; + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + + var accessor = comp.GetMember("C.set_P"); + AssertEx.Equal([], accessor.GetAttributes().ToStrings()); + AssertEx.Equal([], accessor.PartialImplementationPart.GetAttributes().ToStrings()); + AssertEx.Equal(["A(2)", "B(2)", "A(1)", "B(1)"], accessor.Parameters.Single().GetAttributes().ToStrings()); + AssertEx.Equal(["A(2)", "B(2)", "A(1)", "B(1)"], accessor.PartialImplementationPart!.Parameters.Single().GetAttributes().ToStrings()); + } + + [Fact] + public void Attributes_Indexer() + { + var source = """ + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class A(int i) : Attribute { } + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class B(int i) : Attribute { } + + partial class C + { + [A(2), B(2)] + public partial int this[int i] { get => 1; set { } } + + [A(1), B(1)] + public partial int this[int i] { get; set; } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + + var indexer = (SourcePropertySymbol)comp.GetMember("C").Indexers.Single(); + AssertEx.Equal(["A(1)", "B(1)", "A(2)", "B(2)"], indexer.GetAttributes().ToStrings()); + AssertEx.Equal(["A(1)", "B(1)", "A(2)", "B(2)"], indexer.PartialImplementationPart!.GetAttributes().ToStrings()); + } + + [Fact] + public void Attributes_IndexerParameter() + { + // Unlike other symbol kinds, for parameters, the implementation part attributes are emitted first, then the definition part attributes. + // This is consistent with partial methods. + var source = """ + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class A(int i) : Attribute { } + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class B(int i) : Attribute { } + + partial class C + { + public partial int this[[A(2), B(2)] int i] { get => 1; set { } } + + public partial int this[[A(1), B(1)] int i] { get; set; } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + + var indexer = (SourcePropertySymbol)comp.GetMember("C").Indexers.Single(); + + verify(indexer.Parameters.Single()); + verify(indexer.GetMethod!.Parameters.Single()); + verify(indexer.SetMethod!.Parameters[0]); + + verify(indexer.PartialImplementationPart!.Parameters.Single()); + verify(indexer.PartialImplementationPart!.GetMethod!.Parameters.Single()); + verify(indexer.PartialImplementationPart!.SetMethod!.Parameters[0]); + + void verify(ParameterSymbol param) + { + AssertEx.Equal(["A(2)", "B(2)", "A(1)", "B(1)"], param.GetAttributes().ToStrings()); + } + } + + [Fact] + public void Attributes_Property_DisallowedDuplicates() + { + var source = """ + using System; + + class Attr : Attribute { } + + partial class C + { + [Attr] + public partial int P + { + [Attr] + get; + + [Attr] + [param: Attr] set; // 1 + } + + [Attr] // 2 + public partial int P + { + [Attr] // 3 + get => 1; + + [Attr] // 4 + [param: Attr] set { } + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (14,17): error CS0579: Duplicate 'Attr' attribute + // [param: Attr] set; // 1 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(14, 17), + // (17,6): error CS0579: Duplicate 'Attr' attribute + // [Attr] // 2 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(17, 6), + // (20,10): error CS0579: Duplicate 'Attr' attribute + // [Attr] // 3 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(20, 10), + // (23,10): error CS0579: Duplicate 'Attr' attribute + // [Attr] // 4 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(23, 10)); + + var property = comp.GetMember("C.P"); + AssertEx.Equal(["Attr", "Attr"], property.GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.GetMethod!.GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.SetMethod!.GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.SetMethod!.Parameters.Single().GetAttributes().ToStrings()); + } + + [Fact] + public void Attributes_Indexer_DisallowedDuplicates() + { + var source = """ + using System; + + class Attr : Attribute { } + + partial class C + { + [Attr] + public partial int this[ + [Attr] int x, // 1 + [Attr] int y] // 2 + { + [Attr] get; + [Attr] + [param: Attr] set; // 3 + } + + [Attr] // 4 + public partial int this[ + [Attr] int x, + [Attr] int y] + { + [Attr] // 5 + get => 1; + + [Attr] // 6 + [param: Attr] set { } + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (9,10): error CS0579: Duplicate 'Attr' attribute + // [Attr] int x, // 1 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(9, 10), + // (10,10): error CS0579: Duplicate 'Attr' attribute + // [Attr] int y] // 2 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(10, 10), + // (14,17): error CS0579: Duplicate 'Attr' attribute + // [param: Attr] set; // 3 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(14, 17), + // (17,6): error CS0579: Duplicate 'Attr' attribute + // [Attr] // 4 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(17, 6), + // (22,10): error CS0579: Duplicate 'Attr' attribute + // [Attr] // 5 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(22, 10), + // (25,10): error CS0579: Duplicate 'Attr' attribute + // [Attr] // 6 + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "Attr").WithArguments("Attr").WithLocation(25, 10)); + + var property = (SourcePropertySymbol)comp.GetMember("C").Indexers.Single(); + AssertEx.Equal(["Attr", "Attr"], property.GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.GetMethod!.GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.GetMethod!.Parameters[0].GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.GetMethod!.Parameters[1].GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.SetMethod!.GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.SetMethod!.Parameters[0].GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.SetMethod!.Parameters[1].GetAttributes().ToStrings()); + AssertEx.Equal(["Attr", "Attr"], property.SetMethod!.Parameters[2].GetAttributes().ToStrings()); + } + + [Fact] + public void Attributes_Property_BackingField() + { + var source = """ + using System; + + class Attr : Attribute { } + + partial class C + { + [field: Attr] + public partial int P { get; set; } + + [field: Attr] + public partial int P { get => 1; set { } } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (7,6): warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'property'. All attributes in this block will be ignored. + // [field: Attr] + Diagnostic(ErrorCode.WRN_AttributeLocationOnBadDeclaration, "field").WithArguments("field", "property").WithLocation(7, 6), + // (10,6): warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'property'. All attributes in this block will be ignored. + // [field: Attr] + Diagnostic(ErrorCode.WRN_AttributeLocationOnBadDeclaration, "field").WithArguments("field", "property").WithLocation(10, 6)); + + // https://github.com/dotnet/roslyn/issues/57012 + // 'field' keyword in properties feature should test partial properties where the implementation uses 'field' and one or both parts have 'field:' targeted attribute lists. + var property = comp.GetMember("C.P"); + AssertEx.Equal([], property.GetAttributes().ToStrings()); + } + + [Theory] + [InlineData("", "[UnscopedRef] ")] + [InlineData("[UnscopedRef] ", "")] + public void Attributes_UnscopedRef(string defAttrs, string implAttrs) + { + // There aren't many interesting scenarios to test with UnscopedRef because: + // - no out parameters (so no removing the implicit 'scoped' from them) + // - no ref parameters either (so no interesting differences in ref safety analysis for ref readonlys marked with `[UnscopedRef]`) + var source = $$""" + #pragma warning disable CS9113 // Primary constructor parameter is unread + + using System.Diagnostics.CodeAnalysis; + + public ref struct RS([UnscopedRef] ref readonly int ri) { } + + partial class C + { + public partial RS this[{{defAttrs}}ref readonly int i] { get; } + public partial RS this[{{implAttrs}}ref readonly int i] + { + get => new RS(in i); + } + } + """; + + var comp = CreateCompilation([source, UnscopedRefAttributeDefinition]); + comp.VerifyEmitDiagnostics(); + + var indexer = (SourcePropertySymbol)comp.GetMember("C").Indexers.Single(); + Assert.True(indexer.Parameters[0].HasUnscopedRefAttribute); + Assert.True(indexer.PartialImplementationPart!.Parameters[0].HasUnscopedRefAttribute); + Assert.True(indexer.GetMethod!.Parameters[0].HasUnscopedRefAttribute); + Assert.True(indexer.GetMethod!.PartialImplementationPart!.Parameters[0].HasUnscopedRefAttribute); + } + + [Fact] + public void Attributes_CallerLineNumber_OnDefinition() + { + var source = $$""" + using System; + using System.Runtime.CompilerServices; + + partial class C + { + public static void Main() + { + var c = new C(); + #line 1 + Console.Write(c[2]); + } + + public partial int this[int x, [CallerLineNumber] int lineNumber = 0] { get; } + public partial int this[int x, int lineNumber] + { + get + { + Console.Write(lineNumber); + return x; + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "12", symbolValidator: verify); + verifier.VerifyDiagnostics(); + + void verify(ModuleSymbol module) + { + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerLineNumberAttribute"], indexer.Parameters[1].GetAttributes().ToStrings()); + } + } + + [Fact] + public void Attributes_CallerLineNumber_OnImplementation() + { + var source = $$""" + using System; + using System.Runtime.CompilerServices; + + partial class C + { + public static void Main() + { + var c = new C(); + #line 1 + Console.Write(c[2]); + } + + public partial int this[int x, int lineNumber = 0] { get; } + public partial int this[int x, [CallerLineNumber] int lineNumber] + { + get + { + Console.Write(lineNumber); + return x; + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "02", symbolValidator: verify); + verifier.VerifyDiagnostics( + // (5,37): warning CS4024: The CallerLineNumberAttribute applied to parameter 'lineNumber' will have no effect because it applies to a member that is used in contexts that do not allow optional arguments + // public partial int this[int x, [CallerLineNumber] int lineNumber] + Diagnostic(ErrorCode.WRN_CallerLineNumberParamForUnconsumedLocation, "CallerLineNumber").WithArguments("lineNumber").WithLocation(5, 37)); + + void verify(ModuleSymbol module) + { + // https://github.com/dotnet/roslyn/issues/73482 + // The attribute is still written out to metadata even though it is ignored in source. + // We could consider changing this for both properties and methods. + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerLineNumberAttribute"], indexer.Parameters[1].GetAttributes().ToStrings()); + } + } + + [Fact] + public void Attributes_CallerFilePath_OnDefinition() + { + var source = ($$""" + using System; + using System.Runtime.CompilerServices; + + partial class C + { + public static void Main() + { + var c = new C(); + Console.Write(c[2]); + } + + public partial int this[int x, [CallerFilePath] string filePath = "0"] { get; } + public partial int this[int x, string filePath] + { + get + { + Console.Write(filePath); + return x; + } + } + } + """, filePath: "Program.cs"); + var verifier = CompileAndVerify(source, expectedOutput: "Program.cs2", symbolValidator: verify); + verifier.VerifyDiagnostics(); + + void verify(ModuleSymbol module) + { + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerFilePathAttribute"], indexer.Parameters[1].GetAttributes().ToStrings()); + } + } + + [Fact] + public void Attributes_CallerFilePath_OnImplementation() + { + var source = ($$""" + using System; + using System.Runtime.CompilerServices; + + partial class C + { + public static void Main() + { + var c = new C(); + Console.Write(c[2]); + } + + public partial int this[int x, string filePath = "0"] { get; } + public partial int this[int x, [CallerFilePath] string filePath] + { + get + { + Console.Write(filePath); + return x; + } + } + } + """, filePath: "Program.cs"); + var verifier = CompileAndVerify(source, expectedOutput: "02", symbolValidator: verify); + verifier.VerifyDiagnostics( + // Program.cs(13,37): warning CS4025: The CallerFilePathAttribute applied to parameter 'filePath' will have no effect because it applies to a member that is used in contexts that do not allow optional arguments + // public partial int this[int x, [CallerFilePath] string filePath] + Diagnostic(ErrorCode.WRN_CallerFilePathParamForUnconsumedLocation, "CallerFilePath").WithArguments("filePath").WithLocation(13, 37)); + + void verify(ModuleSymbol module) + { + // https://github.com/dotnet/roslyn/issues/73482 + // The attribute is still written out to metadata even though it is ignored in source. + // We could consider changing this for both properties and methods. + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerFilePathAttribute"], indexer.Parameters[1].GetAttributes().ToStrings()); + } + } + + [Fact] + public void Attributes_CallerMemberName_OnDefinition() + { + var source = $$""" + using System; + using System.Runtime.CompilerServices; + + partial class C + { + public static void Main() + { + var c = new C(); + Console.Write(c[2]); + } + + public partial int this[int x, [CallerMemberName] string filePath = "0"] { get; } + public partial int this[int x, string filePath] + { + get + { + Console.Write(filePath); + return x; + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "Main2", symbolValidator: verify); + verifier.VerifyDiagnostics(); + + void verify(ModuleSymbol module) + { + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerMemberNameAttribute"], indexer.Parameters[1].GetAttributes().ToStrings()); + } + } + + [Fact] + public void Attributes_CallerMemberName_OnImplementation() + { + var source = $$""" + using System; + using System.Runtime.CompilerServices; + + partial class C + { + public static void Main() + { + var c = new C(); + Console.Write(c[2]); + } + + public partial int this[int x, string filePath = "0"] { get; } + public partial int this[int x, [CallerMemberName] string filePath] + { + get + { + Console.Write(filePath); + return x; + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "02", symbolValidator: verify); + verifier.VerifyDiagnostics( + // (13,37): warning CS4026: The CallerMemberNameAttribute applied to parameter 'filePath' will have no effect because it applies to a member that is used in contexts that do not allow optional arguments + // public partial int this[int x, [CallerMemberName] string filePath] + Diagnostic(ErrorCode.WRN_CallerMemberNameParamForUnconsumedLocation, "CallerMemberName").WithArguments("filePath").WithLocation(13, 37)); + + void verify(ModuleSymbol module) + { + // https://github.com/dotnet/roslyn/issues/73482 + // The attribute is still written out to metadata even though it is ignored in source. + // We could consider changing this for both properties and methods. + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["System.Runtime.CompilerServices.CallerMemberNameAttribute"], indexer.Parameters[1].GetAttributes().ToStrings()); + } + } + + [Fact] + public void Attributes_CallerArgumentExpression_OnDefinition() + { + var source = $$""" + using System; + using System.Runtime.CompilerServices; + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = false)] + public sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + public string ParameterName { get; } + } + } + + partial class C + { + public static void Main() + { + var c = new C(); + Console.Write(c[GetNumber()]); + } + + public static int GetNumber() => 2; + + public partial int this[int x, [CallerArgumentExpression("x")] string argumentExpression = "0"] { get; } + public partial int this[int x, string argumentExpression] + { + get + { + Console.Write(argumentExpression); + return x; + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "GetNumber()2", symbolValidator: verify); + verifier.VerifyDiagnostics(); + + void verify(ModuleSymbol module) + { + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["""System.Runtime.CompilerServices.CallerArgumentExpressionAttribute("x")"""], indexer.Parameters[1].GetAttributes().ToStrings()); + } + } + + [Fact] + public void Attributes_CallerArgumentExpression_OnImplementation() + { + var source = """ + using System; + using System.Runtime.CompilerServices; + + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = false)] + public sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + public string ParameterName { get; } + } + } + + partial class C + { + public static void Main() + { + var c = new C(); + Console.Write(c[GetNumber()]); + } + + public static int GetNumber() => 2; + + public partial int this[int x, string argumentExpression = "0"] { get; } + public partial int this[int x, [CallerArgumentExpression("x")] string argumentExpression] + { + get + { + Console.Write(argumentExpression); + return x; + } + } + } + """; + var verifier = CompileAndVerify(source, expectedOutput: "02", symbolValidator: verify); + verifier.VerifyDiagnostics( + // (28,37): warning CS8966: The CallerArgumentExpressionAttribute applied to parameter 'argumentExpression' will have no effect because it applies to a member that is used in contexts that do not allow optional arguments + // public partial int this[int x, [CallerArgumentExpression("x")] string argumentExpression] + Diagnostic(ErrorCode.WRN_CallerArgumentExpressionParamForUnconsumedLocation, "CallerArgumentExpression").WithArguments("argumentExpression").WithLocation(28, 37)); + + void verify(ModuleSymbol module) + { + // https://github.com/dotnet/roslyn/issues/73482 + // The attribute is still written out to metadata even though it is ignored in source. + // We could consider changing this for both properties and methods. + var indexer = (PropertySymbol)module.GlobalNamespace.GetMember("C").Indexers.Single(); + AssertEx.Equal(["""System.Runtime.CompilerServices.CallerArgumentExpressionAttribute("x")"""], indexer.Parameters[1].GetAttributes().ToStrings()); + } + } + + [Fact] + public void CallerMemberName_SetterValueParam_ImplementationPart() + { + // Counterpart to test 'AttributeTests_CallerInfoAttributes.CallerMemberName_SetterValueParam' + // There is no way in C# to call a setter without passing an argument for the value, so the CallerMemberName effectively does nothing. + // It would be reasonable to also warn here about the CallerInfo attribute on implementation, + // but this is a corner case that clearly won't work regardless of which part the attribute is on, so it's not a big deal that the warning is missing. + // Verify that our checks for caller-info attributes on implementation part parameters behave gracefully here. + var source = """ + using System; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + partial class C + { + public static void Main() + { + var c = new C(); + c[1] = "1"; + } + + public partial string this[int x] { set; } + public partial string this[int x] + { + [param: Optional, DefaultParameterValue("0")] + [param: CallerMemberName] + set + { + Console.Write(value); + } + } + } + """; + + var verifier = CompileAndVerify(source, expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Theory] + [InlineData("[AllowNull] ", "")] + [InlineData("", "[AllowNull] ")] + public void AllowNull_Property(string defAttrs, string implAttrs) + { + var source = $$""" + #nullable enable + + using System; + using System.Diagnostics.CodeAnalysis; + + partial class C + { + public static void Main() + { + var c = new C(); + try + { + c.Prop = null; // no warning + } + catch + { + Console.Write(1); + } + } + + {{defAttrs}} + public partial string Prop { get; set; } + + {{implAttrs}} + public partial string Prop + { + get => ""; + set + { + value.ToString(); // warning + } + } + } + """; + + var verifier = CompileAndVerify([source, AllowNullAttributeDefinition], expectedOutput: "1"); + verifier.VerifyDiagnostics( + // (30,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); // warning + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(30, 13)); + } + + [Theory] + [InlineData("[AllowNull] ", "")] + [InlineData("", "[AllowNull] ")] + public void AllowNull_IndexerParam(string defAttrs, string implAttrs) + { + var source = $$""" + #nullable enable + + using System; + using System.Diagnostics.CodeAnalysis; + + partial class C + { + public static void Main() + { + var c = new C(); + try + { + _ = c[null]; // no warning + } + catch + { + Console.Write(1); + } + } + + public partial string this[{{defAttrs}}string s] { get; } + public partial string this[{{implAttrs}}string s] + { + get + { + return s.ToString(); // https://github.com/dotnet/roslyn/issues/73484: missing a warning here + } + } + } + """; + + var verifier = CompileAndVerify([source, AllowNullAttributeDefinition], expectedOutput: "1"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void Obsolete_01() + { + var source = """ + using System; + + partial class C + { + [Obsolete] + public partial int Prop { get; } + + public partial int Prop { get => M(); } // no diagnostic for use of obsolete member + + [Obsolete] + int M() => 1; + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void Obsolete_02() + { + var source = """ + using System; + + partial class C + { + public partial int this[int x, int y = VALUE] { get; } // no diagnostic for use of obsolete const + + [Obsolete] + public partial int this[int x, int y] => x; + + [Obsolete] + public const int VALUE = 1; + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void Obsolete_03() + { + var source = """ + using System; + + partial class C + { + public partial int this[int x, int y = VALUE] { get; } // 1 + + public partial int this[int x, int y] { [Obsolete] get => x; } + + [Obsolete] + public const int VALUE = 1; + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (5,44): warning CS0612: 'C.VALUE' is obsolete + // public partial int this[int x, int y = VALUE] { get; } // 1 + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "VALUE").WithArguments("C.VALUE").WithLocation(5, 44)); + } + + [Theory] + [CombinatorialData] + public void Obsolete_04( + [CombinatorialValues("public partial int Prop { [Obsolete] get; }", "[Obsolete] public partial int Prop { get; }")] + string declPart, + [CombinatorialValues("public partial int Prop { get => M(); }", "public partial int Prop => M();")] + string implPart) + { + // note that one of the combinations here is redundant with Obsolete_01, but that seems fine, + // as a failure in Obsolete_01 may be easier to triage. + var source = $$""" + using System; + + partial class C + { + {{declPart}} + {{implPart}} // no diagnostic for use of obsolete member + + [Obsolete] + int M() => 1; + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + } + + [Theory] + [InlineData("[Obsolete]", "")] + [InlineData("", "[Obsolete]")] + public void Obsolete_05(string defAttrs, string implAttrs) + { + var source = $$""" + using System; + + partial class C + { + {{defAttrs}} + public partial int this[int x] { get; } + + {{implAttrs}} + public partial int this[int x] { get => x; } + } + + class D + { + void M() + { + var c = new C(); + _ = c[1]; // 1 + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (17,13): warning CS0612: 'C.this[int]' is obsolete + // _ = c[1]; // 1 + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "c[1]").WithArguments("C.this[int]").WithLocation(17, 13)); + } + + [Theory] + [InlineData("[Obsolete]", "")] + [InlineData("", "[Obsolete]")] + public void Obsolete_06(string defAttrs, string implAttrs) + { + var source = $$""" + using System; + + partial class C + { + public partial int this[int x] { {{defAttrs}} get; } + + public partial int this[int x] { {{implAttrs}} get => x; } + } + + class D + { + void M() + { + var c = new C(); + _ = c[1]; // 1 + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (15,13): warning CS0612: 'C.this[int].get' is obsolete + // _ = c[1]; // 1 + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "c[1]").WithArguments("C.this[int].get").WithLocation(15, 13)); + } + + [Fact] + public void UnmanagedCallersOnly() + { + var source = $$""" + using System.Runtime.InteropServices; + + partial class C + { + [UnmanagedCallersOnly] // 1 + public partial int P1 { get; } + public partial int P1 => 1; + + public partial int P2 { get; } + [UnmanagedCallersOnly] // 2 + public partial int P2 => 1; + + public partial int P3 { [UnmanagedCallersOnly] get; } // 3 + public partial int P3 => 1; + + public partial int P4 { get; } + public partial int P4 { [UnmanagedCallersOnly] get => 1; } // 4 + } + """; + + var comp = CreateCompilation([source, UnmanagedCallersOnlyAttributeDefinition]); + comp.VerifyEmitDiagnostics( + // (5,6): error CS0592: Attribute 'UnmanagedCallersOnly' is not valid on this declaration type. It is only valid on 'method' declarations. + // [UnmanagedCallersOnly] // 1 + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "UnmanagedCallersOnly").WithArguments("UnmanagedCallersOnly", "method").WithLocation(5, 6), + // (10,6): error CS0592: Attribute 'UnmanagedCallersOnly' is not valid on this declaration type. It is only valid on 'method' declarations. + // [UnmanagedCallersOnly] // 2 + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "UnmanagedCallersOnly").WithArguments("UnmanagedCallersOnly", "method").WithLocation(10, 6), + // (13,30): error CS8896: 'UnmanagedCallersOnly' can only be applied to ordinary static non-abstract, non-virtual methods or static local functions. + // public partial int P3 { [UnmanagedCallersOnly] get; } // 3 + Diagnostic(ErrorCode.ERR_UnmanagedCallersOnlyRequiresStatic, "UnmanagedCallersOnly").WithLocation(13, 30), + // (17,30): error CS8896: 'UnmanagedCallersOnly' can only be applied to ordinary static non-abstract, non-virtual methods or static local functions. + // public partial int P4 { [UnmanagedCallersOnly] get => 1; } // 4 + Diagnostic(ErrorCode.ERR_UnmanagedCallersOnlyRequiresStatic, "UnmanagedCallersOnly").WithLocation(17, 30)); + } + // PROTOTYPE(partial-properties): override partial property where base has modopt - // PROTOTYPE(partial-properties): test indexers incl parameters with attributes - // PROTOTYPE(partial-properties): test merging property attributes - // PROTOTYPE(partial-properties): [UnscopedRef]+scoped difference across partials // PROTOTYPE(partial-properties): test that doc comments work consistently with partial methods (and probably spec it as well) - // PROTOTYPE(partial-properties): test CallerInfo attributes applied to either definition or implementation part } } diff --git a/src/Compilers/Test/Utilities/CSharp/Extensions.cs b/src/Compilers/Test/Utilities/CSharp/Extensions.cs index 76fa6c8d66365..6b4e838a76726 100644 --- a/src/Compilers/Test/Utilities/CSharp/Extensions.cs +++ b/src/Compilers/Test/Utilities/CSharp/Extensions.cs @@ -416,6 +416,9 @@ public static void VerifyNamedArgumentValue(this CSharpAttributeData attr, in Assert.True(IsEqual(arg, v)); } + public static ImmutableArray ToStrings(this ImmutableArray attributes) + => attributes.SelectAsArray(a => a.ToString()); + internal static bool IsEqual(TypedConstant arg, object expected) { switch (arg.Kind)