File: Simplification\Simplifiers\AbstractCSharpSimplifier.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.
 
#nullable disable
 
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Simplification.Simplifiers;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers;
 
/// <summary>
/// Contains helpers used by several simplifier subclasses.
/// </summary>
internal abstract class AbstractCSharpSimplifier<TSyntax, TSimplifiedSyntax>
    : AbstractSimplifier<TSyntax, TSimplifiedSyntax, CSharpSimplifierOptions>
    where TSyntax : SyntaxNode
    where TSimplifiedSyntax : SyntaxNode
{
    private static readonly ConditionalWeakTable<SemanticModel, StrongBox<bool>> s_modelToHasUsingAliasesMap = new();
 
    /// <summary>
    /// Returns the predefined keyword kind for a given <see cref="SpecialType"/>.
    /// </summary>
    /// <param name="specialType">The <see cref="SpecialType"/> of this type.</param>
    /// <returns>The keyword kind for a given special type, or SyntaxKind.None if the type name is not a predefined type.</returns>
    protected static SyntaxToken? TryGetPredefinedKeywordToken(SemanticModel semanticModel, SpecialType specialType)
    {
        var kind = specialType switch
        {
            SpecialType.System_Boolean => SyntaxKind.BoolKeyword,
            SpecialType.System_Byte => SyntaxKind.ByteKeyword,
            SpecialType.System_SByte => SyntaxKind.SByteKeyword,
            SpecialType.System_Int32 => SyntaxKind.IntKeyword,
            SpecialType.System_UInt32 => SyntaxKind.UIntKeyword,
            SpecialType.System_Int16 => SyntaxKind.ShortKeyword,
            SpecialType.System_UInt16 => SyntaxKind.UShortKeyword,
            SpecialType.System_Int64 => SyntaxKind.LongKeyword,
            SpecialType.System_UInt64 => SyntaxKind.ULongKeyword,
            SpecialType.System_Single => SyntaxKind.FloatKeyword,
            SpecialType.System_Double => SyntaxKind.DoubleKeyword,
            SpecialType.System_Decimal => SyntaxKind.DecimalKeyword,
            SpecialType.System_String => SyntaxKind.StringKeyword,
            SpecialType.System_Char => SyntaxKind.CharKeyword,
            SpecialType.System_Object => SyntaxKind.ObjectKeyword,
            SpecialType.System_Void => SyntaxKind.VoidKeyword,
            _ => SyntaxKind.None,
        };
 
        if (kind != SyntaxKind.None)
            return SyntaxFactory.Token(kind);
 
        if (specialType is SpecialType.System_IntPtr or SpecialType.System_UIntPtr && semanticModel.UnifiesNativeIntegers())
            return SyntaxFactory.Identifier(specialType == SpecialType.System_IntPtr ? "nint" : "nuint");
 
        return null;
    }
 
    [PerformanceSensitive(
        "https://github.com/dotnet/roslyn/issues/23582",
        Constraint = "Most trees do not have using alias directives, so avoid the expensive " + nameof(CSharpExtensions.GetSymbolInfo) + " call for this case.")]
    protected static bool TryReplaceExpressionWithAlias(
        ExpressionSyntax node, SemanticModel semanticModel,
        ISymbol symbol, CancellationToken cancellationToken, out IAliasSymbol aliasReplacement)
    {
        aliasReplacement = null;
 
        if (!IsAliasReplaceableExpression(node))
            return false;
 
        // Avoid the TryReplaceWithAlias algorithm if the tree has no using alias directives. Since the input node
        // might be a speculative node (not fully rooted in a tree), we use the original semantic model to find the
        // equivalent node in the original tree, and from there determine if the tree has any using alias
        // directives.
        var originalModel = semanticModel.GetOriginalSemanticModel();
        var hasUsingAliases = HasUsingAliases(originalModel, cancellationToken);
        if (!hasUsingAliases)
            return false;
 
        // If the Symbol is a constructor get its containing type
        if (symbol.IsConstructor())
        {
            symbol = symbol.ContainingType;
        }
 
        if (node is QualifiedNameSyntax or AliasQualifiedNameSyntax)
        {
            SyntaxAnnotation aliasAnnotationInfo = null;
 
            // The following condition checks if the user has used alias in the original code and
            // if so the expression is replaced with the Alias
            if (node is QualifiedNameSyntax qualifiedNameNode)
            {
                if (qualifiedNameNode.Right.Identifier.HasAnnotations(AliasAnnotation.Kind))
                {
                    aliasAnnotationInfo = qualifiedNameNode.Right.Identifier.GetAnnotations(AliasAnnotation.Kind).Single();
                }
            }
 
            if (node is AliasQualifiedNameSyntax aliasQualifiedNameNode)
            {
                if (aliasQualifiedNameNode.Name.Identifier.HasAnnotations(AliasAnnotation.Kind))
                {
                    aliasAnnotationInfo = aliasQualifiedNameNode.Name.Identifier.GetAnnotations(AliasAnnotation.Kind).Single();
                }
            }
 
            if (aliasAnnotationInfo != null)
            {
                var aliasName = AliasAnnotation.GetAliasName(aliasAnnotationInfo);
                var aliasIdentifier = SyntaxFactory.IdentifierName(aliasName);
 
                var aliasTypeInfo = semanticModel.GetSpeculativeAliasInfo(node.SpanStart, aliasIdentifier, SpeculativeBindingOption.BindAsTypeOrNamespace);
 
                if (aliasTypeInfo != null)
                {
                    aliasReplacement = aliasTypeInfo;
                    return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol);
                }
            }
        }
 
        if (node.Kind() == SyntaxKind.IdentifierName &&
            semanticModel.GetAliasInfo((IdentifierNameSyntax)node, cancellationToken) != null)
        {
            return false;
        }
 
        // an alias can only replace a type or namespace
        if (symbol == null ||
            (symbol.Kind != SymbolKind.Namespace && symbol.Kind != SymbolKind.NamedType))
        {
            return false;
        }
 
        var preferAliasToQualifiedName = true;
        if (node is QualifiedNameSyntax qualifiedName)
        {
            if (!qualifiedName.Right.HasAnnotation(Simplifier.SpecialTypeAnnotation))
            {
                var type = semanticModel.GetTypeInfo(node, cancellationToken).Type;
                if (type != null)
                {
                    var keywordToken = TryGetPredefinedKeywordToken(semanticModel, type.SpecialType);
                    if (keywordToken != null)
                        preferAliasToQualifiedName = false;
                }
            }
        }
 
        if (node is AliasQualifiedNameSyntax aliasQualifiedNameSyntax)
        {
            if (!aliasQualifiedNameSyntax.Name.HasAnnotation(Simplifier.SpecialTypeAnnotation))
            {
                var type = semanticModel.GetTypeInfo(node, cancellationToken).Type;
                if (type != null)
                {
                    var keywordToken = TryGetPredefinedKeywordToken(semanticModel, type.SpecialType);
                    if (keywordToken != null)
                        preferAliasToQualifiedName = false;
                }
            }
        }
 
        aliasReplacement = GetAliasForSymbol((INamespaceOrTypeSymbol)symbol, node.GetFirstToken(), semanticModel, cancellationToken);
        if (aliasReplacement != null && preferAliasToQualifiedName)
        {
            return ValidateAliasForTarget(aliasReplacement, semanticModel, node, symbol);
        }
 
        return false;
 
        static bool IsAliasReplaceableExpression(ExpressionSyntax expression)
        {
            var current = expression;
            while (current is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) currentMember)
            {
                current = currentMember.Expression;
                continue;
            }
 
            return current.Kind() is SyntaxKind.AliasQualifiedName or SyntaxKind.IdentifierName or SyntaxKind.GenericName or SyntaxKind.QualifiedName;
        }
    }
 
    private static bool HasUsingAliases(SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        if (!s_modelToHasUsingAliasesMap.TryGetValue(semanticModel, out var hasAliases))
        {
            hasAliases = new StrongBox<bool>(ComputeHasUsingAliases(semanticModel, cancellationToken));
            lock (s_modelToHasUsingAliasesMap)
            {
                s_modelToHasUsingAliasesMap.Remove(semanticModel);
                s_modelToHasUsingAliasesMap.Add(semanticModel, hasAliases);
            }
        }
 
        return hasAliases.Value;
    }
 
    private static bool ComputeHasUsingAliases(SemanticModel model, CancellationToken cancellationToken)
    {
        if (!model.SyntaxTree.HasCompilationUnitRoot)
            return false;
 
        var root = (CompilationUnitSyntax)model.SyntaxTree.GetRoot(cancellationToken);
        if (HasUsingAliasDirective(root))
            return true;
 
        var firstMember =
            root.Members.Count > 0 ? root.Members[0] :
            root.AttributeLists.Count > 0 ? root.AttributeLists[0] : (SyntaxNode)null;
        if (firstMember == null)
            return false;
 
        var scopes = model.GetImportScopes(firstMember.SpanStart, cancellationToken);
        return scopes.Any(static s => s.Aliases.Length > 0);
 
        static bool HasUsingAliasDirective(SyntaxNode syntax)
        {
            var (usings, members) = syntax switch
            {
                BaseNamespaceDeclarationSyntax ns => (ns.Usings, ns.Members),
                CompilationUnitSyntax compilationUnit => (compilationUnit.Usings, compilationUnit.Members),
                _ => default,
            };
 
            foreach (var usingDirective in usings)
            {
                if (usingDirective.Alias != null)
                    return true;
            }
 
            foreach (var member in members)
            {
                if (HasUsingAliasDirective(member))
                    return true;
            }
 
            return false;
        }
    }
 
    // We must verify that the alias actually binds back to the thing it's aliasing.
    // It's possible there's another symbol with the same name as the alias that binds
    // first
    private static bool ValidateAliasForTarget(IAliasSymbol aliasReplacement, SemanticModel semanticModel, ExpressionSyntax node, ISymbol symbol)
    {
        var aliasName = aliasReplacement.Name;
 
        // If we're the argument of a nameof(X.Y) call, then we can't simplify to an
        // alias unless the alias has the same name as us (i.e. 'Y').
        if (node.IsNameOfArgumentExpression())
        {
            var nameofValueOpt = semanticModel.GetConstantValue(node.Parent.Parent.Parent);
            if (!nameofValueOpt.HasValue)
            {
                return false;
            }
 
            if (nameofValueOpt.Value is string existingVal &&
                existingVal != aliasName)
            {
                return false;
            }
        }
 
        // If something is dotting off the node we need to make sure the name couldn't
        // be a different symbol that has a different type to the alias.
        if (node.IsLeftSideOfDot())
        {
            var aliasIdentifier = SyntaxFactory.IdentifierName(aliasName);
 
            var symbolInfo = semanticModel.GetSpeculativeSymbolInfo(node.SpanStart, aliasIdentifier, SpeculativeBindingOption.BindAsExpression);
            if (symbolInfo.Symbol is not INamespaceOrTypeSymbol)
            {
                // We bound the alias to something other than a namespace or a type, which is normally not good, but if the
                // types are the same then it is okay.
                var typeInfo = semanticModel.GetSpeculativeTypeInfo(node.SpanStart, aliasIdentifier, SpeculativeBindingOption.BindAsExpression);
                if (!symbol.Equals(typeInfo.Type))
                {
                    return false;
                }
            }
        }
 
        var boundSymbols = semanticModel.LookupNamespacesAndTypes(node.SpanStart, name: aliasName);
 
        if (boundSymbols.Length == 1)
        {
            if (boundSymbols[0] is IAliasSymbol && aliasReplacement.Target.Equals(symbol))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static IAliasSymbol GetAliasForSymbol(INamespaceOrTypeSymbol symbol, SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        var originalSemanticModel = semanticModel.GetOriginalSemanticModel();
        if (!originalSemanticModel.SyntaxTree.HasCompilationUnitRoot)
            return null;
 
        var namespaceId = GetNamespaceIdForAliasSearch(semanticModel, token, cancellationToken);
        if (namespaceId == null)
            return null;
 
        if (!AliasSymbolCache.TryGetAliasSymbol(originalSemanticModel, namespaceId.Value, symbol, out var aliasSymbol))
        {
            // add cache
            AliasSymbolCache.AddAliasSymbols(
                originalSemanticModel, namespaceId.Value, semanticModel.LookupNamespacesAndTypes(token.SpanStart).OfType<IAliasSymbol>());
 
            // retry
            AliasSymbolCache.TryGetAliasSymbol(originalSemanticModel, namespaceId.Value, symbol, out aliasSymbol);
        }
 
        return aliasSymbol;
    }
 
    private static int? GetNamespaceIdForAliasSearch(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken)
    {
        var startNode = GetStartNodeForNamespaceId(semanticModel, token, cancellationToken);
        if (!startNode.SyntaxTree.HasCompilationUnitRoot)
            return null;
 
        // NOTE: If we're currently in a block of usings, then we want to collect the
        // aliases that are higher up than this block.  Using aliases declared in a block of
        // usings are not usable from within that same block.
        var usingDirective = startNode.GetAncestorOrThis<UsingDirectiveSyntax>();
        if (usingDirective != null)
        {
            startNode = usingDirective.Parent.Parent;
            if (startNode == null)
                return null;
        }
 
        // check whether I am under a namespace
        var @namespace = startNode.GetAncestorOrThis<BaseNamespaceDeclarationSyntax>();
        if (@namespace != null)
            return @namespace.SpanStart;
 
        // no namespace, under compilation unit directly.  Pass -1 so there is no ambiguity with a namespace decl
        // that starts at position 0.
        return -1;
    }
 
    private static SyntaxNode GetStartNodeForNamespaceId(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken)
    {
        if (!semanticModel.IsSpeculativeSemanticModel)
            return token.Parent;
 
        var originalSemanticMode = semanticModel.GetOriginalSemanticModel();
        token = originalSemanticMode.SyntaxTree.GetRoot(cancellationToken).FindToken(semanticModel.OriginalPositionForSpeculation);
 
        return token.Parent;
    }
 
    protected static TypeSyntax CreatePredefinedTypeSyntax(SyntaxNode nodeToReplace, SyntaxToken token)
    {
        TypeSyntax node = token.Kind() == SyntaxKind.IdentifierToken
            ? SyntaxFactory.IdentifierName(token)
            : SyntaxFactory.PredefinedType(token);
        return node.WithTriviaFrom(nodeToReplace);
    }
 
    protected static bool InsideNameOfExpression(ExpressionSyntax expression, SemanticModel semanticModel)
    {
        var nameOfInvocationExpr = expression.FirstAncestorOrSelf<InvocationExpressionSyntax>(
            invocationExpr =>
            {
                return invocationExpr.Expression is IdentifierNameSyntax identifierName &&
                    identifierName.Identifier.Text == "nameof" &&
                    semanticModel.GetConstantValue(invocationExpr).HasValue &&
                    semanticModel.GetTypeInfo(invocationExpr).Type.SpecialType == SpecialType.System_String;
            });
 
        return nameOfInvocationExpr != null;
    }
 
    protected static bool PreferPredefinedTypeKeywordInMemberAccess(ExpressionSyntax expression, CSharpSimplifierOptions options, SemanticModel semanticModel)
    {
        if (!options.PreferPredefinedTypeKeywordInMemberAccess.Value)
            return false;
 
        return (expression.IsDirectChildOfMemberAccessExpression() || expression.InsideCrefReference()) &&
               !InsideNameOfExpression(expression, semanticModel);
    }
 
    protected static bool WillConflictWithExistingLocal(
        ExpressionSyntax expression, ExpressionSyntax simplifiedNode, SemanticModel semanticModel)
    {
        if (simplifiedNode is IdentifierNameSyntax identifierName &&
            !SyntaxFacts.IsInNamespaceOrTypeContext(expression))
        {
            var symbols = semanticModel.LookupSymbols(expression.SpanStart, name: identifierName.Identifier.ValueText);
            return symbols.Any(static s => s is ILocalSymbol);
        }
 
        return false;
    }
}