11using System . IO ;
22using Microsoft . CodeAnalysis ;
3+ using Microsoft . CodeAnalysis . CSharp ;
34using Microsoft . CodeAnalysis . CSharp . Syntax ;
45using Semmle . Extraction . Kinds ;
56
@@ -8,7 +9,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
89 internal abstract class ElementAccess : Expression < ExpressionSyntax >
910 {
1011 protected ElementAccess ( ExpressionNodeInfo info , ExpressionSyntax qualifier , BracketedArgumentListSyntax argumentList )
11- : base ( info . SetKind ( GetKind ( info . Context , qualifier ) ) )
12+ : base ( info . SetKind ( GetKind ( info . Context , info . Node , qualifier ) ) )
1213 {
1314 this . qualifier = qualifier ;
1415 this . argumentList = argumentList ;
@@ -17,6 +18,125 @@ protected ElementAccess(ExpressionNodeInfo info, ExpressionSyntax qualifier, Bra
1718 private readonly ExpressionSyntax qualifier ;
1819 private readonly BracketedArgumentListSyntax argumentList ;
1920
21+
22+ private ISymbol ? GetTargetSymbol ( )
23+ {
24+ return Context . GetSymbolInfo ( base . Syntax ) . Symbol ;
25+ }
26+
27+ private static void SetExprArgument ( TextWriter trapFile , Expression left , Expression right )
28+ {
29+ trapFile . expr_argument ( left , 0 ) ;
30+ trapFile . expr_argument ( right , 0 ) ;
31+ }
32+
33+ private Expression MakeZeroFromEndExpression ( IExpressionParentEntity parent , int child )
34+ {
35+ var info = new ExpressionInfo (
36+ Context ,
37+ AnnotatedTypeSymbol . CreateNotAnnotated ( Context . Compilation . GetSpecialType ( SpecialType . System_Int32 ) ) ,
38+ Location ,
39+ ExprKind . INDEX ,
40+ parent ,
41+ child ,
42+ isCompilerGenerated : true ,
43+ null ) ;
44+
45+ var index = new Expression ( info ) ;
46+
47+ MakeZeroLiteral ( index , 0 ) ;
48+ return index ;
49+ }
50+
51+ private Expression MakeZeroLiteral ( IExpressionParentEntity parent , int child )
52+ {
53+ return Literal . CreateGenerated ( Context , parent , child , Context . Compilation . GetSpecialType ( SpecialType . System_Int32 ) , 0 , Location ) ;
54+ }
55+
56+
57+ /// <summary>
58+ /// It is assumed that either the input is
59+ /// 1. A normal expression that can be used as endpoint (e.g a constant like "3").
60+ /// 2. An index expression indicating that we should read from the end (e.g "^1").
61+ /// </summary>
62+ /// <param name="syntax">The syntax node representing the range endpoint.</param>
63+ /// <param name="parent">The parent expression entity.</param>
64+ /// <param name="child">The child index within the parent.</param>
65+ /// <returns>An expression representing the endpoint of a range to be used in conjunction with a slice operation.</returns>
66+ private Expression MakeFromRangeEndpoint ( ExpressionSyntax syntax , IExpressionParentEntity parent , int child )
67+ {
68+ var info = new ExpressionNodeInfo ( Context , syntax , parent , child ) ;
69+
70+ return syntax . Kind ( ) == SyntaxKind . IndexExpression
71+ ? PrefixUnary . Create ( info . SetKind ( ExprKind . INDEX ) )
72+ : Factory . Create ( info ) ;
73+ }
74+
75+ /// <summary>
76+ /// Determines whether the given method is a slice method, which is defined as a method with
77+ /// the name "Slice" or "Substring" and two parameters.
78+ /// </summary>
79+ /// <param name="method">The method symbol to check.</param>
80+ /// <returns>True if the method is a slice method; false otherwise.</returns>
81+ private bool IsSlice ( IMethodSymbol method , out RangeExpressionSyntax ? range )
82+ {
83+ range = null ;
84+
85+ if ( argumentList . Arguments . Count == 1 )
86+ {
87+ range = argumentList . Arguments [ 0 ] . Expression as RangeExpressionSyntax ;
88+ }
89+
90+ return ( method . Name == "Slice" || method . Name == "Substring" )
91+ && method . Parameters . Length == 2 ;
92+ }
93+
94+ /// <summary>
95+ /// Populates a slice method call based on the given range.
96+ /// Roslyn translates indexer accesses with range expressions in the following way.
97+ /// 1. s[a..b] -> s.Slice(a, b - a)
98+ /// 2. s[..b] -> s.Slice(0, b)
99+ /// 3. s[a..] -> s.Slice(a, s.Length - a)
100+ /// 4. s[..] -> s.Slice(0, s.Length)
101+ /// However, it is possible that both the qualifier or the index endpoints may contain method calls.
102+ /// If we want to translate this accurately, we would need to introduce synthetic statements for qualifier and
103+ /// the endpoints, which should then be used in the slice method call.
104+ /// To avoid this, we translate as follows.
105+ /// 1. s[a..b] -> s.Slice(a, b)
106+ /// 2. s[..b] -> s.Slice(0, b)
107+ /// 3. s[a..] -> s.Slice(a, ^0)
108+ /// 4. s[..] -> s.Slice(0, ^0)
109+ ///
110+ /// Even though index expressions can't technically be used in this way, they signal that we
111+ /// could perceive ^b as "length - b".
112+ ///
113+ /// Call arguments are only populated when a range expression is directly available in
114+ /// the list of arguments.
115+ /// This means that cases like below are not handled.
116+ /// System.Range x = 1..3;
117+ /// s[x]
118+ /// </summary>
119+ /// <param name="trapFile">The trap file to write to.</param>
120+ /// <param name="slice">The slice method symbol.</param>
121+ /// <param name="range">The range expression syntax.</param>
122+ private void PopulateSlice ( TextWriter trapFile , IMethodSymbol slice , RangeExpressionSyntax ? range )
123+ {
124+ if ( range is not null )
125+ {
126+ // Populate the call arguments
127+ var left = range . LeftOperand is ExpressionSyntax lsyntax
128+ ? MakeFromRangeEndpoint ( lsyntax , this , 0 )
129+ : MakeZeroLiteral ( this , 0 ) ;
130+
131+ var right = range . RightOperand is ExpressionSyntax rsyntax
132+ ? MakeFromRangeEndpoint ( rsyntax , this , 1 )
133+ : MakeZeroFromEndExpression ( this , 1 ) ;
134+
135+ SetExprArgument ( trapFile , left , right ) ;
136+ }
137+ trapFile . expr_call ( this , Method . Create ( Context , slice ) ) ;
138+ }
139+
20140 protected override void PopulateExpression ( TextWriter trapFile )
21141 {
22142 if ( Kind == ExprKind . POINTER_INDIRECTION )
@@ -30,11 +150,19 @@ protected override void PopulateExpression(TextWriter trapFile)
30150 else
31151 {
32152 Create ( Context , qualifier , this , - 1 ) ;
33- PopulateArguments ( trapFile , argumentList , 0 ) ;
34153
35- var symbolInfo = Context . GetSymbolInfo ( base . Syntax ) ;
154+ var target = GetTargetSymbol ( ) ;
155+ if ( target is IMethodSymbol method && IsSlice ( method , out var range ) )
156+ {
157+ // When an indexer on a span or string is used in conjunction with a range expression, the compiler translates
158+ // this into a call to the "Slice" or "Substring" method.
159+ // In this case, we want to populate a slice/substring method call instead of an indexer access.
160+ PopulateSlice ( trapFile , method , range ) ;
161+ return ;
162+ }
36163
37- if ( symbolInfo . Symbol is IPropertySymbol indexer )
164+ PopulateArguments ( trapFile , argumentList , 0 ) ;
165+ if ( target is IPropertySymbol { IsIndexer : true } indexer )
38166 {
39167 trapFile . expr_access ( this , Indexer . Create ( Context , indexer ) ) ;
40168 }
@@ -46,8 +174,11 @@ protected override void PopulateExpression(TextWriter trapFile)
46174 private static bool IsArray ( ITypeSymbol symbol ) =>
47175 symbol . TypeKind == Microsoft . CodeAnalysis . TypeKind . Array || symbol . IsInlineArray ( ) ;
48176
49- private static ExprKind GetKind ( Context cx , ExpressionSyntax qualifier )
177+ private static ExprKind GetKind ( Context cx , ExpressionSyntax syntax , ExpressionSyntax qualifier )
50178 {
179+ if ( cx . GetSymbolInfo ( syntax ) . Symbol is IMethodSymbol )
180+ return ExprKind . METHOD_INVOCATION ;
181+
51182 var qualifierType = cx . GetType ( qualifier ) ;
52183
53184 // This is a compilation error, so make a guess and continue.
0 commit comments