File: CSharpInlineDeclarationDiagnosticAnalyzer.cs
Web Access
Project: ..\..\..\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.Collections.Immutable;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.InlineDeclaration
{
    /// <summary>
    /// Looks for code of the form:
    /// 
    ///     int i;
    ///     if (int.TryParse(s, out i)) { }
    ///     
    /// And offers to convert it to:
    /// 
    ///     if (int.TryParse(s, out var i)) { }   or
    ///     if (int.TryParse(s, out int i)) { }   or
    /// 
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    internal class CSharpInlineDeclarationDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    {
        private const string CS0165 = nameof(CS0165); // Use of unassigned local variable 's'
 
        public CSharpInlineDeclarationDiagnosticAnalyzer()
            : base(IDEDiagnosticIds.InlineDeclarationDiagnosticId,
                   EnforceOnBuildValues.InlineDeclaration,
                   CSharpCodeStyleOptions.PreferInlinedVariableDeclaration,
                   new LocalizableResourceString(nameof(CSharpAnalyzersResources.Inline_variable_declaration), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
                   new LocalizableResourceString(nameof(CSharpAnalyzersResources.Variable_declaration_can_be_inlined), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
        {
        }
 
        public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
            => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
        protected override void InitializeWorker(AnalysisContext context)
        {
            context.RegisterCompilationStartAction(compilationContext =>
            {
                var compilation = compilationContext.Compilation;
                var expressionType = compilation.GetTypeByMetadataName(typeof(Expression<>).FullName!);
                compilationContext.RegisterSyntaxNodeAction(
                    syntaxContext => AnalyzeSyntaxNode(syntaxContext, expressionType), SyntaxKind.Argument);
            });
        }
 
        private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressionType)
        {
            var syntaxTree = context.Node.SyntaxTree;
            var csOptions = (CSharpParseOptions)syntaxTree.Options;
            if (csOptions.LanguageVersion < LanguageVersion.CSharp7)
            {
                // out-vars are not supported prior to C# 7.0.
                return;
            }
 
            var option = context.GetCSharpAnalyzerOptions().PreferInlinedVariableDeclaration;
            if (!option.Value)
            {
                // Don't bother doing any work if the user doesn't even have this preference set.
                return;
            }
 
            var argumentNode = (ArgumentSyntax)context.Node;
            if (argumentNode.RefOrOutKeyword.Kind() != SyntaxKind.OutKeyword)
            {
                // Immediately bail if this is not an out-argument.  If it's not an out-argument
                // we clearly can't convert it to be an out-variable-declaration.
                return;
            }
 
            var argumentExpression = argumentNode.Expression;
            if (argumentExpression is not IdentifierNameSyntax identifierName)
            {
                // has to be exactly the form "out i".  i.e. "out this.i" or "out v[i]" are legal
                // cases for out-arguments, but could not be converted to an out-variable-declaration.
                return;
            }
 
            if (argumentNode.Parent is not ArgumentListSyntax argumentList)
            {
                return;
            }
 
            var invocationOrCreation = argumentList.Parent;
            if (!invocationOrCreation.IsKind(SyntaxKind.InvocationExpression) &&
                !invocationOrCreation.IsKind(SyntaxKind.ObjectCreationExpression))
            {
                // Out-variables are only legal with invocations and object creations.
                // If we don't have one of those bail.  Note: we need hte parent to be
                // one of these forms so we can accurately verify that inlining the 
                // variable doesn't change semantics.
                return;
            }
 
            // Don't offer to inline variables named "_".  It can cause is to create a discard symbol
            // which would cause a break.
            if (identifierName.Identifier.ValueText == "_")
            {
                return;
            }
 
            var containingStatement = argumentExpression.FirstAncestorOrSelf<StatementSyntax>();
            if (containingStatement == null)
            {
                return;
            }
 
            var cancellationToken = context.CancellationToken;
 
            var semanticModel = context.SemanticModel;
            if (semanticModel.GetSymbolInfo(argumentExpression, cancellationToken).Symbol is not ILocalSymbol outLocalSymbol)
            {
                // The out-argument wasn't referencing a local.  So we don't have an local
                // declaration that we can attempt to inline here.
                return;
            }
 
            // Ensure that the local-symbol actually belongs to LocalDeclarationStatement.
            // Trying to do things like inline a var-decl in a for-statement is just too 
            // esoteric and would make us have to write a lot more complex code to support
            // that scenario.
            var localReference = outLocalSymbol.DeclaringSyntaxReferences.FirstOrDefault();
            if (localReference?.GetSyntax(cancellationToken) is not VariableDeclaratorSyntax localDeclarator)
            {
                return;
            }
 
            var localDeclaration = localDeclarator.Parent as VariableDeclarationSyntax;
            if (localDeclaration?.Parent is not LocalDeclarationStatementSyntax localStatement)
            {
                return;
            }
 
            if (localDeclarator.SpanStart >= argumentNode.SpanStart)
            {
                // We have an error situation where the local was declared after the out-var.  
                // Don't even bother offering anything here.
                return;
            }
 
            // If the local has an initializer, only allow the refactoring if it is initialized
            // with a simple literal or 'default' expression.  i.e. it's ok to inline "var v = 0"
            // since there are no side-effects of the initialization.  However something like
            // "var v = M()" should not be inlined as that could break program semantics.
            if (localDeclarator.Initializer != null)
            {
                if (localDeclarator.Initializer.Value is not LiteralExpressionSyntax and
                    not DefaultExpressionSyntax)
                {
                    return;
                }
            }
 
            // Get the block that the local is scoped inside of.  We'll search that block
            // for references to the local to make sure that no reads/writes happen before
            // the out-argument.  If there are any reads/writes we can't inline as those
            // accesses will become invalid.
            if (localStatement.Parent is not BlockSyntax enclosingBlockOfLocalStatement)
            {
                return;
            }
 
            if (argumentExpression.IsInExpressionTree(semanticModel, expressionType, cancellationToken))
            {
                // out-vars are not allowed inside expression-trees.  So don't offer to
                // fix if we're inside one.
                return;
            }
 
            // Find the scope that the out-declaration variable will live in after we
            // rewrite things.
            var outArgumentScope = GetOutArgumentScope(argumentExpression);
            if (outArgumentScope == null)
                return;
 
            if (!outLocalSymbol.CanSafelyMoveLocalToBlock(enclosingBlockOfLocalStatement, outArgumentScope))
            {
                return;
            }
 
            // Make sure that variable is not accessed outside of that scope.
            var dataFlow = semanticModel.AnalyzeDataFlow(outArgumentScope);
            if (dataFlow.ReadOutside.Contains(outLocalSymbol) || dataFlow.WrittenOutside.Contains(outLocalSymbol))
            {
                // The variable is read or written from outside the block that the new variable
                // would be scoped in.  This would cause a break.
                //
                // Note(cyrusn): We could still offer the refactoring, but just show an error in the
                // preview in this case.
                return;
            }
 
            // Make sure the variable isn't ever accessed before the usage in this out-var.
            if (IsAccessed(semanticModel, outLocalSymbol, enclosingBlockOfLocalStatement,
                           localStatement, argumentNode, cancellationToken))
            {
                return;
            }
 
            // See if inlining this variable would make it so that some variables were no
            // longer definitely assigned.
            if (WouldCauseDefiniteAssignmentErrors(semanticModel, localStatement,
                                                   enclosingBlockOfLocalStatement, outLocalSymbol))
            {
                return;
            }
 
            // Collect some useful nodes for the fix provider to use so it doesn't have to
            // find them again.
            var allLocations = ImmutableArray.Create(
                localDeclarator.GetLocation(),
                identifierName.GetLocation(),
                invocationOrCreation.GetLocation());
 
            // If the local variable only has one declarator, then report the suggestion on the whole
            // declaration.  Otherwise, return the suggestion only on the single declarator.
            var reportNode = localDeclaration.Variables.Count == 1
                ? (SyntaxNode)localDeclaration
                : localDeclarator;
 
            context.ReportDiagnostic(DiagnosticHelper.Create(
                Descriptor,
                reportNode.GetLocation(),
                option.Notification.Severity,
                additionalLocations: allLocations,
                properties: null));
        }
 
        private static bool WouldCauseDefiniteAssignmentErrors(
            SemanticModel semanticModel,
            LocalDeclarationStatementSyntax localStatement,
            BlockSyntax enclosingBlock,
            ILocalSymbol outLocalSymbol)
        {
            // See if we have something like:
            //
            //      int i = 0;
            //      if (Goo() || Bar(out i))
            //      {
            //          Console.WriteLine(i);
            //      }
            //
            // In this case, inlining the 'i' would cause it to longer be definitely
            // assigned in the WriteLine invocation.
            var nextStatement = localStatement.GetNextStatement();
            Contract.ThrowIfNull(nextStatement);
 
            var dataFlow = semanticModel.AnalyzeDataFlow(
                nextStatement,
                enclosingBlock.Statements.Last());
            Contract.ThrowIfNull(dataFlow);
            return dataFlow.DataFlowsIn.Contains(outLocalSymbol);
        }
 
        private static SyntaxNode? GetOutArgumentScope(SyntaxNode argumentExpression)
        {
            for (var current = argumentExpression; current != null; current = current.Parent)
            {
                if (current.Parent is LambdaExpressionSyntax lambda &&
                    current == lambda.Body)
                {
                    // We were in a lambda.  The lambda body will be the new scope of the 
                    // out var.
                    return current;
                }
 
                // Any loop construct defines a scope for out-variables, as well as each of the following:
                // * Using statements
                // * Fixed statements
                // * Try statements (specifically for exception filters)
                switch (current.Kind())
                {
                    case SyntaxKind.WhileStatement:
                    case SyntaxKind.DoStatement:
                    case SyntaxKind.ForStatement:
                    case SyntaxKind.ForEachStatement:
                    case SyntaxKind.UsingStatement:
                    case SyntaxKind.FixedStatement:
                    case SyntaxKind.TryStatement:
                        return current;
                }
 
                if (current is StatementSyntax)
                {
                    // We hit a statement containing the out-argument.  Statements can have one of 
                    // two forms.  They're either parented by a block, or by another statement 
                    // (i.e. they're an embedded statement).  If we're parented by a block, then
                    // that block will be the scope of the new out-var.
                    //
                    // However, if our containing statement is not parented by a block, then that
                    // means we have something like:
                    //
                    //      if (x)
                    //          if (Try(out y))
                    //
                    // In this case, there is a 'virtual' block scope surrounding the embedded 'if'
                    // statement, and that will be the scope the out-var goes into.
                    return current.IsParentKind(SyntaxKind.Block)
                        ? current.Parent
                        : current;
                }
            }
 
            return null;
        }
 
        private static bool IsAccessed(
            SemanticModel semanticModel,
            ISymbol outSymbol,
            BlockSyntax enclosingBlockOfLocalStatement,
            LocalDeclarationStatementSyntax localStatement,
            ArgumentSyntax argumentNode,
            CancellationToken cancellationToken)
        {
            var localStatementStart = localStatement.Span.Start;
            var argumentNodeStart = argumentNode.Span.Start;
            var variableName = outSymbol.Name;
 
            // Walk the block that the local is declared in looking for accesses.
            // We can ignore anything prior to the actual local declaration point,
            // and we only need to check up until we reach the out-argument.
            foreach (var descendentNode in enclosingBlockOfLocalStatement.DescendantNodes())
            {
                var descendentStart = descendentNode.Span.Start;
                if (descendentStart <= localStatementStart)
                {
                    // This node is before the local declaration.  Can ignore it entirely as it could
                    // not be an access to the local.
                    continue;
                }
 
                if (descendentStart >= argumentNodeStart)
                {
                    // We reached the out-var.  We can stop searching entirely.
                    break;
                }
 
                if (descendentNode is IdentifierNameSyntax identifierName)
                {
                    // See if this looks like an accessor to the local variable syntactically.
                    if (identifierName.Identifier.ValueText == variableName)
                    {
                        // Confirm that it is a access of the local.
                        var symbol = semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol;
                        if (outSymbol.Equals(symbol))
                        {
                            // We definitely accessed the local before the out-argument.  We 
                            // can't inline this local.
                            return true;
                        }
                    }
                }
            }
 
            // No accesses detected
            return false;
        }
    }
}