diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ElementAccess.cs index 6f832132185..15f7fe709bb 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 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions private readonly BracketedArgumentListSyntax argumentList; - private IPropertySymbol? GetIndexerSymbol() + private ISymbol? GetTargetSymbol() { - var symbol = Context.GetSymbolInfo(base.Syntax).Symbol; + return Context.GetSymbolInfo(base.Syntax).Symbol; + } - if (symbol is IPropertySymbol { IsIndexer: true } indexer) - return indexer; + private static void SetExprArgument(TextWriter trapFile, Expression left, Expression right) + { + trapFile.expr_argument(left, 0); + trapFile.expr_argument(right, 0); + } - // 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) + 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) { - var indexers = method - .ContainingType - .GetMembers() - .OfType() - .Where(p => p.IsIndexer); + IsCompilerGenerated = true + }; + Factory.Create(info); + return sub; + } - var intIndexer = indexers - .Where(i => i.Parameters.Length == 1 && i.Parameters[0].Type.SpecialType == SpecialType.System_Int32) - .FirstOrDefault(); - - return intIndexer; + /// + /// 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); } - return null; + 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; + } + + return (method.Name == "Slice" || method.Name == "Substring") + && method.Parameters.Length == 2 + && lengthPropertySymbol is not null + && range is not null; + } + + /// + /// 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) + { + case (ExpressionSyntax lsyntax, ExpressionSyntax rsyntax): + { + var left = CreateFromRangeEndpoint(trapFile, lengthPropertySymbol, lsyntax, this, 0); + var right = MakeSubtractionExpression(this, 1); + + 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): + { + + 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; + } + } + + trapFile.expr_call(this, Method.Create(Context, slice)); } protected override void PopulateExpression(TextWriter trapFile) @@ -60,10 +186,20 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions 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 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions 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.