Skip to content
Draft
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 @@ -69,6 +69,7 @@ public override void Populate(TextWriter trapFile)
}

Overrides(trapFile);
ExtractRefReturn(trapFile, Symbol, this);

if (Symbol.FromSource() && !HasBody)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Extraction.Kinds;
Expand All @@ -17,6 +18,35 @@ protected ElementAccess(ExpressionNodeInfo info, ExpressionSyntax qualifier, Bra
private readonly ExpressionSyntax qualifier;
private readonly BracketedArgumentListSyntax argumentList;


private IPropertySymbol? GetIndexerSymbol()
{
var symbol = Context.GetSymbolInfo(base.Syntax).Symbol;

if (symbol is IPropertySymbol { IsIndexer: true } indexer)
return indexer;

// In some cases, Roslyn translates the use of range expressions directly into method calls.
// E.g. `a[0..3]` is translated into `a.Slice(0, 3)`, if `a` is a `Span<T>`.
// In this case, we want to populate the indexer access as normal (as this reflects the source code more accurately).
if (symbol is IMethodSymbol method)
{
var indexers = method
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.IsIndexer);

var intIndexer = indexers
.Where(i => i.Parameters.Length == 1 && i.Parameters[0].Type.SpecialType == SpecialType.System_Int32)
.FirstOrDefault();

return intIndexer;
}

return null;
}

protected override void PopulateExpression(TextWriter trapFile)
{
if (Kind == ExprKind.POINTER_INDIRECTION)
Expand All @@ -32,9 +62,8 @@ protected override void PopulateExpression(TextWriter trapFile)
Create(Context, qualifier, this, -1);
PopulateArguments(trapFile, argumentList, 0);

var symbolInfo = Context.GetSymbolInfo(base.Syntax);

if (symbolInfo.Symbol is IPropertySymbol indexer)
var indexer = GetIndexerSymbol();
if (indexer is not null)
{
trapFile.expr_access(this, Indexer.Create(Context, indexer));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Improved call target resolution for ref-return properties and indexers.
4 changes: 4 additions & 0 deletions csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Improved extraction of span range-access expressions (for example, `a[0..3]`) so the indexer is recorded as the call target.
22 changes: 20 additions & 2 deletions csharp/ql/lib/semmle/code/csharp/exprs/Call.qll
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,16 @@ class PropertyCall extends AccessorCall, PropertyAccessExpr {
}

override Accessor getWriteTarget() {
this instanceof AssignableWrite and result = this.getProperty().getSetter()
this instanceof AssignableWrite and
exists(Property p | p = this.getProperty() |
result = p.getSetter()
or
result =
any(Getter g |
g = p.getGetter() and
g.getAnnotatedReturnType().isRef()
)
)
}

override Expr getArgument(int i) {
Expand Down Expand Up @@ -801,7 +810,16 @@ class IndexerCall extends AccessorCall, IndexerAccessExpr {
}

override Accessor getWriteTarget() {
this instanceof AssignableWrite and result = this.getIndexer().getSetter()
this instanceof AssignableWrite and
exists(Indexer i | i = this.getIndexer() |
result = i.getSetter()
or
result =
any(Getter g |
g = i.getGetter() and
g.getAnnotatedReturnType().isRef()
)
)
}

override Expr getArgument(int i) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ returnTypes
| NullableRefTypes.cs:107:26:107:36 | ReturnsRef5 | readonly MyClass! |
| NullableRefTypes.cs:108:26:108:36 | ReturnsRef6 | readonly MyClass! |
| NullableRefTypes.cs:110:10:110:20 | Parameters1 | Void! |
| NullableRefTypes.cs:113:32:113:44 | get_RefProperty | MyClass! |
| NullableRefTypes.cs:113:32:113:44 | get_RefProperty | ref MyClass! |
| NullableRefTypes.cs:116:7:116:23 | <object initializer> | Void |
| NullableRefTypes.cs:116:7:116:23 | ToStringWithTypes | Void! |
| NullableRefTypes.cs:136:7:136:24 | <object initializer> | Void |
Expand Down
4 changes: 4 additions & 0 deletions csharp/ql/test/library-tests/indexers/Indexers13.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
| indexers.cs:24:21:24:24 | Item | indexers.cs:62:22:62:29 | access to indexer | indexers.cs:26:13:26:15 | get_Item |
| indexers.cs:24:21:24:24 | Item | indexers.cs:65:25:65:32 | access to indexer | indexers.cs:34:13:34:15 | set_Item |
| indexers.cs:143:24:143:27 | Item | indexers.cs:156:13:156:16 | access to indexer | indexers.cs:145:13:145:15 | get_Item |
| indexers.cs:143:24:143:27 | Item | indexers.cs:157:21:157:24 | access to indexer | indexers.cs:145:13:145:15 | get_Item |
8 changes: 8 additions & 0 deletions csharp/ql/test/library-tests/indexers/Indexers13.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import csharp

from IndexerCall ic, Indexer i, Accessor target
where
ic.getIndexer() = i and
ic.getTarget() = target and
i.fromSource()
select i, ic, target
54 changes: 54 additions & 0 deletions csharp/ql/test/library-tests/indexers/PrintAst.expected
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,57 @@ indexers.cs:
# 130| 4: [BlockStmt] {...}
# 130| 0: [ReturnStmt] return ...;
# 130| 0: [IntLiteral] 0
# 134| 5: [RefStruct] S
# 136| 6: [Field] x
# 136| -1: [TypeMention] int
# 138| 7: [InstanceConstructor] S
#-----| 2: (Parameters)
# 138| 0: [Parameter] v
# 138| -1: [TypeMention] int
# 139| 4: [BlockStmt] {...}
# 140| 0: [ExprStmt] ...;
# 140| 0: [AssignExpr] ... = ...
# 140| 0: [FieldAccess] access to field x
# 140| 1: [RefExpr] ref ...
# 140| 0: [ParameterAccess] access to parameter v
# 143| 8: [Indexer] Item
# 143| -1: [TypeMention] int
#-----| 1: (Parameters)
# 143| 0: [Parameter] i
# 143| -1: [TypeMention] int
# 145| 3: [Getter] get_Item
#-----| 2: (Parameters)
# 143| 0: [Parameter] i
# 145| 4: [BlockStmt] {...}
# 145| 0: [ReturnStmt] return ...;
# 145| 0: [RefExpr] ref ...
# 145| 0: [FieldAccess] access to field x
# 149| 6: [Class] TestRefReturns
# 151| 6: [Method] M
# 151| -1: [TypeMention] Void
# 152| 4: [BlockStmt] {...}
# 153| 0: [LocalVariableDeclStmt] ... ...;
# 153| 0: [LocalVariableDeclAndInitExpr] Int32 a = ...
# 153| -1: [TypeMention] int
# 153| 0: [LocalVariableAccess] access to local variable a
# 153| 1: [IntLiteral] 0
# 155| 1: [LocalVariableDeclStmt] ... ...;
# 155| 0: [LocalVariableDeclAndInitExpr] S s = ...
# 155| -1: [TypeMention] S
# 155| 0: [LocalVariableAccess] access to local variable s
# 155| 1: [ObjectCreation] object creation of type S
# 155| -1: [TypeMention] S
# 155| 0: [LocalVariableAccess] access to local variable a
# 156| 2: [ExprStmt] ...;
# 156| 0: [AssignExpr] ... = ...
# 156| 0: [IndexerCall] access to indexer
# 156| -1: [LocalVariableAccess] access to local variable s
# 156| 0: [IntLiteral] 0
# 156| 1: [IntLiteral] 1
# 157| 3: [LocalVariableDeclStmt] ... ...;
# 157| 0: [LocalVariableDeclAndInitExpr] Int32 x = ...
# 157| -1: [TypeMention] int
# 157| 0: [LocalVariableAccess] access to local variable x
# 157| 1: [IndexerCall] access to indexer
# 157| -1: [LocalVariableAccess] access to local variable s
# 157| 0: [IntLiteral] 0
27 changes: 27 additions & 0 deletions csharp/ql/test/library-tests/indexers/indexers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,31 @@ public bool this[int index]
get { return 0; }
}
}

public ref struct S
{
private ref int x;

public S(ref int v)
{
x = ref v;
}

public ref int this[int i]
{
get { return ref x; }
}
}

public class TestRefReturns
{
public void M()
{
int a = 0;

S s = new S(ref a);
s[0] = 1;
var x = s[0];
}
}
}
47 changes: 47 additions & 0 deletions csharp/ql/test/library-tests/properties/PrintAst.expected
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,50 @@ properties.cs:
# 133| 0: [FieldAccess] access to field Prop.field
# 133| 1: [ParameterAccess] access to parameter value
# 130| 7: [Field] Prop.field
# 137| 11: [RefStruct] S
# 139| 6: [Field] x
# 139| -1: [TypeMention] int
# 141| 7: [InstanceConstructor] S
#-----| 2: (Parameters)
# 141| 0: [Parameter] v
# 141| -1: [TypeMention] int
# 142| 4: [BlockStmt] {...}
# 143| 0: [ExprStmt] ...;
# 143| 0: [AssignExpr] ... = ...
# 143| 0: [FieldAccess] access to field x
# 143| 1: [RefExpr] ref ...
# 143| 0: [ParameterAccess] access to parameter v
# 146| 8: [Property] Prop
# 146| -1: [TypeMention] int
# 148| 3: [Getter] get_Prop
# 148| 4: [BlockStmt] {...}
# 148| 0: [ReturnStmt] return ...;
# 148| 0: [RefExpr] ref ...
# 148| 0: [FieldAccess] access to field x
# 152| 12: [Class] TestRefReturns
# 154| 6: [Method] M
# 154| -1: [TypeMention] Void
# 155| 4: [BlockStmt] {...}
# 156| 0: [LocalVariableDeclStmt] ... ...;
# 156| 0: [LocalVariableDeclAndInitExpr] Int32 a = ...
# 156| -1: [TypeMention] int
# 156| 0: [LocalVariableAccess] access to local variable a
# 156| 1: [IntLiteral] 0
# 158| 1: [LocalVariableDeclStmt] ... ...;
# 158| 0: [LocalVariableDeclAndInitExpr] S s = ...
# 158| -1: [TypeMention] S
# 158| 0: [LocalVariableAccess] access to local variable s
# 158| 1: [ObjectCreation] object creation of type S
# 158| -1: [TypeMention] S
# 158| 0: [LocalVariableAccess] access to local variable a
# 159| 2: [ExprStmt] ...;
# 159| 0: [AssignExpr] ... = ...
# 159| 0: [PropertyCall] access to property Prop
# 159| -1: [LocalVariableAccess] access to local variable s
# 159| 1: [IntLiteral] 1
# 160| 3: [LocalVariableDeclStmt] ... ...;
# 160| 0: [LocalVariableDeclAndInitExpr] Int32 x = ...
# 160| -1: [TypeMention] int
# 160| 0: [LocalVariableAccess] access to local variable x
# 160| 1: [PropertyCall] access to property Prop
# 160| -1: [LocalVariableAccess] access to local variable s
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
| Prop.field |
| caption |
| next |
| x |
| y |
| z |
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
| properties.cs:12:23:12:29 | Caption | properties.cs:29:13:29:28 | access to property Caption | properties.cs:17:13:17:15 | set_Caption |
| properties.cs:12:23:12:29 | Caption | properties.cs:30:24:30:39 | access to property Caption | properties.cs:15:13:15:15 | get_Caption |
| properties.cs:57:20:57:20 | X | properties.cs:61:13:61:13 | access to property X | properties.cs:57:37:57:39 | set_X |
| properties.cs:58:20:58:20 | Y | properties.cs:62:13:62:13 | access to property Y | properties.cs:58:37:58:39 | set_Y |
| properties.cs:70:28:70:28 | X | properties.cs:82:46:82:51 | access to property X | properties.cs:70:32:70:34 | get_X |
| properties.cs:71:28:71:28 | Y | properties.cs:83:39:83:44 | access to property Y | properties.cs:74:13:74:15 | set_Y |
| properties.cs:146:24:146:27 | Prop | properties.cs:159:13:159:18 | access to property Prop | properties.cs:148:13:148:15 | get_Prop |
| properties.cs:146:24:146:27 | Prop | properties.cs:160:21:160:26 | access to property Prop | properties.cs:148:13:148:15 | get_Prop |
8 changes: 8 additions & 0 deletions csharp/ql/test/library-tests/properties/Properties19.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import csharp

from PropertyCall pc, Property p, Accessor target
where
pc.getProperty() = p and
pc.getTarget() = target and
p.fromSource()
select p, pc, target
27 changes: 27 additions & 0 deletions csharp/ql/test/library-tests/properties/properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,31 @@ public object Prop
set { field = value; }
}
}

public ref struct S
{
private ref int x;

public S(ref int v)
{
x = ref v;
}

public ref int Prop
{
get { return ref x; }
}
}

public class TestRefReturns
{
public void M()
{
int a = 0;

S s = new S(ref a);
s.Prop = 1;
var x = s.Prop;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
| Quality.cs:26:19:26:26 | access to indexer | Call without target $@. | Quality.cs:26:19:26:26 | access to indexer | access to indexer |
| Quality.cs:29:21:29:27 | access to indexer | Call without target $@. | Quality.cs:29:21:29:27 | access to indexer | access to indexer |
| Quality.cs:32:9:32:21 | access to indexer | Call without target $@. | Quality.cs:32:9:32:21 | access to indexer | access to indexer |
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,5 @@
| Quality.cs:20:13:20:23 | access to property MyProperty6 | Call without target $@. | Quality.cs:20:13:20:23 | access to property MyProperty6 | access to property MyProperty6 |
| Quality.cs:23:9:23:14 | access to event Event1 | Call without target $@. | Quality.cs:23:9:23:14 | access to event Event1 | access to event Event1 |
| Quality.cs:23:9:23:30 | delegate call | Call without target $@. | Quality.cs:23:9:23:30 | delegate call | delegate call |
| Quality.cs:26:19:26:26 | access to indexer | Call without target $@. | Quality.cs:26:19:26:26 | access to indexer | access to indexer |
| Quality.cs:29:21:29:27 | access to indexer | Call without target $@. | Quality.cs:29:21:29:27 | access to indexer | access to indexer |
| Quality.cs:32:9:32:21 | access to indexer | Call without target $@. | Quality.cs:32:9:32:21 | access to indexer | access to indexer |
| Quality.cs:38:16:38:26 | access to property MyProperty2 | Call without target $@. | Quality.cs:38:16:38:26 | access to property MyProperty2 | access to property MyProperty2 |
| Quality.cs:50:20:50:26 | object creation of type T | Call without target $@. | Quality.cs:50:20:50:26 | object creation of type T | object creation of type T |
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ public Test()
Event1.Invoke(this, 5);

var str = "abcd";
var sub = str[..3]; // TODO: this is not an indexer call, but rather a `str.Substring(0, 3)` call.
var sub = str[..3];

Span<int> sp = null;
var slice = sp[..3]; // TODO: this is not an indexer call, but rather a `sp.Slice(0, 3)` call.
var slice = sp[..3];

Span<byte> guidBytes = stackalloc byte[16];
guidBytes[08] = 1; // TODO: this indexer call has no target, because the target is a `ref` returning getter.
guidBytes[08] = 1;

new MyList([new(), new Test()]);
}
Expand Down
Loading