File: src\Analyzers\CSharp\CodeFixes\InlineDeclaration\CSharpInlineDeclarationCodeFixProvider.cs
Web Access
Project: src\src\CodeStyle\CSharp\CodeFixes\Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes)
// 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 disable
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Simplification;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp.InlineDeclaration;
 
using static SyntaxFactory;
 
[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.InlineDeclaration), Shared]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed partial class CSharpInlineDeclarationCodeFixProvider() : SyntaxEditorBasedCodeFixProvider
{
    public override ImmutableArray<string> FixableDiagnosticIds
        => [IDEDiagnosticIds.InlineDeclarationDiagnosticId];
 
    public override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        RegisterCodeFix(context, CSharpAnalyzersResources.Inline_variable_declaration, nameof(CSharpAnalyzersResources.Inline_variable_declaration));
        return Task.CompletedTask;
    }
 
    protected override async Task FixAllAsync(
        Document document, ImmutableArray<Diagnostic> diagnostics,
        SyntaxEditor editor, CancellationToken cancellationToken)
    {
        var options = await document.GetCSharpSimplifierOptionsAsync(cancellationToken).ConfigureAwait(false);
 
        // Gather all statements to be removed
        // We need this to find the statements we can safely attach trivia to
        var declarationsToRemove = new HashSet<StatementSyntax>();
        foreach (var diagnostic in diagnostics)
        {
            declarationsToRemove.Add((LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken).Parent.Parent);
        }
 
        // Attempt to use an out-var declaration if that's the style the user prefers.
        // Note: if using 'var' would cause a problem, we will use the actual type
        // of the local.  This is necessary in some cases (for example, when the
        // type of the out-var-decl affects overload resolution or generic instantiation).
        var originalRoot = editor.OriginalRoot;
 
        var originalNodes = diagnostics.SelectAsArray(diagnostic => FindDiagnosticNodes(diagnostic, cancellationToken));
 
        await editor.ApplyExpressionLevelSemanticEditsAsync(
            document,
            originalNodes,
            static t => (t.invocationOrCreation, ImmutableArray.Create<SyntaxNode>(t.identifier, t.declarator)),
            (_, _, _) => true,
            (semanticModel, currentRoot, t, currentNode)
                => ReplaceIdentifierWithInlineDeclaration(
                    document, options, semanticModel, currentRoot, t.declarator,
                    t.identifier, currentNode, declarationsToRemove, cancellationToken),
            cancellationToken).ConfigureAwait(false);
    }
 
    private static (VariableDeclaratorSyntax declarator, IdentifierNameSyntax identifier, SyntaxNode invocationOrCreation) FindDiagnosticNodes(
        Diagnostic diagnostic, CancellationToken cancellationToken)
    {
        // Recover the nodes we care about.
        var declaratorLocation = diagnostic.AdditionalLocations[0];
        var identifierLocation = diagnostic.AdditionalLocations[1];
        var invocationOrCreationLocation = diagnostic.AdditionalLocations[2];
 
        var declarator = (VariableDeclaratorSyntax)declaratorLocation.FindNode(cancellationToken);
        var identifier = (IdentifierNameSyntax)identifierLocation.FindNode(cancellationToken);
        var invocationOrCreation = (ExpressionSyntax)invocationOrCreationLocation.FindNode(
            getInnermostNodeForTie: true, cancellationToken: cancellationToken);
 
        return (declarator, identifier, invocationOrCreation);
    }
 
    private static SyntaxNode ReplaceIdentifierWithInlineDeclaration(
        Document document,
        CSharpSimplifierOptions options, SemanticModel semanticModel,
        SyntaxNode currentRoot, VariableDeclaratorSyntax declarator,
        IdentifierNameSyntax identifier, SyntaxNode currentNode,
        HashSet<StatementSyntax> declarationsToRemove,
        CancellationToken cancellationToken)
    {
        declarator = currentRoot.GetCurrentNode(declarator);
        identifier = currentRoot.GetCurrentNode(identifier);
 
        var editor = new SyntaxEditor(currentRoot, document.Project.Solution.Services);
        var sourceText = currentRoot.GetText();
 
        var declaration = (VariableDeclarationSyntax)declarator.Parent;
        var singleDeclarator = declaration.Variables.Count == 1;
 
        if (singleDeclarator)
        {
            // This was a local statement with a single variable in it.  Just Remove
            // the entire local declaration statement. Note that comments belonging to
            // this local statement will be moved to be above the statement containing
            // the out-var.
            var localDeclarationStatement = (LocalDeclarationStatementSyntax)declaration.Parent;
            var block = CSharpInlineDeclarationDiagnosticAnalyzer.GetEnclosingPseudoBlock(localDeclarationStatement.Parent);
            var statements = GetStatements(block);
            var declarationIndex = statements.IndexOf(localDeclarationStatement);
 
            // Try to find a predecessor Statement on the same line that isn't going to be removed
            StatementSyntax priorStatementSyntax = null;
            var localDeclarationToken = localDeclarationStatement.GetFirstToken();
            for (var i = declarationIndex - 1; i >= 0; i--)
            {
                var statementSyntax = statements[i];
                if (declarationsToRemove.Contains(statementSyntax))
                    continue;
 
                if (sourceText.AreOnSameLine(statementSyntax.GetLastToken(), localDeclarationToken))
                    priorStatementSyntax = statementSyntax;
 
                break;
            }
 
            if (priorStatementSyntax != null)
            {
                // There's another statement on the same line as this declaration statement.
                // i.e.   int a; int b;
                //
                // Just move all trivia from our statement to be trailing trivia of the previous
                // statement
                editor.ReplaceNode(
                    priorStatementSyntax,
                    (s, g) => s.WithAppendedTrailingTrivia(localDeclarationStatement.GetTrailingTrivia()));
            }
            else
            {
                // Trivia on the local declaration will move to the next statement.
                // use the callback form as the next statement may be the place where we're
                // inlining the declaration, and thus need to see the effects of that change.
 
                // Find the next Statement that isn't going to be removed.
                // We initialize this to null here but we must see at least the statement
                // into which the declaration is going to be inlined so this will be not null
                StatementSyntax nextStatementSyntax = null;
                for (var i = declarationIndex + 1; i < statements.Length; i++)
                {
                    var statement = statements[i];
                    if (!declarationsToRemove.Contains(statement))
                    {
                        nextStatementSyntax = statement;
                        break;
                    }
                }
 
                editor.ReplaceNode(
                    nextStatementSyntax,
                    (s, g) => s.WithPrependedNonIndentationTriviaFrom(localDeclarationStatement)
                               .WithAdditionalAnnotations(Formatter.Annotation));
            }
 
            // The above code handled the moving of trivia.  So remove the node, keeping around no trivia from it.
            editor.RemoveNode(localDeclarationStatement.Parent is GlobalStatementSyntax globalStatement
                ? globalStatement
                : localDeclarationStatement, SyntaxRemoveOptions.KeepNoTrivia);
        }
        else
        {
            // Otherwise, just remove the single declarator. Note: we'll move the comments
            // 'on' the declarator to the out-var location.  This is a little bit trickier
            // than normal due to how our comment-association rules work.  i.e. if you have:
            //
            //      var /*c1*/ i /*c2*/, /*c3*/ j /*c4*/;
            //
            // In this case 'c1' is owned by the 'var' token, not 'i', and 'c3' is owned by
            // the comment token not 'j'.
 
            editor.RemoveNode(declarator);
            if (declarator == declaration.Variables[0])
            {
                // If we're removing the first declarator, and it's on the same line
                // as the previous token, then we want to remove all the trivia belonging
                // to the previous token.  We're going to move it along with this declarator.
                // If we don't, then the comment will stay with the previous token.
                //
                // Note that the moving of the comment happens later on when we make the
                // declaration expression.
                if (sourceText.AreOnSameLine(declarator.GetFirstToken(), declarator.GetFirstToken().GetPreviousToken(includeSkipped: true)))
                {
                    editor.ReplaceNode(
                        declaration.Type,
                        (t, g) => t.WithTrailingTrivia(ElasticSpace).WithoutAnnotations(Formatter.Annotation));
                }
            }
        }
 
        // get the type that we want to put in the out-var-decl based on the user's options.
        // i.e. prefer 'out var' if that is what the user wants.  Note: if we have:
        //
        //      Method(out var x)
        //
        // Then the type is not-apparent, and we should not use var if the user only wants
        // it for apparent types
 
        var local = (ILocalSymbol)semanticModel.GetDeclaredSymbol(declarator, cancellationToken);
        var newType = GenerateTypeSyntaxOrVar(local.Type, options);
 
        var declarationExpression = GetDeclarationExpression(
            sourceText, identifier, newType, singleDeclarator ? null : declarator);
 
        // Check if using out-var changed problem semantics.
        var semanticsChanged = SemanticsChanged(semanticModel, currentNode, identifier, declarationExpression, cancellationToken);
        if (semanticsChanged)
        {
            // Switching to 'var' changed semantics.  Just use the original type of the local.
 
            // If the user originally wrote it something other than 'var', then use what they
            // wrote.  Otherwise, synthesize the actual type of the local.
            var explicitType = declaration.Type.IsVar ? local.Type?.GenerateTypeSyntax() : declaration.Type;
            declarationExpression = DeclarationExpression(explicitType, declarationExpression.Designation);
        }
 
        editor.ReplaceNode(identifier, declarationExpression);
 
        return editor.GetChangedRoot();
    }
 
    private static ImmutableArray<StatementSyntax> GetStatements(SyntaxNode pseudoBlock)
    {
        if (pseudoBlock is BlockSyntax block)
            return [.. block.Statements];
 
        if (pseudoBlock is SwitchSectionSyntax switchSection)
            return [.. switchSection.Statements];
 
        if (pseudoBlock is CompilationUnitSyntax compilationUnit)
            return compilationUnit.Members.OfType<GlobalStatementSyntax>().Select(g => g.Statement).ToImmutableArray();
 
        return [];
    }
 
    public static TypeSyntax GenerateTypeSyntaxOrVar(
       ITypeSymbol symbol, CSharpSimplifierOptions options)
    {
        var useVar = IsVarDesired(symbol, options);
 
        // Note: we cannot use ".GenerateTypeSyntax()" only here.  that's because we're
        // actually creating a DeclarationExpression and currently the Simplifier cannot
        // analyze those due to limitations between how it uses Speculative SemanticModels
        // and how those don't handle new declarations well.
        return useVar
            ? IdentifierName("var")
            : symbol.GenerateTypeSyntax();
    }
 
    private static bool IsVarDesired(ITypeSymbol type, CSharpSimplifierOptions options)
    {
        // If they want it for intrinsics, and this is an intrinsic, then use var.
        if (type.IsSpecialType() == true)
        {
            return options.VarForBuiltInTypes.Value;
        }
 
        // If they want "var" whenever possible, then use "var".
        return options.VarElsewhere.Value;
    }
 
    private static DeclarationExpressionSyntax GetDeclarationExpression(
        SourceText sourceText, IdentifierNameSyntax identifier,
        TypeSyntax newType, VariableDeclaratorSyntax declaratorOpt)
    {
        var designation = SingleVariableDesignation(identifier.Identifier);
 
        if (declaratorOpt != null)
        {
            // We're removing a single declarator.  Copy any comments it has to the out-var.
            //
            // Note: this is tricky due to comment ownership.  We want the comments that logically
            // belong to the declarator, even if our syntax model attaches them to other tokens.
            var precedingTrivia = declaratorOpt.GetAllPrecedingTriviaToPreviousToken(
                sourceText, includePreviousTokenTrailingTriviaOnlyIfOnSameLine: true);
            if (precedingTrivia.Any(t => t.IsSingleOrMultiLineComment()))
            {
                designation = designation.WithPrependedLeadingTrivia(MassageTrivia(precedingTrivia));
            }
 
            if (declaratorOpt.GetTrailingTrivia().Any(t => t.IsSingleOrMultiLineComment()))
            {
                designation = designation.WithAppendedTrailingTrivia(MassageTrivia(declaratorOpt.GetTrailingTrivia()));
            }
        }
 
        newType = newType.WithoutTrivia().WithAdditionalAnnotations(Formatter.Annotation);
        // We need trivia between the type declaration and designation or this will generate
        // "out inti", but we might have trivia in the form of comments etc from the original
        // designation and in those cases adding elastic trivia will break formatting.
        if (!designation.HasLeadingTrivia)
        {
            newType = newType.WithAppendedTrailingTrivia(ElasticSpace);
        }
 
        return DeclarationExpression(newType, designation);
    }
 
    private static IEnumerable<SyntaxTrivia> MassageTrivia(IEnumerable<SyntaxTrivia> triviaList)
    {
        foreach (var trivia in triviaList)
        {
            if (trivia.IsSingleOrMultiLineComment())
            {
                yield return trivia;
            }
            else if (trivia.IsWhitespace())
            {
                // Condense whitespace down to single spaces. We don't want things like
                // indentation spaces to be inserted in the out-var location.  It is appropriate
                // though to have single spaces to help separate out things like comments and
                // tokens though.
                yield return Space;
            }
        }
    }
 
    private static bool SemanticsChanged(
        SemanticModel semanticModel,
        SyntaxNode nodeToReplace,
        IdentifierNameSyntax identifier,
        DeclarationExpressionSyntax declarationExpression,
        CancellationToken cancellationToken)
    {
        if (declarationExpression.Type.IsVar)
        {
            // Options want us to use 'var' if we can.  Make sure we didn't change
            // the semantics of the call by doing this.
 
            // Find the symbol that the existing invocation points to.
            var previousSymbol = semanticModel.GetSymbolInfo(nodeToReplace, cancellationToken).Symbol;
 
            // Now, create a speculative model in which we make the change.  Make sure
            // we still point to the same symbol afterwards.
 
            var topmostContainer = GetTopmostContainer(nodeToReplace);
            if (topmostContainer == null)
            {
                // Couldn't figure out what we were contained in.  Have to assume that semantics
                // Are changing.
                return true;
            }
 
            var annotation = new SyntaxAnnotation();
            var updatedTopmostContainer = topmostContainer.ReplaceNode(
                nodeToReplace, nodeToReplace.ReplaceNode(identifier, declarationExpression)
                                                          .WithAdditionalAnnotations(annotation));
 
            if (!TryGetSpeculativeSemanticModel(semanticModel,
                    topmostContainer.SpanStart, updatedTopmostContainer, out var speculativeModel))
            {
                // Couldn't figure out the new semantics.  Assume semantics changed.
                return true;
            }
 
            var updatedInvocationOrCreation = updatedTopmostContainer.GetAnnotatedNodes(annotation).Single();
            var updatedSymbolInfo = speculativeModel.GetSymbolInfo(updatedInvocationOrCreation, cancellationToken);
 
            if (!SymbolEquivalenceComparer.Instance.Equals(previousSymbol, updatedSymbolInfo.Symbol))
            {
                // We're pointing at a new symbol now.  Semantic have changed.
                return true;
            }
        }
 
        return false;
    }
 
    private static SyntaxNode GetTopmostContainer(SyntaxNode expression)
    {
        return expression.GetAncestorsOrThis(
            a => a is StatementSyntax or
                 EqualsValueClauseSyntax or
                 ArrowExpressionClauseSyntax or
                 ConstructorInitializerSyntax).LastOrDefault();
    }
 
    private static bool TryGetSpeculativeSemanticModel(
        SemanticModel semanticModel,
        int position, SyntaxNode topmostContainer,
        out SemanticModel speculativeModel)
    {
        switch (topmostContainer)
        {
            case StatementSyntax statement:
                return semanticModel.TryGetSpeculativeSemanticModel(position, statement, out speculativeModel);
            case EqualsValueClauseSyntax equalsValue:
                return semanticModel.TryGetSpeculativeSemanticModel(position, equalsValue, out speculativeModel);
            case ArrowExpressionClauseSyntax arrowExpression:
                return semanticModel.TryGetSpeculativeSemanticModel(position, arrowExpression, out speculativeModel);
            case ConstructorInitializerSyntax constructorInitializer:
                return semanticModel.TryGetSpeculativeSemanticModel(position, constructorInitializer, out speculativeModel);
        }
 
        speculativeModel = null;
        return false;
    }
}