Skip to content

Commit 4231ffc

Browse files
committed
C#: Extract Slice and Substring operations and synthesize the call arguments, when using indexers in conjunction with ranges on spans and strings.
1 parent 34f69f2 commit 4231ffc

1 file changed

Lines changed: 162 additions & 23 deletions

File tree

csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs

Lines changed: 162 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.IO;
23
using System.Linq;
34
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
46
using Microsoft.CodeAnalysis.CSharp.Syntax;
57
using Semmle.Extraction.Kinds;
68

@@ -9,7 +11,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
911
internal abstract class ElementAccess : Expression<ExpressionSyntax>
1012
{
1113
protected ElementAccess(ExpressionNodeInfo info, ExpressionSyntax qualifier, BracketedArgumentListSyntax argumentList)
12-
: base(info.SetKind(GetKind(info.Context, qualifier)))
14+
: base(info.SetKind(GetKind(info.Context, info.Node, qualifier)))
1315
{
1416
this.qualifier = qualifier;
1517
this.argumentList = argumentList;
@@ -19,32 +21,156 @@ protected ElementAccess(ExpressionNodeInfo info, ExpressionSyntax qualifier, Bra
1921
private readonly BracketedArgumentListSyntax argumentList;
2022

2123

22-
private IPropertySymbol? GetIndexerSymbol()
24+
private ISymbol? GetTargetSymbol()
2325
{
24-
var symbol = Context.GetSymbolInfo(base.Syntax).Symbol;
26+
return Context.GetSymbolInfo(base.Syntax).Symbol;
27+
}
28+
29+
private static void SetExprArgument(TextWriter trapFile, Expression left, Expression right)
30+
{
31+
trapFile.expr_argument(left, 0);
32+
trapFile.expr_argument(right, 0);
33+
}
34+
35+
private Expression MakeSubtractionExpression(IExpressionParentEntity parent, int child)
36+
{
37+
var info = new ExpressionInfo(
38+
Context,
39+
AnnotatedTypeSymbol.CreateNotAnnotated(Context.Compilation.GetSpecialType(SpecialType.System_Int32)),
40+
Location,
41+
ExprKind.SUB,
42+
parent,
43+
child,
44+
isCompilerGenerated: true,
45+
null);
46+
47+
return new Expression(info);
48+
}
49+
50+
private void MakeLengthPropertyCall(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, IExpressionParentEntity parent, int child)
51+
{
52+
var lengthInfo = new ExpressionInfo(
53+
Context,
54+
AnnotatedTypeSymbol.CreateNotAnnotated(Context.Compilation.GetSpecialType(SpecialType.System_Int32)),
55+
Location,
56+
ExprKind.PROPERTY_ACCESS,
57+
parent,
58+
child,
59+
isCompilerGenerated: true,
60+
null);
61+
var length = new Expression(lengthInfo);
62+
Create(Context, qualifier, length, -1);
63+
64+
var lengthProp = Property.Create(Context, lengthPropertySymbol);
65+
trapFile.expr_access(length, lengthProp);
66+
}
67+
68+
private Expression CreateFromIndexExpression(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, IExpressionParentEntity parent, int child, PrefixUnaryExpressionSyntax index)
69+
{
70+
var sub = MakeSubtractionExpression(parent, child);
71+
MakeLengthPropertyCall(trapFile, lengthPropertySymbol, sub, 0);
72+
var info = new ExpressionNodeInfo(Context, index.Operand, sub, 1)
73+
{
74+
IsCompilerGenerated = true
75+
};
76+
Factory.Create(info);
77+
return sub;
78+
}
79+
80+
/// <summary>
81+
/// It is assumed that either the input is
82+
/// 1. A normal expression that can be used as endpoint (e.g a constant like "3").
83+
/// 2. An index expression indicating that we should read from the end (e.g "^1").
84+
/// </summary>
85+
/// <param name="syntax">The syntax node representing the range endpoint.</param>
86+
/// <param name="parent">The parent expression entity.</param>
87+
/// <param name="child">The child index within the parent.</param>
88+
/// <returns>An expression representing the endpoint of a range to be used in conjunction with a slice operation.</returns>
89+
private Expression CreateFromRangeEndpoint(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, ExpressionSyntax syntax, IExpressionParentEntity parent, int child)
90+
{
91+
if (syntax.Kind() == SyntaxKind.IndexExpression && syntax is PrefixUnaryExpressionSyntax index)
92+
{
93+
return CreateFromIndexExpression(trapFile, lengthPropertySymbol, parent, child, index);
94+
}
95+
96+
var info = new ExpressionNodeInfo(Context, syntax, parent, child)
97+
{
98+
IsCompilerGenerated = true
99+
};
100+
return Factory.Create(info);
101+
}
102+
103+
/// <summary>
104+
/// Determines whether the given method is a slice method, which is defined as a method with
105+
/// the name "Slice" or "SubString" and two parameters.
106+
/// </summary> <param name="method">The method symbol to check.</param>
107+
/// <returns>True if the method is a slice method, false otherwise.</returns>
108+
private bool IsSliceWithRange(IMethodSymbol method, [NotNullWhen(true)] out IPropertySymbol? lengthPropertySymbol, [NotNullWhen(true)] out RangeExpressionSyntax? range)
109+
{
110+
range = null;
111+
lengthPropertySymbol = method
112+
.ContainingType
113+
.GetMembers("Length")
114+
.OfType<IPropertySymbol>()
115+
.FirstOrDefault();
116+
117+
if (argumentList.Arguments.Count == 1)
118+
{
119+
range = argumentList.Arguments[0].Expression as RangeExpressionSyntax;
120+
}
25121

26-
if (symbol is IPropertySymbol { IsIndexer: true } indexer)
27-
return indexer;
122+
return (method.Name == "Slice" || method.Name == "Substring")
123+
&& method.Parameters.Length == 2
124+
&& lengthPropertySymbol is not null
125+
&& range is not null;
126+
}
28127

29-
// In some cases, Roslyn translates the use of range expressions directly into method calls.
30-
// E.g. `a[0..3]` is translated into `a.Slice(0, 3)`, if `a` is a `Span<T>`.
31-
// In this case, we want to populate the indexer access as normal (as this reflects the source code more accurately).
32-
if (symbol is IMethodSymbol method)
128+
/// <summary>
129+
/// Populates a slice method call based on the given range and length property symbol.
130+
/// </summary>
131+
/// <param name="trapFile">The trap file to write to.</param>
132+
/// <param name="lengthPropertySymbol">The length property symbol.</param>
133+
/// <param name="slice">The slice method symbol.</param>
134+
/// <param name="range">The range expression syntax.</param>
135+
private void PopulateSlice(TextWriter trapFile, IPropertySymbol lengthPropertySymbol, IMethodSymbol slice, RangeExpressionSyntax range)
136+
{
137+
// 1. s[a..b] -> s.Slice(a, b - a)
138+
// 2. s[..b] -> s.Slice(0, b)
139+
// 3. s[a..] -> s.Slice(a, s.Length - a)
140+
// Furthermore, note that uses of index expressions (e.g. s[2..^1]) within the range
141+
// get translated to length - index, so we need to handle this as well.
142+
switch (range.LeftOperand, range.RightOperand)
33143
{
34-
var indexers = method
35-
.ContainingType
36-
.GetMembers()
37-
.OfType<IPropertySymbol>()
38-
.Where(p => p.IsIndexer);
144+
case (ExpressionSyntax lsyntax, ExpressionSyntax rsyntax):
145+
{
146+
var left = CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, this, 0);
147+
var right = MakeSubtractionExpression(this, 1);
39148

40-
var intIndexer = indexers
41-
.Where(i => i.Parameters.Length == 1 && i.Parameters[0].Type.SpecialType == SpecialType.System_Int32)
42-
.FirstOrDefault();
149+
CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, rsyntax, right, 0);
150+
CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, right, 1);
151+
SetExprArgument(trapFile, left, right);
152+
break;
153+
}
154+
case (null, ExpressionSyntax rsyntax):
155+
{
156+
var left = Literal.CreateGenerated(Context, this, 0, Context.Compilation.GetSpecialType(SpecialType.System_Int32), 0, Location);
157+
var right = CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, rsyntax, this, 1);
158+
SetExprArgument(trapFile, left, right);
159+
break;
160+
}
161+
case (ExpressionSyntax lsyntax, null):
162+
{
43163

44-
return intIndexer;
164+
var left = CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, this, 0);
165+
var right = MakeSubtractionExpression(this, 1);
166+
MakeLengthPropertyCall(trapFile, lengthPropertySymbol, right, 0);
167+
CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, right, 1);
168+
SetExprArgument(trapFile, left, right);
169+
break;
170+
}
45171
}
46172

47-
return null;
173+
trapFile.expr_call(this, Method.Create(Context, slice));
48174
}
49175

50176
protected override void PopulateExpression(TextWriter trapFile)
@@ -60,10 +186,20 @@ protected override void PopulateExpression(TextWriter trapFile)
60186
else
61187
{
62188
Create(Context, qualifier, this, -1);
63-
PopulateArguments(trapFile, argumentList, 0);
64189

65-
var indexer = GetIndexerSymbol();
66-
if (indexer is not null)
190+
var target = GetTargetSymbol();
191+
if (target is IMethodSymbol method && IsSliceWithRange(method, out var lengthPropertySymbol, out var range))
192+
{
193+
// When an indexer on a span or string is used in conjunction with a range expression, the compiler translates
194+
// this into a call to the "Slice" or "Substring" method.
195+
// In this case, we want to populate a slice/substring method call instead of an indexer access.
196+
// E.g s[1..4] gets translated to s.Slice(1, 4 - 1) if s is a span.
197+
PopulateSlice(trapFile, lengthPropertySymbol, method, range);
198+
return;
199+
}
200+
201+
PopulateArguments(trapFile, argumentList, 0);
202+
if (target is IPropertySymbol { IsIndexer: true } indexer)
67203
{
68204
trapFile.expr_access(this, Indexer.Create(Context, indexer));
69205
}
@@ -75,8 +211,11 @@ protected override void PopulateExpression(TextWriter trapFile)
75211
private static bool IsArray(ITypeSymbol symbol) =>
76212
symbol.TypeKind == Microsoft.CodeAnalysis.TypeKind.Array || symbol.IsInlineArray();
77213

78-
private static ExprKind GetKind(Context cx, ExpressionSyntax qualifier)
214+
private static ExprKind GetKind(Context cx, ExpressionSyntax syntax, ExpressionSyntax qualifier)
79215
{
216+
if (cx.GetSymbolInfo(syntax).Symbol is IMethodSymbol)
217+
return ExprKind.METHOD_INVOCATION;
218+
80219
var qualifierType = cx.GetType(qualifier);
81220

82221
// This is a compilation error, so make a guess and continue.

0 commit comments

Comments
 (0)