// 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.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Extensions; using static CSharpSyntaxTokens; internal static partial class ExpressionSyntaxExtensions { [return: NotNullIfNotNull(nameof(expression))] public static ExpressionSyntax? WalkUpParentheses(this ExpressionSyntax? expression) { while (expression?.Parent is ParenthesizedExpressionSyntax parentExpr) expression = parentExpr; return expression; } public static ExpressionSyntax WalkDownParentheses(this ExpressionSyntax expression) { while (expression is ParenthesizedExpressionSyntax parenExpression) expression = parenExpression.Expression; return expression; } public static ExpressionSyntax WalkDownSuppressions(this ExpressionSyntax expression) { while (expression is PostfixUnaryExpressionSyntax(SyntaxKind.SuppressNullableWarningExpression) postfixExpression) expression = postfixExpression.Operand; return expression; } public static bool IsQualifiedCrefName([NotNullWhen(true)] this ExpressionSyntax? expression) => expression is { Parent: NameMemberCrefSyntax { Parent: QualifiedCrefSyntax } }; public static bool IsSimpleMemberAccessExpressionName([NotNullWhen(true)] this ExpressionSyntax? expression) => expression?.Parent is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess && memberAccess.Name == expression; public static bool IsAnyMemberAccessExpressionName([NotNullWhen(true)] this ExpressionSyntax? expression) { if (expression == null) return false; return expression == (expression.Parent as MemberAccessExpressionSyntax)?.Name || expression.IsMemberBindingExpressionName(); } public static bool IsMemberBindingExpressionName([NotNullWhen(true)] this ExpressionSyntax? expression) => expression?.Parent is MemberBindingExpressionSyntax memberBinding && memberBinding.Name == expression; public static bool IsRightSideOfQualifiedName([NotNullWhen(true)] this ExpressionSyntax? expression) => expression?.Parent is QualifiedNameSyntax qualifiedName && qualifiedName.Right == expression; public static bool IsRightSideOfColonColon(this ExpressionSyntax expression) => expression?.Parent is AliasQualifiedNameSyntax aliasName && aliasName.Name == expression; public static bool IsRightSideOfDot([NotNullWhen(true)] this ExpressionSyntax? name) => IsSimpleMemberAccessExpressionName(name) || IsMemberBindingExpressionName(name) || IsRightSideOfQualifiedName(name) || IsQualifiedCrefName(name); public static bool IsRightSideOfDotOrArrow([NotNullWhen(true)] this ExpressionSyntax? name) => IsAnyMemberAccessExpressionName(name) || IsRightSideOfQualifiedName(name); public static bool IsRightSideOfDotOrColonColon(this ExpressionSyntax name) => IsRightSideOfDot(name) || IsRightSideOfColonColon(name); public static bool IsRightSideOfDotOrArrowOrColonColon([NotNullWhen(true)] this ExpressionSyntax name) => IsRightSideOfDotOrArrow(name) || IsRightSideOfColonColon(name); public static bool IsRightOfCloseParen(this ExpressionSyntax expression) { var firstToken = expression.GetFirstToken(); return firstToken.Kind() != SyntaxKind.None && firstToken.GetPreviousToken().Kind() == SyntaxKind.CloseParenToken; } public static bool IsLeftSideOfDot([NotNullWhen(true)] this ExpressionSyntax? expression) { if (expression == null) return false; return IsLeftSideOfQualifiedName(expression) || IsLeftSideOfSimpleMemberAccessExpression(expression); } public static bool IsLeftSideOfSimpleMemberAccessExpression(this ExpressionSyntax expression) => (expression?.Parent) is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess && memberAccess.Expression == expression; public static bool IsLeftSideOfDotOrArrow(this ExpressionSyntax expression) => IsLeftSideOfQualifiedName(expression) || (expression.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Expression == expression); public static bool IsLeftSideOfQualifiedName(this ExpressionSyntax expression) => (expression?.Parent) is QualifiedNameSyntax qualifiedName && qualifiedName.Left == expression; public static bool IsLeftSideOfExplicitInterfaceSpecifier([NotNullWhen(true)] this NameSyntax? name) => name.IsParentKind(SyntaxKind.ExplicitInterfaceSpecifier); public static bool IsExpressionOfInvocation(this ExpressionSyntax expression) => expression?.Parent is InvocationExpressionSyntax invocation && invocation.Expression == expression; public static bool TryGetNameParts(this ExpressionSyntax expression, [NotNullWhen(true)] out IList<string>? parts) { var partsList = new List<string>(); if (!TryGetNameParts(expression, partsList)) { parts = null; return false; } parts = partsList; return true; } public static bool TryGetNameParts(this ExpressionSyntax expression, List<string> parts) { if (expression is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess) { if (!TryGetNameParts(memberAccess.Expression, parts)) { return false; } return AddSimpleName(memberAccess.Name, parts); } else if (expression is QualifiedNameSyntax qualifiedName) { if (!TryGetNameParts(qualifiedName.Left, parts)) { return false; } return AddSimpleName(qualifiedName.Right, parts); } else if (expression is SimpleNameSyntax simpleName) { return AddSimpleName(simpleName, parts); } else { return false; } } private static bool AddSimpleName(SimpleNameSyntax simpleName, List<string> parts) { if (!simpleName.IsKind(SyntaxKind.IdentifierName)) { return false; } parts.Add(simpleName.Identifier.ValueText); return true; } public static bool IsInConstantContext([NotNullWhen(true)] this ExpressionSyntax? expression) { if (expression == null) return false; if (expression.GetAncestor<ParameterSyntax>() != null) return true; var attributeArgument = expression.GetAncestor<AttributeArgumentSyntax>(); if (attributeArgument != null) { if (attributeArgument.NameEquals == null || expression != attributeArgument.NameEquals.Name) { return true; } } if (expression.IsParentKind(SyntaxKind.ConstantPattern)) return true; // note: the above list is not intended to be exhaustive. If more cases // are discovered that should be considered 'constant' contexts in the // language, then this should be updated accordingly. return false; } public static bool IsInOutContext([NotNullWhen(true)] this ExpressionSyntax? expression) => expression?.Parent is ArgumentSyntax { RefOrOutKeyword: SyntaxToken(SyntaxKind.OutKeyword) } argument && argument.Expression == expression; public static bool IsInRefContext([NotNullWhen(true)] this ExpressionSyntax? expression) => IsInRefContext(expression, out _); /// <summary> /// Returns true if this expression is in some <c>ref</c> keyword context. If <see langword="true"/> then /// <paramref name="refParent"/> will be the node containing the <see langword="ref"/> keyword. /// </summary> public static bool IsInRefContext([NotNullWhen(true)] this ExpressionSyntax? expression, [NotNullWhen(true)] out SyntaxNode? refParent) { while (expression?.Parent is ParenthesizedExpressionSyntax or PostfixUnaryExpressionSyntax(SyntaxKind.SuppressNullableWarningExpression)) expression = (ExpressionSyntax)expression.Parent; if (expression?.Parent is RefExpressionSyntax or ArgumentSyntax { RefOrOutKeyword.RawKind: (int)SyntaxKind.RefKeyword }) { refParent = expression.Parent; return true; } refParent = null; return false; } public static bool IsInInContext([NotNullWhen(true)] this ExpressionSyntax? expression) => expression?.Parent is ArgumentSyntax { RefKindKeyword: SyntaxToken(SyntaxKind.InKeyword) }; [return: NotNullIfNotNull(nameof(expression))] private static ExpressionSyntax? GetExpressionToAnalyzeForWrites(ExpressionSyntax? expression) { if (expression.IsRightSideOfDotOrArrow()) { expression = (ExpressionSyntax)expression.GetRequiredParent(); } expression = expression.WalkUpParentheses(); return expression; } public static bool IsOnlyWrittenTo([NotNullWhen(true)] this ExpressionSyntax? expression) { expression = GetExpressionToAnalyzeForWrites(expression); if (expression != null) { if (expression.IsInOutContext()) return true; if (expression.Parent != null) { if (expression.IsLeftSideOfAssignExpression()) return true; if (expression.IsAttributeNamedArgumentIdentifier()) return true; } if (IsExpressionOfArgumentInDeconstruction(expression)) { return true; } } return false; } /// <summary> /// If this declaration or identifier is part of a deconstruction, find the deconstruction. /// If found, returns either an assignment expression or a foreach variable statement. /// Returns null otherwise. /// /// copied from SyntaxExtensions.GetContainingDeconstruction /// </summary> private static bool IsExpressionOfArgumentInDeconstruction(ExpressionSyntax expr) { if (!expr.IsParentKind(SyntaxKind.Argument)) { return false; } while (true) { var parent = expr.Parent; if (parent == null) { return false; } switch (parent.Kind()) { case SyntaxKind.Argument: if (parent.Parent?.Kind() == SyntaxKind.TupleExpression) { expr = (TupleExpressionSyntax)parent.Parent; continue; } return false; case SyntaxKind.SimpleAssignmentExpression: if (((AssignmentExpressionSyntax)parent).Left == expr) { return true; } return false; case SyntaxKind.ForEachVariableStatement: if (((ForEachVariableStatementSyntax)parent).Variable == expr) { return true; } return false; default: return false; } } } public static bool IsWrittenTo( [NotNullWhen(true)] this ExpressionSyntax? expression, SemanticModel semanticModel, CancellationToken cancellationToken) { if (expression == null) return false; expression = GetExpressionToAnalyzeForWrites(expression); if (expression.IsOnlyWrittenTo()) return true; if (expression.IsInRefContext(out var refParent)) { // most cases of `ref x` will count as a potential write of `x`. An important exception is: // `ref readonly y = ref x`. In that case, because 'y' can't be written to, this would not // be a write of 'x'. if (refParent.Parent is EqualsValueClauseSyntax { Parent: VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type: { } variableDeclarationType } } }) { if (variableDeclarationType is ScopedTypeSyntax scopedType) { variableDeclarationType = scopedType.Type; } if (variableDeclarationType is RefTypeSyntax refType && refType.ReadOnlyKeyword != default) { return false; } } return true; } // Similar to `ref x`, `&x` allows reads and write of the value, meaning `x` may be (but is not definitely) // written to. if (expression.Parent.IsKind(SyntaxKind.AddressOfExpression)) return true; // We're written if we're used in a ++, or -- expression. if (expression.IsOperandOfIncrementOrDecrementExpression()) return true; if (expression.IsLeftSideOfAnyAssignExpression()) return true; // An extension method invocation with a ref-this parameter can write to an expression. if (expression.Parent is MemberAccessExpressionSyntax { Parent: InvocationExpressionSyntax } memberAccess && expression == memberAccess.Expression) { var symbol = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).Symbol; if (symbol is IMethodSymbol { MethodKind: MethodKind.ReducedExtension, ReducedFrom.Parameters: [{ RefKind: RefKind.Ref }, ..], }) { return true; } } // An inline array passed as a Span<T> can be written into by the callee, despite no ref at the callsite. e.g.: // // void Mutate(Span<byte> bytes); // Mutate(this.inlineArray) if (expression.Parent is ArgumentSyntax) { var expressionTypes = semanticModel.GetTypeInfo(expression, cancellationToken); if (expressionTypes.ConvertedType.IsSpan() && expressionTypes.Type.IsInlineArray()) { return true; } } return false; } public static bool IsAttributeNamedArgumentIdentifier([NotNullWhen(true)] this ExpressionSyntax? expression) { var nameEquals = expression?.Parent as NameEqualsSyntax; return nameEquals.IsParentKind(SyntaxKind.AttributeArgument); } public static bool IsOperandOfIncrementOrDecrementExpression(this ExpressionSyntax expression) { if (expression?.Parent is SyntaxNode parent) { switch (parent.Kind()) { case SyntaxKind.PostIncrementExpression: case SyntaxKind.PreIncrementExpression: case SyntaxKind.PostDecrementExpression: case SyntaxKind.PreDecrementExpression: return true; } } return false; } public static bool IsNamedArgumentIdentifier(this ExpressionSyntax expression) => expression is IdentifierNameSyntax { Parent: NameColonSyntax }; public static bool IsInsideNameOfExpression( [NotNullWhen(true)] this ExpressionSyntax? expression, SemanticModel semanticModel, CancellationToken cancellationToken) { var invocation = expression?.GetAncestor<InvocationExpressionSyntax>(); if (invocation?.Expression is IdentifierNameSyntax name && name.Identifier.Text == SyntaxFacts.GetText(SyntaxKind.NameOfKeyword)) { return semanticModel.GetMemberGroup(name, cancellationToken).IsDefaultOrEmpty; } return false; } private static bool CanReplace(ISymbol symbol) { switch (symbol.Kind) { case SymbolKind.Field: case SymbolKind.Local: case SymbolKind.Method: case SymbolKind.Parameter: case SymbolKind.Property: case SymbolKind.RangeVariable: case SymbolKind.FunctionPointerType: return true; } return false; } public static bool CanReplaceWithRValue( [NotNullWhen(true)] this ExpressionSyntax? expression, SemanticModel semanticModel, CancellationToken cancellationToken) { // An RValue can't be written into. // i.e. you can't replace "a" in "a = b" with "Goo() = b". return expression != null && !expression.IsWrittenTo(semanticModel, cancellationToken) && CanReplaceWithLValue(expression, semanticModel, cancellationToken); } public static bool CanReplaceWithLValue( this ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken) { if (expression.IsKind(SyntaxKind.StackAllocArrayCreationExpression)) { // Stack alloc is very interesting. While it appears to be an expression, it is only // such so it can appear in a variable decl. It is not a normal expression that can // go anywhere. return false; } if (expression.Kind() is SyntaxKind.BaseExpression or SyntaxKind.CollectionInitializerExpression or SyntaxKind.ObjectInitializerExpression or SyntaxKind.ComplexElementInitializerExpression) { return false; } // literal can be always replaced. if (expression is LiteralExpressionSyntax && !expression.IsParentKind(SyntaxKind.UnaryMinusExpression)) { return true; } if (expression is TupleExpressionSyntax) { return true; } if (expression is not ObjectCreationExpressionSyntax && expression is not AnonymousObjectCreationExpressionSyntax && !expression.IsLeftSideOfAssignExpression()) { var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); if (!symbolInfo.GetBestOrAllSymbols().All(CanReplace)) { // If the expression is actually a reference to a type, then it can't be replaced // with an arbitrary expression. return false; } } // If we are a conditional access expression: // case (1) : obj?.Method(), obj1.obj2?.Property // case (2) : obj?.GetAnotherObj()?.Length, obj?.AnotherObj?.Length // in case (1), the entire expression forms the conditional access expression, which can be replaced with an LValue. // in case (2), the nested conditional access expression is ".GetAnotherObj()?.Length" or ".AnotherObj()?.Length" // essentially, the first expression (before the operator) in a nested conditional access expression // is some form of member binding expression and they cannot be replaced with an LValue. if (expression.IsKind(SyntaxKind.ConditionalAccessExpression)) { return expression is { Parent.RawKind: not (int)SyntaxKind.ConditionalAccessExpression }; } if (expression.Parent == null) return false; switch (expression.Parent.Kind()) { case SyntaxKind.InvocationExpression: // Technically, you could introduce an LValue for "Goo" in "Goo()" even if "Goo" binds // to a method. (i.e. by assigning to a Func<...> type). However, this is so contrived // and none of the features that use this extension consider this replaceable. if (expression.IsKind(SyntaxKind.IdentifierName) || expression is MemberAccessExpressionSyntax) { // If it looks like a method then we don't allow it to be replaced if it is a // method (or if it doesn't bind). var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); return symbolInfo.GetBestOrAllSymbols().Any() && !symbolInfo.GetBestOrAllSymbols().Any(static s => s is IMethodSymbol); } else { // It doesn't look like a method, we allow this to be replaced. return true; } // If the parent is a conditional access expression, we could introduce an LValue // for the given expression, unless it is itself a MemberBindingExpression or starts with one. // Case (1) : The WhenNotNull clause always starts with a MemberBindingExpression. // expression '.Method()' in a?.Method() // Case (2) : The Expression clause always starts with a MemberBindingExpression if // the grandparent is a conditional access expression. // expression '.Method' in a?.Method()?.Length // Case (3) : The child Conditional access expression always starts with a MemberBindingExpression if // the parent is a conditional access expression. This case is already covered before the parent kind switch case SyntaxKind.ConditionalAccessExpression: var parentConditionalAccessExpression = (ConditionalAccessExpressionSyntax)expression.Parent; return expression != parentConditionalAccessExpression.WhenNotNull && !parentConditionalAccessExpression.Parent.IsKind(SyntaxKind.ConditionalAccessExpression); case SyntaxKind.IsExpression: case SyntaxKind.AsExpression: // Can't introduce a variable for the type portion of an is/as check. var isOrAsExpression = (BinaryExpressionSyntax)expression.Parent; return expression == isOrAsExpression.Left; case SyntaxKind.EqualsValueClause: case SyntaxKind.ExpressionStatement: case SyntaxKind.ArrayInitializerExpression: case SyntaxKind.CollectionInitializerExpression: case SyntaxKind.Argument: case SyntaxKind.AttributeArgument: case SyntaxKind.AnonymousObjectMemberDeclarator: case SyntaxKind.ArrowExpressionClause: case SyntaxKind.AwaitExpression: case SyntaxKind.ReturnStatement: case SyntaxKind.YieldReturnStatement: case SyntaxKind.ParenthesizedLambdaExpression: case SyntaxKind.SimpleLambdaExpression: case SyntaxKind.ParenthesizedExpression: case SyntaxKind.ArrayRankSpecifier: case SyntaxKind.ConditionalExpression: case SyntaxKind.IfStatement: case SyntaxKind.CatchFilterClause: case SyntaxKind.WhileStatement: case SyntaxKind.DoStatement: case SyntaxKind.ThrowStatement: case SyntaxKind.SwitchStatement: case SyntaxKind.InterpolatedStringExpression: case SyntaxKind.ComplexElementInitializerExpression: case SyntaxKind.Interpolation: case SyntaxKind.RefExpression: case SyntaxKind.LockStatement: case SyntaxKind.ElementAccessExpression: case SyntaxKind.SwitchExpressionArm: case SyntaxKind.WhenClause: // Direct parent kind checks. return true; } if (expression.Parent is PrefixUnaryExpressionSyntax) { if (!(expression is LiteralExpressionSyntax && expression.IsParentKind(SyntaxKind.UnaryMinusExpression))) { return true; } } var parentNonExpression = expression.GetAncestors().SkipWhile(n => n is ExpressionSyntax).FirstOrDefault(); var topExpression = expression; while (topExpression.Parent is TypeSyntax typeSyntax) { topExpression = typeSyntax; } if (parentNonExpression != null && parentNonExpression is FromClauseSyntax fromClause && topExpression != null && fromClause.Type == topExpression) { return false; } // Parent type checks. if (expression.Parent is PostfixUnaryExpressionSyntax or BinaryExpressionSyntax or AssignmentExpressionSyntax or QueryClauseSyntax or SelectOrGroupClauseSyntax or CheckedExpressionSyntax) { return true; } // Specific child checks. if (expression.CheckParent<CommonForEachStatementSyntax>(f => f.Expression == expression) || expression.CheckParent<MemberAccessExpressionSyntax>(m => m.Expression == expression) || expression.CheckParent<CastExpressionSyntax>(c => c.Expression == expression)) { return true; } // Misc checks. if ((expression.IsParentKind(SyntaxKind.NameEquals) && expression.Parent.IsParentKind(SyntaxKind.AttributeArgument)) || expression.IsLeftSideOfAnyAssignExpression()) { return true; } return false; } public static bool IsNameOfArgumentExpression(this ExpressionSyntax expression) => expression is { Parent: ArgumentSyntax { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax invocation } } } && invocation.IsNameOfInvocation(); public static bool IsNameOfInvocation(this InvocationExpressionSyntax invocation) { return invocation.Expression is IdentifierNameSyntax identifierName && identifierName.Identifier.IsKindOrHasMatchingText(SyntaxKind.NameOfKeyword); } public static SimpleNameSyntax? GetRightmostName(this ExpressionSyntax node) { if (node is MemberAccessExpressionSyntax { Name: not null } memberAccess) { return memberAccess.Name; } if (node is QualifiedNameSyntax { Right: not null } qualified) { return qualified.Right; } if (node is SimpleNameSyntax simple) { return simple; } if (node is ConditionalAccessExpressionSyntax conditional) { return conditional.WhenNotNull.GetRightmostName(); } if (node is MemberBindingExpressionSyntax memberBinding) { return memberBinding.Name; } if (node is AliasQualifiedNameSyntax { Name: not null } aliasQualifiedName) { return aliasQualifiedName.Name; } return null; } public static OperatorPrecedence GetOperatorPrecedence(this ExpressionSyntax expression) { switch (expression.Kind()) { case SyntaxKind.SimpleMemberAccessExpression: case SyntaxKind.ConditionalAccessExpression: case SyntaxKind.InvocationExpression: case SyntaxKind.ElementAccessExpression: case SyntaxKind.PostIncrementExpression: case SyntaxKind.PostDecrementExpression: case SyntaxKind.ObjectCreationExpression: case SyntaxKind.ImplicitObjectCreationExpression: case SyntaxKind.TypeOfExpression: case SyntaxKind.DefaultExpression: case SyntaxKind.CheckedExpression: case SyntaxKind.UncheckedExpression: case SyntaxKind.AnonymousMethodExpression: case SyntaxKind.SuppressNullableWarningExpression: // unsafe code case SyntaxKind.SizeOfExpression: case SyntaxKind.PointerMemberAccessExpression: // From C# spec, 7.3.1: // Primary: x.y x?.y x?[y] f(x) a[x] x++ x-- new typeof default checked unchecked delegate x! return OperatorPrecedence.Primary; case SyntaxKind.UnaryPlusExpression: case SyntaxKind.UnaryMinusExpression: case SyntaxKind.LogicalNotExpression: case SyntaxKind.BitwiseNotExpression: case SyntaxKind.PreIncrementExpression: case SyntaxKind.PreDecrementExpression: case SyntaxKind.CastExpression: case SyntaxKind.AwaitExpression: // unsafe code. case SyntaxKind.PointerIndirectionExpression: case SyntaxKind.AddressOfExpression: // From C# spec, 7.3.1: // Unary: + - ! ~ ++x --x (T)x await Task return OperatorPrecedence.Unary; case SyntaxKind.RangeExpression: // From C# spec, https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#systemrange // Range: .. return OperatorPrecedence.Range; case SyntaxKind.MultiplyExpression: case SyntaxKind.DivideExpression: case SyntaxKind.ModuloExpression: // From C# spec, 7.3.1: // Multiplicative: * / % return OperatorPrecedence.Multiplicative; case SyntaxKind.AddExpression: case SyntaxKind.SubtractExpression: // From C# spec, 7.3.1: // Additive: + - return OperatorPrecedence.Additive; case SyntaxKind.LeftShiftExpression: case SyntaxKind.RightShiftExpression: // From C# spec, 7.3.1: // Shift: << >> return OperatorPrecedence.Shift; case SyntaxKind.LessThanExpression: case SyntaxKind.GreaterThanExpression: case SyntaxKind.LessThanOrEqualExpression: case SyntaxKind.GreaterThanOrEqualExpression: case SyntaxKind.IsExpression: case SyntaxKind.AsExpression: case SyntaxKind.IsPatternExpression: // From C# spec, 7.3.1: // Relational and type testing: < > <= >= is as return OperatorPrecedence.RelationalAndTypeTesting; case SyntaxKind.EqualsExpression: case SyntaxKind.NotEqualsExpression: // From C# spec, 7.3.1: // Equality: == != return OperatorPrecedence.Equality; case SyntaxKind.BitwiseAndExpression: // From C# spec, 7.3.1: // Logical AND: & return OperatorPrecedence.LogicalAnd; case SyntaxKind.ExclusiveOrExpression: // From C# spec, 7.3.1: // Logical XOR: ^ return OperatorPrecedence.LogicalXor; case SyntaxKind.BitwiseOrExpression: // From C# spec, 7.3.1: // Logical OR: | return OperatorPrecedence.LogicalOr; case SyntaxKind.LogicalAndExpression: // From C# spec, 7.3.1: // Conditional AND: && return OperatorPrecedence.ConditionalAnd; case SyntaxKind.LogicalOrExpression: // From C# spec, 7.3.1: // Conditional AND: || return OperatorPrecedence.ConditionalOr; case SyntaxKind.CoalesceExpression: // From C# spec, 7.3.1: // Null coalescing: ?? return OperatorPrecedence.NullCoalescing; case SyntaxKind.ConditionalExpression: // From C# spec, 7.3.1: // Conditional: ?: return OperatorPrecedence.Conditional; case SyntaxKind.SimpleAssignmentExpression: case SyntaxKind.MultiplyAssignmentExpression: case SyntaxKind.DivideAssignmentExpression: case SyntaxKind.ModuloAssignmentExpression: case SyntaxKind.AddAssignmentExpression: case SyntaxKind.SubtractAssignmentExpression: case SyntaxKind.LeftShiftAssignmentExpression: case SyntaxKind.RightShiftAssignmentExpression: case SyntaxKind.AndAssignmentExpression: case SyntaxKind.ExclusiveOrAssignmentExpression: case SyntaxKind.OrAssignmentExpression: case SyntaxKind.SimpleLambdaExpression: case SyntaxKind.ParenthesizedLambdaExpression: // From C# spec, 7.3.1: // Conditional: ?: return OperatorPrecedence.AssignmentAndLambdaExpression; case SyntaxKind.SwitchExpression: return OperatorPrecedence.Switch; default: return OperatorPrecedence.None; } } public static bool TryConvertToStatement( this ExpressionSyntax expression, SyntaxToken? semicolonTokenOpt, bool createReturnStatementForExpression, [NotNullWhen(true)] out StatementSyntax? statement) { // It's tricky to convert an arrow expression with directives over to a block. // We'd need to find and remove the directives *after* the arrow expression and // move them accordingly. So, for now, we just disallow this. if (expression.GetLeadingTrivia().Any(t => t.IsDirective)) { statement = null; return false; } var semicolonToken = semicolonTokenOpt ?? SemicolonToken; statement = ConvertToStatement(expression, semicolonToken, createReturnStatementForExpression); return true; } private static StatementSyntax ConvertToStatement(ExpressionSyntax expression, SyntaxToken semicolonToken, bool createReturnStatementForExpression) { if (expression is ThrowExpressionSyntax throwExpression) { return SyntaxFactory.ThrowStatement(throwExpression.ThrowKeyword, throwExpression.Expression, semicolonToken); } else if (createReturnStatementForExpression) { if (expression.GetLeadingTrivia().Any(t => t.IsSingleOrMultiLineComment())) { return SyntaxFactory.ReturnStatement(expression.WithLeadingTrivia(SyntaxFactory.ElasticSpace)) .WithSemicolonToken(semicolonToken) .WithLeadingTrivia(expression.GetLeadingTrivia()) .WithPrependedLeadingTrivia(SyntaxFactory.ElasticMarker); } else { return SyntaxFactory.ReturnStatement(expression) .WithSemicolonToken(semicolonToken); } } else { return SyntaxFactory.ExpressionStatement(expression) .WithSemicolonToken(semicolonToken); } } public static bool IsDirectChildOfMemberAccessExpression(this ExpressionSyntax expression) => expression?.Parent is MemberAccessExpressionSyntax; public static bool InsideCrefReference(this ExpressionSyntax expression) => expression.FirstAncestorOrSelf<XmlCrefAttributeSyntax>() != null; public static ITypeSymbol? GetTargetType( this ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken) { var topExpression = expression.WalkUpParentheses(); var parent = topExpression.Parent; return parent switch { EqualsValueClauseSyntax equalsValue => GetTargetTypeForEqualsValueClause(equalsValue), CastExpressionSyntax castExpression => GetTargetTypedForCastExpression(castExpression), // a ? [1, 2, 3] : ... is target typed if either the other side is *not* a collection, // or the entire ternary is target typed itself. ConditionalExpressionSyntax conditionalExpression => GetTargetTypeForConditionalExpression(conditionalExpression, topExpression), // Similar rules for switches. SwitchExpressionArmSyntax switchExpressionArm => GetTargetTypeForSwitchExpressionArm(switchExpressionArm), InitializerExpressionSyntax initializerExpression => GetTargetTypeForInitializerExpression(initializerExpression, topExpression), CollectionElementSyntax collectionElement => GetTargetTypeForCollectionElement(collectionElement), AssignmentExpressionSyntax assignmentExpression => GetTargetTypeForAssignmentExpression(assignmentExpression, topExpression), BinaryExpressionSyntax binaryExpression => GetTargetTypeForBinaryExpression(binaryExpression, topExpression), LambdaExpressionSyntax lambda => GetTargetTypeForLambdaExpression(lambda, topExpression), ArgumentSyntax argument => GetTargetTypeForArgument(argument), AttributeArgumentSyntax attributeArgument => GetTargetTypeForAttributeArgument(attributeArgument), ReturnStatementSyntax returnStatement => GetTargetTypeForReturnStatement(returnStatement), ArrowExpressionClauseSyntax arrowExpression => GetTargetTypeForArrowExpression(arrowExpression), _ => null, }; // return result is IErrorTypeSymbol ? null : result; bool HasType(ExpressionSyntax expression, [NotNullWhen(true)] out ITypeSymbol? type) { type = semanticModel.GetTypeInfo(expression, cancellationToken).Type; return type is not null; // and not IErrorTypeSymbol; } ITypeSymbol? GetTargetTypeForArgument(ArgumentSyntax argument) { if (argument.Parent is TupleExpressionSyntax tupleExpression) { var tupleType = tupleExpression.GetTargetType(semanticModel, cancellationToken); if (tupleType is null) return null; var typeArguments = tupleType.GetTypeArguments(); var index = tupleExpression.Arguments.IndexOf(argument); return index < typeArguments.Length ? typeArguments[index] : null; } else { return argument.DetermineParameter(semanticModel, allowUncertainCandidates: false, allowParams: true, cancellationToken)?.Type; } } ITypeSymbol? GetTargetTypeForAttributeArgument(AttributeArgumentSyntax argument) => argument.DetermineParameter(semanticModel, allowUncertainCandidates: false, allowParams: true, cancellationToken)?.Type; ITypeSymbol? GetTargetTypeForArrowExpression(ArrowExpressionClauseSyntax arrowExpression) { var parent = arrowExpression.GetRequiredParent(); var symbol = semanticModel.GetSymbolInfo(parent, cancellationToken).Symbol ?? semanticModel.GetDeclaredSymbol(parent, cancellationToken); return symbol.GetMemberType(); } ITypeSymbol? GetTargetTypeForReturnStatement(ReturnStatementSyntax returnStatement) { for (SyntaxNode? current = returnStatement; current != null; current = current.Parent) { if (current.IsReturnableConstruct()) { var symbol = semanticModel.GetSymbolInfo(current, cancellationToken).Symbol ?? semanticModel.GetDeclaredSymbol(current, cancellationToken); return symbol.GetMemberType(); } } return null; } ITypeSymbol? GetTargetTypeForEqualsValueClause(EqualsValueClauseSyntax equalsValue) { // If we're after an `x = ...` and it's not `var x`, this is target typed. if (equalsValue.Parent is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type.IsVar: true } }) return null; var symbol = semanticModel.GetDeclaredSymbol(equalsValue.GetRequiredParent(), cancellationToken); return symbol.GetMemberType(); } ITypeSymbol? GetTargetTypedForCastExpression(CastExpressionSyntax castExpression) => semanticModel.GetTypeInfo(castExpression.Type, cancellationToken).Type; ITypeSymbol? GetTargetTypeForConditionalExpression(ConditionalExpressionSyntax conditionalExpression, ExpressionSyntax expression) { if (conditionalExpression.WhenTrue == expression) return HasType(conditionalExpression.WhenFalse, out var falseType) ? falseType : conditionalExpression.GetTargetType(semanticModel, cancellationToken); else if (conditionalExpression.WhenFalse == expression) return HasType(conditionalExpression.WhenTrue, out var trueType) ? trueType : conditionalExpression.GetTargetType(semanticModel, cancellationToken); else return null; } ITypeSymbol? GetTargetTypeForLambdaExpression(LambdaExpressionSyntax lambda, ExpressionSyntax expression) => lambda.ExpressionBody == expression && lambda.GetTargetType(semanticModel, cancellationToken) is INamedTypeSymbol { DelegateInvokeMethod.ReturnType: var returnType } ? returnType : null; ITypeSymbol? GetTargetTypeForSwitchExpressionArm(SwitchExpressionArmSyntax switchExpressionArm) { var switchExpression = (SwitchExpressionSyntax)switchExpressionArm.GetRequiredParent(); // check if any other arm has a type that this would be target typed against. foreach (var arm in switchExpression.Arms) { if (arm != switchExpressionArm && HasType(arm.Expression, out var armType)) return armType; } // All arms do not have a type, this is target typed if the switch itself is target typed. return switchExpression.GetTargetType(semanticModel, cancellationToken); } ITypeSymbol? GetTargetTypeForCollectionElement(CollectionElementSyntax collectionElement) { // We do not currently target type spread expressions in a collection expression. if (collectionElement is not ExpressionElementSyntax) return null; // The element it target typed if the parent collection is itself target typed. var collectionExpression = (CollectionExpressionSyntax)collectionElement.GetRequiredParent(); var collectionTargetType = collectionExpression.GetTargetType(semanticModel, cancellationToken); if (collectionTargetType is null) return null; if (collectionTargetType.IsSpanOrReadOnlySpan()) return collectionTargetType.GetTypeArguments().Single(); var ienumerableType = semanticModel.Compilation.IEnumerableOfTType(); if (collectionTargetType.OriginalDefinition.Equals(ienumerableType)) return collectionTargetType.GetTypeArguments().Single(); foreach (var iface in collectionTargetType.AllInterfaces) { if (iface.OriginalDefinition.Equals(ienumerableType)) return iface.TypeArguments.Single(); } return null; } ITypeSymbol? GetTargetTypeForInitializerExpression(InitializerExpressionSyntax initializerExpression, ExpressionSyntax expression) { // new X[] { [1, 2, 3] }. Elements are target typed by array type. if (initializerExpression.Parent is ArrayCreationExpressionSyntax arrayCreation) return HasType(arrayCreation.Type, out var elementType) ? elementType : null; // new [] { [1, 2, 3], ... }. Elements are target typed if there's another element with real type. if (initializerExpression.Parent is ImplicitArrayCreationExpressionSyntax) { foreach (var sibling in initializerExpression.Expressions) { if (sibling != expression && HasType(sibling, out var siblingType)) return siblingType; } return null; } // T[] x = [1, 2, 3]; if (initializerExpression.Parent is EqualsValueClauseSyntax equalsValue) return GetTargetTypeForEqualsValueClause(equalsValue); // TODO: Handle these. if (initializerExpression.Parent is StackAllocArrayCreationExpressionSyntax or ImplicitStackAllocArrayCreationExpressionSyntax) return null; return null; } ITypeSymbol? GetTargetTypeForAssignmentExpression(AssignmentExpressionSyntax assignmentExpression, ExpressionSyntax expression) { return expression == assignmentExpression.Right && HasType(assignmentExpression.Left, out var leftType) ? leftType : null; } ITypeSymbol? GetTargetTypeForBinaryExpression(BinaryExpressionSyntax binaryExpression, ExpressionSyntax expression) { return binaryExpression.Kind() == SyntaxKind.CoalesceExpression && binaryExpression.Right == expression && HasType(binaryExpression.Left, out var leftType) ? leftType : null; } } } |