// 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.Diagnostics;
using System.Linq;
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;
#if CODE_STYLE
using OptionSet = Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions;
using Microsoft.CodeAnalysis.CSharp.Internal.CodeStyle.TypeStyle;
#else
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.CSharp.CodeStyle.TypeStyle;
#endif
namespace Microsoft.CodeAnalysis.CSharp.Utilities
{
internal sealed class CSharpUseImplicitTypeHelper : CSharpTypeStyleHelper
{
public static readonly CSharpUseImplicitTypeHelper Instance = new CSharpUseImplicitTypeHelper();
private CSharpUseImplicitTypeHelper()
{
}
public override TypeStyleResult AnalyzeTypeName(
TypeSyntax typeName, SemanticModel semanticModel,
OptionSet optionSet, CancellationToken cancellationToken)
{
if (typeName.StripRefIfNeeded().IsVar)
{
return default;
}
if (typeName.HasAnnotation(DoNotAllowVarAnnotation.Annotation))
{
return default;
}
return base.AnalyzeTypeName(
typeName, semanticModel, optionSet, cancellationToken);
}
protected override bool ShouldAnalyzeVariableDeclaration(VariableDeclarationSyntax variableDeclaration, SemanticModel semanticModel, CancellationToken cancellationToken)
{
var type = variableDeclaration.Type.StripRefIfNeeded();
if (type.IsVar)
{
// If the type is already 'var' or 'ref var', this analyzer has no work to do
return false;
}
// The base analyzer may impose further limitations
return base.ShouldAnalyzeVariableDeclaration(variableDeclaration, semanticModel, cancellationToken);
}
protected override bool ShouldAnalyzeForEachStatement(ForEachStatementSyntax forEachStatement, SemanticModel semanticModel, CancellationToken cancellationToken)
{
var type = forEachStatement.Type;
if (type.IsVar || (type.Kind() == SyntaxKind.RefType && ((RefTypeSyntax)type).Type.IsVar))
{
// If the type is already 'var', this analyze has no work to do
return false;
}
// The base analyzer may impose further limitations
return base.ShouldAnalyzeForEachStatement(forEachStatement, semanticModel, cancellationToken);
}
protected override bool IsStylePreferred(in State state)
{
var stylePreferences = state.TypeStylePreference;
if (state.IsInIntrinsicTypeContext)
{
return stylePreferences.HasFlag(UseVarPreference.ForBuiltInTypes);
}
else if (state.IsTypeApparentInContext)
{
return stylePreferences.HasFlag(UseVarPreference.WhenTypeIsApparent);
}
else
{
return stylePreferences.HasFlag(UseVarPreference.Elsewhere);
}
}
internal override bool TryAnalyzeVariableDeclaration(
TypeSyntax typeName, SemanticModel semanticModel,
OptionSet optionSet, 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?.IsKind(SymbolKind.NamedType) == true)
{
return false;
}
if (typeName.Parent.IsKind(SyntaxKind.VariableDeclaration, out VariableDeclarationSyntax variableDeclaration) &&
typeName.Parent.IsParentKind(SyntaxKind.LocalDeclarationStatement, SyntaxKind.ForStatement, SyntaxKind.UsingStatement))
{
// implicitly typed variables cannot be constants.
if ((variableDeclaration.Parent as LocalDeclarationStatementSyntax)?.IsConst == true)
{
return false;
}
if (variableDeclaration.Variables.Count != 1)
{
return false;
}
var variable = variableDeclaration.Variables[0];
if (variable.Initializer == null)
{
return false;
}
var initializer = variable.Initializer.Value;
// Do not suggest var replacement for stackalloc span expressions.
// This will change the bound type from a span to a pointer.
if (!variableDeclaration.Type.IsKind(SyntaxKind.PointerType))
{
var containsStackAlloc = initializer
.DescendantNodesAndSelf(descendIntoChildren: node => !node.IsAnyLambdaOrAnonymousMethod())
.Any(node => node.IsKind(SyntaxKind.StackAllocArrayCreationExpression));
if (containsStackAlloc)
{
return false;
}
}
if (AssignmentSupportsStylePreference(
variable.Identifier, typeName, initializer,
semanticModel, optionSet, 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, optionSet, cancellationToken))
{
return true;
}
return false;
}
private bool TryAnalyzeDeclarationExpression(
DeclarationExpressionSyntax declarationExpression,
SemanticModel semanticModel,
OptionSet optionSet,
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 (declarationExpression.Parent is ArgumentSyntax argument &&
argument.Parent is ArgumentListSyntax argumentList &&
argumentList.Parent is InvocationExpressionSyntax invocationExpression)
{
// If there was only one member in the group, and it was non-generic itself,
// then this change is safe to make without doing any complex analysis.
// Multiple methods mean that switching to 'var' might remove information
// that affects overload resolution. And if the method is generic, then
// switching to 'var' may mean that inference might not work properly.
var memberGroup = semanticModel.GetMemberGroup(invocationExpression.Expression, cancellationToken);
if (memberGroup.Length == 1 &&
memberGroup[0].GetTypeParameters().IsEmpty)
{
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.Instance.Equals(declarationType, newDeclarationType);
}
///
/// Analyzes the assignment expression and rejects a given declaration if it is unsuitable for implicit typing.
///
///
/// false, if implicit typing cannot be used.
/// true, otherwise.
///
protected override bool AssignmentSupportsStylePreference(
SyntaxToken identifier,
TypeSyntax typeName,
ExpressionSyntax initializer,
SemanticModel semanticModel,
OptionSet optionSet,
CancellationToken cancellationToken)
{
var expression = GetInitializerExpression(initializer);
// var cannot be assigned null
if (expression.IsKind(SyntaxKind.NullLiteralExpression))
{
return false;
}
// cannot use implicit typing on method group or on dynamic
var declaredType = semanticModel.GetTypeInfo(typeName.StripRefIfNeeded(), cancellationToken).Type;
if (declaredType != null && declaredType.TypeKind == TypeKind.Dynamic)
{
return false;
}
if (IsSwitchExpressionAndCannotUseVar(typeName, initializer, semanticModel))
{
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.Equals(initializerType);
}
return false;
}
internal static ExpressionSyntax GetInitializerExpression(ExpressionSyntax initializer)
=> initializer is CheckedExpressionSyntax
? ((CheckedExpressionSyntax)initializer).Expression.WalkDownParentheses()
: initializer.WalkDownParentheses();
protected override bool ShouldAnalyzeDeclarationExpression(DeclarationExpressionSyntax declaration, SemanticModel semanticModel, CancellationToken cancellationToken)
{
if (declaration.Type.IsVar)
{
// If the type is already 'var', this analyze has no work to do
return false;
}
// The base analyzer may impose further limitations
return base.ShouldAnalyzeDeclarationExpression(declaration, semanticModel, cancellationToken);
}
private bool IsSwitchExpressionAndCannotUseVar(TypeSyntax typeName, ExpressionSyntax initializer, SemanticModel semanticModel)
{
if (initializer.IsKind(SyntaxKind.SwitchExpression))
{
// We compare the variable declaration type to each arm's type to see if there is an exact match, or if the
// arm type inherits from the variable declaration type. If not, we must use the explicit type instead of var.
// Even if 'true' is returned from this method, it is not guaranteed that we can use var. Further checks should occur
// after this method is called, such as checking if multiple implicit coversions exist.
var declarationType = semanticModel.GetTypeInfo(typeName).Type;
var noValidTypeExpressions = true;
if (declarationType != null)
{
foreach (var arm in ((SwitchExpressionSyntax)initializer).Arms)
{
var expression = arm.Expression;
if (expression.IsKind(SyntaxKind.ParenthesizedExpression, out ParenthesizedExpressionSyntax parenExpression))
{
expression = parenExpression.WalkDownParentheses();
}
if (!expression.IsKind(SyntaxKind.ThrowExpression) && !expression.IsKind(SyntaxKind.NullLiteralExpression) && !expression.IsKind(SyntaxKind.DefaultLiteralExpression))
{
noValidTypeExpressions = false;
var expressionType = semanticModel.GetTypeInfo(expression).Type;
if (expressionType != null && !expressionType.InheritsFromOrEquals(declarationType))
{
return true;
}
}
}
}
// If all arms are either throw statements, null literal expressions, or default literal expressions, return true.
if (noValidTypeExpressions)
{
return true;
}
}
return false;
}
}
}