using System.IO; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Semmle.Extraction.Kinds; 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, info.Node, qualifier))) { this.qualifier = qualifier; this.argumentList = argumentList; } 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 MakeZeroFromEndExpression(IExpressionParentEntity parent, int child) { var info = new ExpressionInfo( Context, AnnotatedTypeSymbol.CreateNotAnnotated(Context.Compilation.GetSpecialType(SpecialType.System_Int32)), Location, ExprKind.INDEX, parent, child, isCompilerGenerated: true, null); var index = new Expression(info); MakeZeroLiteral(index, 0); return index; } private Expression MakeZeroLiteral(IExpressionParentEntity parent, int child) { return Literal.CreateGenerated(Context, parent, child, Context.Compilation.GetSpecialType(SpecialType.System_Int32), 0, Location); } /// /// 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 MakeFromRangeEndpoint(ExpressionSyntax syntax, IExpressionParentEntity parent, int child) { var info = new ExpressionNodeInfo(Context, syntax, parent, child); return syntax.Kind() == SyntaxKind.IndexExpression ? PrefixUnary.Create(info.SetKind(ExprKind.INDEX)) : 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 IsSlice(IMethodSymbol method, out RangeExpressionSyntax? range) { range = null; if (argumentList.Arguments.Count == 1) { range = argumentList.Arguments[0].Expression as RangeExpressionSyntax; } return (method.Name == "Slice" || method.Name == "Substring") && method.Parameters.Length == 2; } /// /// Populates a slice method call based on the given range. /// Roslyn translates indexer accesses with range expressions in the following way. /// 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) /// However, it is possible that both the qualifier or the index endpoints may contain method calls. /// If we want to translate this accurately, we would need to introduce synthetic statements for qualifier and /// the endpoints, which should then be used in the slice method call. /// To avoid this, we translate as follows. /// 1. s[a..b] -> s.Slice(a, b) /// 2. s[..b] -> s.Slice(0, b) /// 3. s[a..] -> s.Slice(a, ^0) /// 4. s[..] -> s.Slice(0, ^0) /// /// Even though index expressions can't technically be used in this way, they signal that we /// could perceive ^b as "length - b". /// /// Call arguments are only populated when a range expression is directly available in /// the list of arguments. /// This means that cases like below are not handled. /// System.Range x = 1..3; /// s[x] /// /// The trap file to write to. /// The slice method symbol. /// The range expression syntax. private void PopulateSlice(TextWriter trapFile, IMethodSymbol slice, RangeExpressionSyntax? range) { if (range is not null) { // Populate the call arguments var left = range.LeftOperand is ExpressionSyntax lsyntax ? MakeFromRangeEndpoint(lsyntax, this, 0) : MakeZeroLiteral(this, 0); var right = range.RightOperand is ExpressionSyntax rsyntax ? MakeFromRangeEndpoint(rsyntax, this, 1) : MakeZeroFromEndExpression(this, 1); SetExprArgument(trapFile, left, right); } trapFile.expr_call(this, Method.Create(Context, slice)); } protected override void PopulateExpression(TextWriter trapFile) { if (Kind == ExprKind.POINTER_INDIRECTION) { var qualifierInfo = new ExpressionNodeInfo(Context, qualifier, this, 0); var add = new Expression(new ExpressionInfo(Context, qualifierInfo.Type, Location, ExprKind.ADD, this, 0, isCompilerGenerated: false, null)); qualifierInfo.SetParent(add, 0); CreateFromNode(qualifierInfo); PopulateArguments(trapFile, argumentList, 1); } else { Create(Context, qualifier, this, -1); var target = GetTargetSymbol(); if (target is IMethodSymbol method && IsSlice(method, 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. PopulateSlice(trapFile, method, range); return; } PopulateArguments(trapFile, argumentList, 0); if (target is IPropertySymbol { IsIndexer: true } indexer) { trapFile.expr_access(this, Indexer.Create(Context, indexer)); } } } public sealed override Microsoft.CodeAnalysis.Location? ReportingLocation => base.ReportingLocation; private static bool IsArray(ITypeSymbol symbol) => symbol.TypeKind == Microsoft.CodeAnalysis.TypeKind.Array || symbol.IsInlineArray(); 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. if (qualifierType.Symbol is null) return ExprKind.ARRAY_ACCESS; if (qualifierType.Symbol.TypeKind == Microsoft.CodeAnalysis.TypeKind.Pointer) { // Convert expressions of the form a[b] into *(a+b) return ExprKind.POINTER_INDIRECTION; } return IsDynamic(cx, qualifier) ? ExprKind.DYNAMIC_ELEMENT_ACCESS : IsArray(qualifierType.Symbol) ? ExprKind.ARRAY_ACCESS : ExprKind.INDEXER_ACCESS; } } internal class NormalElementAccess : ElementAccess { private NormalElementAccess(ExpressionNodeInfo info) : base(info, ((ElementAccessExpressionSyntax)info.Node).Expression, ((ElementAccessExpressionSyntax)info.Node).ArgumentList) { } public static Expression Create(ExpressionNodeInfo info) => new NormalElementAccess(info).TryPopulate(); } internal class BindingElementAccess : ElementAccess { private BindingElementAccess(ExpressionNodeInfo info) : base(info, FindConditionalQualifier(info.Node), ((ElementBindingExpressionSyntax)info.Node).ArgumentList) { } public static Expression Create(ExpressionNodeInfo info) => new BindingElementAccess(info).TryPopulate(); protected override void PopulateExpression(TextWriter trapFile) { base.PopulateExpression(trapFile); MakeConditional(trapFile); } } }