Skip to content

C#: Extract indexer when accessing a span with a range.#21877

Draft
michaelnebel wants to merge 10 commits into
github:mainfrom
michaelnebel:csharp/spanaccessrange
Draft

C#: Extract indexer when accessing a span with a range.#21877
michaelnebel wants to merge 10 commits into
github:mainfrom
michaelnebel:csharp/spanaccessrange

Conversation

@michaelnebel
Copy link
Copy Markdown
Contributor

@michaelnebel michaelnebel commented May 21, 2026

Improves the extraction of span access with range expressions (such as a[1..3]).

Roslyn translates a[1..3] into a.Slice(1,2) for spans (and something similar for strings).
In this PR we introduce some special handling in the extractor for such cases

  • Instead of extracting an indexer access on a we extract a a.Slice method call.
  • Instead of extracting the range expression, we use the range expression to synthesize call arguments for a.Slice. Note, that the semantics of a[1..3] means the elements from a starting from index 1 up to but NOT including 3. That is, the corresponding Slice call for a[1..3] is a.Slice(1, 3 - 1).

The translation on spans is as follows

  • a[x..y] is translated to a.Slice(x, y - x).
  • a[..y] is translated to a.Slice(0, y)
  • a[x..] is translated to a.Slice(x, a.Length - x).
  • a[..] is translated to a.Slice(0, a.Length).

Furthermore, it is possible to use read from the end as well (e.g. ^2).

  • a[x..^y] translates into a.Slice(x, a.Length - y - x).

and similar for the other constructs (that is, we can replace ^y with Length - y).
This means that synthesizing Slicecalls in the extractor requires that we synthesize

  • 0 literal.
  • - expressions.
  • .Length property calls.

Similar to span access with range expressions, it is also possible to use range expressions in conjunction with strings. If s is a string, then s[1..3] translates into s.Substring(1, 3 - 1). This is also handled in this PR.

@michaelnebel michaelnebel force-pushed the csharp/spanaccessrange branch from 4eec760 to 9167814 Compare May 28, 2026 10:36
@michaelnebel michaelnebel marked this pull request as ready for review May 28, 2026 11:52
@michaelnebel michaelnebel requested a review from a team as a code owner May 28, 2026 11:52
Copilot AI review requested due to automatic review settings May 28, 2026 11:52
@michaelnebel michaelnebel requested a review from hvitved May 28, 2026 11:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the C# extractor’s handling of element-access expressions that use range syntax (for example, a[0..3]) when Roslyn lowers them into method calls (for example, Slice(...)). The goal is to preserve the source-level “indexer access” shape by still recording an indexer as the call target, improving database-quality telemetry.

Changes:

  • Added extractor special-casing to recover an indexer symbol even when Roslyn binds the element access to a method symbol.
  • Updated the Telemetry/DatabaseQuality query test inputs and expected outputs to reflect the improved extraction.
  • Added a C# library change note documenting the analysis-impacting extraction improvement.
Show a summary per file
File Description
csharp/ql/test/query-tests/Telemetry/DatabaseQuality/Quality.cs Updates test code to treat range element access as properly-targeted indexer calls.
csharp/ql/test/query-tests/Telemetry/DatabaseQuality/NoTarget.expected Removes expected “missing call target” results now that targets are extracted.
csharp/ql/test/query-tests/Telemetry/DatabaseQuality/IsNotOkayCall.expected Eliminates expected “not ok” call-target results for the fixed cases.
csharp/ql/lib/change-notes/2026-05-21-spanaccess-range.md Documents the extraction change as a minor analysis improvement.
csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs Implements the new symbol recovery logic for range-based element access.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 1

Comment on lines +32 to +45
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;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think it is reasonable to assume that Roslyn only translates a[range] into a method call for integer range expressions (and thus we implicitly want to extract the indexer with an int parameter).

Copy link
Copy Markdown
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

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

I'm not convinced that extracting the indexer is what we want; what about extracting the Roslyn-resolved targets instead and treating the index expressions as ordinary method calls?

@michaelnebel
Copy link
Copy Markdown
Contributor Author

michaelnebel commented May 29, 2026

I'm not convinced that extracting the indexer is what we want; what about extracting the Roslyn-resolved targets instead and treating the index expressions as ordinary method calls?

Yes, I also considered that, but thought it would be better to extract something that more closely resembles the syntax instead of what it is being translated into. If I were to query our code and asking for uses of indexers, then I would expect cases like a[1..3] to show up. Otherwise I would need to know that for spans, then a[1..3] translates into a.Slice(1,3-1) and for strings it would translate into a.Substring(1,3-1).
If you prefer that we extract the actual methods calls, then we can definitely do that instead?

@michaelnebel michaelnebel self-assigned this Jun 1, 2026
@michaelnebel michaelnebel marked this pull request as draft June 2, 2026 13:48
@michaelnebel michaelnebel force-pushed the csharp/spanaccessrange branch from 9167814 to ee734c9 Compare June 2, 2026 14:05
| 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 |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be Length - 7 + 1?

| 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 |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

6 + 1?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe the length arguments are correct in the mentioned cases.

The semantics of s[a..b] means: Take the elements from s from index a up to and NOT including the element at index b (semantics of s[..b] is similar).
Example

var s = "Hello";
var sub = s[1..3];

then sub is el and it has length 3 - 1 = 2.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, I thought the end index was inclusive, sorry.

| 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 |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be a - 5 + 1? For example, if a is 6 then the length should be 2.

@michaelnebel michaelnebel force-pushed the csharp/spanaccessrange branch from ee734c9 to 8d0575c Compare June 3, 2026 09:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 8/8 changed files
  • Comments generated: 3

Comment on lines +103 to +107
/// <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>
---
category: minorAnalysis
---
* Improved extraction of span range-access expressions (for example, `a[0..3]`). These expressions are now extracted as span `Slice` calls.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants