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