File: IntroduceVariable\CSharpIntroduceVariableService_IntroduceField.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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
 
namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable;
 
using static CSharpSyntaxTokens;
using static SyntaxFactory;
 
internal sealed partial class CSharpIntroduceVariableService
{
    protected override Task<Document> IntroduceFieldAsync(
        SemanticDocument document,
        ExpressionSyntax expression,
        bool allOccurrences,
        bool isConstant,
        CancellationToken cancellationToken)
    {
        var oldTypeDeclaration = expression.GetAncestorOrThis<TypeDeclarationSyntax>();
 
        var oldType = oldTypeDeclaration != null
            ? document.SemanticModel.GetDeclaredSymbol(oldTypeDeclaration, cancellationToken)
            : document.SemanticModel.Compilation.ScriptClass;
        var newNameToken = GenerateUniqueFieldName(document, expression, isConstant, cancellationToken);
        var typeDisplayString = oldType.ToMinimalDisplayString(document.SemanticModel, expression.SpanStart);
 
        var newQualifiedName = oldTypeDeclaration != null
            ? MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ParseName(typeDisplayString), IdentifierName(newNameToken))
            : (ExpressionSyntax)IdentifierName(newNameToken);
 
        newQualifiedName = newQualifiedName.WithAdditionalAnnotations(Simplifier.Annotation);
 
        var newFieldDeclaration = FieldDeclaration(
            default,
            MakeFieldModifiers(isConstant, inScript: oldType.IsScriptClass),
            VariableDeclaration(
                GetTypeSymbol(document, expression, cancellationToken).GenerateTypeSyntax(),
                [VariableDeclarator(
                    newNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()),
                    null,
                    EqualsValueClause(expression.WithoutTrivia()))])).WithAdditionalAnnotations(Formatter.Annotation);
 
        if (oldTypeDeclaration != null)
        {
            var newTypeDeclaration = Rewrite(
                document, expression, newQualifiedName, document, oldTypeDeclaration, allOccurrences, cancellationToken);
 
            var insertionIndex = GetFieldInsertionIndex(isConstant, oldTypeDeclaration, newTypeDeclaration, cancellationToken);
            var finalTypeDeclaration = InsertMember(newTypeDeclaration, newFieldDeclaration, insertionIndex);
 
            var newRoot = document.Root.ReplaceNode(oldTypeDeclaration, finalTypeDeclaration);
            return Task.FromResult(document.Document.WithSyntaxRoot(newRoot));
        }
        else
        {
            var oldCompilationUnit = (CompilationUnitSyntax)document.Root;
            var newCompilationUnit = Rewrite(
                document, expression, newQualifiedName, document, oldCompilationUnit, allOccurrences, cancellationToken);
 
            var insertionIndex = isConstant
                ? DetermineConstantInsertPosition(oldCompilationUnit.Members, newCompilationUnit.Members)
                : DetermineFieldInsertPosition(oldCompilationUnit.Members, newCompilationUnit.Members);
 
            var newRoot = newCompilationUnit.WithMembers(newCompilationUnit.Members.Insert(insertionIndex, newFieldDeclaration));
            return Task.FromResult(document.Document.WithSyntaxRoot(newRoot));
        }
    }
 
    protected override int DetermineConstantInsertPosition(TypeDeclarationSyntax oldType, TypeDeclarationSyntax newType)
        => DetermineConstantInsertPosition(oldType.Members, newType.Members);
 
    private static int DetermineConstantInsertPosition(
        SyntaxList<MemberDeclarationSyntax> oldMembers,
        SyntaxList<MemberDeclarationSyntax> newMembers)
    {
        // 1) Place the constant after the last constant.
        //
        // 2) If there is no constant, place it before the first field
        //
        // 3) If the first change is before either of those, then place before the first
        // change
        //
        // 4) Otherwise, place it at the start.
        var index = 0;
        var lastConstantIndex = oldMembers.LastIndexOf(IsConstantField);
 
        if (lastConstantIndex >= 0)
        {
            index = lastConstantIndex + 1;
        }
        else
        {
            var firstFieldIndex = oldMembers.IndexOf(member => member is FieldDeclarationSyntax);
            if (firstFieldIndex >= 0)
            {
                index = firstFieldIndex;
            }
        }
 
        var firstChangeIndex = DetermineFirstChange(oldMembers, newMembers);
        if (firstChangeIndex >= 0)
        {
            index = Math.Min(index, firstChangeIndex);
        }
 
        return index;
    }
 
    protected override int DetermineFieldInsertPosition(TypeDeclarationSyntax oldType, TypeDeclarationSyntax newType)
        => DetermineFieldInsertPosition(oldType.Members, newType.Members);
 
    private static int DetermineFieldInsertPosition(
        SyntaxList<MemberDeclarationSyntax> oldMembers,
        SyntaxList<MemberDeclarationSyntax> newMembers)
    {
        // 1) Place the constant after the last field.
        //
        // 2) If there is no field, place it after the last constant
        //
        // 3) If the first change is before either of those, then place before the first
        // change
        //
        // 4) Otherwise, place it at the start.
        var index = 0;
        var lastFieldIndex = oldMembers.LastIndexOf(member => member is FieldDeclarationSyntax);
        if (lastFieldIndex >= 0)
        {
            index = lastFieldIndex + 1;
        }
        else
        {
            var lastConstantIndex = oldMembers.LastIndexOf(IsConstantField);
            if (lastConstantIndex >= 0)
            {
                index = lastConstantIndex + 1;
            }
        }
 
        var firstChangeIndex = DetermineFirstChange(oldMembers, newMembers);
        if (firstChangeIndex >= 0)
        {
            index = Math.Min(index, firstChangeIndex);
        }
 
        return index;
    }
 
    private static bool IsConstantField(MemberDeclarationSyntax member)
        => member is FieldDeclarationSyntax field && field.Modifiers.Any(SyntaxKind.ConstKeyword);
 
    private static int DetermineFirstChange(SyntaxList<MemberDeclarationSyntax> oldMembers, SyntaxList<MemberDeclarationSyntax> newMembers)
    {
        for (var i = 0; i < oldMembers.Count; i++)
        {
            if (!AreEquivalent(oldMembers[i], newMembers[i], topLevel: false))
            {
                return i;
            }
        }
 
        return -1;
    }
 
    private static TypeDeclarationSyntax InsertMember(
        TypeDeclarationSyntax typeDeclaration,
        MemberDeclarationSyntax memberDeclaration,
        int index)
    {
        return typeDeclaration.WithMembers(
            typeDeclaration.Members.Insert(index, memberDeclaration));
    }
 
    private static SyntaxTokenList MakeFieldModifiers(bool isConstant, bool inScript)
    {
        if (isConstant)
        {
            return [PrivateKeyword, ConstKeyword];
        }
        else if (inScript)
        {
            return [PrivateKeyword, ReadOnlyKeyword];
        }
        else
        {
            return [PrivateKeyword, StaticKeyword, ReadOnlyKeyword];
        }
    }
}