// 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(); } }