File: Simplification\Simplifiers\MemberAccessExpressionSimplifier.cs
Web Access
Project: ..\..\..\src\Workspaces\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Workspaces.csproj (Microsoft.CodeAnalysis.CSharp.Workspaces)
// 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.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification.Simplifiers;
 
namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers
{
    internal class MemberAccessExpressionSimplifier : AbstractMemberAccessExpressionSimplifier<
        ExpressionSyntax,
        MemberAccessExpressionSyntax,
        ThisExpressionSyntax>
    {
        public static readonly MemberAccessExpressionSimplifier Instance = new();
 
        private MemberAccessExpressionSimplifier()
        {
        }
 
        protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance;
 
        protected override ISpeculationAnalyzer GetSpeculationAnalyzer(
            SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessExpression, CancellationToken cancellationToken)
        {
            return new SpeculationAnalyzer(memberAccessExpression, memberAccessExpression.Name, semanticModel, cancellationToken);
        }
 
        protected override bool MayCauseParseDifference(MemberAccessExpressionSyntax memberAccessExpression)
            => ParserWouldTreatReplacementWithNameAsCast(memberAccessExpression);
 
        public static bool ParserWouldTreatReplacementWithNameAsCast(
            MemberAccessExpressionSyntax memberAccessExpression)
        {
            SyntaxNode parent = memberAccessExpression;
            while (true)
            {
                if (parent.IsParentKind(SyntaxKind.SimpleMemberAccessExpression))
                {
                    parent = parent.GetRequiredParent();
                    continue;
                }
 
                if (!parent.IsParentKind(SyntaxKind.ParenthesizedExpression))
                    return false;
 
                break;
            }
 
            // To resolve cast_expression ambiguities, the following rule exists: A sequence of one or more tokens
            // (§6.4) enclosed in parentheses is considered the start of a cast_expression only if at least one of the
            // following are true:
 
            // The sequence of tokens is correct grammar for a type, but not for an expression.
            //
            // The sequence of tokens is correct grammar for a type, and the token immediately following the closing
            // parentheses is the token “~”, the token “!”, the token “(”, an identifier(§6.4.3), a literal(§6.4.5), or
            // any keyword(§6.4.4) except as and is.
 
            // Note: the first cannot be true here.  Because we started with a MemberAccessExpression that we are
            // replacing with it's 'name' portion, this will always be valid as an expression.  So what matters is the
            // second statement.
 
            var parenthesizedExpression = parent.GetRequiredParent();
            var nextToken = parenthesizedExpression.GetLastToken().GetNextToken();
 
            if ((nextToken.Kind() is SyntaxKind.TildeToken or SyntaxKind.ExclamationToken or SyntaxKind.OpenParenToken) ||
                (CSharp.SyntaxFacts.IsKeywordKind(nextToken.Kind()) && nextToken.Kind() is not SyntaxKind.AsKeyword and not SyntaxKind.IsKeyword))
            {
                // This could definitely end up looking like a cast.  See if `The sequence of tokens is correct grammar
                // for a type` holds true here. Note: this check is likely not super accurate.  It probably is missing
                // cases with things like array-syntax or alias-syntax.  But it's likely sufficient for the common case
                // of `this.A.B.C` becoming `A.B.C` which then looks like a type name.
                return IsEntirelySimpleNames(parent.ReplaceNode(memberAccessExpression, memberAccessExpression.Name));
            }
 
            return false;
        }
 
        private static bool IsEntirelySimpleNames(SyntaxNode node)
        {
            return node is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess
                ? IsEntirelySimpleNames(memberAccess.Expression)
                : node is SimpleNameSyntax;
        }
    }
}