// 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 enable
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.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Operations;
#if CODE_STYLE
using OptionSet = Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions;
#else
using OptionSet = Microsoft.CodeAnalysis.Options.OptionSet;
#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, 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 ArgumentSyntax argument) ||
!(argument.Parent is ArgumentListSyntax argumentList) ||
!(argumentList.Parent is InvocationExpressionSyntax invocationExpression))
{
return false;
}
var memberGroup = semanticModel.GetMemberGroup(invocationExpression.Expression, cancellationToken);
if (memberGroup.Length != 1)
return false;
var method = memberGroup[0] as IMethodSymbol;
if (method == null)
return false;
if (!method.GetTypeParameters().IsEmpty)
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.
var invocationOp = semanticModel.GetOperation(invocationExpression, cancellationToken) as IInvocationOperation;
if (invocationOp == null)
return false;
var argumentOp = invocationOp.Arguments.FirstOrDefault(a => a.Syntax == argument);
if (argumentOp == null)
return false;
if (argumentOp.Value?.Type == null)
return false;
if (argumentOp.Parameter?.Type == null)
return false;
return argumentOp.Value.Type.Equals(argumentOp.Parameter.Type);
}
///
/// 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;
}
// var cannot be used with target typed new
#if CODE_STYLE
if (expression.IsKind(Formatting.SyntaxKindEx.ImplicitObjectCreationExpression))
#else
if (expression.IsKind(SyntaxKind.ImplicitObjectCreationExpression))
#endif
{
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;
}
// 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);
}
return false;
}
internal static ExpressionSyntax GetInitializerExpression(ExpressionSyntax initializer)
{
var current = (initializer as RefExpressionSyntax)?.Expression ?? initializer;
current = (current as CheckedExpressionSyntax)?.Expression ?? current;
return current.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);
}
}
}