File: Simplification\Simplifiers\ExpressionSimplifier.cs
Web Access
Project: src\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;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers;
 
internal sealed partial class ExpressionSimplifier : AbstractCSharpSimplifier<ExpressionSyntax, ExpressionSyntax>
{
    public static readonly ExpressionSimplifier Instance = new();
 
    private ExpressionSimplifier()
    {
    }
 
    public override bool TrySimplify(
        ExpressionSyntax expression,
        SemanticModel semanticModel,
        CSharpSimplifierOptions options,
        [NotNullWhen(true)] out ExpressionSyntax? replacementNode,
        out TextSpan issueSpan,
        CancellationToken cancellationToken)
    {
        replacementNode = null;
        issueSpan = default;
 
        if (expression is MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax } memberAccessExpression)
        {
            if (!MemberAccessExpressionSimplifier.Instance.ShouldSimplifyThisMemberAccessExpression(
                    memberAccessExpression, semanticModel, options, out _, out _, cancellationToken))
            {
                return false;
            }
 
            replacementNode = memberAccessExpression.GetNameWithTriviaMoved();
            issueSpan = memberAccessExpression.Expression.Span;
            return true;
        }
 
        if (TryReduceExplicitName(expression, semanticModel, out var replacementTypeNode, out issueSpan, options, cancellationToken))
        {
            replacementNode = replacementTypeNode;
            return true;
        }
 
        return TrySimplify(expression, semanticModel, out replacementNode, out issueSpan, cancellationToken);
    }
 
    private static bool TryReduceExplicitName(
        ExpressionSyntax expression,
        SemanticModel semanticModel,
        [NotNullWhen(true)] out TypeSyntax? replacementNode,
        out TextSpan issueSpan,
        CSharpSimplifierOptions options,
        CancellationToken cancellationToken)
    {
        replacementNode = null;
        issueSpan = default;
 
        if (expression.ContainsInterleavedDirective(cancellationToken))
            return false;
 
        if (expression is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess)
            return TryReduceMemberAccessExpression(memberAccess, semanticModel, out replacementNode, out issueSpan, options, cancellationToken);
 
        if (expression is NameSyntax name)
            return NameSimplifier.Instance.TrySimplify(name, semanticModel, options, out replacementNode, out issueSpan, cancellationToken);
 
        return false;
    }
 
    private static bool TryReduceMemberAccessExpression(
        MemberAccessExpressionSyntax memberAccess,
        SemanticModel semanticModel,
        [NotNullWhen(true)] out TypeSyntax? replacementNode,
        out TextSpan issueSpan,
        CSharpSimplifierOptions options,
        CancellationToken cancellationToken)
    {
        replacementNode = null;
        issueSpan = default;
 
        if (memberAccess.Name == null || memberAccess.Expression == null)
            return false;
 
        // if this node is annotated as being a SpecialType, let's use this information.
        if (memberAccess.HasAnnotations(SpecialTypeAnnotation.Kind))
        {
            var replacementToken = TryGetPredefinedKeywordToken(
                semanticModel, SpecialTypeAnnotation.GetSpecialType(memberAccess.GetAnnotations(SpecialTypeAnnotation.Kind).First()));
            if (replacementToken != null)
            {
                replacementNode = CreatePredefinedTypeSyntax(memberAccess, replacementToken.Value);
                issueSpan = memberAccess.Span;
                return true;
            }
        }
 
        // See https://github.com/dotnet/roslyn/issues/40974
        //
        // To be very safe, we only support simplifying code that bound to a symbol without any
        // sort of problems.  We could potentially relax this in the future.  However, we would
        // need to be very careful about the implications of us offering to fixup 'broken' code 
        // in a manner that might end up making things worse or confusing the user.
        var symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, memberAccess);
        if (symbol == null)
        {
            // nameof(X.Y) does not support GetSymbolInfo on X.Y.
            // use GetMemberGroup instead to see if we can find a real symbol to use here.
            symbol =
                memberAccess.IsNameOfArgumentExpression() &&
                semanticModel.GetMemberGroup(memberAccess, cancellationToken) is [ISymbol member, ..]
                    ? member
                    : null;
 
            if (symbol == null)
                return false;
        }
 
        // if this node is on the left side, we could simplify to aliases
        if (!memberAccess.IsRightSideOfDot())
        {
            // Check if we need to replace this syntax with an alias identifier
            if (TryReplaceExpressionWithAlias(
                    memberAccess, semanticModel, symbol,
                    cancellationToken, out var aliasReplacement))
            {
                // get the token text as it appears in source code to preserve e.g. unicode character escaping
                var text = aliasReplacement.Name;
                var syntaxRef = aliasReplacement.DeclaringSyntaxReferences.FirstOrDefault();
 
                if (syntaxRef != null)
                {
                    var declIdentifier = ((UsingDirectiveSyntax)syntaxRef.GetSyntax(cancellationToken)).Alias!.Name.Identifier;
                    text = declIdentifier.IsVerbatimIdentifier() ? declIdentifier.ToString()[1..] : declIdentifier.ToString();
                }
 
                replacementNode = SyntaxFactory.IdentifierName(
                    memberAccess.Name.Identifier.CopyAnnotationsTo(SyntaxFactory.Identifier(
                        memberAccess.GetLeadingTrivia(),
                        SyntaxKind.IdentifierToken,
                        text,
                        aliasReplacement.Name,
                        memberAccess.GetTrailingTrivia())));
 
                replacementNode = memberAccess.CopyAnnotationsTo(replacementNode);
                replacementNode = memberAccess.Name.CopyAnnotationsTo(replacementNode);
 
                issueSpan = memberAccess.Span;
 
                // In case the alias name is the same as the last name of the alias target, we only include 
                // the left part of the name in the unnecessary span to Not confuse uses.
                if (memberAccess.Name.Identifier.ValueText == ((IdentifierNameSyntax)replacementNode).Identifier.ValueText)
                {
                    issueSpan = memberAccess.Expression.Span;
                }
 
                return true;
            }
 
            // Check if the Expression can be replaced by Predefined Type keyword
            if (PreferPredefinedTypeKeywordInMemberAccess(memberAccess, options, semanticModel))
            {
                if (symbol is INamedTypeSymbol namedType)
                {
                    var keywordToken = TryGetPredefinedKeywordToken(semanticModel, namedType.SpecialType);
                    if (keywordToken != null)
                    {
                        replacementNode = CreatePredefinedTypeSyntax(memberAccess, keywordToken.Value);
 
                        replacementNode = replacementNode
                            .WithAdditionalAnnotations<TypeSyntax>(new SyntaxAnnotation(
                                nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess)));
 
                        issueSpan = memberAccess.Span; // we want to show the whole expression as unnecessary
 
                        return true;
                    }
                }
            }
        }
 
        // Try to eliminate cases without actually calling CanReplaceWithReducedName. For expressions of the form
        // 'this.Name' or 'base.Name', no additional check here is required.
        if (!memberAccess.Expression.IsKind(SyntaxKind.BaseExpression))
        {
            GetReplacementCandidates(
                semanticModel,
                memberAccess,
                symbol,
                out var speculativeSymbols,
                out var speculativeNamespacesAndTypes);
 
            if (!IsReplacementCandidate(symbol, speculativeSymbols, speculativeNamespacesAndTypes))
            {
                return false;
            }
        }
 
        replacementNode = memberAccess.GetNameWithTriviaMoved();
        issueSpan = memberAccess.Expression.Span;
 
        return CanReplaceWithMemberAccessName(
            memberAccess, semanticModel, symbol, cancellationToken);
    }
 
    private static void GetReplacementCandidates(
        SemanticModel semanticModel,
        MemberAccessExpressionSyntax memberAccess,
        ISymbol actualSymbol,
        out ImmutableArray<ISymbol> speculativeSymbols,
        out ImmutableArray<ISymbol> speculativeNamespacesAndTypes)
    {
        var containsNamespaceOrTypeSymbol = actualSymbol is INamespaceOrTypeSymbol;
        var containsOtherSymbol = !containsNamespaceOrTypeSymbol;
 
        speculativeSymbols = containsOtherSymbol
            ? semanticModel.LookupSymbols(memberAccess.SpanStart, name: memberAccess.Name.Identifier.ValueText)
            : [];
        speculativeNamespacesAndTypes = containsNamespaceOrTypeSymbol
            ? semanticModel.LookupNamespacesAndTypes(memberAccess.SpanStart, name: memberAccess.Name.Identifier.ValueText)
            : [];
    }
 
    /// <summary>
    /// Determines if <paramref name="speculativeSymbols"/> and <paramref name="speculativeNamespacesAndTypes"/>
    /// together contain a superset of the symbols in <paramref name="actualSymbol"/>.
    /// </summary>
    private static bool IsReplacementCandidate(ISymbol actualSymbol, ImmutableArray<ISymbol> speculativeSymbols, ImmutableArray<ISymbol> speculativeNamespacesAndTypes)
    {
        if (speculativeSymbols.IsEmpty && speculativeNamespacesAndTypes.IsEmpty)
            return false;
 
        if (actualSymbol is object)
        {
            return speculativeSymbols.Contains(actualSymbol, CandidateSymbolEqualityComparer.Instance)
                || speculativeNamespacesAndTypes.Contains(actualSymbol, CandidateSymbolEqualityComparer.Instance);
        }
 
        return true;
    }
 
    private static bool TrySimplify(
        ExpressionSyntax expression,
        SemanticModel semanticModel,
        [NotNullWhen(true)] out ExpressionSyntax? replacementNode,
        out TextSpan issueSpan,
        CancellationToken cancellationToken)
    {
        if (!TrySimplifyWorker(expression, semanticModel, out replacementNode, out issueSpan, cancellationToken))
            return false;
 
        // Ensure that replacement doesn't change semantics.
        var speculationAnalyzer = new SpeculationAnalyzer(expression, replacementNode, semanticModel, cancellationToken);
        return !speculationAnalyzer.ReplacementChangesSemantics();
 
        static bool TrySimplifyWorker(
            ExpressionSyntax expression,
            SemanticModel semanticModel,
            [NotNullWhen(true)] out ExpressionSyntax? replacementNode,
            out TextSpan issueSpan,
            CancellationToken cancellationToken)
        {
            replacementNode = null;
            issueSpan = default;
 
            switch (expression)
            {
                case MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess:
                    {
                        if (IsNonRemovablePartOfDynamicMethodInvocation(semanticModel, memberAccess, cancellationToken))
                            return false;
 
                        if (!TrySimplifyMemberAccessOrQualifiedName(memberAccess.Expression, memberAccess.Name, semanticModel, out var newLeft, out issueSpan))
                            return false;
 
                        // replacement node might not be in it's simplest form, so add simplify annotation to it.
                        replacementNode = memberAccess.Update(newLeft, memberAccess.OperatorToken, memberAccess.Name)
                            .WithAdditionalAnnotations(Simplifier.Annotation);
                        return true;
                    }
 
                case QualifiedNameSyntax qualifiedName:
                    {
                        if (!TrySimplifyMemberAccessOrQualifiedName(qualifiedName.Left, qualifiedName.Right, semanticModel, out var newLeft, out issueSpan))
                            return false;
 
                        // replacement node might not be in it's simplest form, so add simplify annotation to it.
                        replacementNode = qualifiedName.Update((NameSyntax)newLeft, qualifiedName.DotToken, qualifiedName.Right)
                            .WithAdditionalAnnotations(Simplifier.Annotation);
 
                        return true;
                    }
            }
 
            return false;
        }
    }
 
    private static bool CanReplaceWithMemberAccessName(
        MemberAccessExpressionSyntax memberAccess,
        SemanticModel semanticModel,
        ISymbol symbol,
        CancellationToken cancellationToken)
    {
        if (!SimplificationHelpers.IsNamespaceOrTypeOrThisParameter(memberAccess.Expression, semanticModel))
            return false;
 
        var speculationAnalyzer = new SpeculationAnalyzer(memberAccess, memberAccess.Name, semanticModel, cancellationToken);
        if (!speculationAnalyzer.SymbolsForOriginalAndReplacedNodesAreCompatible() ||
            speculationAnalyzer.ReplacementChangesSemantics())
        {
            return false;
        }
 
        if (WillConflictWithExistingLocal(memberAccess, memberAccess.Name, semanticModel))
        {
            return false;
        }
 
        if (IsNonRemovablePartOfDynamicMethodInvocation(semanticModel, memberAccess, cancellationToken))
        {
            return false;
        }
 
        if (AccessMethodWithDynamicArgumentInsideStructConstructor(memberAccess, semanticModel))
        {
            return false;
        }
 
        if (memberAccess.Expression.Kind() == SyntaxKind.BaseExpression)
        {
            var enclosingNamedType = semanticModel.GetEnclosingNamedType(memberAccess.SpanStart, cancellationToken);
            if (enclosingNamedType != null &&
                !enclosingNamedType.IsSealed &&
                symbol != null &&
                symbol.IsOverridable())
            {
                return false;
            }
        }
 
        return !MemberAccessExpressionSimplifier.ParserWouldTreatReplacementWithNameAsCast(memberAccess);
    }
 
    /// <summary>
    /// Tells if the member access is dynamically invoked and cannot be reduced. In the case of
    /// <c>NS1.NS2.T1.T2.Method(...dynamic...)</c> we can only remove the <c>NS1.NS2</c>
    /// portion. The namespace part is not encoded into the IL, but the specific types in
    /// <c>T1.T2</c> and cannot be removed.
    /// </summary>
    private static bool IsNonRemovablePartOfDynamicMethodInvocation(
        SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccess, CancellationToken cancellationToken)
    {
        var ancestorInvocation = memberAccess.FirstAncestorOrSelf<InvocationExpressionSyntax>();
        if (ancestorInvocation?.SpanStart == memberAccess.SpanStart)
        {
            var leftSymbol = semanticModel.GetSymbolInfo(memberAccess.Expression, cancellationToken).GetAnySymbol();
            if (leftSymbol is INamedTypeSymbol)
            {
                var type = semanticModel.GetTypeInfo(memberAccess.GetRequiredParent(), cancellationToken).Type;
                if (type?.Kind == SymbolKind.DynamicType)
                {
                    return true;
                }
            }
        }
 
        return false;
    }
 
    /*
     * Name Reduction, to implicitly mean "this", is possible only after the initialization of all member variables but
     * since the check for initialization of all member variable is a lot of work for this simplification we don't simplify
     * even if all the member variables are initialized
     */
    private static bool AccessMethodWithDynamicArgumentInsideStructConstructor(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel)
    {
        var constructor = memberAccess.Ancestors().OfType<ConstructorDeclarationSyntax>().SingleOrDefault();
 
        if (constructor == null || constructor.GetRequiredParent().Kind() is not (SyntaxKind.StructDeclaration or SyntaxKind.RecordStructDeclaration))
        {
            return false;
        }
 
        return semanticModel.GetSymbolInfo(memberAccess.Name).CandidateReason == CandidateReason.LateBound;
    }
 
    // Note: The caller needs to verify that replacement doesn't change semantics of the original expression.
    private static bool TrySimplifyMemberAccessOrQualifiedName(
        ExpressionSyntax left,
        ExpressionSyntax right,
        SemanticModel semanticModel,
        [NotNullWhen(true)] out ExpressionSyntax? replacementNode,
        out TextSpan issueSpan)
    {
        replacementNode = null;
        issueSpan = default;
 
        if (left != null && right != null)
        {
            var leftSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, left);
            if (leftSymbol is INamedTypeSymbol)
            {
                var rightSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, right);
                if (rightSymbol != null && (rightSymbol.IsStatic || rightSymbol.Kind == SymbolKind.NamedType))
                {
                    // Static member access or nested type member access.
                    if (rightSymbol.ContainingType is { TypeArguments.Length: 0 } containingType &&
                        !containingType.Equals(leftSymbol))
                    {
                        // Don't simplify to a base type if it has the EditorBrowsable attribute on it.  This is
                        // occasionally done in some APIs to have a 'pseudo internal' base type that has functionality,
                        // which a user is supposed to only access through some 'pseudo public' derived type instead.
                        if (!containingType.IsEditorBrowsable(hideAdvancedMembers: true, semanticModel.Compilation, includingSourceSymbols: true))
                            return false;
 
                        // We have a static member access or a nested type member access using a more derived type.
                        // Simplify syntax so as to use accessed member's most immediate containing type instead of the
                        // derived type.
                        replacementNode = containingType.GenerateTypeSyntax().WithTriviaFrom(left);
                        issueSpan = left.Span;
                        return true;
                    }
                }
            }
        }
 
        return false;
    }
}