|
// 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;
}
|