From 7771ee5a64190dc78b4f75dfa79399ddfea70a9f Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 21 May 2026 13:12:24 +0200 Subject: [PATCH 01/10] C#: Extract the indexer as the call target when using range expressions with spans. --- .../Entities/Expressions/ElementAccess.cs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs index 345e691a8a85..6f8321321855 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Semmle.Extraction.Kinds; @@ -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`. + // 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() + .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) @@ -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)); } From fd24d9d9729beec5d480d881e8ff1bf6a43d0a59 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 21 May 2026 13:30:20 +0200 Subject: [PATCH 02/10] C#: Update DB quality expected test output. --- .../Telemetry/DatabaseQuality/IsNotOkayCall.expected | 2 -- .../query-tests/Telemetry/DatabaseQuality/NoTarget.expected | 2 -- .../ql/test/query-tests/Telemetry/DatabaseQuality/Quality.cs | 4 ++-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/IsNotOkayCall.expected b/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/IsNotOkayCall.expected index dcdb8b09058c..e69de29bb2d1 100644 --- a/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/IsNotOkayCall.expected +++ b/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/IsNotOkayCall.expected @@ -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 | diff --git a/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/NoTarget.expected b/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/NoTarget.expected index a76dd08cdb6b..b96815507f14 100644 --- a/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/NoTarget.expected +++ b/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/NoTarget.expected @@ -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 | diff --git a/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/Quality.cs b/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/Quality.cs index e10ce10f6c4b..648083edad8d 100644 --- a/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/Quality.cs +++ b/csharp/ql/test/query-tests/Telemetry/DatabaseQuality/Quality.cs @@ -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 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 guidBytes = stackalloc byte[16]; guidBytes[08] = 1; From 34f69f23a68585e07eb34bcbf040f3c2e0ffdd16 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 21 May 2026 13:34:43 +0200 Subject: [PATCH 03/10] C#: Add change-note. --- csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md diff --git a/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md b/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md new file mode 100644 index 000000000000..b7bc97b9a94e --- /dev/null +++ b/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md @@ -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. From 4231ffc0981f70a41988bccec4664476166d42f0 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Tue, 2 Jun 2026 15:52:43 +0200 Subject: [PATCH 04/10] C#: Extract Slice and Substring operations and synthesize the call arguments, when using indexers in conjunction with ranges on spans and strings. --- .../Entities/Expressions/ElementAccess.cs | 185 +++++++++++++++--- 1 file changed, 162 insertions(+), 23 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs index 6f8321321855..15f7fe709bbb 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs @@ -1,6 +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; @@ -9,7 +11,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions internal abstract class ElementAccess : Expression { 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; @@ -19,32 +21,156 @@ protected ElementAccess(ExpressionNodeInfo info, ExpressionSyntax qualifier, Bra private readonly BracketedArgumentListSyntax argumentList; - private IPropertySymbol? GetIndexerSymbol() + private ISymbol? GetTargetSymbol() { - var symbol = Context.GetSymbolInfo(base.Syntax).Symbol; + 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 void 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); + } + + 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; + } + + /// + /// 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"). + /// + /// The syntax node representing the range endpoint. + /// The parent expression entity. + /// The child index within the parent. + /// An expression representing the endpoint of a range to be used in conjunction with a slice operation. + 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); + } + + /// + /// Determines whether the given method is a slice method, which is defined as a method with + /// the name "Slice" or "SubString" and two parameters. + /// The method symbol to check. + /// True if the method is a slice method, false otherwise. + private bool IsSliceWithRange(IMethodSymbol method, [NotNullWhen(true)] out IPropertySymbol? lengthPropertySymbol, [NotNullWhen(true)] out RangeExpressionSyntax? range) + { + range = null; + lengthPropertySymbol = method + .ContainingType + .GetMembers("Length") + .OfType() + .FirstOrDefault(); + + if (argumentList.Arguments.Count == 1) + { + range = argumentList.Arguments[0].Expression as RangeExpressionSyntax; + } - if (symbol is IPropertySymbol { IsIndexer: true } indexer) - return indexer; + return (method.Name == "Slice" || method.Name == "Substring") + && method.Parameters.Length == 2 + && lengthPropertySymbol is not null + && range is not null; + } - // 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`. - // 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) + /// + /// Populates a slice method call based on the given range and length property symbol. + /// + /// The trap file to write to. + /// The length property symbol. + /// The slice method symbol. + /// The range expression syntax. + 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) + // 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) { - var indexers = method - .ContainingType - .GetMembers() - .OfType() - .Where(p => p.IsIndexer); + case (ExpressionSyntax lsyntax, ExpressionSyntax rsyntax): + { + var left = CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, this, 0); + var right = MakeSubtractionExpression(this, 1); - var intIndexer = indexers - .Where(i => i.Parameters.Length == 1 && i.Parameters[0].Type.SpecialType == SpecialType.System_Int32) - .FirstOrDefault(); + 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): + { - return intIndexer; + 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; + } } - return null; + trapFile.expr_call(this, Method.Create(Context, slice)); } protected override void PopulateExpression(TextWriter trapFile) @@ -60,10 +186,20 @@ protected override void PopulateExpression(TextWriter trapFile) else { Create(Context, qualifier, this, -1); - PopulateArguments(trapFile, argumentList, 0); - var indexer = GetIndexerSymbol(); - if (indexer is not null) + 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; + } + + PopulateArguments(trapFile, argumentList, 0); + if (target is IPropertySymbol { IsIndexer: true } indexer) { trapFile.expr_access(this, Indexer.Create(Context, indexer)); } @@ -75,8 +211,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. From 8d0575ca146d3d47b662cd00f55fc6756ac65b41 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Tue, 2 Jun 2026 16:05:03 +0200 Subject: [PATCH 05/10] C#: Add test case and expected output. --- csharp/ql/test/library-tests/spans/Slice.cs | 19 ++++++++++++++++ .../test/library-tests/spans/slice.expected | 22 +++++++++++++++++++ csharp/ql/test/library-tests/spans/slice.ql | 18 +++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 csharp/ql/test/library-tests/spans/Slice.cs create mode 100644 csharp/ql/test/library-tests/spans/slice.expected create mode 100644 csharp/ql/test/library-tests/spans/slice.ql diff --git a/csharp/ql/test/library-tests/spans/Slice.cs b/csharp/ql/test/library-tests/spans/Slice.cs new file mode 100644 index 000000000000..882cdf599173 --- /dev/null +++ b/csharp/ql/test/library-tests/spans/Slice.cs @@ -0,0 +1,19 @@ +using System; + +public class C +{ + public void M(int a) + { + var s = "hello world"; + var sub1 = s[1..a]; + var sub2 = s[..2]; + var sub3 = s[3..]; + var sub4 = s[..^4]; + + Span sp = null; + var slice1 = sp[5..a]; + var slice2 = sp[..6]; + var slice3 = sp[7..]; + var slice4 = sp[..^8]; + } +} diff --git a/csharp/ql/test/library-tests/spans/slice.expected b/csharp/ql/test/library-tests/spans/slice.expected new file mode 100644 index 000000000000..feadf14a78d2 --- /dev/null +++ b/csharp/ql/test/library-tests/spans/slice.expected @@ -0,0 +1,22 @@ +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:14:22:14:29 | call to method Slice | Slice(int, int) | 0 | 5 | +| Slice.cs:14:22:14:29 | call to method Slice | Slice(int, int) | 1 | access to parameter a - 5 | +| Slice.cs:15:22:15:28 | call to method Slice | Slice(int, int) | 0 | 0 | +| Slice.cs:15:22:15:28 | call to method Slice | Slice(int, int) | 1 | 6 | +| Slice.cs:16:22:16:28 | call to method Slice | Slice(int, int) | 0 | 7 | +| Slice.cs:16:22:16:28 | call to method Slice | Slice(int, int) | 1 | access to property Length - 7 | +| Slice.cs:17:22:17:29 | call to method Slice | Slice(int, int) | 0 | 0 | +| Slice.cs:17:22:17:29 | call to method Slice | Slice(int, int) | 1 | access to property Length - 8 | +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:16:22:16:28 | access to property Length | Slice.cs:16:22:16:23 | access to local variable sp | +| Slice.cs:17:22:17:29 | access to property Length | Slice.cs:17:22:17:23 | access to local variable sp | diff --git a/csharp/ql/test/library-tests/spans/slice.ql b/csharp/ql/test/library-tests/spans/slice.ql new file mode 100644 index 000000000000..8cddc4a875af --- /dev/null +++ b/csharp/ql/test/library-tests/spans/slice.ql @@ -0,0 +1,18 @@ +import csharp + +private string printExpr(Expr e) { + e = + any(SubExpr sub | + result = sub.getLeftOperand().toString() + " - " + sub.getRightOperand().toString() + ) + 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() } From bd1392e8fd4ab9b4e0d74651629dfa643b7dea04 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 3 Jun 2026 11:43:22 +0200 Subject: [PATCH 06/10] C#: Update change note. --- csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md b/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md index b7bc97b9a94e..72858accfa1b 100644 --- a/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md +++ b/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md @@ -1,4 +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. +* Improved extraction of span range-access expressions (for example, `a[0..3]`). These expressions are now extracted as span `Slice` calls. From 48b96a808eba3c1dd28fc465fee45bd7f7d1ce18 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 3 Jun 2026 12:21:53 +0200 Subject: [PATCH 07/10] C#: Add more testcases and update expected output. --- csharp/ql/test/library-tests/spans/Slice.cs | 4 ++- .../test/library-tests/spans/slice.expected | 26 ++++++++++++------- csharp/ql/test/library-tests/spans/slice.ql | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/csharp/ql/test/library-tests/spans/Slice.cs b/csharp/ql/test/library-tests/spans/Slice.cs index 882cdf599173..3d207406076c 100644 --- a/csharp/ql/test/library-tests/spans/Slice.cs +++ b/csharp/ql/test/library-tests/spans/Slice.cs @@ -2,18 +2,20 @@ public class C { - public void M(int a) + 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]; Span sp = null; var slice1 = sp[5..a]; var slice2 = sp[..6]; var slice3 = sp[7..]; var slice4 = sp[..^8]; + var slice5 = sp[a..^b]; } } diff --git a/csharp/ql/test/library-tests/spans/slice.expected b/csharp/ql/test/library-tests/spans/slice.expected index feadf14a78d2..9b52e1aa3314 100644 --- a/csharp/ql/test/library-tests/spans/slice.expected +++ b/csharp/ql/test/library-tests/spans/slice.expected @@ -7,16 +7,22 @@ methodCalls | 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:14:22:14:29 | call to method Slice | Slice(int, int) | 0 | 5 | -| Slice.cs:14:22:14:29 | call to method Slice | Slice(int, int) | 1 | access to parameter a - 5 | -| Slice.cs:15:22:15:28 | call to method Slice | Slice(int, int) | 0 | 0 | -| Slice.cs:15:22:15:28 | call to method Slice | Slice(int, int) | 1 | 6 | -| Slice.cs:16:22:16:28 | call to method Slice | Slice(int, int) | 0 | 7 | -| Slice.cs:16:22:16:28 | call to method Slice | Slice(int, int) | 1 | access to property Length - 7 | -| Slice.cs:17:22:17:29 | call to method Slice | Slice(int, int) | 0 | 0 | -| Slice.cs:17:22:17:29 | call to method Slice | Slice(int, int) | 1 | access to property Length - 8 | +| 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:15:22:15:29 | call to method Slice | Slice(int, int) | 0 | 5 | +| Slice.cs:15:22:15:29 | call to method Slice | Slice(int, int) | 1 | access to parameter a - 5 | +| Slice.cs:16:22:16:28 | call to method Slice | Slice(int, int) | 0 | 0 | +| Slice.cs:16:22:16:28 | call to method Slice | Slice(int, int) | 1 | 6 | +| Slice.cs:17:22:17:28 | call to method Slice | Slice(int, int) | 0 | 7 | +| Slice.cs:17:22:17:28 | call to method Slice | Slice(int, int) | 1 | access to property Length - 7 | +| Slice.cs:18:22:18:29 | call to method Slice | Slice(int, int) | 0 | 0 | +| Slice.cs:18:22:18:29 | call to method Slice | Slice(int, int) | 1 | access to property Length - 8 | +| Slice.cs:19:22:19:30 | call to method Slice | Slice(int, int) | 0 | access to parameter a | +| Slice.cs:19:22:19:30 | call to method Slice | Slice(int, int) | 1 | access to property Length - access to parameter b - access to parameter a | 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:16:22:16:28 | access to property Length | Slice.cs:16:22:16:23 | access to local variable sp | -| Slice.cs:17:22:17:29 | access to property Length | Slice.cs:17:22:17:23 | access to local variable sp | +| Slice.cs:12:20:12:27 | access to property Length | Slice.cs:12:20:12:20 | access to local variable s | +| Slice.cs:17:22:17:28 | access to property Length | Slice.cs:17:22:17:23 | access to local variable sp | +| Slice.cs:18:22:18:29 | access to property Length | Slice.cs:18:22:18:23 | access to local variable sp | +| Slice.cs:19:22:19:30 | access to property Length | Slice.cs:19:22:19:23 | access to local variable sp | diff --git a/csharp/ql/test/library-tests/spans/slice.ql b/csharp/ql/test/library-tests/spans/slice.ql index 8cddc4a875af..11efb1d824da 100644 --- a/csharp/ql/test/library-tests/spans/slice.ql +++ b/csharp/ql/test/library-tests/spans/slice.ql @@ -3,7 +3,7 @@ import csharp private string printExpr(Expr e) { e = any(SubExpr sub | - result = sub.getLeftOperand().toString() + " - " + sub.getRightOperand().toString() + result = printExpr(sub.getLeftOperand()) + " - " + printExpr(sub.getRightOperand()) ) or not e instanceof SubExpr and From dc37e6f2c43d6a92f7b177e04404c1c72a1900ea Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 3 Jun 2026 13:54:15 +0200 Subject: [PATCH 08/10] C#: Add case for open ended range. --- .../Entities/Expressions/ElementAccess.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs index 15f7fe709bbb..f6f44799312d 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs @@ -47,7 +47,7 @@ private Expression MakeSubtractionExpression(IExpressionParentEntity parent, int return new Expression(info); } - private void MakeLengthPropertyCall(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, IExpressionParentEntity parent, int child) + private Expression MakeLengthPropertyCall(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, IExpressionParentEntity parent, int child) { var lengthInfo = new ExpressionInfo( Context, @@ -63,6 +63,7 @@ private void MakeLengthPropertyCall(TextWriter trapFile, IPropertySymbol lengthP 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) @@ -137,6 +138,7 @@ private void PopulateSlice(TextWriter trapFile, IPropertySymbol lengthPropertySy // 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) @@ -168,6 +170,13 @@ private void PopulateSlice(TextWriter trapFile, IPropertySymbol lengthPropertySy 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)); From 5e0baa45f38fc3c896585c9e6f95e9ec20495688 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 3 Jun 2026 13:54:49 +0200 Subject: [PATCH 09/10] C#: Add test case. --- csharp/ql/test/library-tests/spans/Slice.cs | 2 ++ .../test/library-tests/spans/slice.expected | 32 +++++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/csharp/ql/test/library-tests/spans/Slice.cs b/csharp/ql/test/library-tests/spans/Slice.cs index 3d207406076c..07d03c4d587e 100644 --- a/csharp/ql/test/library-tests/spans/Slice.cs +++ b/csharp/ql/test/library-tests/spans/Slice.cs @@ -10,6 +10,7 @@ public void M(int a, int b) var sub3 = s[3..]; var sub4 = s[..^4]; var sub5 = s[a..^b]; + var sub6 = s[..]; Span sp = null; var slice1 = sp[5..a]; @@ -17,5 +18,6 @@ public void M(int a, int b) var slice3 = sp[7..]; var slice4 = sp[..^8]; var slice5 = sp[a..^b]; + var slice6 = sp[..]; } } diff --git a/csharp/ql/test/library-tests/spans/slice.expected b/csharp/ql/test/library-tests/spans/slice.expected index 9b52e1aa3314..c60e4139764a 100644 --- a/csharp/ql/test/library-tests/spans/slice.expected +++ b/csharp/ql/test/library-tests/spans/slice.expected @@ -9,20 +9,26 @@ methodCalls | 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:15:22:15:29 | call to method Slice | Slice(int, int) | 0 | 5 | -| Slice.cs:15:22:15:29 | call to method Slice | Slice(int, int) | 1 | access to parameter a - 5 | -| Slice.cs:16:22:16:28 | call to method Slice | Slice(int, int) | 0 | 0 | -| Slice.cs:16:22:16:28 | call to method Slice | Slice(int, int) | 1 | 6 | -| Slice.cs:17:22:17:28 | call to method Slice | Slice(int, int) | 0 | 7 | -| Slice.cs:17:22:17:28 | call to method Slice | Slice(int, int) | 1 | access to property Length - 7 | -| Slice.cs:18:22:18:29 | call to method Slice | Slice(int, int) | 0 | 0 | -| Slice.cs:18:22:18:29 | call to method Slice | Slice(int, int) | 1 | access to property Length - 8 | -| Slice.cs:19:22:19:30 | call to method Slice | Slice(int, int) | 0 | access to parameter a | -| Slice.cs:19:22:19:30 | call to method Slice | Slice(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:17:22:17:28 | access to property Length | Slice.cs:17:22:17:23 | access to local variable sp | -| Slice.cs:18:22:18:29 | access to property Length | Slice.cs:18:22:18:23 | access to local variable sp | -| Slice.cs:19:22:19:30 | access to property Length | Slice.cs:19:22:19:23 | access to local variable sp | +| 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 | From da699b9c39ea2849e3bca770331f8f9989302c5b Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 3 Jun 2026 13:57:45 +0200 Subject: [PATCH 10/10] C#: Address other CoPilot review comments. --- .../Entities/Expressions/ElementAccess.cs | 7 ++++--- csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs index f6f44799312d..bc81b9254d46 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs @@ -103,9 +103,10 @@ private Expression CreateFromRangeEndpoint(TextWriter trapFile, IPropertySymbol /// /// Determines whether the given method is a slice method, which is defined as a method with - /// the name "Slice" or "SubString" and two parameters. - /// The method symbol to check. - /// True if the method is a slice method, false otherwise. + /// the name "Slice" or "Substring" and two parameters. + /// + /// The method symbol to check. + /// True if the method is a slice method; false otherwise. private bool IsSliceWithRange(IMethodSymbol method, [NotNullWhen(true)] out IPropertySymbol? lengthPropertySymbol, [NotNullWhen(true)] out RangeExpressionSyntax? range) { range = null; diff --git a/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md b/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md index 72858accfa1b..b5e81d9adb99 100644 --- a/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md +++ b/csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md @@ -1,4 +1,4 @@ --- category: minorAnalysis --- -* Improved extraction of span range-access expressions (for example, `a[0..3]`). These expressions are now extracted as span `Slice` calls. +* 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.