File: Syntax\LambdaUtilities.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    internal static class LambdaUtilities
    {
        /// <summary>
        /// Returns true if the specified node represents a lambda.
        /// </summary>
        public static bool IsLambda(SyntaxNode node)
        {
            switch (node.Kind())
            {
                case SyntaxKind.ParenthesizedLambdaExpression:
                case SyntaxKind.SimpleLambdaExpression:
                case SyntaxKind.AnonymousMethodExpression:
                case SyntaxKind.LetClause:
                case SyntaxKind.WhereClause:
                case SyntaxKind.AscendingOrdering:
                case SyntaxKind.DescendingOrdering:
                case SyntaxKind.JoinClause:
                case SyntaxKind.GroupClause:
                case SyntaxKind.LocalFunctionStatement:
                    return true;
 
                case SyntaxKind.SelectClause:
                    var selectClause = (SelectClauseSyntax)node;
                    return !IsReducedSelectOrGroupByClause(selectClause, selectClause.Expression);
 
                case SyntaxKind.FromClause:
                    // The first from clause of a query expression is not a lambda.
                    return !node.Parent.IsKind(SyntaxKind.QueryExpression);
            }
 
            return false;
        }
 
        public static bool IsNotLambda(SyntaxNode node) => !IsLambda(node);
 
        /// <summary>
        /// Given a node that represents a lambda body returns a node that represents the lambda.
        /// </summary>
        public static SyntaxNode GetLambda(SyntaxNode lambdaBody)
        {
            Debug.Assert(lambdaBody.Parent is object);
            var lambda = lambdaBody.Parent;
            if (lambda.IsKind(SyntaxKind.ArrowExpressionClause))
            {
                // In case of expression bodied local functions there is a three level hierarchy: 
                // LocalFunctionStatement -> ArrowExpressionClause -> Expression.
                // And the lambda is the LocalFunctionStatement.
                lambda = lambda.Parent;
                Debug.Assert(lambda.IsKind(SyntaxKind.LocalFunctionStatement));
            }
 
            Debug.Assert(IsLambda(lambda));
            return lambda;
        }
 
        /// <summary>
        /// See SyntaxNode.GetCorrespondingLambdaBody.
        /// </summary>
        internal static SyntaxNode? TryGetCorrespondingLambdaBody(SyntaxNode oldBody, SyntaxNode newLambda)
        {
            Debug.Assert(oldBody.Parent is object);
 
            switch (newLambda.Kind())
            {
                case SyntaxKind.ParenthesizedLambdaExpression:
                case SyntaxKind.SimpleLambdaExpression:
                case SyntaxKind.AnonymousMethodExpression:
                    return ((AnonymousFunctionExpressionSyntax)newLambda).Body;
 
                case SyntaxKind.FromClause:
                    return ((FromClauseSyntax)newLambda).Expression;
 
                case SyntaxKind.LetClause:
                    return ((LetClauseSyntax)newLambda).Expression;
 
                case SyntaxKind.WhereClause:
                    return ((WhereClauseSyntax)newLambda).Condition;
 
                case SyntaxKind.AscendingOrdering:
                case SyntaxKind.DescendingOrdering:
                    return ((OrderingSyntax)newLambda).Expression;
 
                case SyntaxKind.SelectClause:
                    var selectClause = (SelectClauseSyntax)newLambda;
 
                    // Select clause is not considered to be lambda if it's reduced,
                    // however to avoid complexity we allow it to be passed in and just return null.
                    return IsReducedSelectOrGroupByClause(selectClause, selectClause.Expression) ? null : selectClause.Expression;
 
                case SyntaxKind.JoinClause:
                    var oldJoin = (JoinClauseSyntax)oldBody.Parent;
                    var newJoin = (JoinClauseSyntax)newLambda;
                    Debug.Assert(oldJoin.LeftExpression == oldBody || oldJoin.RightExpression == oldBody);
                    return (oldJoin.LeftExpression == oldBody) ? newJoin.LeftExpression : newJoin.RightExpression;
 
                case SyntaxKind.GroupClause:
                    var oldGroup = (GroupClauseSyntax)oldBody.Parent;
                    var newGroup = (GroupClauseSyntax)newLambda;
                    Debug.Assert(oldGroup.GroupExpression == oldBody || oldGroup.ByExpression == oldBody);
                    return (oldGroup.GroupExpression == oldBody) ?
                        (IsReducedSelectOrGroupByClause(newGroup, newGroup.GroupExpression) ? null : newGroup.GroupExpression) : newGroup.ByExpression;
 
                case SyntaxKind.LocalFunctionStatement:
                    return GetLocalFunctionBody((LocalFunctionStatementSyntax)newLambda);
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(newLambda.Kind());
            }
        }
 
        public static SyntaxNode GetNestedFunctionBody(SyntaxNode nestedFunction)
            => nestedFunction switch
            {
                AnonymousFunctionExpressionSyntax anonymousFunctionExpressionSyntax => anonymousFunctionExpressionSyntax.Body,
                LocalFunctionStatementSyntax localFunctionStatementSyntax => (CSharpSyntaxNode?)localFunctionStatementSyntax.Body ?? localFunctionStatementSyntax.ExpressionBody!.Expression,
                _ => throw ExceptionUtilities.UnexpectedValue(nestedFunction),
            };
 
        public static bool IsNotLambdaBody(SyntaxNode node)
        {
            return !IsLambdaBody(node);
        }
 
        /// <summary>
        /// Returns true if the specified <paramref name="node"/> represents a body of a lambda.
        /// </summary>
        public static bool IsLambdaBody(SyntaxNode node, bool allowReducedLambdas = false)
        {
            var parent = node?.Parent;
            if (parent == null)
            {
                return false;
            }
 
            switch (parent.Kind())
            {
                case SyntaxKind.ParenthesizedLambdaExpression:
                case SyntaxKind.SimpleLambdaExpression:
                case SyntaxKind.AnonymousMethodExpression:
                    var anonymousFunction = (AnonymousFunctionExpressionSyntax)parent;
                    return anonymousFunction.Body == node;
 
                case SyntaxKind.LocalFunctionStatement:
                    var localFunction = (LocalFunctionStatementSyntax)parent;
                    return localFunction.Body == node;
 
                case SyntaxKind.ArrowExpressionClause:
                    var arrowExpressionClause = (ArrowExpressionClauseSyntax)parent;
                    return arrowExpressionClause.Expression == node && arrowExpressionClause.Parent is LocalFunctionStatementSyntax;
 
                case SyntaxKind.FromClause:
                    var fromClause = (FromClauseSyntax)parent;
                    return fromClause.Expression == node && fromClause.Parent is QueryBodySyntax;
 
                case SyntaxKind.JoinClause:
                    var joinClause = (JoinClauseSyntax)parent;
                    return joinClause.LeftExpression == node || joinClause.RightExpression == node;
 
                case SyntaxKind.LetClause:
                    var letClause = (LetClauseSyntax)parent;
                    return letClause.Expression == node;
 
                case SyntaxKind.WhereClause:
                    var whereClause = (WhereClauseSyntax)parent;
                    return whereClause.Condition == node;
 
                case SyntaxKind.AscendingOrdering:
                case SyntaxKind.DescendingOrdering:
                    var ordering = (OrderingSyntax)parent;
                    return ordering.Expression == node;
 
                case SyntaxKind.SelectClause:
                    var selectClause = (SelectClauseSyntax)parent;
                    return selectClause.Expression == node && (allowReducedLambdas || !IsReducedSelectOrGroupByClause(selectClause, selectClause.Expression));
 
                case SyntaxKind.GroupClause:
                    var groupClause = (GroupClauseSyntax)parent;
                    return (groupClause.GroupExpression == node && (allowReducedLambdas || !IsReducedSelectOrGroupByClause(groupClause, groupClause.GroupExpression))) ||
                           groupClause.ByExpression == node;
            }
 
            return false;
        }
 
        /// <summary>
        /// When queries are translated into expressions select and group-by expressions such that
        /// 1) select/group-by expression is the same identifier as the "source" identifier and
        /// 2) at least one Where or OrderBy clause but no other clause is present in the contained query body or
        ///    the expression in question is a group-by expression and the body has no clause
        /// 
        /// do not translate into lambdas.
        /// By "source" identifier we mean the identifier specified in the from clause that initiates the query or the query continuation that includes the body.
        /// 
        /// The above condition can be derived from the language specification (chapter 7.16.2) as follows:
        /// - In order for 7.16.2.5 "Select clauses" to be applicable the following conditions must hold:
        ///   - There has to be at least one clause in the body, otherwise the query is reduced into a final form by 7.16.2.3 "Degenerate query expressions".
        ///   - Only where and order-by clauses may be present in the query body, otherwise a transformation in 7.16.2.4 "From, let, where, join and orderby clauses"
        ///     produces pattern that doesn't match the requirements of 7.16.2.5.
        ///   
        /// - In order for 7.16.2.6 "Groupby clauses" to be applicable the following conditions must hold:
        ///   - Only where and order-by clauses may be present in the query body, otherwise a transformation in 7.16.2.4 "From, let, where, join and orderby clauses"
        ///     produces pattern that doesn't match the requirements of 7.16.2.5.
        /// </summary>
        private static bool IsReducedSelectOrGroupByClause(SelectOrGroupClauseSyntax selectOrGroupClause, ExpressionSyntax selectOrGroupExpression)
        {
            if (!selectOrGroupExpression.IsKind(SyntaxKind.IdentifierName))
            {
                return false;
            }
 
            var selectorIdentifier = ((IdentifierNameSyntax)selectOrGroupExpression).Identifier;
 
            SyntaxToken sourceIdentifier;
            QueryBodySyntax containingBody;
 
            Debug.Assert(selectOrGroupClause.Parent!.Parent is object);
            var containingQueryOrContinuation = selectOrGroupClause.Parent.Parent;
            if (containingQueryOrContinuation.IsKind(SyntaxKind.QueryExpression))
            {
                var containingQuery = (QueryExpressionSyntax)containingQueryOrContinuation;
                containingBody = containingQuery.Body;
                sourceIdentifier = containingQuery.FromClause.Identifier;
            }
            else
            {
                var containingContinuation = (QueryContinuationSyntax)containingQueryOrContinuation;
                sourceIdentifier = containingContinuation.Identifier;
                containingBody = containingContinuation.Body;
            }
 
            if (!SyntaxFactory.AreEquivalent(sourceIdentifier, selectorIdentifier))
            {
                return false;
            }
 
            if (selectOrGroupClause.IsKind(SyntaxKind.SelectClause) && containingBody.Clauses.Count == 0)
            {
                return false;
            }
 
            foreach (var clause in containingBody.Clauses)
            {
                if (!clause.IsKind(SyntaxKind.WhereClause) && !clause.IsKind(SyntaxKind.OrderByClause))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        /// <remarks>
        /// In C# lambda bodies are expressions or block statements. In both cases it's a single node.
        /// In VB a lambda body might be a sequence of nodes (statements). 
        /// We define this function to minimize differences between C# and VB implementation.
        /// </remarks>
        public static bool IsLambdaBodyStatementOrExpression(SyntaxNode node)
        {
            return IsLambdaBody(node);
        }
 
        public static bool IsLambdaBodyStatementOrExpression(SyntaxNode node, out SyntaxNode lambdaBody)
        {
            lambdaBody = node;
            return IsLambdaBody(node);
        }
 
        /// <summary>
        /// If the specified node represents a lambda returns a node (or nodes) that represent its body (bodies).
        /// </summary>
        public static bool TryGetLambdaBodies(SyntaxNode node, [NotNullWhen(true)] out SyntaxNode? lambdaBody1, out SyntaxNode? lambdaBody2)
        {
            lambdaBody1 = null;
            lambdaBody2 = null;
 
            switch (node.Kind())
            {
                case SyntaxKind.ParenthesizedLambdaExpression:
                case SyntaxKind.SimpleLambdaExpression:
                case SyntaxKind.AnonymousMethodExpression:
                    lambdaBody1 = ((AnonymousFunctionExpressionSyntax)node).Body;
                    return true;
 
                case SyntaxKind.FromClause:
                    // The first from clause of a query expression is not a lambda.
                    if (node.Parent.IsKind(SyntaxKind.QueryExpression))
                    {
                        return false;
                    }
 
                    lambdaBody1 = ((FromClauseSyntax)node).Expression;
                    return true;
 
                case SyntaxKind.JoinClause:
                    var joinClause = (JoinClauseSyntax)node;
                    lambdaBody1 = joinClause.LeftExpression;
                    lambdaBody2 = joinClause.RightExpression;
                    return true;
 
                case SyntaxKind.LetClause:
                    lambdaBody1 = ((LetClauseSyntax)node).Expression;
                    return true;
 
                case SyntaxKind.WhereClause:
                    lambdaBody1 = ((WhereClauseSyntax)node).Condition;
                    return true;
 
                case SyntaxKind.AscendingOrdering:
                case SyntaxKind.DescendingOrdering:
                    lambdaBody1 = ((OrderingSyntax)node).Expression;
                    return true;
 
                case SyntaxKind.SelectClause:
                    var selectClause = (SelectClauseSyntax)node;
                    if (IsReducedSelectOrGroupByClause(selectClause, selectClause.Expression))
                    {
                        return false;
                    }
 
                    lambdaBody1 = selectClause.Expression;
                    return true;
 
                case SyntaxKind.GroupClause:
                    var groupClause = (GroupClauseSyntax)node;
                    if (IsReducedSelectOrGroupByClause(groupClause, groupClause.GroupExpression))
                    {
                        lambdaBody1 = groupClause.ByExpression;
                    }
                    else
                    {
                        lambdaBody1 = groupClause.GroupExpression;
                        lambdaBody2 = groupClause.ByExpression;
                    }
 
                    return true;
 
                case SyntaxKind.LocalFunctionStatement:
                    lambdaBody1 = GetLocalFunctionBody((LocalFunctionStatementSyntax)node);
                    return lambdaBody1 != null;
            }
 
            return false;
        }
 
        /// <summary>
        /// Compares content of two nodes ignoring lambda bodies and trivia.
        /// </summary>
        public static bool AreEquivalentIgnoringLambdaBodies(SyntaxNode oldNode, SyntaxNode newNode)
        {
            // all tokens that don't belong to a lambda body:
            var oldTokens = oldNode.DescendantTokens(node => node == oldNode || !IsLambdaBodyStatementOrExpression(node));
            var newTokens = newNode.DescendantTokens(node => node == newNode || !IsLambdaBodyStatementOrExpression(node));
 
            return oldTokens.SequenceEqual(newTokens, SyntaxFactory.AreEquivalent);
        }
 
        /// <summary>
        /// "Pair lambda" is a synthesized lambda that creates an instance of an anonymous type representing a pair of values. 
        /// </summary>
        internal static bool IsQueryPairLambda(SyntaxNode syntax)
        {
            // TODO (bug https://github.com/dotnet/roslyn/issues/2663): 
            // Avoid generating these lambdas. Instead generate a static factory method on the anonymous type.
            return syntax.IsKind(SyntaxKind.GroupClause) ||
                   syntax.IsKind(SyntaxKind.JoinClause) ||
                   syntax.IsKind(SyntaxKind.FromClause);
        }
 
        /// <summary>
        /// Returns true if the specified node is of a kind that could represent a closure scope -- that
        /// is, a scope of a captured variable.
        /// Doesn't check whether or not the node actually declares any captured variable.
        /// </summary>
        internal static bool IsClosureScope(SyntaxNode node)
        {
            switch (node.Kind())
            {
                case SyntaxKind.CompilationUnit:
                case SyntaxKind.Block:
                case SyntaxKind.SwitchStatement:
                case SyntaxKind.ArrowExpressionClause:  // expression-bodied member
                case SyntaxKind.CatchClause:
                case SyntaxKind.ForStatement:
                case SyntaxKind.ForEachStatement:
                case SyntaxKind.ForEachVariableStatement:
                case SyntaxKind.UsingStatement:
                case SyntaxKind.TryStatement:
 
                // ctor parameter captured by a lambda in a ctor initializer
                case SyntaxKind.ConstructorDeclaration:
                    return true;
 
                // Due to pattern-matching, any statement that contains an expression may introduce a scope.
                case SyntaxKind.DoStatement:
                case SyntaxKind.ExpressionStatement:
                case SyntaxKind.FixedStatement:
                case SyntaxKind.GotoCaseStatement:
                case SyntaxKind.IfStatement:
                case SyntaxKind.LockStatement:
                case SyntaxKind.ReturnStatement:
                case SyntaxKind.ThrowStatement:
                case SyntaxKind.WhileStatement:
                case SyntaxKind.YieldReturnStatement:
                    return true;
 
                case SyntaxKind.ClassDeclaration:
                case SyntaxKind.StructDeclaration:
                case SyntaxKind.RecordDeclaration:
                case SyntaxKind.RecordStructDeclaration:
                    // Captured primary constructor parameters.
                    //
                    // With dynamic analysis instrumentation, a type declaration can also be the syntax associated
                    // with the analysis payload local of a synthesized constructor.
                    // If the synthesized constructor includes an initializer with a lambda,
                    // that lambda needs a closure that captures the analysis payload of the constructor.
                    return true;
 
                case SyntaxKind.SwitchExpression:
                case SyntaxKind.AwaitExpression:
                    // These translate into a BoundSpillSequence, which is then translated into a block
                    // containing temps required for spilling subexpressions. That block has the syntax of the switch
                    // expression or await expression.
                    return true;
 
                default:
                    // With the introduction of pattern-matching, many nodes now contain top-level
                    // expressions that may introduce pattern variables.
                    if (node.Parent != null)
                    {
                        switch (node.Parent.Kind())
                        {
                            case SyntaxKind.EqualsValueClause:
                                return true;
 
                            case SyntaxKind.ForStatement:
                                SeparatedSyntaxList<ExpressionSyntax> incrementors = ((ForStatementSyntax)node.Parent).Incrementors;
                                if (incrementors.FirstOrDefault() == node)
                                {
                                    return true;
                                }
                                break;
                        }
                    }
 
                    break;
            }
 
            if (IsLambdaBody(node))
            {
                return true;
            }
 
            // TODO: EE expression
            if (node is ExpressionSyntax && node.Parent != null && node.Parent.Parent == null)
            {
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Given a node that represents a variable declaration, lambda or a closure scope return the position to be used to calculate 
        /// the node's syntax offset with respect to its containing member.
        /// </summary>
        internal static int GetDeclaratorPosition(SyntaxNode node)
        {
            // To differentiate between nested switch expressions that start at the same offset, use the offset of the `switch` keyword.
            return (node is SwitchExpressionSyntax switchExpression) ? switchExpression.SwitchKeyword.SpanStart : node.SpanStart;
        }
 
        private static SyntaxNode? GetLocalFunctionBody(LocalFunctionStatementSyntax localFunctionStatementSyntax)
            => (SyntaxNode?)localFunctionStatementSyntax.Body ?? localFunctionStatementSyntax.ExpressionBody?.Expression;
    }
}