File: src\roslyn\src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Utilities\TypeStyle\CSharpUseImplicitTypeHelper.cs
Web Access
Project: src\roslyn\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle)
// 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.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.CodeStyle.TypeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Simplification;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;

namespace Microsoft.CodeAnalysis.CSharp.Utilities;

internal sealed class CSharpUseImplicitTypeHelper : CSharpTypeStyleHelper
{
    public static readonly CSharpUseImplicitTypeHelper Instance = new();

    private CSharpUseImplicitTypeHelper()
    {
    }

    public override TypeStyleResult AnalyzeTypeName(
        TypeSyntax typeName, SemanticModel semanticModel,
        CSharpSimplifierOptions options, CancellationToken cancellationToken)
    {
        if (typeName.StripRefIfNeeded().IsVar)
        {
            return default;
        }

        if (typeName.HasAnnotation(DoNotAllowVarAnnotation.Annotation))
        {
            return default;
        }

        return base.AnalyzeTypeName(
            typeName, semanticModel, options, cancellationToken);
    }

    public override bool ShouldAnalyzeVariableDeclaration(VariableDeclarationSyntax variableDeclaration, CancellationToken cancellationToken)
    {
        // If the type is already 'var' or 'ref var', this analyzer has no work to do
        var type = variableDeclaration.Type.StripRefIfNeeded();
        if (type.IsVar)
            return false;

        // The base analyzer may impose further limitations
        return base.ShouldAnalyzeVariableDeclaration(variableDeclaration, cancellationToken);
    }

    protected override bool ShouldAnalyzeForEachStatement(ForEachStatementSyntax forEachStatement, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        // If the type is already 'var' or 'ref var', this analyzer has no work to do
        var type = forEachStatement.Type.StripRefIfNeeded();
        if (type.IsVar)
            return false;

        // The base analyzer may impose further limitations
        return base.ShouldAnalyzeForEachStatement(forEachStatement, semanticModel, cancellationToken);
    }

    protected override bool ShouldAnalyzeDeclarationExpression(DeclarationExpressionSyntax declaration, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        // If the type is already 'var' or 'ref var', this analyzer has no work to do
        if (declaration.Type.StripRefIfNeeded().IsVar)
            return false;

        // The base analyzer may impose further limitations
        return base.ShouldAnalyzeDeclarationExpression(declaration, semanticModel, cancellationToken);
    }

    protected override bool IsStylePreferred(in State state)
    {
        var stylePreferences = state.TypeStylePreference;
        return state.Context switch
        {
            Context.BuiltInType => stylePreferences.HasFlag(UseVarPreference.ForBuiltInTypes),
            Context.TypeIsApparent => stylePreferences.HasFlag(UseVarPreference.WhenTypeIsApparent),
            Context.Elsewhere => stylePreferences.HasFlag(UseVarPreference.Elsewhere),
            _ => throw ExceptionUtilities.UnexpectedValue(state.Context),
        };
    }

    internal override bool TryAnalyzeVariableDeclaration(
        TypeSyntax typeName, SemanticModel semanticModel,
        CSharpSimplifierOptions options, CancellationToken cancellationToken)
    {
        Debug.Assert(!typeName.StripRefIfNeeded().IsVar, "'var' special case should have prevented analysis of this variable.");

        var candidateReplacementNode = SyntaxFactory.IdentifierName("var");

        // If there exists a type named var, return.
        var conflict = semanticModel.GetSpeculativeSymbolInfo(typeName.SpanStart, candidateReplacementNode, SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol;
        if (conflict is INamedTypeSymbol)
            return false;

        if (typeName is { Parent: VariableDeclarationSyntax variableDeclaration, Parent.Parent: (kind: SyntaxKind.LocalDeclarationStatement or SyntaxKind.ForStatement or SyntaxKind.UsingStatement) })
        {
            // implicitly typed variables cannot be constants.
            if (variableDeclaration.Parent is LocalDeclarationStatementSyntax { IsConst: true })
                return false;

            if (variableDeclaration.Variables is not [{ Initializer.Value: var initializer } variable])
                return false;

            // Do not suggest var replacement for stackalloc span expressions. This will change the bound type from a
            // span to a pointer.  Note: this only applies to `var v = stackalloc ...;`  If `stackalloc` is anywhere
            // lower (including `var v = (stackalloc ...);`), then this is will be a span, and it will be ok to change
            // to use 'var'.
            if (!variableDeclaration.Type.IsKind(SyntaxKind.PointerType) &&
                initializer is StackAllocArrayCreationExpressionSyntax)
            {
                return false;
            }

            if (AssignmentSupportsStylePreference(
                    variable.Identifier, typeName, initializer,
                    semanticModel, options, cancellationToken))
            {
                return true;
            }
        }
        else if (typeName.Parent is ForEachStatementSyntax foreachStatement &&
                 foreachStatement.Type == typeName)
        {
            var foreachStatementInfo = semanticModel.GetForEachStatementInfo(foreachStatement);
            if (foreachStatementInfo.ElementConversion.IsIdentity)
            {
                return true;
            }
        }
        else if (typeName.Parent is DeclarationExpressionSyntax declarationExpression &&
                 TryAnalyzeDeclarationExpression(declarationExpression, semanticModel, cancellationToken))
        {
            return true;
        }

        return false;
    }

    private static bool TryAnalyzeDeclarationExpression(
        DeclarationExpressionSyntax declarationExpression,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        // First try to do the cheap check to see if we could replace this decl-expression with
        // "var".  If not, we'll fall out below to the much more expensive case where we change
        // the actual type to "var" and see if semantics stay the same.
        if (IsSafeToSwitchToVarWithoutNeedingSpeculation(declarationExpression, semanticModel, cancellationToken))
            return true;

        if (!semanticModel.SyntaxTree.HasCompilationUnitRoot)
            return false;

        // Do the expensive check.  Note: we can't use the SpeculationAnalyzer (or any
        // speculative analyzers) here.  This is due to
        // https://github.com/dotnet/roslyn/issues/20724. Specifically, all the speculative
        // helpers do not deal with  changes to code that introduces a variable (in this case,
        // the declaration expression).  The compiler sees this as an error because there are
        // now two colliding variables, which causes all sorts of errors to be reported.
        var tree = semanticModel.SyntaxTree;
        var root = tree.GetRoot(cancellationToken);
        var annotation = new SyntaxAnnotation();

        var declarationTypeNode = declarationExpression.Type;
        var declarationType = semanticModel.GetTypeInfo(declarationTypeNode, cancellationToken).Type;

        var newRoot = root.ReplaceNode(
            declarationTypeNode,
            SyntaxFactory.IdentifierName("var").WithTriviaFrom(declarationTypeNode).WithAdditionalAnnotations(annotation));

        var newTree = tree.WithRootAndOptions(newRoot, tree.Options);
        var newSemanticModel = semanticModel.Compilation.ReplaceSyntaxTree(tree, newTree).GetSemanticModel(newTree);

        var newDeclarationTypeNode = newTree.GetRoot(cancellationToken).GetAnnotatedNodes(annotation).Single();
        var newDeclarationType = newSemanticModel.GetTypeInfo(newDeclarationTypeNode, cancellationToken).Type;

        return SymbolEquivalenceComparer.TupleNamesMustMatchInstance.Equals(
            declarationType, newDeclarationType);
    }

    private static bool IsSafeToSwitchToVarWithoutNeedingSpeculation(DeclarationExpressionSyntax declarationExpression, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        // It's not always safe to convert a decl expression like "Method(out int i)" to
        // "Method(out var i)".  Changing to 'var' may cause overload resolution errors.
        // Have to see if using 'var' means not resolving to the same type as before.
        // Note: this is fairly expensive, so we try to avoid this if we can by seeing if
        // there are multiple candidates with the original call.  If not, then we don't
        // have to do anything.

        // If there was only one member in the group, and it was non-generic itself, then this
        // change is commonly safe to make without having to actually change to `var` and
        // speculatively determine if the change is ok or not.
        if (declarationExpression.Parent is not ArgumentSyntax
            {
                Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax invocationExpression }
            } argument)
        {
            return false;
        }

        var memberGroup = semanticModel.GetMemberGroup(invocationExpression.Expression, cancellationToken);
        if (memberGroup.Length != 1)
            return false;

        if (memberGroup[0] is not IMethodSymbol { TypeParameters.IsEmpty: true } method)
            return false;

        // Looks pretty good so far.  However, this change is not allowed if the user is specifying something like `out
        // (int x, int y) t` and the method signature has different names for those tuple elements.  Check and make sure
        // the types are the same before proceeding.

        if (semanticModel.GetOperation(invocationExpression, cancellationToken) is not IInvocationOperation invocationOp)
            return false;

        var argumentOp = invocationOp.Arguments.FirstOrDefault(a => a.Syntax == argument);
        if (argumentOp is not { Value.Type: { } valueType, Parameter.Type: { } parameterType })
            return false;

        return valueType.Equals(parameterType);
    }

    /// <summary>
    /// Analyzes the assignment expression and rejects a given declaration if it is unsuitable for implicit typing.
    /// </summary>
    /// <returns>
    /// false, if implicit typing cannot be used.
    /// true, otherwise.
    /// </returns>
    protected override bool AssignmentSupportsStylePreference(
        SyntaxToken identifier,
        TypeSyntax typeName,
        ExpressionSyntax initializer,
        SemanticModel semanticModel,
        CSharpSimplifierOptions options,
        CancellationToken cancellationToken)
    {
        var expression = GetInitializerExpression(initializer);

        // var cannot be assigned null
        if (expression.IsKind(SyntaxKind.NullLiteralExpression))
            return false;

        // var cannot be used with target typed new
        if (expression.IsKind(SyntaxKind.ImplicitObjectCreationExpression))
            return false;

        // cannot use implicit typing on method group or on dynamic
        var declaredType = semanticModel.GetTypeInfo(typeName.StripRefIfNeeded(), cancellationToken).Type;
        if (declaredType is null)
            return false;

        if (declaredType.TypeKind == TypeKind.Dynamic)
            return false;

        // variables declared using var cannot be used further in the same initialization expression.
        if (initializer.DescendantNodesAndSelf()
            .Where(n => n is IdentifierNameSyntax id && id.Identifier.ValueText.Equals(identifier.ValueText))
            .Any(n =>
            {
                // case of variable direct use: int x = x * 2;
                if (semanticModel.GetSymbolInfo(n, cancellationToken).Symbol.IsKind(SymbolKind.Local) == true)
                {
                    return true;
                }

                // case of qualification starting with the variable name: SomeEnum SomeEnum = SomeEnum.EnumVal1;
                // note that: SomeEnum SomeEnum = global::SomeEnum.EnumVal1; // is ok and 'var' can be offered
                // https://github.com/dotnet/roslyn/issues/26894
                if (n.Parent is MemberAccessExpressionSyntax memberAccessParent && memberAccessParent.Expression == n)
                {
                    return true;
                }

                return false;
            }))
        {
            return false;
        }

        // Get the conversion that occurred between the expression's type and type implied by the expression's context
        // and filter out implicit conversions. If an implicit conversion (other than identity) exists and if we're
        // replacing the declaration with 'var' we'd be changing the semantics by inferring type of initializer
        // expression and thereby losing the conversion.
        var conversion = semanticModel.GetConversion(expression, cancellationToken);
        if (conversion.IsIdentity)
        {
            // final check to compare type information on both sides of assignment.
            var initializerType = semanticModel.GetTypeInfo(expression, cancellationToken).Type;
            return declaredType != null && declaredType.Equals(initializerType);
        }

        // This also applies to a lambda assigned to a variable.  This will have no conversion, but can be converted as
        // long as the type is Func<> or Action<> as that's what the language will infer here.
        if (!conversion.Exists && expression is LambdaExpressionSyntax && semanticModel.Compilation.LanguageVersion() >= LanguageVersion.CSharp10)
        {
            var initializerType = semanticModel.GetTypeInfo(expression, cancellationToken).Type;
            return declaredType.Equals(initializerType) &&
                declaredType is
                {
                    Name: nameof(Func<>) or nameof(Action<>),
                    ContainingSymbol: INamespaceSymbol { Name: nameof(System), ContainingNamespace.IsGlobalNamespace: true }
                };
        }

        return false;
    }

    internal static ExpressionSyntax GetInitializerExpression(ExpressionSyntax initializer)
    {
        var current = (initializer as RefExpressionSyntax)?.Expression ?? initializer;
        current = (current as CheckedExpressionSyntax)?.Expression ?? current;
        return current.WalkDownParentheses();
    }
}