? existingNames = null)
{
+ // A property backing field that stayed declared (the property is not an
+ // auto-property and the "field" keyword is unavailable or insufficient) gets a
+ // readable name instead of the generic escape of "k__BackingField". The mapping
+ // is name-deterministic like the escape itself, so declarations and references
+ // stay consistent without symbol tracking.
+ if (s.StartsWith("<", System.StringComparison.Ordinal)
+ && PatternStatementTransform.NameCouldBeBackingFieldOfAutomaticProperty(s, out string? propertyName))
+ {
+ var readable = ReplaceInvalid(propertyName) + "__BackingField";
+ // "P__BackingField" is a plausible identifier a human could have written, unlike
+ // the generic escape it replaces. If the tree already contains that name, the
+ // rename would produce two members with one name; fall back to the escape, which
+ // keeps the output compilable at the cost of readability.
+ if (existingNames?.Contains(readable) != true)
+ return readable;
+ }
string name = string.Concat(s.Select(ch => IsValid(ch) ? ch.ToString() : string.Format("_{0:X4}", (int)ch)));
if (name.Length >= 1 && !(char.IsLetter(name[0]) || name[0] == '_'))
name = "_" + name;
@@ -54,9 +71,13 @@ string ReplaceInvalid(string s)
public void Run(AstNode rootNode, TransformContext context)
{
+ // Every name already in the tree, so the readable backing-field rename can detect
+ // that its target name is taken.
+ var existingNames = new HashSet(
+ rootNode.DescendantsAndSelf.OfType().Select(i => i.Name), StringComparer.Ordinal);
foreach (var ident in rootNode.DescendantsAndSelf.OfType())
{
- string newName = ReplaceInvalid(ident.Name);
+ string newName = ReplaceInvalid(ident.Name, existingNames);
if (newName != ident.Name)
{
context.Step($"Escape identifier '{ident.Name}'", ident);
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
index 41e7a28b89..2f4adf6e40 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
@@ -755,6 +755,8 @@ bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCom
IProperty? property = propertyDeclaration.GetSymbol() as IProperty;
if (property == null)
return null;
+ if (context.Settings.FieldKeyword)
+ return TransformFieldBackedProperty(propertyDeclaration, property);
if (!CanTransformToAutomaticProperty(property, !(property.DeclaringTypeDefinition?.Fields.Any(f => f.Name == "_" + property.Name && f.IsCompilerGenerated()) ?? false)))
return null;
IField? field = null;
@@ -773,6 +775,9 @@ bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCom
}
if (field == null || !NameCouldBeBackingFieldOfAutomaticProperty(field.Name, out _))
return null;
+ // In generic types the accessor bodies reference the field specialized by the
+ // type's own type parameters; the field declaration's symbol is the definition.
+ field = (IField)field.MemberDefinition;
if (propertyDeclaration.Setter?.HasModifier(Modifiers.Readonly) == true || (propertyDeclaration.HasModifier(Modifiers.Readonly) && propertyDeclaration.Setter is not null))
return null;
if (field.IsCompilerGenerated() && field.DeclaringTypeDefinition == property.DeclaringTypeDefinition)
@@ -814,6 +819,227 @@ bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCom
return null;
}
+ static readonly BlockStatement trivialFieldGetterBody = new BlockStatement {
+ new ReturnStatement {
+ Expression = new NamedNode("fieldReference", new IdentifierExpression("field"))
+ }
+ };
+
+ static readonly BlockStatement trivialFieldSetterBody = new BlockStatement {
+ new AssignmentExpression {
+ Left = new NamedNode("fieldReference", new IdentifierExpression("field")),
+ Right = new IdentifierExpression("value")
+ }
+ };
+
+ ///
+ /// Handles all field-backed properties when the C# 14 "field" keyword is available:
+ /// accessor bodies already refer to the backing field as "field" (see
+ /// ExpressionBuilder.ConvertField), so compiler-generated trivial accessors collapse
+ /// individually to "get;"/"set;" and the backing-field declaration disappears, with its
+ /// attributes re-hosted as "field:" sections on the property.
+ ///
+ PropertyDeclaration? TransformFieldBackedProperty(PropertyDeclaration propertyDeclaration, IProperty property)
+ {
+ if (!TryGetBackingField(property, out var field))
+ return null;
+ if (!OutsideReferencesAreExpressible(propertyDeclaration, field))
+ {
+ // The field stays declared, so the "field" keyword references emitted by
+ // ExpressionBuilder.ConvertField have to become ordinary field references again.
+ foreach (var identifierExpression in propertyDeclaration.Descendants.OfType())
+ {
+ if (identifierExpression.Identifier == "field"
+ && identifierExpression.GetSymbol() is IField referencedField
+ && field.Equals(referencedField.MemberDefinition))
+ {
+ identifierExpression.Identifier = field.Name;
+ }
+ }
+ return null;
+ }
+ context.Step("Transform field-backed property", propertyDeclaration);
+ var getter = propertyDeclaration.Getter;
+ var setter = propertyDeclaration.Setter;
+ if (context.Settings.AutomaticProperties)
+ {
+ // VB auto-properties do not mark their accessors [CompilerGenerated]; the
+ // pre-C# 14 transform recognizes them by their "_" backing field
+ // instead. Keep that rule, or a VB auto-property grows explicit "field"
+ // accessors where every other compiler's collapses to "{ get; set; }".
+ bool accessorsMustBeCompilerGenerated = field.Name != "_" + property.Name;
+ CollapseTrivialAccessor(getter, trivialFieldGetterBody, field, accessorsMustBeCompilerGenerated);
+ // A readonly setter cannot become an auto-accessor (same rule as the pre-C# 14
+ // transform); readonly getters collapse fine because auto-getters are
+ // implicitly readonly.
+ if (setter?.HasModifier(Modifiers.Readonly) != true
+ && !(propertyDeclaration.HasModifier(Modifiers.Readonly) && setter is not null))
+ {
+ CollapseTrivialAccessor(setter, trivialFieldSetterBody, field, accessorsMustBeCompilerGenerated);
+ }
+ }
+ if (getter?.Body == null && setter?.Body == null)
+ {
+ // The property became a full auto-property; readonly is implied like before.
+ propertyDeclaration.Modifiers &= ~Modifiers.Readonly;
+ if (getter is not null)
+ getter.Modifiers &= ~Modifiers.Readonly;
+ }
+ var fieldDecl = propertyDeclaration.Parent?.Children.OfType()
+ .FirstOrDefault(fd => field.Equals(fd.GetSymbol()));
+ if (fieldDecl != null)
+ {
+ fieldDecl.Remove();
+ CSharpDecompiler.RemoveAttribute(fieldDecl, KnownAttribute.CompilerGenerated);
+ CSharpDecompiler.RemoveAttribute(fieldDecl, KnownAttribute.DebuggerBrowsable);
+ foreach (var section in fieldDecl.Attributes)
+ {
+ section.AttributeTarget = "field";
+ propertyDeclaration.Attributes.Add(section.Detach());
+ }
+ }
+ return null;
+ }
+
+ void CollapseTrivialAccessor(Accessor? accessor, BlockStatement pattern, IField field,
+ bool accessorMustBeCompilerGenerated)
+ {
+ if (accessor?.Body is null)
+ return;
+ if (accessorMustBeCompilerGenerated
+ && (accessor.GetSymbol() is not IMethod method || !method.IsCompilerGenerated()))
+ {
+ return;
+ }
+ Match m = pattern.Match(accessor.Body);
+ if (!m.Success)
+ return;
+ if (m.Get("fieldReference").Single().GetSymbol() is not IField referencedField
+ || !field.Equals(referencedField.MemberDefinition))
+ {
+ return;
+ }
+ RemoveCompilerGeneratedAttribute(accessor.Attributes);
+ // Auto-accessors are implicitly readonly.
+ accessor.Modifiers &= ~Modifiers.Readonly;
+ // Clearing the accessor body turns it into an auto-property accessor.
+ accessor.Body = null;
+ }
+
+ internal static bool TryGetBackingField(IProperty property, [NotNullWhen(true)] out IField? field)
+ {
+ field = null;
+ if (property.Parameters.Count > 0 || property.DeclaringTypeDefinition == null)
+ return false;
+ // A type definition's fields are unspecialized, so compare against the property
+ // DEFINITION's return type; a specialized property in a generic type would
+ // otherwise never match its own backing field.
+ var propertyType = ((IProperty)property.MemberDefinition).ReturnType;
+ foreach (var candidate in property.DeclaringTypeDefinition.Fields)
+ {
+ if (candidate.IsCompilerGenerated()
+ && candidate.IsStatic == property.IsStatic
+ // The trivial accessor bodies of a classic auto-property guaranteed this
+ // structurally; arbitrary accessor bodies do not. A field of a different
+ // type is not this property's storage, and removing it while printing
+ // `field` would substitute storage of the property's type instead.
+ && candidate.Type.Equals(propertyType)
+ && NameCouldBeBackingFieldOfAutomaticProperty(candidate.Name, out var propertyName)
+ && propertyName == property.Name)
+ {
+ field = candidate;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// The "field" keyword cannot express backing-field accesses outside the owning
+ /// property's accessors. The only shapes C# can express are stores in a constructor of
+ /// the declaring type (property initializers, or assignments to setter-less
+ /// properties); anything else means the field declaration has to be kept.
+ ///
+ bool OutsideReferencesAreExpressible(AstNode nodeInTree, IField field)
+ {
+ var root = nodeInTree.Ancestors.LastOrDefault() ?? nodeInTree;
+ if (outsideReferenceRoot != root)
+ {
+ outsideReferenceRoot = root;
+ outsideReferenceVerdicts = BuildOutsideReferenceIndex(root);
+ }
+ // Absent means no reference to this field was found outside its own property.
+ return !outsideReferenceVerdicts!.TryGetValue((IField)field.MemberDefinition, out bool expressible)
+ || expressible;
+ }
+
+ // The verdict per backing field for one syntax tree. Answering each property with its
+ // own full walk made whole-module output quadratic in the number of properties, which
+ // a property-heavy assembly feels as minutes instead of seconds. Later transforms only
+ // ever REMOVE references to the field of the property they are rewriting, so a verdict
+ // computed up front stays valid for every other property in the tree.
+ AstNode? outsideReferenceRoot;
+ Dictionary? outsideReferenceVerdicts;
+
+ ///
+ /// Whether a constructor store to still has a home once the
+ /// field declaration is gone. Something has to turn it into an initializer or a
+ /// property assignment, and not every store qualifies.
+ ///
+ bool StoreSurvivesAsInitializer(IField field)
+ {
+ // A setter-less property's store is rewritten to a property assignment by
+ // ReplaceBackingFieldUsage, in this same transform - no later transform involved.
+ if (IsBackingFieldOfAutomaticProperty(field, out var property) && !property.CanSet)
+ return true;
+ // Everything else waits for TransformFieldAndConstructorInitializers, which runs
+ // after this transform and declines to move non-constant stores out of an EXPLICIT
+ // static constructor (see its `onlyMoveConstants`). Removing the declaration here
+ // would leave that store referencing a field that no longer exists.
+ if (field.IsStatic && !context.Settings.AlwaysMoveInitializer
+ && !IsBeforeFieldInit(field.DeclaringTypeDefinition))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ bool IsBeforeFieldInit(ITypeDefinition? typeDefinition)
+ {
+ if (typeDefinition?.MetadataToken.IsNil != false)
+ return false;
+ var metadata = context.TypeSystem.MainModule.MetadataFile.Metadata;
+ var td = metadata.GetTypeDefinition((TypeDefinitionHandle)typeDefinition.MetadataToken);
+ return td.HasFlag(System.Reflection.TypeAttributes.BeforeFieldInit);
+ }
+
+ Dictionary BuildOutsideReferenceIndex(AstNode root)
+ {
+ var verdicts = new Dictionary();
+ foreach (var node in root.Descendants)
+ {
+ if (node is not (IdentifierExpression or MemberReferenceExpression))
+ continue;
+ if (node.GetSymbol() is not IField referencedField)
+ continue;
+ var definition = (IField)referencedField.MemberDefinition;
+ // A reference inside the accessors of the field's own property is exactly what
+ // the "field" keyword expresses; anything else is an outside reference.
+ if (node.Ancestors.OfType().FirstOrDefault()?.GetSymbol() is IProperty owner
+ && TryGetBackingField(owner, out var ownerField)
+ && definition.Equals(ownerField.MemberDefinition))
+ {
+ continue;
+ }
+ var enclosingMethod = node.Ancestors.OfType().FirstOrDefault()?.GetSymbol() as IMethod;
+ if (!IsConstructorStore(node, definition, enclosingMethod) || !StoreSurvivesAsInitializer(definition))
+ verdicts[definition] = false;
+ else if (!verdicts.ContainsKey(definition))
+ verdicts[definition] = true;
+ }
+ return verdicts;
+ }
+
static void RemoveCompilerGeneratedAttribute(AstNodeCollection attributeSections)
{
RemoveCompilerGeneratedAttribute(attributeSections, "System.Runtime.CompilerServices.CompilerGeneratedAttribute");
@@ -839,7 +1065,7 @@ static void RemoveCompilerGeneratedAttribute(AstNodeCollection
public override AstNode VisitIdentifier(Identifier identifier)
{
- if (context.Settings.AutomaticProperties)
+ if (context.Settings.AutomaticProperties || context.Settings.FieldKeyword)
{
var newIdentifier = ReplaceBackingFieldUsage(identifier);
if (newIdentifier != null)
@@ -875,7 +1101,7 @@ internal static bool IsBackingFieldOfAutomaticProperty(IField field, [NotNullWhe
static readonly System.Text.RegularExpressions.Regex automaticPropertyBackingFieldNameRegex
= new System.Text.RegularExpressions.Regex(@"^(<(?.+)>k__BackingField|_(?.+))$");
- static bool NameCouldBeBackingFieldOfAutomaticProperty(string name, [NotNullWhen(true)] out string? propertyName)
+ internal static bool NameCouldBeBackingFieldOfAutomaticProperty(string name, [NotNullWhen(true)] out string? propertyName)
{
propertyName = null;
var m = automaticPropertyBackingFieldNameRegex.Match(name);
@@ -894,11 +1120,31 @@ static bool NameCouldBeBackingFieldOfAutomaticProperty(string name, [NotNullWhen
return null;
var mrr = parent.Annotation();
if (mrr?.Member is IField field && IsBackingFieldOfAutomaticProperty(field, out var property)
- && CanTransformToAutomaticProperty(property, !(field.IsCompilerGenerated() && field.Name == "_" + property.Name))
&& currentMethod?.AccessorOwner != property)
{
- if (!property.CanSet && !context.Settings.GetterOnlyAutomaticProperties)
+ if (CanTransformToAutomaticProperty(property, !(field.IsCompilerGenerated() && field.Name == "_" + property.Name)))
+ {
+ if (!property.CanSet && !context.Settings.GetterOnlyAutomaticProperties && !context.Settings.FieldKeyword)
+ return null;
+ }
+ else if (context.Settings.FieldKeyword && !property.CanSet && IsConstructorStoreTarget(parent, field)
+ && BackingFieldWillBeRemoved(property, field, parent))
+ {
+ // A direct store to the backing field of a setter-less field-backed
+ // property is expressible as a property assignment in a constructor -
+ // but only where the property declaration actually becomes field-backed.
+ // If TransformFieldBackedProperty bails, the property keeps explicit
+ // accessors and no setter, so assigning it would not compile (CS0200).
+ }
+ else
+ {
+ // Stores that initialize a field-backed property with a setter are left
+ // as field references and lifted into the property initializer by
+ // TransformFieldAndConstructorInitializers (a property assignment would
+ // invoke the setter); everything else is inexpressible with the "field"
+ // keyword and keeps the field declared.
return null;
+ }
context.Step("Replace backing field use with property", identifier);
parent.RemoveAnnotations();
parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, property));
@@ -908,6 +1154,47 @@ static bool NameCouldBeBackingFieldOfAutomaticProperty(string name, [NotNullWhen
return null;
}
+ bool IsConstructorStoreTarget(AstNode node, IField field)
+ => IsConstructorStore(node, field, currentMethod);
+
+ ///
+ /// Whether will remove this field's
+ /// declaration. Rewriting a store before knowing that produces an assignment to a
+ /// property that keeps explicit, setter-less accessors.
+ ///
+ bool BackingFieldWillBeRemoved(IProperty property, IField field, AstNode nodeInTree)
+ {
+ return TryGetBackingField(property, out var backingField)
+ && field.MemberDefinition.Equals(backingField.MemberDefinition)
+ && OutsideReferencesAreExpressible(nodeInTree, backingField);
+ }
+
+ ///
+ /// True when is the left-hand side of a plain assignment to
+ /// inside a constructor of the field's declaring type - the
+ /// only outside reference the "field" keyword can still express (as a property
+ /// initializer, or an assignment to a setter-less property).
+ ///
+ ///
+ /// Shared by , which decides whether the
+ /// field declaration may be removed, and , which
+ /// rewrites the store. The two must agree: if only one of them accepts a store, the
+ /// output either references a removed field or assigns a property that stayed
+ /// field-backed. They differ only in how the enclosing method is known, which is why
+ /// it is a parameter here.
+ ///
+ static bool IsConstructorStore(AstNode node, IField field, IMethod? enclosingMethod)
+ {
+ if (node.Parent is not AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment
+ || assignment.Left != node)
+ {
+ return false;
+ }
+ return enclosingMethod is { IsConstructor: true } ctor
+ && ctor.IsStatic == field.IsStatic
+ && ctor.DeclaringTypeDefinition == field.DeclaringTypeDefinition;
+ }
+
#region Automatic Events
internal static readonly string[] attributeTypesToRemoveFromAutoProperties = new[] {
"System.Runtime.CompilerServices.CompilerGeneratedAttribute",
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs
index 09af9bbfcf..48aefd8731 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs
@@ -187,6 +187,30 @@ class InitializerSequence
return sequence;
}
+ internal static bool IsDefaultValueInitializer(Expression initializer)
+ {
+ if (initializer is DefaultValueExpression)
+ return true;
+ var rr = initializer.GetResolveResult();
+ if (!rr.IsCompileTimeConstant)
+ return false;
+ return rr.ConstantValue switch {
+ null or false or '\0'
+ or (sbyte)0 or (byte)0 or (short)0 or (ushort)0
+ or 0 or 0u or 0L or 0uL => true,
+ // Equality is not identity for these: -0.0 equals 0.0, and 0.00m equals 0m
+ // while carrying a different scale. Both differences are observable
+ // (1.0 / -0.0 is negative infinity; 0.00m prints as "0.00"), so dropping
+ // such an initializer as a redundant default would change behaviour on
+ // recompile. Compare the representation rather than the value.
+ // (GetBytes rather than SingleToInt32Bits: netstandard2.0 lacks the latter.)
+ float f => BitConverter.ToInt32(BitConverter.GetBytes(f), 0) == 0,
+ double d => BitConverter.DoubleToInt64Bits(d) == 0,
+ decimal m => m == 0m && decimal.GetBits(m)[3] == 0,
+ _ => false,
+ };
+ }
+
private static bool CanHaveInitializer(IMember member, ConstructorInitializerAnalyzer context)
{
if (context.MemberToDeclaringSyntaxNodeMap == null)
@@ -195,7 +219,9 @@ private static bool CanHaveInitializer(IMember member, ConstructorInitializerAna
return true;
return declaringSyntaxNode is FieldDeclaration
or PropertyDeclaration { IsAutomaticProperty: true }
- or EventDeclaration;
+ or EventDeclaration
+ // backing-field store of a field-backed property (initializers bypass the setter)
+ || (declaringSyntaxNode is PropertyDeclaration && member is IField);
}
public bool IsMatch(ConstructorDeclaration ctor)
@@ -286,6 +312,22 @@ public bool Analyze(IEnumerable members)
.Where(_ => _.symbol is IMember)
.ToDictionary(_ => (IMember)_.symbol!, _ => _.entity);
+ if (context.Settings.FieldKeyword)
+ {
+ // Constructor stores to the backing field of a field-backed property become
+ // the property's initializer. The field only keeps its own declaration (and
+ // mapping) when the property could not be transformed.
+ foreach (var pd in members.OfType())
+ {
+ if (pd.GetSymbol() is IProperty property
+ && PatternStatementTransform.TryGetBackingField(property, out var backingField)
+ && !MemberToDeclaringSyntaxNodeMap.ContainsKey(backingField))
+ {
+ MemberToDeclaringSyntaxNodeMap.Add(backingField, pd);
+ }
+ }
+ }
+
List constructorsNotChainedWithThis = [];
List allCtors = [];
@@ -586,8 +628,16 @@ public bool MoveFieldInitializersToDeclarations(InitializerSequence sequence, In
}
break;
case PropertyDeclaration pd:
- Debug.Assert(pd.IsAutomaticProperty);
- if (pd.Initializer is null)
+ Debug.Assert(pd.IsAutomaticProperty || member is IField);
+ if (member is IField { DeclaringTypeDefinition.Kind: TypeKind.Struct }
+ && InitializerSequence.IsDefaultValueInitializer(initializer))
+ {
+ // Struct constructors zero-initialize backing fields the constructor
+ // does not assign (auto-default structs); recompilation regenerates
+ // these stores, so they are dropped instead of becoming initializers.
+ context.Step("Drop implicit struct default initialization", stmt);
+ }
+ else if (pd.Initializer is null)
{
context.Step("Move assignment to property initializer", stmt);
var movedInitializer = initializer.Detach();
diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs
index 31082c6244..be0f91eebf 100644
--- a/ICSharpCode.Decompiler/DecompilerSettings.cs
+++ b/ICSharpCode.Decompiler/DecompilerSettings.cs
@@ -854,6 +854,14 @@ public bool LifetimeAnnotations {
[DecompilerSetting(CSharp.LanguageVersion.CSharp14_0)]
public partial bool FirstClassSpanTypes { get; set; }
+ ///
+ /// Gets/Sets whether property accessors should use the C# 14.0 "field" keyword to
+ /// refer to the compiler-generated backing field.
+ ///
+ [Description("DecompilerSettings.FieldKeyword")]
+ [DecompilerSetting(CSharp.LanguageVersion.CSharp14_0)]
+ public partial bool FieldKeyword { get; set; }
+
///
/// Gets/sets whether the decompiler should separate local variable declarations
/// from their initialization.
diff --git a/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs b/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs
index 06acdff08b..2aa1f5a4db 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs
@@ -128,6 +128,14 @@ public VariableScope(ILFunction function, ILTransformContext context, VariableSc
}
this.currentLowerCaseTypeOrMemberNames = currentLowerCaseTypeOrMemberNames.ToImmutableHashSet();
+ if (context.Settings.FieldKeyword
+ && function.Method?.AccessorOwner is IProperty { Parameters.Count: 0 })
+ {
+ // "field" is a keyword in C# 14 property accessors; a local of that name
+ // would shadow the backing field.
+ AddExistingName(reservedVariableNames, "field");
+ }
+
// handle implicit parameters of set or event accessors
if (function.Method != null && IsSetOrEventAccessor(function.Method) && function.Parameters.Count > 0)
{
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index 7135b23dcf..f65ae29f58 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -438,6 +438,9 @@ Are you sure you want to continue?
F#-specific options
+
+ Use the 'field' keyword in property accessors
+
Use file-scoped namespace declarations