Skip to content
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Extraction.Kinds;

Expand All @@ -8,7 +11,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
internal abstract class ElementAccess : Expression<ExpressionSyntax>
{
protected ElementAccess(ExpressionNodeInfo info, ExpressionSyntax qualifier, BracketedArgumentListSyntax argumentList)
: base(info.SetKind(GetKind(info.Context, qualifier)))
: base(info.SetKind(GetKind(info.Context, info.Node, qualifier)))
{
this.qualifier = qualifier;
this.argumentList = argumentList;
Expand All @@ -17,6 +20,169 @@ protected ElementAccess(ExpressionNodeInfo info, ExpressionSyntax qualifier, Bra
private readonly ExpressionSyntax qualifier;
private readonly BracketedArgumentListSyntax argumentList;


private ISymbol? GetTargetSymbol()
{
return Context.GetSymbolInfo(base.Syntax).Symbol;
}

private static void SetExprArgument(TextWriter trapFile, Expression left, Expression right)
{
trapFile.expr_argument(left, 0);
trapFile.expr_argument(right, 0);
}

private Expression MakeSubtractionExpression(IExpressionParentEntity parent, int child)
{
var info = new ExpressionInfo(
Context,
AnnotatedTypeSymbol.CreateNotAnnotated(Context.Compilation.GetSpecialType(SpecialType.System_Int32)),
Location,
ExprKind.SUB,
parent,
child,
isCompilerGenerated: true,
null);

return new Expression(info);
}

private Expression MakeLengthPropertyCall(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, IExpressionParentEntity parent, int child)
{
var lengthInfo = new ExpressionInfo(
Context,
AnnotatedTypeSymbol.CreateNotAnnotated(Context.Compilation.GetSpecialType(SpecialType.System_Int32)),
Location,
ExprKind.PROPERTY_ACCESS,
parent,
child,
isCompilerGenerated: true,
null);
var length = new Expression(lengthInfo);
Create(Context, qualifier, length, -1);

var lengthProp = Property.Create(Context, lengthPropertySymbol);
trapFile.expr_access(length, lengthProp);
return length;
}

private Expression CreateFromIndexExpression(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, IExpressionParentEntity parent, int child, PrefixUnaryExpressionSyntax index)
{
var sub = MakeSubtractionExpression(parent, child);
MakeLengthPropertyCall(trapFile, lengthPropertySymbol, sub, 0);
var info = new ExpressionNodeInfo(Context, index.Operand, sub, 1)
{
IsCompilerGenerated = true
};
Factory.Create(info);
return sub;
}

/// <summary>
/// It is assumed that either the input is
/// 1. A normal expression that can be used as endpoint (e.g a constant like "3").
/// 2. An index expression indicating that we should read from the end (e.g "^1").
/// </summary>
/// <param name="syntax">The syntax node representing the range endpoint.</param>
/// <param name="parent">The parent expression entity.</param>
/// <param name="child">The child index within the parent.</param>
/// <returns>An expression representing the endpoint of a range to be used in conjunction with a slice operation.</returns>
private Expression CreateFromRangeEndpoint(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, ExpressionSyntax syntax, IExpressionParentEntity parent, int child)
{
if (syntax.Kind() == SyntaxKind.IndexExpression && syntax is PrefixUnaryExpressionSyntax index)
{
return CreateFromIndexExpression(trapFile, lengthPropertySymbol, parent, child, index);
}

var info = new ExpressionNodeInfo(Context, syntax, parent, child)
{
IsCompilerGenerated = true
};
return Factory.Create(info);
}

/// <summary>
/// Determines whether the given method is a slice method, which is defined as a method with
/// the name "Slice" or "Substring" and two parameters.
/// </summary>
/// <param name="method">The method symbol to check.</param>
/// <returns>True if the method is a slice method; false otherwise.</returns>
private bool IsSliceWithRange(IMethodSymbol method, [NotNullWhen(true)] out IPropertySymbol? lengthPropertySymbol, [NotNullWhen(true)] out RangeExpressionSyntax? range)
{
range = null;
lengthPropertySymbol = method
.ContainingType
.GetMembers("Length")
.OfType<IPropertySymbol>()
.FirstOrDefault();

if (argumentList.Arguments.Count == 1)
{
range = argumentList.Arguments[0].Expression as RangeExpressionSyntax;
}

return (method.Name == "Slice" || method.Name == "Substring")
&& method.Parameters.Length == 2
&& lengthPropertySymbol is not null
&& range is not null;
}

/// <summary>
/// Populates a slice method call based on the given range and length property symbol.
/// </summary>
/// <param name="trapFile">The trap file to write to.</param>
/// <param name="lengthPropertySymbol">The length property symbol.</param>
/// <param name="slice">The slice method symbol.</param>
/// <param name="range">The range expression syntax.</param>
private void PopulateSlice(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, IMethodSymbol slice, RangeExpressionSyntax range)
{
// 1. s[a..b] -> s.Slice(a, b - a)
// 2. s[..b] -> s.Slice(0, b)
// 3. s[a..] -> s.Slice(a, s.Length - a)
// 4. s[..] -> s.Slice(0, s.Length)
// Furthermore, note that uses of index expressions (e.g. s[2..^1]) within the range
// get translated to length - index, so we need to handle this as well.
switch (range.LeftOperand, range.RightOperand)
{
case (ExpressionSyntax lsyntax, ExpressionSyntax rsyntax):
Comment thread
michaelnebel marked this conversation as resolved.
{
var left = CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, this, 0);
var right = MakeSubtractionExpression(this, 1);

CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, rsyntax, right, 0);
CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, right, 1);
SetExprArgument(trapFile, left, right);
break;
}
case (null, ExpressionSyntax rsyntax):
{
var left = Literal.CreateGenerated(Context, this, 0, Context.Compilation.GetSpecialType(SpecialType.System_Int32), 0, Location);
var right = CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, rsyntax, this, 1);
SetExprArgument(trapFile, left, right);
break;
}
case (ExpressionSyntax lsyntax, null):
{

var left = CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, this, 0);
var right = MakeSubtractionExpression(this, 1);
MakeLengthPropertyCall(trapFile, lengthPropertySymbol, right, 0);
CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, right, 1);
SetExprArgument(trapFile, left, right);
break;
}
case (null, null):
{
var left = Literal.CreateGenerated(Context, this, 0, Context.Compilation.GetSpecialType(SpecialType.System_Int32), 0, Location);
var right = MakeLengthPropertyCall(trapFile, lengthPropertySymbol, this, 1);
SetExprArgument(trapFile, left, right);
break;
}
}

trapFile.expr_call(this, Method.Create(Context, slice));
}

protected override void PopulateExpression(TextWriter trapFile)
{
if (Kind == ExprKind.POINTER_INDIRECTION)
Expand All @@ -30,11 +196,20 @@ protected override void PopulateExpression(TextWriter trapFile)
else
{
Create(Context, qualifier, this, -1);
PopulateArguments(trapFile, argumentList, 0);

var symbolInfo = Context.GetSymbolInfo(base.Syntax);
var target = GetTargetSymbol();
if (target is IMethodSymbol method && IsSliceWithRange(method, out var lengthPropertySymbol, out var range))
{
// When an indexer on a span or string is used in conjunction with a range expression, the compiler translates
// this into a call to the "Slice" or "Substring" method.
// In this case, we want to populate a slice/substring method call instead of an indexer access.
// E.g s[1..4] gets translated to s.Slice(1, 4 - 1) if s is a span.
PopulateSlice(trapFile, lengthPropertySymbol, method, range);
return;
}

if (symbolInfo.Symbol is IPropertySymbol indexer)
PopulateArguments(trapFile, argumentList, 0);
if (target is IPropertySymbol { IsIndexer: true } indexer)
{
trapFile.expr_access(this, Indexer.Create(Context, indexer));
}
Expand All @@ -46,8 +221,11 @@ protected override void PopulateExpression(TextWriter trapFile)
private static bool IsArray(ITypeSymbol symbol) =>
symbol.TypeKind == Microsoft.CodeAnalysis.TypeKind.Array || symbol.IsInlineArray();

private static ExprKind GetKind(Context cx, ExpressionSyntax qualifier)
private static ExprKind GetKind(Context cx, ExpressionSyntax syntax, ExpressionSyntax qualifier)
{
if (cx.GetSymbolInfo(syntax).Symbol is IMethodSymbol)
return ExprKind.METHOD_INVOCATION;

var qualifierType = cx.GetType(qualifier);

// This is a compilation error, so make a guess and continue.
Expand Down
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 range-access expressions on spans and strings (for example, `a[0..3]`). These expressions are now extracted as `Slice` (span) or `Substring` (string) calls.
23 changes: 23 additions & 0 deletions csharp/ql/test/library-tests/spans/Slice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;

public class C
{
public void M(int a, int b)
{
var s = "hello world";
var sub1 = s[1..a];
var sub2 = s[..2];
var sub3 = s[3..];
var sub4 = s[..^4];
var sub5 = s[a..^b];
var sub6 = s[..];

Span<int> sp = null;
var slice1 = sp[5..a];
var slice2 = sp[..6];
var slice3 = sp[7..];
var slice4 = sp[..^8];
var slice5 = sp[a..^b];
var slice6 = sp[..];
}
}
34 changes: 34 additions & 0 deletions csharp/ql/test/library-tests/spans/slice.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
methodCalls
| Slice.cs:8:20:8:26 | call to method Substring | Substring(int, int) | 0 | 1 |
| Slice.cs:8:20:8:26 | call to method Substring | Substring(int, int) | 1 | access to parameter a - 1 |
| Slice.cs:9:20:9:25 | call to method Substring | Substring(int, int) | 0 | 0 |
| Slice.cs:9:20:9:25 | call to method Substring | Substring(int, int) | 1 | 2 |
| Slice.cs:10:20:10:25 | call to method Substring | Substring(int, int) | 0 | 3 |
| Slice.cs:10:20:10:25 | call to method Substring | Substring(int, int) | 1 | access to property Length - 3 |
| Slice.cs:11:20:11:26 | call to method Substring | Substring(int, int) | 0 | 0 |
| Slice.cs:11:20:11:26 | call to method Substring | Substring(int, int) | 1 | access to property Length - 4 |
| Slice.cs:12:20:12:27 | call to method Substring | Substring(int, int) | 0 | access to parameter a |
| Slice.cs:12:20:12:27 | call to method Substring | Substring(int, int) | 1 | access to property Length - access to parameter b - access to parameter a |
| Slice.cs:13:20:13:24 | call to method Substring | Substring(int, int) | 0 | 0 |
| Slice.cs:13:20:13:24 | call to method Substring | Substring(int, int) | 1 | access to property Length |
| Slice.cs:16:22:16:29 | call to method Slice | Slice(int, int) | 0 | 5 |
| Slice.cs:16:22:16:29 | call to method Slice | Slice(int, int) | 1 | access to parameter a - 5 |
| Slice.cs:17:22:17:28 | call to method Slice | Slice(int, int) | 0 | 0 |
| Slice.cs:17:22:17:28 | call to method Slice | Slice(int, int) | 1 | 6 |
| Slice.cs:18:22:18:28 | call to method Slice | Slice(int, int) | 0 | 7 |
| Slice.cs:18:22:18:28 | call to method Slice | Slice(int, int) | 1 | access to property Length - 7 |
| Slice.cs:19:22:19:29 | call to method Slice | Slice(int, int) | 0 | 0 |
| Slice.cs:19:22:19:29 | call to method Slice | Slice(int, int) | 1 | access to property Length - 8 |
| Slice.cs:20:22:20:30 | call to method Slice | Slice(int, int) | 0 | access to parameter a |
| Slice.cs:20:22:20:30 | call to method Slice | Slice(int, int) | 1 | access to property Length - access to parameter b - access to parameter a |
| Slice.cs:21:22:21:27 | call to method Slice | Slice(int, int) | 0 | 0 |
| Slice.cs:21:22:21:27 | call to method Slice | Slice(int, int) | 1 | access to property Length |
propertyCalls
| Slice.cs:10:20:10:25 | access to property Length | Slice.cs:10:20:10:20 | access to local variable s |
| Slice.cs:11:20:11:26 | access to property Length | Slice.cs:11:20:11:20 | access to local variable s |
| Slice.cs:12:20:12:27 | access to property Length | Slice.cs:12:20:12:20 | access to local variable s |
| Slice.cs:13:20:13:24 | access to property Length | Slice.cs:13:20:13:20 | access to local variable s |
| Slice.cs:18:22:18:28 | access to property Length | Slice.cs:18:22:18:23 | access to local variable sp |
| Slice.cs:19:22:19:29 | access to property Length | Slice.cs:19:22:19:23 | access to local variable sp |
| Slice.cs:20:22:20:30 | access to property Length | Slice.cs:20:22:20:23 | access to local variable sp |
| Slice.cs:21:22:21:27 | access to property Length | Slice.cs:21:22:21:23 | access to local variable sp |
18 changes: 18 additions & 0 deletions csharp/ql/test/library-tests/spans/slice.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import csharp

private string printExpr(Expr e) {
e =
any(SubExpr sub |
result = printExpr(sub.getLeftOperand()) + " - " + printExpr(sub.getRightOperand())
)
or
not e instanceof SubExpr and
result = e.toString()
}

query predicate methodCalls(MethodCall mc, string m, int i, string arg) {
m = mc.getTarget().toStringWithTypes() and
arg = printExpr(mc.getArgument(i))
}

query predicate propertyCalls(PropertyCall p, Expr qualifier) { qualifier = p.getQualifier() }
Original file line number Diff line number Diff line change
@@ -1,2 +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 |
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +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: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,10 +23,10 @@ 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;
Expand Down
Loading