Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ internal static Expression Create(ExpressionNodeInfo info)
case SyntaxKind.ThisExpression:
return This.CreateExplicit(info);

case SyntaxKind.FieldExpression:
return PropertyFieldAccess.Create(info);

case SyntaxKind.AddressOfExpression:
return Unary.Create(info.SetKind(ExprKind.ADDRESS_OF));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.IO;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Extraction.Kinds;

namespace Semmle.Extraction.CSharp.Entities.Expressions
{
internal class PropertyFieldAccess : Expression<FieldExpressionSyntax>
{
private PropertyFieldAccess(ExpressionNodeInfo info) : base(info.SetKind(ExprKind.FIELD_ACCESS)) { }

public static Expression Create(ExpressionNodeInfo info) => new PropertyFieldAccess(info).TryPopulate();

protected override void PopulateExpression(TextWriter trapFile)
{
var symbolInfo = Context.GetSymbolInfo(Syntax);
if (symbolInfo.Symbol is IFieldSymbol field)
{
var target = PropertyField.Create(Context, field);
trapFile.expr_access(this, target);
if (!field.IsStatic)
{
This.CreateImplicit(Context, field.ContainingType, Location, this, -1);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Semmle.Extraction.CSharp.Entities
{
internal class Field : CachedSymbol<IFieldSymbol>, IExpressionParentEntity
{
private Field(Context cx, IFieldSymbol init)
protected Field(Context cx, IFieldSymbol init)
: base(cx, init)
{
type = new Lazy<Type>(() => Entities.Type.Create(cx, Symbol.Type));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.IO;
using Microsoft.CodeAnalysis;
using Semmle.Extraction.CSharp.Util;
using Semmle.Extraction.Kinds;

namespace Semmle.Extraction.CSharp.Entities
{
/// <summary>
/// Represents the autogenerated backing field `field` for a property.
/// It is only created for properties that use the `field` keyword in their getter or setter, and
/// is not created for auto-properties.
/// </summary>
internal class PropertyField : Field
{
protected PropertyField(Context cx, IFieldSymbol init)
: base(cx, init)
{
}

public static new PropertyField Create(Context cx, IFieldSymbol field) => PropertyFieldFactory.Instance.CreateEntity(cx, (field, field.AssociatedSymbol), field);

public override bool NeedsPopulation => true;

public override void Populate(TextWriter trapFile)
{
PopulateNullability(trapFile, Symbol.GetAnnotatedType());

var unboundFieldKey = PropertyField.Create(Context, Symbol.OriginalDefinition);
var name = Symbol.AssociatedSymbol is not null ? $"{Symbol.AssociatedSymbol.GetName()}.field" : Symbol.Name;
trapFile.fields(this, VariableKind.None, name, ContainingType!, Type.TypeRef, unboundFieldKey);
Comment on lines +26 to +30
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PropertyField.Populate only emits the fields row (plus nullability/location) but does not extract attributes/modifiers/ref-kind like Field.Populate does. As a result the synthetic backing field will be missing has_modifiers (e.g. private/static/readonly/required) and any [field: ...] attributes that Roslyn attaches to the backing field symbol. Consider reusing the same extraction steps as Field.Populate (at least PopulateAttributes() + PopulateModifiers(trapFile) and PopulateRefKind).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, it is a really good point that we should extract the modifiers. For static properties, the generated backing field is also static (which might be relevant - otherwise it looks like a static property accesses a non-static field).
However,

  • Properties can't be ref, in, out etc, so there is no need to attempt to extract such information.
  • Also, I don't see the need to extracting attributes either.

trapFile.compiler_generated(this);

PopulateModifiers(trapFile);

if (Context.OnlyScaffold)
{
return;
}

if (Context.ExtractLocation(Symbol))
{
WriteLocationsToTrap(trapFile.field_location, this, Locations);
}
}

private class PropertyFieldFactory : CachedEntityFactory<IFieldSymbol, PropertyField>
{
public static PropertyFieldFactory Instance { get; } = new PropertyFieldFactory();

public override PropertyField Create(Context cx, IFieldSymbol init) => new PropertyField(cx, init);
}
}
}
4 changes: 4 additions & 0 deletions csharp/ql/lib/change-notes/2026-02-12-field-keyword.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* C# 14: Added support for the `field` keyword in properties.
62 changes: 62 additions & 0 deletions csharp/ql/test/library-tests/dataflow/fields/D.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,65 @@ public static void Sink(object o) { }

static T Source<T>(object source) => throw null;
}

public class DFieldProps
{
object FieldProp0
{
get { return field; }
set { field = value; }
} = Source<object>(0);

object FieldProp1
{
get { return field; }
set { field = value; }
}

object FieldProp2
{
get { return field; }
set
{
var x = value;
field = x;
}
}

static object StaticFieldProp
{
get { return field; }
set { field = value; }
}

private void M()
{
var d0 = new DFieldProps();
Sink(d0.FieldProp0); // $ hasValueFlow=0
Sink(d0.FieldProp1); // no flow
Sink(d0.FieldProp2); // no flow

var d1 = new DFieldProps();
var o1 = Source<object>(1);
d1.FieldProp1 = o1;
Sink(d1.FieldProp0); // $ hasValueFlow=0
Sink(d1.FieldProp1); // $ hasValueFlow=1
Sink(d1.FieldProp2); // no flow

var d2 = new DFieldProps();
var o2 = Source<object>(2);
d2.FieldProp2 = o2;
Sink(d2.FieldProp0); // $ hasValueFlow=0
Sink(d2.FieldProp1); // no flow
Sink(d2.FieldProp2); // $ hasValueFlow=2

var o3 = Source<object>(3);
DFieldProps.StaticFieldProp = o3;
Sink(DFieldProps.StaticFieldProp); // $ hasValueFlow=3
}

public static void Sink(object o) { }

static T Source<T>(object source) => throw null;

}
Loading