File: ConvertForEachToFor\CSharpConvertForEachToForCodeRefactoringProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// 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.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.ConvertForEachToFor;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;
 
namespace Microsoft.CodeAnalysis.CSharp.ConvertForEachToFor;
 
using static SyntaxFactory;
 
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertForEachToFor), Shared]
internal sealed class CSharpConvertForEachToForCodeRefactoringProvider :
    AbstractConvertForEachToForCodeRefactoringProvider<StatementSyntax, ForEachStatementSyntax>
{
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    public CSharpConvertForEachToForCodeRefactoringProvider()
    {
    }
 
    protected override string Title => CSharpFeaturesResources.Convert_to_for;
 
    // https://github.com/dotnet/roslyn/issues/30584: Add tests for this scenario
    protected override bool IsValid(ForEachStatementSyntax foreachStatement)
        => foreachStatement.AwaitKeyword == default;
 
    protected override bool ValidLocation(ForEachInfo foreachInfo)
    {
        if (!foreachInfo.RequireCollectionStatement)
        {
            return true;
        }
 
        // for now, we don't support converting in embedded statement if 
        // new local declaration for collection is required.
        // we can support this by using Introduce local variable service
        // but the service is not currently written in a way that can be
        // easily reused here.
        return foreachInfo.ForEachStatement.Parent.IsKind(SyntaxKind.Block);
    }
 
    protected override (SyntaxNode start, SyntaxNode end) GetForEachBody(ForEachStatementSyntax foreachStatement)
        => (foreachStatement.Statement, foreachStatement.Statement);
 
    protected override void ConvertToForStatement(
        SemanticModel model, ForEachInfo foreachInfo, SyntaxEditor editor, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        var generator = editor.Generator;
        var foreachStatement = foreachInfo.ForEachStatement;
 
        var foreachCollectionExpression = foreachStatement.Expression;
        var collectionVariable = GetCollectionVariableName(
            model, generator, foreachInfo, foreachCollectionExpression, cancellationToken);
 
        var typeSymbol = foreachInfo.ExplicitCastInterface ??
            model.GetTypeInfo(foreachCollectionExpression, cancellationToken).Type ??
            model.Compilation.GetSpecialType(SpecialType.System_Object);
 
        var collectionStatementType = typeSymbol.GenerateTypeSyntax();
 
        // first, see whether we need to introduce new statement to capture collection
        IntroduceCollectionStatement(
            foreachInfo, editor, collectionStatementType, foreachCollectionExpression, collectionVariable);
 
        var indexVariable = CreateUniqueName(foreachInfo.SemanticFacts, model, foreachStatement.Statement, "i", cancellationToken);
 
        // do not cast when the element is identity - fixes 'var x in T![]' under nullable context
        var foreachStatementInfo = model.GetForEachStatementInfo(foreachStatement);
        var donotCastElement = foreachStatementInfo.ElementConversion.IsIdentity;
 
        // put variable statement in body
        var bodyStatement = GetForLoopBody(generator, foreachInfo, collectionVariable, indexVariable, donotCastElement);
 
        // create for statement from foreach statement
        var forStatement = ForStatement(
            VariableDeclaration(
                model.Compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax(),
                [VariableDeclarator(
                    indexVariable.WithAdditionalAnnotations(RenameAnnotation.Create()),
                    argumentList: null,
                    EqualsValueClause((ExpressionSyntax)generator.LiteralExpression(0)))]),
            initializers: [],
            (ExpressionSyntax)generator.LessThanExpression(
                generator.IdentifierName(indexVariable),
                generator.MemberAccessExpression(collectionVariable, foreachInfo.CountName)),
            [PostfixUnaryExpression(
                SyntaxKind.PostIncrementExpression, IdentifierName(indexVariable))],
            bodyStatement);
 
        if (!foreachInfo.RequireCollectionStatement)
        {
            // move comments before "foreach" keyword to "for". if collection statement is introduced,
            // it should have attached to the new collection statement, so no need to do it here.
            forStatement = forStatement.WithLeadingTrivia(foreachStatement.GetLeadingTrivia());
        }
 
        // replace close parenthese from "foreach" statement
        forStatement = forStatement.WithCloseParenToken(foreachStatement.CloseParenToken);
 
        editor.ReplaceNode(foreachStatement, forStatement);
    }
 
    private StatementSyntax GetForLoopBody(
        SyntaxGenerator generator, ForEachInfo foreachInfo, SyntaxNode collectionVariableName, SyntaxToken indexVariable, bool donotCastElement)
    {
        var foreachStatement = foreachInfo.ForEachStatement;
        if (foreachStatement.Statement is EmptyStatementSyntax)
        {
            return foreachStatement.Statement;
        }
 
        // create variable statement
        var variableStatement = AddItemVariableDeclaration(
            generator, foreachInfo.ForEachElementType.GenerateTypeSyntax(),
            foreachStatement.Identifier, donotCastElement ? null : foreachInfo.ForEachElementType,
            collectionVariableName, indexVariable);
 
        var bodyBlock = foreachStatement.Statement is BlockSyntax block ? block : Block(foreachStatement.Statement);
        if (bodyBlock.Statements.Count == 0)
        {
            // If the block was empty, still put the new variable inside of it. This handles the case where the user
            // writes the foreach and immediately decides to change it to a for-loop.  Now they'll still have their
            // variable to use in the body instead of having to write it again.
            return bodyBlock.AddStatements(variableStatement);
        }
        else
        {
            if (IsForEachVariableWrittenInside)
            {
                variableStatement = variableStatement.WithAdditionalAnnotations(CreateWarningAnnotation());
            }
 
            return bodyBlock.InsertNodesBefore(
                bodyBlock.Statements[0],
                [variableStatement]);
        }
    }
 
    protected override bool IsSupported(ILocalSymbol foreachVariable, IForEachLoopOperation forEachOperation, ForEachStatementSyntax foreachStatement)
        => true;
}