Skip to content

Commit 69b7f75

Browse files
authored
Add warning when property has a backing field but a manually implemented accessor does not use it (#75325)
1 parent 5a5854a commit 69b7f75

27 files changed

+957
-92
lines changed

src/Compilers/CSharp/Portable/CSharpResources.resx

+6
Original file line numberDiff line numberDiff line change
@@ -8008,4 +8008,10 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
80088008
<data name="IDS_FeatureFirstClassSpan" xml:space="preserve">
80098009
<value>first-class Span types</value>
80108010
</data>
8011+
<data name="WRN_AccessorDoesNotUseBackingField" xml:space="preserve">
8012+
<value>The '{0}' accessor of property '{1}' should use 'field' because the other accessor is using it.</value>
8013+
</data>
8014+
<data name="WRN_AccessorDoesNotUseBackingField_Title" xml:space="preserve">
8015+
<value>Property accessor should use 'field' because the other accessor is using it.</value>
8016+
</data>
80118017
</root>

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

+1
Original file line numberDiff line numberDiff line change
@@ -2347,6 +2347,7 @@ internal enum ErrorCode
23472347

23482348
WRN_UninitializedNonNullableBackingField = 9264,
23492349
WRN_UnassignedInternalRefField = 9265,
2350+
WRN_AccessorDoesNotUseBackingField = 9266,
23502351

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

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

+2
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@ internal static int GetWarningLevel(ErrorCode code)
564564
case ErrorCode.WRN_PartialPropertySignatureDifference:
565565
case ErrorCode.WRN_FieldIsAmbiguous:
566566
case ErrorCode.WRN_UninitializedNonNullableBackingField:
567+
case ErrorCode.WRN_AccessorDoesNotUseBackingField:
567568
return 1;
568569
default:
569570
return 0;
@@ -2464,6 +2465,7 @@ or ErrorCode.ERR_CannotApplyOverloadResolutionPriorityToMember
24642465
or ErrorCode.ERR_PartialPropertyDuplicateInitializer
24652466
or ErrorCode.WRN_UninitializedNonNullableBackingField
24662467
or ErrorCode.WRN_UnassignedInternalRefField
2468+
or ErrorCode.WRN_AccessorDoesNotUseBackingField
24672469
=> false,
24682470
};
24692471
#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.

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

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

+17-10
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ private static SourcePropertySymbol Create(
4343
out bool isExpressionBodied,
4444
out bool hasGetAccessorImplementation,
4545
out bool hasSetAccessorImplementation,
46-
out bool usesFieldKeyword,
46+
out bool getterUsesFieldKeyword,
47+
out bool setterUsesFieldKeyword,
4748
out var getSyntax,
4849
out var setSyntax);
4950

50-
Debug.Assert(!usesFieldKeyword ||
51+
Debug.Assert(!(getterUsesFieldKeyword || setterUsesFieldKeyword) ||
5152
((CSharpParseOptions)syntax.SyntaxTree.Options).IsFeatureEnabled(MessageID.IDS_FeatureFieldKeyword));
5253

5354
bool accessorsHaveImplementation = hasGetAccessorImplementation || hasSetAccessorImplementation;
@@ -90,7 +91,8 @@ private static SourcePropertySymbol Create(
9091
hasAutoPropertySet: hasAutoPropertySet,
9192
isExpressionBodied: isExpressionBodied,
9293
accessorsHaveImplementation: accessorsHaveImplementation,
93-
usesFieldKeyword: usesFieldKeyword,
94+
getterUsesFieldKeyword: getterUsesFieldKeyword,
95+
setterUsesFieldKeyword: setterUsesFieldKeyword,
9496
memberName,
9597
location,
9698
diagnostics);
@@ -110,7 +112,8 @@ private SourcePropertySymbol(
110112
bool hasAutoPropertySet,
111113
bool isExpressionBodied,
112114
bool accessorsHaveImplementation,
113-
bool usesFieldKeyword,
115+
bool getterUsesFieldKeyword,
116+
bool setterUsesFieldKeyword,
114117
string memberName,
115118
Location location,
116119
BindingDiagnosticBag diagnostics)
@@ -129,7 +132,8 @@ private SourcePropertySymbol(
129132
hasAutoPropertySet: hasAutoPropertySet,
130133
isExpressionBodied: isExpressionBodied,
131134
accessorsHaveImplementation: accessorsHaveImplementation,
132-
usesFieldKeyword: usesFieldKeyword,
135+
getterUsesFieldKeyword: getterUsesFieldKeyword,
136+
setterUsesFieldKeyword: setterUsesFieldKeyword,
133137
syntax.Type.SkipScoped(out _).GetRefKindInLocalOrReturn(diagnostics),
134138
memberName,
135139
syntax.AttributeLists,
@@ -214,7 +218,8 @@ private static void GetAccessorDeclarations(
214218
out bool isExpressionBodied,
215219
out bool hasGetAccessorImplementation,
216220
out bool hasSetAccessorImplementation,
217-
out bool usesFieldKeyword,
221+
out bool getterUsesFieldKeyword,
222+
out bool setterUsesFieldKeyword,
218223
out AccessorDeclarationSyntax? getSyntax,
219224
out AccessorDeclarationSyntax? setSyntax)
220225
{
@@ -225,7 +230,8 @@ private static void GetAccessorDeclarations(
225230

226231
if (!isExpressionBodied)
227232
{
228-
usesFieldKeyword = false;
233+
getterUsesFieldKeyword = false;
234+
setterUsesFieldKeyword = false;
229235
hasGetAccessorImplementation = false;
230236
hasSetAccessorImplementation = false;
231237
foreach (var accessor in syntax.AccessorList!.Accessors)
@@ -237,6 +243,7 @@ private static void GetAccessorDeclarations(
237243
{
238244
getSyntax = accessor;
239245
hasGetAccessorImplementation = hasImplementation(accessor);
246+
getterUsesFieldKeyword = containsFieldExpressionInAccessor(accessor);
240247
}
241248
else
242249
{
@@ -249,6 +256,7 @@ private static void GetAccessorDeclarations(
249256
{
250257
setSyntax = accessor;
251258
hasSetAccessorImplementation = hasImplementation(accessor);
259+
setterUsesFieldKeyword = containsFieldExpressionInAccessor(accessor);
252260
}
253261
else
254262
{
@@ -266,16 +274,15 @@ private static void GetAccessorDeclarations(
266274
default:
267275
throw ExceptionUtilities.UnexpectedValue(accessor.Kind());
268276
}
269-
270-
usesFieldKeyword = usesFieldKeyword || containsFieldExpressionInAccessor(accessor);
271277
}
272278
}
273279
else
274280
{
275281
var body = GetArrowExpression(syntax);
276282
hasGetAccessorImplementation = body is object;
277283
hasSetAccessorImplementation = false;
278-
usesFieldKeyword = body is { } && containsFieldExpressionInGreenNode(body.Green);
284+
getterUsesFieldKeyword = body is { } && containsFieldExpressionInGreenNode(body.Green);
285+
setterUsesFieldKeyword = false;
279286
Debug.Assert(hasGetAccessorImplementation); // it's not clear how this even parsed as a property if it has no accessor list and no arrow expression.
280287
}
281288

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

+62-12
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ private enum Flags : ushort
3131
IsExpressionBodied = 1 << 0,
3232
HasAutoPropertyGet = 1 << 1,
3333
HasAutoPropertySet = 1 << 2,
34-
UsesFieldKeyword = 1 << 3,
35-
IsExplicitInterfaceImplementation = 1 << 4,
36-
HasInitializer = 1 << 5,
37-
AccessorsHaveImplementation = 1 << 6,
38-
HasExplicitAccessModifier = 1 << 7,
39-
RequiresBackingField = 1 << 8,
34+
GetterUsesFieldKeyword = 1 << 3,
35+
SetterUsesFieldKeyword = 1 << 4,
36+
IsExplicitInterfaceImplementation = 1 << 5,
37+
HasInitializer = 1 << 6,
38+
AccessorsHaveImplementation = 1 << 7,
39+
HasExplicitAccessModifier = 1 << 8,
40+
RequiresBackingField = 1 << 9,
4041
}
4142

4243
// TODO (tomat): consider splitting into multiple subclasses/rare data.
@@ -90,7 +91,8 @@ protected SourcePropertySymbolBase(
9091
bool hasAutoPropertySet,
9192
bool isExpressionBodied,
9293
bool accessorsHaveImplementation,
93-
bool usesFieldKeyword,
94+
bool getterUsesFieldKeyword,
95+
bool setterUsesFieldKeyword,
9496
RefKind refKind,
9597
string memberName,
9698
SyntaxList<AttributeListSyntax> indexerNameAttributeLists,
@@ -133,9 +135,14 @@ protected SourcePropertySymbolBase(
133135
_propertyFlags |= Flags.HasAutoPropertySet;
134136
}
135137

136-
if (usesFieldKeyword)
138+
if (getterUsesFieldKeyword)
137139
{
138-
_propertyFlags |= Flags.UsesFieldKeyword;
140+
_propertyFlags |= Flags.GetterUsesFieldKeyword;
141+
}
142+
143+
if (setterUsesFieldKeyword)
144+
{
145+
_propertyFlags |= Flags.SetterUsesFieldKeyword;
139146
}
140147

141148
if (hasInitializer)
@@ -171,7 +178,7 @@ protected SourcePropertySymbolBase(
171178
_name = _lazySourceName = memberName;
172179
}
173180

174-
if (usesFieldKeyword || hasAutoPropertyGet || hasAutoPropertySet || hasInitializer)
181+
if (getterUsesFieldKeyword || setterUsesFieldKeyword || hasAutoPropertyGet || hasAutoPropertySet || hasInitializer)
175182
{
176183
Debug.Assert(!IsIndexer);
177184
_propertyFlags |= Flags.RequiresBackingField;
@@ -305,6 +312,48 @@ protected void CheckInitializerIfNeeded(BindingDiagnosticBag diagnostics)
305312
}
306313
}
307314

315+
#nullable enable
316+
private static void CheckFieldKeywordUsage(SourcePropertySymbolBase property, BindingDiagnosticBag diagnostics)
317+
{
318+
Debug.Assert(property.PartialImplementationPart is null);
319+
if (!property.DeclaringCompilation.IsFeatureEnabled(MessageID.IDS_FeatureFieldKeyword))
320+
{
321+
return;
322+
}
323+
324+
SourcePropertyAccessorSymbol? accessorToBlame = null;
325+
var propertyFlags = property._propertyFlags;
326+
var getterUsesFieldKeyword = (propertyFlags & Flags.GetterUsesFieldKeyword) != 0;
327+
var setterUsesFieldKeyword = (propertyFlags & Flags.SetterUsesFieldKeyword) != 0;
328+
if (property._setMethod is { IsAutoPropertyAccessor: false } setMethod
329+
&& !setterUsesFieldKeyword
330+
&& !property.IsSetOnEitherPart(Flags.HasInitializer)
331+
&& (property.HasAutoPropertyGet || getterUsesFieldKeyword))
332+
{
333+
accessorToBlame = setMethod;
334+
}
335+
else if (property._getMethod is { IsAutoPropertyAccessor: false } getMethod
336+
&& !getterUsesFieldKeyword
337+
&& (property.HasAutoPropertySet || setterUsesFieldKeyword))
338+
{
339+
accessorToBlame = getMethod;
340+
}
341+
342+
if (accessorToBlame is not null)
343+
{
344+
var accessorName = accessorToBlame switch
345+
{
346+
{ MethodKind: MethodKind.PropertyGet, IsInitOnly: false } => SyntaxFacts.GetText(SyntaxKind.GetKeyword),
347+
{ MethodKind: MethodKind.PropertySet, IsInitOnly: false } => SyntaxFacts.GetText(SyntaxKind.SetKeyword),
348+
{ MethodKind: MethodKind.PropertySet, IsInitOnly: true } => SyntaxFacts.GetText(SyntaxKind.InitKeyword),
349+
_ => throw ExceptionUtilities.UnexpectedValue(accessorToBlame)
350+
};
351+
352+
diagnostics.Add(ErrorCode.WRN_AccessorDoesNotUseBackingField, accessorToBlame.GetFirstLocation(), accessorName, property);
353+
}
354+
}
355+
#nullable disable
356+
308357
public sealed override RefKind RefKind
309358
{
310359
get
@@ -648,10 +697,10 @@ public bool HasSkipLocalsInitAttribute
648697
}
649698

650699
internal bool IsAutoPropertyOrUsesFieldKeyword
651-
=> IsSetOnEitherPart(Flags.HasAutoPropertyGet | Flags.HasAutoPropertySet | Flags.UsesFieldKeyword);
700+
=> IsSetOnEitherPart(Flags.HasAutoPropertyGet | Flags.HasAutoPropertySet | Flags.GetterUsesFieldKeyword | Flags.SetterUsesFieldKeyword);
652701

653702
internal bool UsesFieldKeyword
654-
=> IsSetOnEitherPart(Flags.UsesFieldKeyword);
703+
=> IsSetOnEitherPart(Flags.GetterUsesFieldKeyword | Flags.SetterUsesFieldKeyword);
655704

656705
protected bool HasExplicitAccessModifier
657706
=> (_propertyFlags & Flags.HasExplicitAccessModifier) != 0;
@@ -811,6 +860,7 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions,
811860
this.CheckModifiers(isExplicitInterfaceImplementation, Location, IsIndexer, diagnostics);
812861

813862
CheckInitializerIfNeeded(diagnostics);
863+
CheckFieldKeywordUsage((SourcePropertySymbolBase?)PartialImplementationPart ?? this, diagnostics);
814864

815865
if (RefKind != RefKind.None && IsRequired)
816866
{

src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public SynthesizedRecordEqualityContractProperty(SourceMemberContainerTypeSymbol
3636
hasAutoPropertySet: false,
3737
isExpressionBodied: false,
3838
accessorsHaveImplementation: true,
39-
usesFieldKeyword: false,
39+
getterUsesFieldKeyword: false,
40+
setterUsesFieldKeyword: false,
4041
RefKind.None,
4142
PropertyName,
4243
indexerNameAttributeLists: new SyntaxList<AttributeListSyntax>(),

src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public SynthesizedRecordPropertySymbol(
3434
hasAutoPropertySet: true,
3535
isExpressionBodied: false,
3636
accessorsHaveImplementation: true,
37-
usesFieldKeyword: false,
37+
getterUsesFieldKeyword: false,
38+
setterUsesFieldKeyword: false,
3839
RefKind.None,
3940
backingParameter.Name,
4041
indexerNameAttributeLists: new SyntaxList<AttributeListSyntax>(),

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

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)